Bevy 0.17 Migration Final PR (#76)
* Get bevy 0.17 compiling and running (#72) * get bevy 0.17 compiling and running * try to fix CI breaking from const assertion for client/server features * fix `bin` -> `lib` for `shared` in CI * typo * fix some collider issues (#73) * Physics/controller improvements (#74) * trying to fix physics prediction * fixed prediction desync * substantial controller improvements * Finish off main bevy 0.17 migration (#75) * fix lookdir issues - airplane moving backwards - player model facing backwards - camera was technically backwards the whole time, and player models were facing the right way; camera is now facing forwards - firing without a target now respects lookdir * fix aim targeting * migrate to bevy_trenchbroom 0.10 crates release * fixed colliders not being adjusted out of worldspace * predict platforms to stop constant rollbacks while riding them * fix key/head drop visuals not working * Fix key/head drop random initial force * fixed static head drops duplicating * fix platform velocity inheritance * fix thrown projectiles not autorotating * fix inconsistent explosion animations * update avian3d to 0.4.1 * fix controller snapping to fixed angle upon switching heads * clean up commented code * fix broken physics positions * Clean comments, fix warnings (#77) * clean comments, fix warnings * fix missing import * steamworks 162 libs * fix mouselook --------- Co-authored-by: extrawurst <mail@rusticorn.com>
This commit is contained in:
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@@ -44,4 +44,7 @@ jobs:
|
||||
|
||||
- name: Tests
|
||||
run: |
|
||||
cargo test
|
||||
cargo test --lib shared --no-default-features --features client
|
||||
cargo test --lib shared --no-default-features --features server
|
||||
cargo test --bin hedz_reloaded --no-default-features --features shared/client
|
||||
cargo test --bin server --no-default-features --features shared/server
|
||||
|
||||
2358
Cargo.lock
generated
2358
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
38
Cargo.toml
@@ -3,7 +3,7 @@ resolver = "3"
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.dependencies]
|
||||
avian3d = { version = "0.3", default-features = false, features = [
|
||||
avian3d = { version = "0.4.0", default-features = false, features = [
|
||||
"3d",
|
||||
"f32",
|
||||
"parry-f32",
|
||||
@@ -13,7 +13,7 @@ avian3d = { version = "0.3", default-features = false, features = [
|
||||
"parallel",
|
||||
"serialize",
|
||||
] }
|
||||
bevy = { version = "0.16.0", default-features = false, features = [
|
||||
bevy = { version = "0.17.0", default-features = false, features = [
|
||||
"animation",
|
||||
"async_executor",
|
||||
"bevy_asset",
|
||||
@@ -49,21 +49,22 @@ bevy = { version = "0.16.0", default-features = false, features = [
|
||||
"x11",
|
||||
"track_location",
|
||||
] }
|
||||
bevy-inspector-egui = { version = "0.31" }
|
||||
bevy-steamworks = "0.13.0"
|
||||
bevy-ui-gradients = "0.4.0"
|
||||
bevy_asset_loader = "0.23.0-rc.3"
|
||||
bevy_ballistic = "0.4.0"
|
||||
bevy_common_assets = { version = "0.13.0", features = ["ron"] }
|
||||
bevy_debug_log = "0.6.0"
|
||||
bevy_sprite3d = "5.0.0"
|
||||
bevy_trenchbroom = { version = "0.8.1", default-features = false, features = [
|
||||
"avian",
|
||||
bevy-inspector-egui = "0.34"
|
||||
bevy-steamworks = "0.15.0"
|
||||
bevy_asset_loader = "=0.24.0-rc.1"
|
||||
bevy_ballistic = { git = "https://github.com/rustunit/bevy_ballistic.git", rev = "b08ffec" }
|
||||
bevy_common_assets = { version = "0.14.0", features = ["ron"] }
|
||||
bevy_debug_log = { git = "https://github.com/rustunit/bevy_debug_log.git", rev = "86051a0" }
|
||||
bevy_sprite3d = "7.0.0"
|
||||
bevy_trenchbroom = { version = "0.10", default-features = false, features = [
|
||||
"physics-integration",
|
||||
] }
|
||||
happy_feet = { git = "https://github.com/atornity/happy_feet.git", rev = "1b24ed95f166e63af35e7b6f9f0053d6d28e1f1a", features = [
|
||||
bevy_trenchbroom_avian = "0.10"
|
||||
happy_feet = { git = "https://github.com/PROMETHIA-27/happy_feet.git", rev = "48a96cc", features = [
|
||||
"serde",
|
||||
] }
|
||||
lightyear = { version = "0.24", git = "https://github.com/cBournhonesque/lightyear", rev = "69675e5f7b305ae02c3c2c3cd320bbc3e442ee4d", default-features = false, features = [
|
||||
lightyear = { version = "0.25", default-features = false, features = [
|
||||
"avian3d",
|
||||
"input_native",
|
||||
"interpolation",
|
||||
"netcode",
|
||||
@@ -71,18 +72,15 @@ lightyear = { version = "0.24", git = "https://github.com/cBournhonesque/lightye
|
||||
"replication",
|
||||
"std",
|
||||
"udp",
|
||||
"frame_interpolation",
|
||||
] }
|
||||
lightyear_avian3d = { git = "https://github.com/cBournhonesque/lightyear", rev = "69675e5f7b305ae02c3c2c3cd320bbc3e442ee4d" }
|
||||
lightyear_serde = { git = "https://github.com/cBournhonesque/lightyear", rev = "69675e5f7b305ae02c3c2c3cd320bbc3e442ee4d" }
|
||||
lightyear_serde = "0.25"
|
||||
nil = "0.14.0"
|
||||
rand = "=0.8.5"
|
||||
ron = "0.8"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
shared = { path = "crates/shared" }
|
||||
steamworks = "0.11"
|
||||
steamworks = "0.12"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
[patch.crates-io]
|
||||
bevy-steamworks = { git = "https://github.com/HouraiTeahouse/bevy_steamworks.git", rev = "1933e5d" }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -17,13 +17,13 @@ bevy = { workspace = true, default-features = false, features = [
|
||||
] }
|
||||
bevy-inspector-egui = { workspace = true, optional = true }
|
||||
bevy-steamworks = { workspace = true }
|
||||
bevy-ui-gradients = { workspace = true }
|
||||
bevy_asset_loader = { workspace = true }
|
||||
bevy_ballistic = { workspace = true }
|
||||
bevy_common_assets = { workspace = true }
|
||||
bevy_debug_log = { workspace = true }
|
||||
bevy_sprite3d = { workspace = true }
|
||||
bevy_trenchbroom = { workspace = true, features = ["client"] }
|
||||
bevy_trenchbroom_avian = { workspace = true }
|
||||
happy_feet = { workspace = true }
|
||||
lightyear = { workspace = true }
|
||||
nil = { workspace = true }
|
||||
|
||||
@@ -54,7 +54,7 @@ fn setup(mut commands: Commands, assets: Res<UIAssets>) {
|
||||
..default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(JustifyText::Center),
|
||||
TextLayout::new_with_justify(Justify::Center),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(20.0),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use avian3d::prelude::{
|
||||
Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderMassProperties,
|
||||
CollisionEventsEnabled, CollisionLayers, Sensor,
|
||||
Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderOf, ColliderTransform,
|
||||
CollisionEventsEnabled, CollisionLayers, Position, Rotation, Sensor,
|
||||
};
|
||||
use bevy::{
|
||||
ecs::bundle::BundleFromComponents, prelude::*, scene::SceneInstance, utils::synccell::SyncCell,
|
||||
ecs::bundle::BundleFromComponents, platform::cell::SyncCell, prelude::*, scene::SceneInstance,
|
||||
};
|
||||
use bevy_trenchbroom::geometry::Brushes;
|
||||
use lightyear::{
|
||||
frame_interpolation::FrameInterpolate,
|
||||
link::{LinkConditioner, prelude::*},
|
||||
netcode::Key,
|
||||
prelude::{
|
||||
@@ -48,16 +49,22 @@ pub fn plugin(app: &mut App) {
|
||||
);
|
||||
app.add_systems(Last, close_server_processes);
|
||||
app.add_systems(Update, despawn_absent_map_entities);
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
(migrate_remote_entities, ApplyDeferred)
|
||||
.chain()
|
||||
.after(ReplicationSystems::Receive),
|
||||
);
|
||||
|
||||
global_observer!(app, on_connecting);
|
||||
global_observer!(app, on_connection_failed);
|
||||
global_observer!(app, on_connection_succeeded);
|
||||
global_observer!(app, temp_give_player_marker);
|
||||
global_observer!(app, connect_on_local_server_started);
|
||||
global_observer!(app, received_remote_map_entity);
|
||||
global_observer!(app, add_visual_interpolation_components);
|
||||
}
|
||||
|
||||
fn close_server_processes(mut app_exit: EventReader<AppExit>) {
|
||||
fn close_server_processes(mut app_exit: MessageReader<AppExit>) {
|
||||
if app_exit.read().next().is_some() {
|
||||
let mut lock = SERVER_PROCESSES.lock();
|
||||
for mut process in lock.drain(..) {
|
||||
@@ -114,21 +121,22 @@ fn attempt_connection(mut commands: Commands) -> Result {
|
||||
},
|
||||
)?,
|
||||
UdpIo::default(),
|
||||
PredictionManager::default(),
|
||||
InputTimeline(Timeline::from(Input::new(
|
||||
sync_config,
|
||||
InputDelayConfig::balanced(),
|
||||
))),
|
||||
))
|
||||
.trigger(Connect);
|
||||
.trigger(|entity| Connect { entity });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_connection_succeeded(
|
||||
_trigger: Trigger<OnAdd, Connected>,
|
||||
_trigger: On<Add, Connected>,
|
||||
state: Res<State<GameState>>,
|
||||
mut change_state: ResMut<NextState<GameState>>,
|
||||
mut sender: Single<&mut TriggerSender<ClientEnteredPlaying>>,
|
||||
mut sender: Single<&mut EventSender<ClientEnteredPlaying>>,
|
||||
) {
|
||||
if *state == GameState::Connecting {
|
||||
change_state.set(GameState::Playing);
|
||||
@@ -141,21 +149,21 @@ fn on_connection_succeeded(
|
||||
#[derive(Component)]
|
||||
struct ClientActive;
|
||||
|
||||
fn on_connecting(trigger: Trigger<OnAdd, Connecting>, mut commands: Commands) {
|
||||
commands.entity(trigger.target()).insert(ClientActive);
|
||||
fn on_connecting(trigger: On<Add, Connecting>, mut commands: Commands) {
|
||||
commands.entity(trigger.event().entity).insert(ClientActive);
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct LocalServerStdout(SyncCell<mpsc::Receiver<String>>);
|
||||
|
||||
fn on_connection_failed(
|
||||
trigger: Trigger<OnAdd, Disconnected>,
|
||||
trigger: On<Add, Disconnected>,
|
||||
disconnected: Query<&Disconnected>,
|
||||
mut commands: Commands,
|
||||
client_active: Query<&ClientActive>,
|
||||
mut opened_server: Local<bool>,
|
||||
) -> Result {
|
||||
let disconnected = disconnected.get(trigger.target()).unwrap();
|
||||
let disconnected = disconnected.get(trigger.event().entity).unwrap();
|
||||
if *opened_server {
|
||||
panic!(
|
||||
"failed to connect to local server: {:?}",
|
||||
@@ -163,7 +171,7 @@ fn on_connection_failed(
|
||||
);
|
||||
}
|
||||
|
||||
let client = trigger.target();
|
||||
let client = trigger.event().entity;
|
||||
if client_active.contains(client) {
|
||||
commands.entity(client).remove::<ClientActive>();
|
||||
|
||||
@@ -223,73 +231,87 @@ fn parse_local_server_stdout(mut commands: Commands, mut stdout: ResMut<LocalSer
|
||||
}
|
||||
|
||||
fn connect_on_local_server_started(
|
||||
_trigger: Trigger<LocalServerStarted>,
|
||||
_trigger: On<LocalServerStarted>,
|
||||
state: Res<State<GameState>>,
|
||||
mut commands: Commands,
|
||||
client: Single<Entity, With<Client>>,
|
||||
) {
|
||||
if *state == GameState::Connecting {
|
||||
commands.entity(*client).trigger(Connect);
|
||||
commands
|
||||
.entity(*client)
|
||||
.trigger(|entity| Connect { entity });
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_give_player_marker(trigger: Trigger<OnAdd, Player>, mut commands: Commands) {
|
||||
fn temp_give_player_marker(trigger: On<Add, Player>, mut commands: Commands) {
|
||||
commands
|
||||
.entity(trigger.target())
|
||||
.entity(trigger.event().entity)
|
||||
.insert(InputMarker::<ControlState>::default());
|
||||
}
|
||||
|
||||
fn received_remote_map_entity(
|
||||
trigger: Trigger<OnAdd, TbMapEntityId>,
|
||||
world: &mut World,
|
||||
mut child_buffer: Local<Vec<Entity>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn migrate_remote_entities(
|
||||
query: Query<(Entity, &TbMapEntityId), (Added<TbMapEntityId>, With<Replicated>)>,
|
||||
children: Query<&Children>,
|
||||
mut commands: Commands,
|
||||
mut mapping: ResMut<TbMapEntityMapping>,
|
||||
) {
|
||||
let serverside = trigger.target();
|
||||
|
||||
if world.get::<Replicated>(serverside).is_none() {
|
||||
return;
|
||||
for (serverside, tb_id) in query.iter() {
|
||||
received_remote_map_entity(serverside, tb_id.id, &children, &mut mapping, &mut commands);
|
||||
}
|
||||
}
|
||||
|
||||
let id = *world.get::<TbMapEntityId>(serverside).unwrap();
|
||||
|
||||
let Some(clientside) = world.resource_mut::<TbMapEntityMapping>().0.remove(&id.id) else {
|
||||
warn!("received unknown MapEntity ID `{id:?}`");
|
||||
fn received_remote_map_entity(
|
||||
serverside: Entity,
|
||||
tb_id: u64,
|
||||
children: &Query<&Children>,
|
||||
mapping: &mut TbMapEntityMapping,
|
||||
commands: &mut Commands,
|
||||
) {
|
||||
let Some(clientside) = mapping.0.remove(&tb_id) else {
|
||||
warn!("received unknown MapEntity ID `{tb_id:?}`");
|
||||
return;
|
||||
};
|
||||
|
||||
// cannot just use `take` directly with a bundle because then any missing component would cause
|
||||
// the entire bundle to fail
|
||||
move_component::<Brushes>(world, clientside, serverside);
|
||||
move_component::<Brushes>(commands, clientside, serverside);
|
||||
move_component::<(
|
||||
Collider,
|
||||
ColliderAabb,
|
||||
ColliderDensity,
|
||||
ColliderMarker,
|
||||
ColliderMassProperties,
|
||||
CollisionLayers,
|
||||
)>(world, clientside, serverside);
|
||||
move_component::<CollisionEventsEnabled>(world, clientside, serverside);
|
||||
move_component::<Platform>(world, clientside, serverside);
|
||||
move_component::<PlatformTarget>(world, clientside, serverside);
|
||||
move_component::<SceneInstance>(world, clientside, serverside);
|
||||
move_component::<SceneRoot>(world, clientside, serverside);
|
||||
move_component::<Sensor>(world, clientside, serverside);
|
||||
)>(commands, clientside, serverside);
|
||||
move_component::<ColliderOf>(commands, clientside, serverside);
|
||||
move_component::<ColliderTransform>(commands, clientside, serverside);
|
||||
move_component::<CollisionEventsEnabled>(commands, clientside, serverside);
|
||||
move_component::<Platform>(commands, clientside, serverside);
|
||||
move_component::<PlatformTarget>(commands, clientside, serverside);
|
||||
move_component::<SceneInstance>(commands, clientside, serverside);
|
||||
move_component::<SceneRoot>(commands, clientside, serverside);
|
||||
move_component::<Sensor>(commands, clientside, serverside);
|
||||
|
||||
if let Some(children) = world.get::<Children>(clientside) {
|
||||
child_buffer.extend(children.iter());
|
||||
for child in child_buffer.drain(..) {
|
||||
world.entity_mut(child).insert(ChildOf(serverside));
|
||||
if let Ok(children) = children.get(clientside) {
|
||||
for child in children.iter() {
|
||||
commands.entity(child).insert(ChildOf(serverside));
|
||||
}
|
||||
}
|
||||
|
||||
world.entity_mut(clientside).despawn();
|
||||
commands.entity(clientside).despawn();
|
||||
}
|
||||
|
||||
fn move_component<B: Bundle + BundleFromComponents>(world: &mut World, from: Entity, to: Entity) {
|
||||
let comp = world.entity_mut(from).take::<B>();
|
||||
if let Some(comp) = comp {
|
||||
world.entity_mut(to).insert(comp);
|
||||
}
|
||||
fn move_component<B: Bundle + BundleFromComponents>(
|
||||
commands: &mut Commands,
|
||||
from: Entity,
|
||||
to: Entity,
|
||||
) {
|
||||
commands.queue(move |world: &mut World| {
|
||||
let comp = world.entity_mut(from).take::<B>();
|
||||
if let Some(comp) = comp {
|
||||
world.entity_mut(to).insert(comp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn despawn_absent_map_entities(
|
||||
@@ -309,3 +331,22 @@ fn despawn_absent_map_entities(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_visual_interpolation_components(trigger: On<Add, Predicted>, mut commands: Commands) {
|
||||
commands.entity(trigger.entity).insert((
|
||||
FrameInterpolate::<Position> {
|
||||
// We must trigger change detection on visual interpolation
|
||||
// to make sure that child entities (sprites, meshes, text)
|
||||
// are also interpolated
|
||||
trigger_change_detection: true,
|
||||
..default()
|
||||
},
|
||||
FrameInterpolate::<Rotation> {
|
||||
// We must trigger change detection on visual interpolation
|
||||
// to make sure that child entities (sprites, meshes, text)
|
||||
// are also interpolated
|
||||
trigger_change_detection: true,
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
54
crates/client/src/control/controller_flying.rs
Normal file
54
crates/client/src/control/controller_flying.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use shared::{
|
||||
GameState,
|
||||
control::{ControlState, ControllerSet, LookDirMovement},
|
||||
player::PlayerBodyMesh,
|
||||
};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
rotate_rig
|
||||
.before(shared::control::controller_flying::apply_controls)
|
||||
.in_set(ControllerSet::ApplyControlsFly)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
fn rotate_rig(
|
||||
actions: Query<&ActionState<ControlState>>,
|
||||
look_dir: Res<LookDirMovement>,
|
||||
mut player: Query<(&mut Transform, &ChildOf), With<PlayerBodyMesh>>,
|
||||
) {
|
||||
for (mut rig_transform, child_of) in player.iter_mut() {
|
||||
let controls = actions.get(child_of.parent()).unwrap();
|
||||
|
||||
if controls.view_mode {
|
||||
continue;
|
||||
}
|
||||
|
||||
let look_dir = look_dir.0;
|
||||
|
||||
// todo: Make consistent with the running controller
|
||||
let sensitivity = 0.001;
|
||||
let max_pitch = 35.0 * PI / 180.0;
|
||||
let min_pitch = -25.0 * PI / 180.0;
|
||||
|
||||
rig_transform.rotate_y(look_dir.x * -sensitivity);
|
||||
|
||||
let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ);
|
||||
let yaw = euler_rot.0;
|
||||
let pitch = euler_rot.1 + look_dir.y * -sensitivity;
|
||||
|
||||
let pitch_clamped = pitch.clamp(min_pitch, max_pitch);
|
||||
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch_clamped, 0.0);
|
||||
|
||||
// The following can be used to limit the amount of rotation per frame
|
||||
// let target_rotation = rig_transform.rotation
|
||||
// * Quat::from_rotation_x(controls.keyboard_state.look_dir.y * sensitivity);
|
||||
// let clamped_rotation = rig_transform.rotation.rotate_towards(target_rotation, 0.01);
|
||||
// rig_transform.rotation = clamped_rotation;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::Controls;
|
||||
use super::{ControlState, Controls};
|
||||
use crate::{GameState, control::CharacterInputEnabled};
|
||||
use bevy::{
|
||||
input::{
|
||||
@@ -9,10 +9,13 @@ use bevy::{
|
||||
prelude::*,
|
||||
};
|
||||
use lightyear::{
|
||||
input::client::InputSet,
|
||||
input::client::InputSystems,
|
||||
prelude::input::native::{ActionState, InputMarker},
|
||||
};
|
||||
use shared::control::{ControlState, ControllerSet};
|
||||
use shared::{
|
||||
control::{ControllerSet, LookDirMovement},
|
||||
player::PlayerBodyMesh,
|
||||
};
|
||||
use std::{collections::HashMap, hash::Hash};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
@@ -26,18 +29,20 @@ pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
FixedPreUpdate,
|
||||
(
|
||||
reset_lookdir,
|
||||
gamepad_controls,
|
||||
keyboard_controls,
|
||||
mouse_rotate,
|
||||
mouse_click,
|
||||
gamepad_connections.run_if(on_event::<GamepadEvent>),
|
||||
gamepad_connections.run_if(on_message::<GamepadEvent>),
|
||||
combine_controls,
|
||||
get_lookdir,
|
||||
clear_keyboard_just,
|
||||
clear_gamepad_just,
|
||||
)
|
||||
.chain()
|
||||
.in_set(ControllerSet::CollectInputs)
|
||||
.before(InputSet::WriteClientInputs)
|
||||
.before(InputSystems::WriteClientInputs)
|
||||
.run_if(
|
||||
in_state(GameState::Playing)
|
||||
.and(resource_exists_and_equals(CharacterInputEnabled::On)),
|
||||
@@ -45,7 +50,7 @@ pub fn plugin(app: &mut App) {
|
||||
)
|
||||
.add_systems(
|
||||
FixedPreUpdate,
|
||||
buffer_inputs.in_set(InputSet::WriteClientInputs),
|
||||
buffer_inputs.in_set(InputSystems::WriteClientInputs),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
@@ -63,6 +68,10 @@ fn buffer_inputs(
|
||||
player.0 = *controls;
|
||||
}
|
||||
|
||||
fn reset_lookdir(mut look_dir: ResMut<LookDirMovement>) {
|
||||
look_dir.0 = Vec2::ZERO;
|
||||
}
|
||||
|
||||
/// Reset character inputs to default when character input is disabled.
|
||||
fn reset_control_state_on_disable(
|
||||
state: Res<CharacterInputEnabled>,
|
||||
@@ -71,7 +80,10 @@ fn reset_control_state_on_disable(
|
||||
) {
|
||||
if state.is_changed() && matches!(*state, CharacterInputEnabled::Off) {
|
||||
*controls = Controls::default();
|
||||
*control_state = ControlState::default();
|
||||
*control_state = ControlState {
|
||||
look_dir: control_state.look_dir,
|
||||
..default()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +177,7 @@ fn combine_controls(controls: Res<Controls>, mut combined_controls: ResMut<Contr
|
||||
let keyboard = controls.keyboard_state;
|
||||
let gamepad = controls.gamepad_state.unwrap_or_default();
|
||||
|
||||
combined_controls.look_dir = gamepad.look_dir + keyboard.look_dir;
|
||||
combined_controls.look_dir = Dir3::NEG_Z;
|
||||
combined_controls.move_dir = gamepad.move_dir + keyboard.move_dir;
|
||||
combined_controls.jump = gamepad.jump | keyboard.jump;
|
||||
combined_controls.view_mode = gamepad.view_mode | keyboard.view_mode;
|
||||
@@ -180,6 +192,17 @@ fn combine_controls(controls: Res<Controls>, mut combined_controls: ResMut<Contr
|
||||
combined_controls.cash_heal = gamepad.cash_heal | keyboard.cash_heal;
|
||||
}
|
||||
|
||||
fn get_lookdir(
|
||||
mut controls: ResMut<ControlState>,
|
||||
rig_transform: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||
) {
|
||||
controls.look_dir = if let Some(ref rig_transform) = rig_transform {
|
||||
rig_transform.forward()
|
||||
} else {
|
||||
Dir3::NEG_Z
|
||||
};
|
||||
}
|
||||
|
||||
/// Applies a square deadzone to a Vec2
|
||||
fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
|
||||
Vec2::new(
|
||||
@@ -192,6 +215,7 @@ fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
|
||||
fn gamepad_controls(
|
||||
gamepads: Query<(Entity, &Gamepad, &InputStateCache<GamepadButton>)>,
|
||||
mut controls: ResMut<Controls>,
|
||||
mut look_dir: ResMut<LookDirMovement>,
|
||||
) {
|
||||
let Some((_e, gamepad, cache)) = gamepads.iter().next() else {
|
||||
if controls.gamepad_state.is_some() {
|
||||
@@ -208,7 +232,7 @@ fn gamepad_controls(
|
||||
.unwrap_or_default();
|
||||
|
||||
// 8BitDo Ultimate wireless Controller for PC
|
||||
let look_dir = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) {
|
||||
look_dir.0 = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) {
|
||||
const EPSILON: f32 = 0.015;
|
||||
Vec2::new(
|
||||
if rotate < 0.5 - EPSILON {
|
||||
@@ -224,9 +248,11 @@ fn gamepad_controls(
|
||||
deadzone_square(gamepad.right_stick(), deadzone_right_stick) * 40.
|
||||
};
|
||||
|
||||
let move_dir = deadzone_square(gamepad.left_stick(), deadzone_left_stick);
|
||||
|
||||
let state = ControlState {
|
||||
move_dir: deadzone_square(gamepad.left_stick(), deadzone_left_stick),
|
||||
look_dir,
|
||||
move_dir,
|
||||
look_dir: Dir3::NEG_Z,
|
||||
jump: gamepad.pressed(GamepadButton::South),
|
||||
view_mode: gamepad.pressed(GamepadButton::LeftTrigger2),
|
||||
trigger: gamepad.pressed(GamepadButton::RightTrigger2),
|
||||
@@ -251,11 +277,9 @@ fn gamepad_controls(
|
||||
}
|
||||
|
||||
/// Collect mouse movement input
|
||||
fn mouse_rotate(mut mouse: EventReader<MouseMotion>, mut controls: ResMut<Controls>) {
|
||||
controls.keyboard_state.look_dir = Vec2::ZERO;
|
||||
|
||||
fn mouse_rotate(mut mouse: MessageReader<MouseMotion>, mut look_dir: ResMut<LookDirMovement>) {
|
||||
for ev in mouse.read() {
|
||||
controls.keyboard_state.look_dir += ev.delta;
|
||||
look_dir.0 += ev.delta;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +316,7 @@ fn keyboard_controls(
|
||||
}
|
||||
|
||||
/// Collect mouse button input when pressed
|
||||
fn mouse_click(mut events: EventReader<MouseButtonInput>, mut controls: ResMut<Controls>) {
|
||||
fn mouse_click(mut events: MessageReader<MouseButtonInput>, mut controls: ResMut<Controls>) {
|
||||
controls.keyboard_state.just_triggered = false;
|
||||
|
||||
for ev in events.read() {
|
||||
@@ -318,7 +342,7 @@ fn mouse_click(mut events: EventReader<MouseButtonInput>, mut controls: ResMut<C
|
||||
}
|
||||
|
||||
/// Receive gamepad connections and disconnections
|
||||
fn gamepad_connections(mut evr_gamepad: EventReader<GamepadEvent>) {
|
||||
fn gamepad_connections(mut evr_gamepad: MessageReader<GamepadEvent>) {
|
||||
for ev in evr_gamepad.read() {
|
||||
if let GamepadEvent::Connection(connection) = ev {
|
||||
match &connection.connection {
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::GameState;
|
||||
use bevy::prelude::*;
|
||||
use shared::control::{ControlState, ControllerSet};
|
||||
|
||||
mod controller_flying;
|
||||
pub mod controls;
|
||||
|
||||
#[derive(Resource, Debug, Default)]
|
||||
@@ -19,7 +20,7 @@ pub enum CharacterInputEnabled {
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.insert_resource(CharacterInputEnabled::On);
|
||||
|
||||
app.add_plugins(controls::plugin);
|
||||
app.add_plugins((controller_flying::plugin, controls::plugin));
|
||||
|
||||
app.configure_sets(
|
||||
FixedPreUpdate,
|
||||
|
||||
@@ -28,7 +28,7 @@ fn setup(mut commands: Commands) {
|
||||
font_size: 12.0,
|
||||
..default()
|
||||
},
|
||||
TextLayout::new_with_justify(JustifyText::Left),
|
||||
TextLayout::new_with_justify(Justify::Left),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(5.0),
|
||||
|
||||
@@ -67,16 +67,16 @@ fn on_added(
|
||||
}
|
||||
|
||||
fn on_removed(
|
||||
trigger: Trigger<OnRemove, Healing>,
|
||||
trigger: On<Remove, Healing>,
|
||||
mut commands: Commands,
|
||||
effects: Query<&HasHealingEffects>,
|
||||
) {
|
||||
let Ok(has_effects) = effects.get(trigger.target()) else {
|
||||
let Ok(has_effects) = effects.get(trigger.event().entity) else {
|
||||
return;
|
||||
};
|
||||
commands.entity(has_effects.effects).try_despawn();
|
||||
commands
|
||||
.entity(trigger.target())
|
||||
.entity(trigger.event().entity)
|
||||
.remove::<HasHealingEffects>();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,10 +19,13 @@ use bevy::{
|
||||
use bevy_common_assets::ron::RonAssetPlugin;
|
||||
use bevy_sprite3d::Sprite3dPlugin;
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
use bevy_ui_gradients::UiGradientsPlugin;
|
||||
use bevy_trenchbroom_avian::AvianPhysicsBackend;
|
||||
use camera::MainCamera;
|
||||
use heads_database::HeadDatabaseAsset;
|
||||
use lightyear::prelude::client::ClientPlugins;
|
||||
use lightyear::{
|
||||
avian3d::plugin::LightyearAvianPlugin, frame_interpolation::FrameInterpolationPlugin,
|
||||
prelude::client::ClientPlugins,
|
||||
};
|
||||
use loading_assets::AudioAssets;
|
||||
use shared::*;
|
||||
use std::time::Duration;
|
||||
@@ -55,6 +58,7 @@ fn main() {
|
||||
level: bevy::log::Level::INFO,
|
||||
// provide custom log layer to receive logging events
|
||||
custom_layer: bevy_debug_log::log_capture_layer,
|
||||
..default()
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -64,22 +68,37 @@ fn main() {
|
||||
bevy_debug_log::LogViewerPlugin::default()
|
||||
.auto_open_threshold(bevy::log::tracing::level_filters::LevelFilter::OFF),
|
||||
);
|
||||
app.add_plugins(PhysicsPlugins::default());
|
||||
app.add_plugins(
|
||||
PhysicsPlugins::default()
|
||||
.build()
|
||||
// TODO: This plugin is *not* disabled on the server. This is to solve a bug related to collider transform inheritance. See the
|
||||
// LightyearAvianPlugin below.
|
||||
// Periwink is looking into it at the moment. This **must** be disabled on the client, or positions break for NPCs and some other things.
|
||||
.disable::<PhysicsTransformPlugin>()
|
||||
// FrameInterpolation handles interpolating Position and Rotation
|
||||
.disable::<PhysicsInterpolationPlugin>(),
|
||||
);
|
||||
// TODO: This plugin is *not* inserted on the server. This is to solve a bug related to collider transform inheritance. See the
|
||||
// `.disable::<PhysicsTransformPlugin>()` above.
|
||||
// Periwink is looking into it at the moment. This **must** be inserted on the client, or positions break for NPCs and some other things.
|
||||
app.add_plugins(LightyearAvianPlugin::default());
|
||||
app.add_plugins(FrameInterpolationPlugin::<Position>::default());
|
||||
app.add_plugins(FrameInterpolationPlugin::<Rotation>::default());
|
||||
app.add_plugins(ClientPlugins {
|
||||
tick_duration: Duration::from_secs_f64(1.0 / 60.0),
|
||||
});
|
||||
app.add_plugins(Sprite3dPlugin);
|
||||
app.add_plugins(TrenchBroomPlugins(
|
||||
TrenchBroomConfig::new("hedz").icon(None),
|
||||
TrenchBroomConfig::new("hedz")
|
||||
.icon(None)
|
||||
.default_solid_spawn_hooks(|| SpawnHooks::new().convex_collider()),
|
||||
));
|
||||
app.add_plugins(UiGradientsPlugin);
|
||||
app.add_plugins(TrenchBroomPhysicsPlugin::new(AvianPhysicsBackend));
|
||||
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
|
||||
|
||||
#[cfg(feature = "dbg")]
|
||||
{
|
||||
app.add_plugins(bevy_inspector_egui::bevy_egui::EguiPlugin {
|
||||
enable_multipass_for_primary_context: true,
|
||||
});
|
||||
app.add_plugins(bevy_inspector_egui::bevy_egui::EguiPlugin::default());
|
||||
app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::new());
|
||||
app.add_plugins(PhysicsDebugPlugin::default());
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ pub enum PlayerAssignmentState {
|
||||
pub struct LocalPlayer;
|
||||
|
||||
fn on_update_head_mesh(
|
||||
trigger: Trigger<ClientHeadChanged>,
|
||||
trigger: On<ClientHeadChanged>,
|
||||
mut commands: Commands,
|
||||
body_mesh: Single<(Entity, &Children), With<PlayerBodyMesh>>,
|
||||
head_db: Res<HeadsDatabase>,
|
||||
|
||||
@@ -7,7 +7,7 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn on_spawn_sounds(
|
||||
trigger: Trigger<PlaySound>,
|
||||
trigger: On<PlaySound>,
|
||||
mut commands: Commands,
|
||||
// settings: SettingsRead,
|
||||
assets: Res<AudioAssets>,
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn plugin(app: &mut App) {
|
||||
);
|
||||
}
|
||||
|
||||
fn log_steam_events(mut events: EventReader<SteamworksEvent>) {
|
||||
fn log_steam_events(mut events: MessageReader<SteamworksEvent>) {
|
||||
for e in events.read() {
|
||||
info!("steam ev: {:?}", e);
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ fn setup(mut commands: Commands, assets: Res<UIAssets>) {
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::linear_rgba(0., 0., 0., 0.6)),
|
||||
StateScoped(PauseMenuState::Open),
|
||||
DespawnOnExit(PauseMenuState::Open),
|
||||
children![
|
||||
spawn_progress(ProgressBar::Music, 100, assets.font.clone()),
|
||||
spawn_progress(ProgressBar::Sound, 80, assets.font.clone())
|
||||
@@ -91,7 +91,7 @@ fn spawn_progress(bar: ProgressBar, value: u8, font: Handle<Font>) -> impl Bundl
|
||||
},
|
||||
BackgroundColor(BLACK.into()),
|
||||
BorderRadius::all(Val::Px(100.)),
|
||||
BorderColor(HEDZ_PURPLE.into()),
|
||||
BorderColor::all(HEDZ_PURPLE),
|
||||
BoxShadow::new(
|
||||
BLACK.into(),
|
||||
Val::Px(2.),
|
||||
@@ -117,7 +117,7 @@ fn spawn_progress(bar: ProgressBar, value: u8, font: Handle<Font>) -> impl Bundl
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(JustifyText::Left),
|
||||
TextLayout::new_with_justify(Justify::Left),
|
||||
),
|
||||
(
|
||||
Node {
|
||||
@@ -131,7 +131,7 @@ fn spawn_progress(bar: ProgressBar, value: u8, font: Handle<Font>) -> impl Bundl
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(JustifyText::Left).with_no_wrap(),
|
||||
TextLayout::new_with_justify(Justify::Left).with_no_wrap(),
|
||||
),
|
||||
(
|
||||
Text::new(format!("{value}",)),
|
||||
@@ -141,7 +141,7 @@ fn spawn_progress(bar: ProgressBar, value: u8, font: Handle<Font>) -> impl Bundl
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(JustifyText::Left).with_no_wrap(),
|
||||
TextLayout::new_with_justify(Justify::Left).with_no_wrap(),
|
||||
),
|
||||
(
|
||||
Node {
|
||||
@@ -155,7 +155,7 @@ fn spawn_progress(bar: ProgressBar, value: u8, font: Handle<Font>) -> impl Bundl
|
||||
..Default::default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(JustifyText::Left).with_no_wrap(),
|
||||
TextLayout::new_with_justify(Justify::Left).with_no_wrap(),
|
||||
)
|
||||
],
|
||||
)
|
||||
@@ -176,11 +176,11 @@ fn selection_changed(
|
||||
) {
|
||||
if state.is_changed() {
|
||||
for (mut border, bar) in query.iter_mut() {
|
||||
border.0 = if *bar == state.0 {
|
||||
HEDZ_GREEN.into()
|
||||
*border = BorderColor::all(if *bar == state.0 {
|
||||
HEDZ_GREEN
|
||||
} else {
|
||||
HEDZ_PURPLE.into()
|
||||
}
|
||||
HEDZ_PURPLE
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ dbg = ["avian3d/debug-plugin", "shared/dbg"]
|
||||
avian3d = { workspace = true }
|
||||
bevy = { workspace = true, default-features = false }
|
||||
bevy-steamworks = { workspace = true }
|
||||
bevy-ui-gradients = { workspace = true }
|
||||
bevy_common_assets = { workspace = true }
|
||||
bevy_sprite3d = { workspace = true }
|
||||
bevy_trenchbroom = { workspace = true }
|
||||
bevy_trenchbroom_avian = { workspace = true }
|
||||
clap = { version = "=4.5.47", features = ["derive"] }
|
||||
happy_feet = { workspace = true }
|
||||
lightyear = { workspace = true }
|
||||
lightyear_avian3d = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
ron = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
shared = { workspace = true }
|
||||
|
||||
115
crates/server/src/head_drop.rs
Normal file
115
crates/server/src/head_drop.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
|
||||
prelude::*,
|
||||
};
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use shared::{
|
||||
global_observer,
|
||||
head_drop::{HeadCollected, HeadDrop, HeadDropEnableTime, HeadDrops, SecretHeadMarker},
|
||||
heads_database::HeadsDatabase,
|
||||
physics_layers::GameLayer,
|
||||
player::Player,
|
||||
protocol::{GltfSceneRoot, PlaySound},
|
||||
utils::{
|
||||
billboards::Billboard, one_shot_force::OneShotForce, squish_animation::SquishAnimation,
|
||||
},
|
||||
};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_head_drop);
|
||||
}
|
||||
|
||||
fn on_head_drop(
|
||||
trigger: On<HeadDrops>,
|
||||
mut commands: Commands,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
time: Res<Time>,
|
||||
) -> Result<(), BevyError> {
|
||||
let drop = trigger.event();
|
||||
let should_impulse = drop.impulse;
|
||||
|
||||
let angle = rand::random::<f32>() * PI * 2.;
|
||||
let spawn_dir = Quat::from_rotation_y(angle) * Vec3::new(0.5, 0.6, 0.).normalize();
|
||||
let spawn_force = if should_impulse {
|
||||
spawn_dir * 180.0 / time.delta_secs()
|
||||
} else {
|
||||
Vec3::ZERO
|
||||
};
|
||||
|
||||
if drop.impulse {
|
||||
commands.trigger(PlaySound::HeadDrop);
|
||||
}
|
||||
|
||||
let mesh_addr = format!("{:?}", heads_db.head_stats(drop.head_id).ability).to_lowercase();
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
Name::new("headdrop"),
|
||||
Transform::from_translation(drop.pos),
|
||||
Visibility::default(),
|
||||
Collider::sphere(1.5),
|
||||
LockedAxes::ROTATION_LOCKED,
|
||||
RigidBody::Dynamic,
|
||||
OneShotForce(spawn_force),
|
||||
CollisionLayers::new(
|
||||
GameLayer::CollectiblePhysics,
|
||||
LayerMask::ALL & !GameLayer::Player.to_bits(),
|
||||
),
|
||||
Restitution::new(0.6),
|
||||
Children::spawn(SpawnWith({
|
||||
let head_id = drop.head_id;
|
||||
let now = time.elapsed_secs();
|
||||
move |parent: &mut RelatedSpawner<ChildOf>| {
|
||||
parent
|
||||
.spawn((
|
||||
Collider::sphere(1.5),
|
||||
CollisionLayers::new(GameLayer::CollectibleSensors, LayerMask::NONE),
|
||||
Sensor,
|
||||
CollisionEventsEnabled,
|
||||
HeadDrop { head_id },
|
||||
HeadDropEnableTime(now + 1.2),
|
||||
))
|
||||
.observe(on_collect_head);
|
||||
}
|
||||
})),
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
))
|
||||
.with_child((
|
||||
Billboard::All,
|
||||
SquishAnimation(2.6),
|
||||
GltfSceneRoot::HeadDrop(mesh_addr),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_collect_head(
|
||||
trigger: On<CollisionStart>,
|
||||
mut commands: Commands,
|
||||
query_player: Query<&Player>,
|
||||
query_collectable: Query<(&HeadDrop, &ChildOf)>,
|
||||
query_secret: Query<&SecretHeadMarker>,
|
||||
) {
|
||||
let collectable = trigger.event().collider1;
|
||||
let collider = trigger.event().collider2;
|
||||
|
||||
if query_player.contains(collider) {
|
||||
let (drop, child_of) = query_collectable.get(collectable).unwrap();
|
||||
|
||||
let is_secret = query_secret.contains(collectable);
|
||||
|
||||
if is_secret {
|
||||
commands.trigger(PlaySound::SecretHeadCollect);
|
||||
} else {
|
||||
commands.trigger(PlaySound::HeadCollect);
|
||||
}
|
||||
|
||||
commands.entity(collider).trigger(|entity| HeadCollected {
|
||||
head: drop.head_id,
|
||||
entity,
|
||||
});
|
||||
commands.entity(child_of.parent()).despawn();
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,15 @@ use bevy::{app::plugin_group, core_pipeline::tonemapping::Tonemapping, prelude::
|
||||
use bevy_common_assets::ron::RonAssetPlugin;
|
||||
use bevy_sprite3d::Sprite3dPlugin;
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
use bevy_ui_gradients::UiGradientsPlugin;
|
||||
use bevy_trenchbroom_avian::AvianPhysicsBackend;
|
||||
use lightyear::prelude::server::ServerPlugins;
|
||||
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
|
||||
use std::time::Duration;
|
||||
|
||||
mod backpack;
|
||||
mod config;
|
||||
mod head_drop;
|
||||
mod platforms;
|
||||
mod player;
|
||||
mod server;
|
||||
mod tb_entities;
|
||||
@@ -31,8 +33,11 @@ plugin_group! {
|
||||
bevy::app:::TerminalCtrlCHandlerPlugin,
|
||||
bevy::asset:::AssetPlugin,
|
||||
bevy::scene:::ScenePlugin,
|
||||
bevy::mesh:::MeshPlugin,
|
||||
bevy::light:::LightPlugin,
|
||||
bevy::camera:::CameraPlugin,
|
||||
bevy::render:::RenderPlugin,
|
||||
bevy::render::texture:::ImagePlugin,
|
||||
bevy::image:::ImagePlugin,
|
||||
bevy::render::pipelined_rendering:::PipelinedRenderingPlugin,
|
||||
bevy::core_pipeline:::CorePipelinePlugin,
|
||||
bevy::sprite:::SpritePlugin,
|
||||
@@ -69,15 +74,20 @@ fn main() {
|
||||
..default()
|
||||
}));
|
||||
|
||||
app.add_plugins(
|
||||
PhysicsPlugins::default()
|
||||
.build()
|
||||
// FrameInterpolation handles interpolating Position and Rotation
|
||||
.disable::<PhysicsInterpolationPlugin>(),
|
||||
);
|
||||
app.add_plugins(ServerPlugins {
|
||||
tick_duration: Duration::from_secs_f32(1.0 / 60.0),
|
||||
});
|
||||
app.add_plugins(PhysicsPlugins::default());
|
||||
app.add_plugins(Sprite3dPlugin);
|
||||
app.add_plugins(TrenchBroomPlugins(
|
||||
TrenchBroomConfig::new("hedz").icon(None),
|
||||
));
|
||||
app.add_plugins(UiGradientsPlugin);
|
||||
app.add_plugins(TrenchBroomPhysicsPlugin::new(AvianPhysicsBackend));
|
||||
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
|
||||
|
||||
app.add_plugins(shared::abilities::plugin);
|
||||
@@ -116,6 +126,8 @@ fn main() {
|
||||
|
||||
app.add_plugins(backpack::plugin);
|
||||
app.add_plugins(config::plugin);
|
||||
app.add_plugins(head_drop::plugin);
|
||||
app.add_plugins(platforms::plugin);
|
||||
app.add_plugins(player::plugin);
|
||||
app.add_plugins(server::plugin);
|
||||
app.add_plugins(tb_entities::plugin);
|
||||
|
||||
41
crates/server/src/platforms.rs
Normal file
41
crates/server/src/platforms.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_trenchbroom::prelude::Target;
|
||||
use lightyear::prelude::{NetworkTarget, PredictionTarget};
|
||||
use shared::{
|
||||
GameState,
|
||||
platforms::ActivePlatform,
|
||||
tb_entities::{Platform, PlatformTarget},
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::Playing), init);
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn init(
|
||||
mut commands: Commands,
|
||||
uninit_platforms: Query<
|
||||
(Entity, &Target, &Transform),
|
||||
(Without<ActivePlatform>, With<Platform>),
|
||||
>,
|
||||
targets: Query<(&PlatformTarget, &Transform)>,
|
||||
) {
|
||||
for (e, target, transform) in uninit_platforms.iter() {
|
||||
let Some(target) = targets
|
||||
.iter()
|
||||
.find(|(t, _)| t.targetname == target.target.clone().unwrap_or_default())
|
||||
.map(|(_, t)| t.translation)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let platform = ActivePlatform {
|
||||
start: transform.translation,
|
||||
target,
|
||||
};
|
||||
|
||||
commands
|
||||
.entity(e)
|
||||
.insert((platform, PredictionTarget::to_clients(NetworkTarget::All)));
|
||||
}
|
||||
}
|
||||
@@ -82,11 +82,11 @@ pub fn spawn(
|
||||
}
|
||||
|
||||
fn on_kill(
|
||||
trigger: Trigger<Kill>,
|
||||
trigger: On<Kill>,
|
||||
mut commands: Commands,
|
||||
mut query: Query<(&Transform, &ActiveHead, &mut ActiveHeads, &mut Hitpoints)>,
|
||||
) {
|
||||
let Ok((transform, active, mut heads, mut hp)) = query.get_mut(trigger.target()) else {
|
||||
let Ok((transform, active, mut heads, mut hp)) = query.get_mut(trigger.event().entity) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -100,10 +100,10 @@ fn on_kill(
|
||||
}
|
||||
|
||||
fn on_update_head_mesh(
|
||||
trigger: Trigger<HeadChanged>,
|
||||
trigger: On<HeadChanged>,
|
||||
mut commands: Commands,
|
||||
mesh_children: Single<&Children, With<PlayerBodyMesh>>,
|
||||
mut sender: Single<&mut TriggerSender<ClientHeadChanged>>,
|
||||
mut sender: Single<&mut EventSender<ClientHeadChanged>>,
|
||||
animated_characters: Query<&AnimatedCharacter>,
|
||||
mut player: Single<&mut ActiveHead, With<Player>>,
|
||||
) -> Result {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::config::ServerConfig;
|
||||
use bevy::prelude::*;
|
||||
use bevy::{ecs::component::Components, prelude::*};
|
||||
use lightyear::{
|
||||
connection::client::PeerMetadata,
|
||||
link::LinkConditioner,
|
||||
@@ -42,11 +42,11 @@ pub fn plugin(app: &mut App) {
|
||||
struct ClientPlayerId(u8);
|
||||
|
||||
fn handle_new_client(
|
||||
trigger: Trigger<OnAdd, Linked>,
|
||||
trigger: On<Add, Linked>,
|
||||
mut commands: Commands,
|
||||
id: Query<&PeerAddr>,
|
||||
) -> Result {
|
||||
let Ok(id) = id.get(trigger.target()) else {
|
||||
let Ok(id) = id.get(trigger.event().entity) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ fn handle_new_client(
|
||||
incoming_loss: 0.0,
|
||||
});
|
||||
|
||||
commands.entity(trigger.target()).insert((
|
||||
commands.entity(trigger.event().entity).insert((
|
||||
ReplicationSender::default(),
|
||||
Link::new(Some(conditioner)),
|
||||
ClientPlayerId(0),
|
||||
@@ -68,18 +68,18 @@ fn handle_new_client(
|
||||
}
|
||||
|
||||
fn on_client_connected(
|
||||
trigger: Trigger<OnAdd, ClientOf>,
|
||||
trigger: On<Add, 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())?;
|
||||
let (id, mut sender) = assign_player.get_mut(trigger.event().entity)?;
|
||||
sender.send::<UnorderedReliableChannel>(AssignClientPlayer(id.0));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_client_playing(
|
||||
trigger: Trigger<RemoteTrigger<ClientEnteredPlaying>>,
|
||||
trigger: On<RemoteEvent<ClientEnteredPlaying>>,
|
||||
commands: Commands,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
@@ -95,9 +95,9 @@ fn on_client_playing(
|
||||
}
|
||||
|
||||
fn close_on_disconnect(
|
||||
_trigger: Trigger<OnRemove, Connected>,
|
||||
_trigger: On<Remove, Connected>,
|
||||
config: Res<ServerConfig>,
|
||||
mut writer: EventWriter<AppExit>,
|
||||
mut writer: MessageWriter<AppExit>,
|
||||
) {
|
||||
if config.close_on_client_disconnect {
|
||||
info!("client disconnected, exiting");
|
||||
@@ -123,7 +123,13 @@ fn start_server(mut commands: Commands) -> Result {
|
||||
}),
|
||||
Link::new(Some(conditioner)),
|
||||
));
|
||||
commands.trigger(server::Start);
|
||||
commands
|
||||
.observe(|on: On<Add>, components: &Components| {
|
||||
for &comp in on.trigger().components {
|
||||
info!("Added {} to server", components.get_name(comp).unwrap());
|
||||
}
|
||||
})
|
||||
.trigger(|entity| server::Start { entity });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -142,7 +148,11 @@ fn setup_timeout_timer(mut commands: Commands, config: Res<ServerConfig>) {
|
||||
commands.insert_resource(TimeoutTimer(config.timeout));
|
||||
}
|
||||
|
||||
fn run_timeout(mut timer: ResMut<TimeoutTimer>, mut writer: EventWriter<AppExit>, time: Res<Time>) {
|
||||
fn run_timeout(
|
||||
mut timer: ResMut<TimeoutTimer>,
|
||||
mut writer: MessageWriter<AppExit>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
timer.0 -= time.delta_secs();
|
||||
|
||||
if timer.0 <= 0.0 {
|
||||
@@ -151,7 +161,7 @@ fn run_timeout(mut timer: ResMut<TimeoutTimer>, mut writer: EventWriter<AppExit>
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_timeout(_trigger: Trigger<OnAdd, Connected>, mut timer: ResMut<TimeoutTimer>) {
|
||||
fn cancel_timeout(_trigger: On<Add, Connected>, mut timer: ResMut<TimeoutTimer>) {
|
||||
info!("client connected, cancelling timeout");
|
||||
timer.0 = f32::INFINITY;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn add_despawned_entities_to_cache(
|
||||
trigger: Trigger<OnRemove, TbMapEntityId>,
|
||||
trigger: On<Remove, TbMapEntityId>,
|
||||
id: Query<&TbMapEntityId>,
|
||||
mut cache: ResMut<DespawnedTbEntityCache>,
|
||||
) {
|
||||
cache.0.push(id.get(trigger.target()).unwrap().id);
|
||||
cache.0.push(id.get(trigger.event().entity).unwrap().id);
|
||||
}
|
||||
|
||||
#[derive(Default, Resource, Reflect)]
|
||||
@@ -32,11 +32,11 @@ fn add_despawned_entities_to_cache(
|
||||
pub struct DespawnedTbEntityCache(pub Vec<u64>);
|
||||
|
||||
fn send_new_client_despawned_cache(
|
||||
trigger: Trigger<OnAdd, Connected>,
|
||||
trigger: On<Add, Connected>,
|
||||
cache: Res<DespawnedTbEntityCache>,
|
||||
mut send: Query<&mut MessageSender<DespawnTbMapEntity>>,
|
||||
) {
|
||||
let mut send = send.get_mut(trigger.target()).unwrap();
|
||||
let mut send = send.get_mut(trigger.event().entity).unwrap();
|
||||
for &id in cache.0.iter() {
|
||||
send.send::<UnorderedReliableChannel>(DespawnTbMapEntity(id));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ pub fn plugin(app: &mut App) {
|
||||
pub struct ReportEntityComponents(pub Entity);
|
||||
|
||||
fn report_entity_components(
|
||||
trigger: Trigger<ReportEntityComponents>,
|
||||
trigger: On<ReportEntityComponents>,
|
||||
entities: &Entities,
|
||||
components: &Components,
|
||||
archetypes: &Archetypes,
|
||||
@@ -27,7 +27,7 @@ fn report_entity_components(
|
||||
};
|
||||
|
||||
let mut output = format!("Entity {:?} Components: ", trigger.event().0);
|
||||
for component in archetype.components() {
|
||||
for &component in archetype.components() {
|
||||
if let Some(name) = components.get_name(component) {
|
||||
output.push_str(&format!("{name}, "));
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ avian3d = { workspace = true }
|
||||
bevy = { workspace = true, default-features = false }
|
||||
bevy-inspector-egui = { workspace = true, optional = true }
|
||||
bevy-steamworks = { workspace = true }
|
||||
bevy-ui-gradients = { workspace = true }
|
||||
bevy_asset_loader = { workspace = true }
|
||||
bevy_ballistic = { workspace = true }
|
||||
bevy_common_assets = { workspace = true }
|
||||
|
||||
@@ -5,9 +5,8 @@ use crate::{
|
||||
utils::sprite_3d_animation::AnimationTimer,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{pbr::NotShadowCaster, prelude::*};
|
||||
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
|
||||
use std::f32::consts::PI;
|
||||
use bevy::{light::NotShadowCaster, prelude::*};
|
||||
use bevy_sprite3d::Sprite3d;
|
||||
|
||||
#[derive(Component)]
|
||||
struct ArrowProjectile {
|
||||
@@ -27,18 +26,18 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_trigger_arrow);
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, assets: Res<GameAssets>, mut sprite_params: Sprite3dParams) {
|
||||
fn setup(mut commands: Commands, assets: Res<GameAssets>, asset_server: Res<AssetServer>) {
|
||||
let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None);
|
||||
let texture_atlas_layout = sprite_params.atlas_layouts.add(layout);
|
||||
let layout = asset_server.add(layout);
|
||||
|
||||
commands.insert_resource(ShotAssets {
|
||||
image: assets.impact_atlas.clone(),
|
||||
layout: texture_atlas_layout,
|
||||
layout,
|
||||
});
|
||||
}
|
||||
|
||||
fn on_trigger_arrow(
|
||||
trigger: Trigger<TriggerArrow>,
|
||||
trigger: On<TriggerArrow>,
|
||||
mut commands: Commands,
|
||||
query_transform: Query<&Transform>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
@@ -55,7 +54,7 @@ fn on_trigger_arrow(
|
||||
.looking_at(t.translation, Vec3::Y)
|
||||
.rotation
|
||||
} else {
|
||||
state.rot.mul_quat(Quat::from_rotation_y(PI))
|
||||
state.rot()
|
||||
};
|
||||
|
||||
let mut transform = Transform::from_translation(state.pos).with_rotation(rotation);
|
||||
@@ -74,7 +73,6 @@ fn update(
|
||||
query: Query<(Entity, &Transform, &ArrowProjectile)>,
|
||||
spatial_query: SpatialQuery,
|
||||
assets: Res<ShotAssets>,
|
||||
mut sprite_params: Sprite3dParams,
|
||||
) {
|
||||
for (e, t, arrow) in query.iter() {
|
||||
let filter = SpatialQueryFilter::from_mask(LayerMask(
|
||||
@@ -89,26 +87,27 @@ fn update(
|
||||
&ShapeCastConfig::from_max_distance(150.),
|
||||
&filter,
|
||||
) {
|
||||
cmds.entity(first_hit.entity).trigger(Hit {
|
||||
cmds.trigger(Hit {
|
||||
damage: arrow.damage,
|
||||
entity: first_hit.entity,
|
||||
});
|
||||
|
||||
cmds.spawn(
|
||||
Sprite3dBuilder {
|
||||
image: assets.image.clone(),
|
||||
cmds.spawn((
|
||||
Sprite3d {
|
||||
pixels_per_metre: 128.,
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
unlit: true,
|
||||
..default()
|
||||
}
|
||||
.bundle_with_atlas(
|
||||
&mut sprite_params,
|
||||
TextureAtlas {
|
||||
},
|
||||
Sprite {
|
||||
image: assets.image.clone(),
|
||||
texture_atlas: Some(TextureAtlas {
|
||||
layout: assets.layout.clone(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
)
|
||||
}),
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.insert((
|
||||
Billboard::All,
|
||||
Transform::from_translation(first_hit.point1),
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn on_trigger_missile(
|
||||
trigger: Trigger<TriggerCurver>,
|
||||
trigger: On<TriggerCurver>,
|
||||
mut commands: Commands,
|
||||
query_transform: Query<&Transform>,
|
||||
time: Res<Time>,
|
||||
@@ -52,7 +52,7 @@ fn on_trigger_missile(
|
||||
.looking_at(t.translation, Vec3::Y)
|
||||
.rotation
|
||||
} else {
|
||||
state.rot.mul_quat(Quat::from_rotation_y(PI))
|
||||
state.rot()
|
||||
};
|
||||
|
||||
let head = heads_db.head_stats(state.head);
|
||||
@@ -88,11 +88,16 @@ fn on_trigger_missile(
|
||||
|
||||
fn enemy_hit(
|
||||
mut commands: Commands,
|
||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||
mut collision_message_reader: MessageReader<CollisionStart>,
|
||||
query_shot: Query<&CurverProjectile>,
|
||||
query_npc: Query<&EnemySpawn>,
|
||||
) {
|
||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||
for CollisionStart {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
} in collision_message_reader.read()
|
||||
{
|
||||
if !query_shot.contains(*e1) && !query_shot.contains(*e2) {
|
||||
continue;
|
||||
}
|
||||
@@ -108,7 +113,9 @@ fn enemy_hit(
|
||||
|
||||
if let Ok(projectile) = projectile {
|
||||
let damage = projectile.damage;
|
||||
commands.entity(enemy_entity).trigger(Hit { damage });
|
||||
commands
|
||||
.entity(enemy_entity)
|
||||
.trigger(|entity| Hit { entity, damage });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,11 +139,16 @@ fn timeout(mut commands: Commands, query: Query<(Entity, &CurverProjectile)>, ti
|
||||
|
||||
fn shot_collision(
|
||||
mut commands: Commands,
|
||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||
mut collision_message_reader: MessageReader<CollisionStart>,
|
||||
query_shot: Query<&Transform, With<CurverProjectile>>,
|
||||
sensors: Query<(), With<Sensor>>,
|
||||
) {
|
||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||
for CollisionStart {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
} in collision_message_reader.read()
|
||||
{
|
||||
if !query_shot.contains(*e1) && !query_shot.contains(*e2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ use crate::{
|
||||
tb_entities::EnemySpawn, utils::sprite_3d_animation::AnimationTimer,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{pbr::NotShadowCaster, prelude::*};
|
||||
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
|
||||
use std::f32::consts::PI;
|
||||
use bevy::{light::NotShadowCaster, prelude::*};
|
||||
use bevy_sprite3d::Sprite3d;
|
||||
|
||||
#[derive(Component)]
|
||||
struct GunProjectile {
|
||||
@@ -37,9 +36,9 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_trigger_gun);
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, assets: Res<GameAssets>, mut sprite_params: Sprite3dParams) {
|
||||
fn setup(mut commands: Commands, assets: Res<GameAssets>, asset_server: Res<AssetServer>) {
|
||||
let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None);
|
||||
let texture_atlas_layout = sprite_params.atlas_layouts.add(layout);
|
||||
let texture_atlas_layout = asset_server.add(layout);
|
||||
|
||||
commands.insert_resource(ShotAssets {
|
||||
image: assets.impact_atlas.clone(),
|
||||
@@ -49,12 +48,17 @@ fn setup(mut commands: Commands, assets: Res<GameAssets>, mut sprite_params: Spr
|
||||
|
||||
fn enemy_hit(
|
||||
mut commands: Commands,
|
||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||
mut collision_message_reader: MessageReader<CollisionStart>,
|
||||
query_shot: Query<&GunProjectile>,
|
||||
query_npc: Query<&EnemySpawn>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) {
|
||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||
for CollisionStart {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
} in collision_message_reader.read()
|
||||
{
|
||||
if !query_shot.contains(*e1) && !query_shot.contains(*e2) {
|
||||
continue;
|
||||
}
|
||||
@@ -70,13 +74,16 @@ fn enemy_hit(
|
||||
|
||||
if let Ok(head) = projectile.map(|p| p.owner_head) {
|
||||
let damage = heads_db.head_stats(head).damage;
|
||||
commands.entity(enemy_entity).trigger(Hit { damage });
|
||||
commands.trigger(Hit {
|
||||
damage,
|
||||
entity: enemy_entity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_trigger_gun(
|
||||
trigger: Trigger<TriggerGun>,
|
||||
trigger: On<TriggerGun>,
|
||||
mut commands: Commands,
|
||||
query_transform: Query<&Transform>,
|
||||
time: Res<Time>,
|
||||
@@ -94,7 +101,7 @@ fn on_trigger_gun(
|
||||
.looking_at(t.translation, Vec3::Y)
|
||||
.rotation
|
||||
} else {
|
||||
state.rot.mul_quat(Quat::from_rotation_y(PI))
|
||||
state.rot()
|
||||
};
|
||||
|
||||
let mut transform = Transform::from_translation(state.pos).with_rotation(rotation);
|
||||
@@ -149,13 +156,17 @@ fn timeout(mut commands: Commands, query: Query<(Entity, &GunProjectile)>, time:
|
||||
|
||||
fn shot_collision(
|
||||
mut commands: Commands,
|
||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||
mut collision_message_reader: MessageReader<CollisionStart>,
|
||||
query_shot: Query<(&GunProjectile, &Transform)>,
|
||||
sensors: Query<(), With<Sensor>>,
|
||||
assets: Res<ShotAssets>,
|
||||
mut sprite_params: Sprite3dParams,
|
||||
) {
|
||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||
for CollisionStart {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
} in collision_message_reader.read()
|
||||
{
|
||||
if !query_shot.contains(*e1) && !query_shot.contains(*e2) {
|
||||
continue;
|
||||
}
|
||||
@@ -180,16 +191,19 @@ fn shot_collision(
|
||||
};
|
||||
|
||||
commands
|
||||
.spawn(
|
||||
Sprite3dBuilder {
|
||||
image: assets.image.clone(),
|
||||
.spawn((
|
||||
Sprite3d {
|
||||
pixels_per_metre: 128.,
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
unlit: true,
|
||||
..default()
|
||||
}
|
||||
.bundle_with_atlas(&mut sprite_params, texture_atlas),
|
||||
)
|
||||
},
|
||||
Sprite {
|
||||
image: assets.image.clone(),
|
||||
texture_atlas: Some(texture_atlas),
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.insert((
|
||||
Billboard::All,
|
||||
Transform::from_translation(shot_pos),
|
||||
|
||||
@@ -8,8 +8,14 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Component, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Healing;
|
||||
|
||||
#[derive(Clone, Event, Debug, Serialize, Deserialize)]
|
||||
pub enum HealingStateChanged {
|
||||
#[derive(Clone, EntityEvent, Debug, Serialize, Deserialize)]
|
||||
pub struct HealingStateChanged {
|
||||
pub entity: Entity,
|
||||
pub state: HealingState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum HealingState {
|
||||
Started,
|
||||
Stopped,
|
||||
}
|
||||
@@ -21,24 +27,24 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn on_heal_start_stop(
|
||||
trigger: Trigger<HealingStateChanged>,
|
||||
trigger: On<HealingStateChanged>,
|
||||
mut cmds: Commands,
|
||||
query: Query<&Healing>,
|
||||
) {
|
||||
if matches!(trigger.event(), HealingStateChanged::Started) {
|
||||
if query.contains(trigger.target()) {
|
||||
if matches!(trigger.event().state, HealingState::Started) {
|
||||
if query.contains(trigger.event().entity) {
|
||||
// already healing, just ignore
|
||||
return;
|
||||
}
|
||||
|
||||
cmds.entity(trigger.target()).insert(Healing);
|
||||
cmds.entity(trigger.event().entity).insert(Healing);
|
||||
} else {
|
||||
if !query.contains(trigger.target()) {
|
||||
if !query.contains(trigger.event().entity) {
|
||||
// Not healing, just ignore
|
||||
return;
|
||||
}
|
||||
|
||||
cmds.entity(trigger.target()).remove::<Healing>();
|
||||
cmds.entity(trigger.event().entity).remove::<Healing>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn on_trigger_missile(
|
||||
trigger: Trigger<TriggerMissile>,
|
||||
trigger: On<TriggerMissile>,
|
||||
mut commands: Commands,
|
||||
query_transform: Query<&Transform>,
|
||||
time: Res<Time>,
|
||||
@@ -53,7 +53,7 @@ fn on_trigger_missile(
|
||||
.looking_at(t.translation, Vec3::Y)
|
||||
.rotation
|
||||
} else {
|
||||
state.rot.mul_quat(Quat::from_rotation_y(PI))
|
||||
state.rot()
|
||||
};
|
||||
|
||||
let head = heads_db.head_stats(state.head);
|
||||
@@ -122,11 +122,16 @@ fn timeout(mut commands: Commands, query: Query<(Entity, &MissileProjectile)>, t
|
||||
|
||||
fn shot_collision(
|
||||
mut commands: Commands,
|
||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||
mut collision_message_reader: MessageReader<CollisionStart>,
|
||||
query_shot: Query<(&MissileProjectile, &Transform)>,
|
||||
sensors: Query<(), With<Sensor>>,
|
||||
) {
|
||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||
for CollisionStart {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
} in collision_message_reader.read()
|
||||
{
|
||||
if !query_shot.contains(*e1) && !query_shot.contains(*e2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -14,17 +14,12 @@ use crate::{
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::{
|
||||
aim::AimTarget,
|
||||
character::CharacterHierarchy,
|
||||
control::ControlState,
|
||||
head::ActiveHead,
|
||||
heads::ActiveHeads,
|
||||
heads_database::HeadsDatabase,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
aim::AimTarget, character::CharacterHierarchy, control::ControlState, head::ActiveHead,
|
||||
heads::ActiveHeads, heads_database::HeadsDatabase, player::Player,
|
||||
utils::explosions::Explosion,
|
||||
};
|
||||
use bevy::{pbr::NotShadowCaster, prelude::*};
|
||||
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
|
||||
use bevy::{light::NotShadowCaster, prelude::*};
|
||||
use bevy_sprite3d::Sprite3d;
|
||||
pub use healing::Healing;
|
||||
#[cfg(feature = "server")]
|
||||
use healing::HealingStateChanged;
|
||||
@@ -51,7 +46,6 @@ pub enum HeadAbility {
|
||||
pub struct TriggerData {
|
||||
target: Option<Entity>,
|
||||
dir: Dir3,
|
||||
rot: Quat,
|
||||
pos: Vec3,
|
||||
target_layer: GameLayer,
|
||||
head: usize,
|
||||
@@ -61,7 +55,6 @@ impl TriggerData {
|
||||
pub fn new(
|
||||
target: Option<Entity>,
|
||||
dir: Dir3,
|
||||
rot: Quat,
|
||||
pos: Vec3,
|
||||
target_layer: GameLayer,
|
||||
head: usize,
|
||||
@@ -69,12 +62,17 @@ impl TriggerData {
|
||||
Self {
|
||||
target,
|
||||
dir,
|
||||
rot,
|
||||
pos,
|
||||
target_layer,
|
||||
head,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rot(&self) -> Quat {
|
||||
// as it turns out, `glam` comes with some `looking_to` functions for left and right handed coordinate systems, but it seems like they're wrong?
|
||||
// at the very least they give some very odd results and the cross multiplications inside all look backwards compared to what bevy transforms do.
|
||||
Transform::default().looking_to(self.dir, Vec3::Y).rotation
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Event, Reflect)]
|
||||
@@ -205,8 +203,7 @@ fn on_trigger_state(
|
||||
fn update(
|
||||
mut res: ResMut<TriggerStateRes>,
|
||||
mut commands: Commands,
|
||||
player_rot: Query<&Transform, With<PlayerBodyMesh>>,
|
||||
player_query: Query<(Entity, &AimTarget), With<Player>>,
|
||||
player_query: Query<(Entity, &AimTarget, &ActionState<ControlState>), With<Player>>,
|
||||
mut active_heads: Single<&mut ActiveHeads, With<Player>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
time: Res<Time>,
|
||||
@@ -222,7 +219,7 @@ fn update(
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((player, target)) = player_query.iter().next() else {
|
||||
let Some((player, target, actions)) = player_query.iter().next() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -233,10 +230,6 @@ fn update(
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((rot, dir)) = player_rot.iter().next().map(|t| (t.rotation, t.forward())) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let head = heads_db.head_stats(state.head);
|
||||
|
||||
if matches!(head.ability, HeadAbility::None | HeadAbility::Medic) {
|
||||
@@ -248,8 +241,7 @@ fn update(
|
||||
res.next_trigger_timestamp = time.elapsed_secs() + (1. / head.aps);
|
||||
|
||||
let trigger_state = TriggerData {
|
||||
dir,
|
||||
rot,
|
||||
dir: actions.look_dir,
|
||||
pos: projectile_origin,
|
||||
target: target.0,
|
||||
target_layer: GameLayer::Npc,
|
||||
@@ -287,10 +279,17 @@ fn update_heal_ability(
|
||||
return;
|
||||
}
|
||||
|
||||
use crate::abilities::healing::HealingState;
|
||||
if res.active {
|
||||
commands.trigger_targets(HealingStateChanged::Started, player);
|
||||
commands.trigger(HealingStateChanged {
|
||||
state: HealingState::Started,
|
||||
entity: player,
|
||||
});
|
||||
} else {
|
||||
commands.trigger_targets(HealingStateChanged::Stopped, player);
|
||||
commands.trigger(HealingStateChanged {
|
||||
state: HealingState::Stopped,
|
||||
entity: player,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,9 +300,9 @@ struct ShotAssets {
|
||||
layout: Handle<TextureAtlasLayout>,
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, assets: Res<GameAssets>, mut sprite_params: Sprite3dParams) {
|
||||
fn setup(mut commands: Commands, assets: Res<GameAssets>, asset_server: Res<AssetServer>) {
|
||||
let layout = TextureAtlasLayout::from_grid(UVec2::splat(256), 7, 6, None, None);
|
||||
let texture_atlas_layout = sprite_params.atlas_layouts.add(layout);
|
||||
let texture_atlas_layout = asset_server.add(layout);
|
||||
|
||||
commands.insert_resource(ShotAssets {
|
||||
image: assets.impact_atlas.clone(),
|
||||
@@ -319,27 +318,26 @@ pub struct BuildExplosionSprite {
|
||||
}
|
||||
|
||||
fn build_explosion_sprite(
|
||||
trigger: Trigger<BuildExplosionSprite>,
|
||||
trigger: On<BuildExplosionSprite>,
|
||||
mut commands: Commands,
|
||||
assets: Res<ShotAssets>,
|
||||
mut sprite_params: Sprite3dParams,
|
||||
) {
|
||||
commands.spawn((
|
||||
Transform::from_translation(trigger.event().pos),
|
||||
Sprite3dBuilder {
|
||||
image: assets.image.clone(),
|
||||
Sprite3d {
|
||||
pixels_per_metre: trigger.event().pixels_per_meter,
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
unlit: true,
|
||||
..default()
|
||||
}
|
||||
.bundle_with_atlas(
|
||||
&mut sprite_params,
|
||||
TextureAtlas {
|
||||
},
|
||||
Sprite {
|
||||
image: assets.image.clone(),
|
||||
texture_atlas: Some(TextureAtlas {
|
||||
layout: assets.layout.clone(),
|
||||
index: 0,
|
||||
},
|
||||
),
|
||||
}),
|
||||
..default()
|
||||
},
|
||||
Billboard::All,
|
||||
NotShadowCaster,
|
||||
AnimationTimer::new(Timer::from_seconds(
|
||||
|
||||
@@ -12,7 +12,6 @@ use bevy_ballistic::launch_velocity;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
#[derive(Component, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ThrownProjectile {
|
||||
@@ -30,7 +29,7 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn on_trigger_thrown(
|
||||
trigger: Trigger<TriggerThrow>,
|
||||
trigger: On<TriggerThrow>,
|
||||
mut commands: Commands,
|
||||
query_transform: Query<&Transform>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
@@ -50,8 +49,7 @@ fn on_trigger_thrown(
|
||||
.map(|(low, _)| low)
|
||||
.unwrap()
|
||||
} else {
|
||||
state.rot.mul_quat(Quat::from_rotation_y(-PI / 2.))
|
||||
* (Vec3::new(2., 1., 0.).normalize() * SPEED)
|
||||
((state.dir.as_vec3() * 2.0) + Vec3::Y).normalize() * SPEED
|
||||
};
|
||||
|
||||
let head = heads_db.head_stats(state.head);
|
||||
@@ -89,11 +87,16 @@ fn on_trigger_thrown(
|
||||
|
||||
fn shot_collision(
|
||||
mut commands: Commands,
|
||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||
mut collision_message_reader: MessageReader<CollisionStart>,
|
||||
query_shot: Query<(&ThrownProjectile, &Transform)>,
|
||||
sensors: Query<(), With<Sensor>>,
|
||||
) {
|
||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||
for CollisionStart {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
} in collision_message_reader.read()
|
||||
{
|
||||
if !query_shot.contains(*e1) && !query_shot.contains(*e2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -163,12 +163,9 @@ fn engage_and_throw(
|
||||
if can_shoot_again && npc_head.has_ammo() {
|
||||
npc.use_ammo(time.elapsed_secs());
|
||||
|
||||
let dir = t.forward();
|
||||
|
||||
commands.trigger(TriggerThrow(TriggerData::new(
|
||||
target.0,
|
||||
dir,
|
||||
t.rotation,
|
||||
t.forward(),
|
||||
t.translation,
|
||||
crate::physics_layers::GameLayer::Player,
|
||||
npc_head.head,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{GameState, global_observer, loading_assets::UIAssets, utils::billboards::Billboard};
|
||||
use bevy::prelude::*;
|
||||
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
|
||||
use bevy_sprite3d::Sprite3d;
|
||||
use ops::sin;
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
@@ -25,10 +25,9 @@ fn move_marker(mut query: Query<&mut Transform, With<TargetMarker>>, time: Res<T
|
||||
}
|
||||
|
||||
fn marker_event(
|
||||
trigger: Trigger<MarkerEvent>,
|
||||
trigger: On<MarkerEvent>,
|
||||
mut commands: Commands,
|
||||
assets: Res<UIAssets>,
|
||||
mut sprite_params: Sprite3dParams,
|
||||
marker: Query<Entity, With<TargetMarker>>,
|
||||
) {
|
||||
for m in marker.iter() {
|
||||
@@ -45,14 +44,16 @@ fn marker_event(
|
||||
Billboard::All,
|
||||
TargetMarker,
|
||||
Transform::default(),
|
||||
Sprite3dBuilder {
|
||||
image: assets.head_selector.clone(),
|
||||
Sprite3d {
|
||||
pixels_per_metre: 30.,
|
||||
alpha_mode: AlphaMode::Blend,
|
||||
unlit: true,
|
||||
..default()
|
||||
}
|
||||
.bundle(&mut sprite_params),
|
||||
},
|
||||
Sprite {
|
||||
image: assets.head_selector.clone(),
|
||||
..default()
|
||||
},
|
||||
))
|
||||
.id();
|
||||
|
||||
|
||||
@@ -2,16 +2,12 @@ mod marker;
|
||||
mod target_ui;
|
||||
|
||||
use crate::{
|
||||
GameState,
|
||||
head::ActiveHead,
|
||||
heads_database::HeadsDatabase,
|
||||
hitpoints::Hitpoints,
|
||||
physics_layers::GameLayer,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
tb_entities::EnemySpawn,
|
||||
GameState, control::ControlState, head::ActiveHead, heads_database::HeadsDatabase,
|
||||
hitpoints::Hitpoints, physics_layers::GameLayer, player::Player, tb_entities::EnemySpawn,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use marker::MarkerEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::PI;
|
||||
@@ -74,20 +70,20 @@ fn head_change(
|
||||
fn update_player_aim(
|
||||
mut commands: Commands,
|
||||
potential_targets: Query<(Entity, &Transform), With<Hitpoints>>,
|
||||
player_rot: Query<(&Transform, &GlobalTransform), With<PlayerBodyMesh>>,
|
||||
mut player_aim: Query<(Entity, &AimState, &mut AimTarget, &Children), With<Player>>,
|
||||
mut player_aim: Query<
|
||||
(
|
||||
Entity,
|
||||
&AimState,
|
||||
&mut AimTarget,
|
||||
&GlobalTransform,
|
||||
&ActionState<ControlState>,
|
||||
),
|
||||
With<Player>,
|
||||
>,
|
||||
spatial_query: SpatialQuery,
|
||||
) {
|
||||
for (player, state, mut aim_target, children) in player_aim.iter_mut() {
|
||||
let player_rot_child = children
|
||||
.iter()
|
||||
.find(|&child| player_rot.contains(child))
|
||||
.expect("expected child with PlayerBodyMesh");
|
||||
|
||||
let (player_pos, player_forward) = player_rot
|
||||
.get(player_rot_child)
|
||||
.map(|(t, global)| (global.translation(), t.forward()))
|
||||
.unwrap();
|
||||
for (player, state, mut aim_target, global_tf, actions) in player_aim.iter_mut() {
|
||||
let (player_pos, player_forward) = (global_tf.translation(), actions.look_dir);
|
||||
|
||||
let mut new_target = None;
|
||||
let mut target_distance = f32::MAX;
|
||||
@@ -97,7 +93,7 @@ fn update_player_aim(
|
||||
continue;
|
||||
}
|
||||
|
||||
let delta = player_pos - t.translation;
|
||||
let delta = t.translation - player_pos;
|
||||
|
||||
let distance = delta.length();
|
||||
|
||||
@@ -191,9 +187,9 @@ fn line_of_sight(
|
||||
) -> bool {
|
||||
if let Some(_hit) = spatial_query.cast_shape(
|
||||
&Collider::sphere(0.1),
|
||||
player_pos + -delta.normalize() + (Vec3::Y * 2.),
|
||||
player_pos + delta.normalize() + (Vec3::Y * 2.),
|
||||
Quat::default(),
|
||||
Dir3::new(-delta).unwrap(),
|
||||
Dir3::new(delta).unwrap(),
|
||||
&ShapeCastConfig {
|
||||
max_distance: distance * 0.98,
|
||||
compute_contact_on_penetration: false,
|
||||
|
||||
@@ -53,7 +53,7 @@ impl AnimationController {
|
||||
}
|
||||
}
|
||||
|
||||
impl AnimationControllerItem<'_> {
|
||||
impl AnimationControllerItem<'_, '_> {
|
||||
pub fn play(
|
||||
&mut self,
|
||||
animation: AnimationNodeIndex,
|
||||
|
||||
@@ -47,14 +47,14 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn on_head_collect(
|
||||
trigger: Trigger<HeadCollected>,
|
||||
trigger: On<HeadCollected>,
|
||||
mut cmds: Commands,
|
||||
mut backpack: Query<&mut Backpack>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) -> Result {
|
||||
let HeadCollected(head) = *trigger.event();
|
||||
let HeadCollected { head, entity } = *trigger.event();
|
||||
|
||||
let mut backpack = backpack.get_mut(trigger.target())?;
|
||||
let mut backpack = backpack.get_mut(entity)?;
|
||||
|
||||
if backpack.contains(head) {
|
||||
cmds.trigger(CashCollectEvent);
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
use crate::GameState;
|
||||
#[cfg(feature = "client")]
|
||||
use crate::physics_layers::GameLayer;
|
||||
#[cfg(feature = "client")]
|
||||
use crate::{
|
||||
GameState, control::ControlState, loading_assets::UIAssets, physics_layers::GameLayer,
|
||||
control::{ControlState, LookDirMovement},
|
||||
loading_assets::UIAssets,
|
||||
};
|
||||
#[cfg(feature = "client")]
|
||||
use avian3d::prelude::SpatialQuery;
|
||||
#[cfg(feature = "client")]
|
||||
use avian3d::prelude::{
|
||||
Collider, LayerMask, PhysicsLayer as _, ShapeCastConfig, SpatialQueryFilter,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -52,10 +62,11 @@ pub fn plugin(app: &mut App) {
|
||||
|
||||
app.init_resource::<CameraState>();
|
||||
app.add_systems(OnEnter(GameState::Playing), startup);
|
||||
#[cfg(feature = "client")]
|
||||
app.add_systems(
|
||||
RunFixedMainLoop,
|
||||
(update, update_ui, update_look_around, rotate_view)
|
||||
.after(RunFixedMainLoopSystem::AfterFixedMainLoop)
|
||||
.after(RunFixedMainLoopSystems::AfterFixedMainLoop)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
@@ -63,11 +74,12 @@ pub fn plugin(app: &mut App) {
|
||||
fn startup(mut commands: Commands) {
|
||||
commands.spawn((
|
||||
Camera3d::default(),
|
||||
MainCamera::new(Vec3::new(0., 1.8, -15.)),
|
||||
MainCamera::new(Vec3::new(0., 1.8, 15.)),
|
||||
CameraRotationInput::default(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn update_look_around(controls: Res<ControlState>, mut cam_state: ResMut<CameraState>) {
|
||||
let look_around = controls.view_mode;
|
||||
|
||||
@@ -76,6 +88,7 @@ fn update_look_around(controls: Res<ControlState>, mut cam_state: ResMut<CameraS
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn update_ui(
|
||||
mut commands: Commands,
|
||||
cam_state: Res<CameraState>,
|
||||
@@ -112,6 +125,7 @@ fn update_ui(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn update(
|
||||
mut cam: Query<
|
||||
(&MainCamera, &mut Transform, &CameraRotationInput),
|
||||
@@ -156,11 +170,16 @@ fn update(
|
||||
*cam_transform = Transform::from_translation(cam_pos).looking_at(target, Vec3::Y);
|
||||
}
|
||||
|
||||
fn rotate_view(controls: Res<ControlState>, mut cam: Single<&mut CameraRotationInput>) {
|
||||
#[cfg(feature = "client")]
|
||||
fn rotate_view(
|
||||
controls: Res<ControlState>,
|
||||
look_dir: Res<LookDirMovement>,
|
||||
mut cam: Single<&mut CameraRotationInput>,
|
||||
) {
|
||||
if !controls.view_mode {
|
||||
cam.x = 0.;
|
||||
cam.x = 0.0;
|
||||
return;
|
||||
}
|
||||
|
||||
cam.0 += controls.look_dir * -0.001;
|
||||
cam.0 += look_dir.0 * -0.001;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ pub fn plugin(app: &mut App) {
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn on_cash_collect(
|
||||
_trigger: Trigger<CashCollectEvent>,
|
||||
_trigger: On<CashCollectEvent>,
|
||||
mut commands: Commands,
|
||||
mut cash: Single<&mut CashResource>,
|
||||
) {
|
||||
@@ -73,7 +73,7 @@ fn setup(mut commands: Commands, assets: Res<UIAssets>) {
|
||||
..default()
|
||||
},
|
||||
TextColor(HEDZ_GREEN.into()),
|
||||
TextLayout::new_with_justify(JustifyText::Center),
|
||||
TextLayout::new_with_justify(Justify::Center),
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
bottom: Val::Px(40.0),
|
||||
|
||||
@@ -126,13 +126,13 @@ fn spawn(
|
||||
}
|
||||
|
||||
fn find_marker_bones(
|
||||
trigger: Trigger<SceneInstanceReady>,
|
||||
trigger: On<SceneInstanceReady>,
|
||||
mut commands: Commands,
|
||||
descendants: Query<&Children>,
|
||||
name: Query<&Name>,
|
||||
mut gizmo_assets: ResMut<Assets<GizmoAsset>>,
|
||||
) {
|
||||
let entity = trigger.target();
|
||||
let entity = trigger.event().entity;
|
||||
|
||||
let mut origin_found = false;
|
||||
for child in descendants.iter_descendants(entity) {
|
||||
|
||||
@@ -25,24 +25,23 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_type::<MovementSpeedFactor>();
|
||||
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
reset_upon_switch.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
FixedPreUpdate,
|
||||
reset_upon_switch
|
||||
.after(super::head_change)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
)
|
||||
.add_systems(
|
||||
PreUpdate,
|
||||
set_animation_flags.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
decelerate
|
||||
.after(ControllerSet::ApplyControlsRun)
|
||||
.after(ControllerSet::ApplyControlsFly)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
app.add_systems(Update, add_controller_bundle);
|
||||
)
|
||||
.add_systems(FixedPostUpdate, add_controller_bundle);
|
||||
}
|
||||
|
||||
fn set_animation_flags(
|
||||
@@ -77,28 +76,39 @@ fn set_animation_flags(
|
||||
/// Reset the pitch and velocity of the character if the controller was switched.
|
||||
pub fn reset_upon_switch(
|
||||
mut c: Commands,
|
||||
mut event_controller_switch: EventReader<ControllerSwitchEvent>,
|
||||
controller: Res<SelectedController>,
|
||||
mut rig_transform_q: Option<Single<&mut Transform, With<PlayerBodyMesh>>>,
|
||||
mut velocity: Single<&mut KinematicVelocity, With<Character>>,
|
||||
character: Single<Entity, With<Character>>,
|
||||
mut event_controller_switch: MessageReader<ControllerSwitchEvent>,
|
||||
selected_controller: Res<SelectedController>,
|
||||
mut rig_transforms: Query<&mut Transform, With<PlayerBodyMesh>>,
|
||||
mut controllers: Query<
|
||||
(
|
||||
&mut KinematicVelocity,
|
||||
&ActionState<ControlState>,
|
||||
&Children,
|
||||
),
|
||||
With<Character>,
|
||||
>,
|
||||
) {
|
||||
for _ in event_controller_switch.read() {
|
||||
for &ControllerSwitchEvent { controller } in event_controller_switch.read() {
|
||||
let (mut velocity, actions, children) = controllers.get_mut(controller).unwrap();
|
||||
|
||||
velocity.0 = Vec3::ZERO;
|
||||
|
||||
// Reset pitch but keep yaw the same
|
||||
if let Some(ref mut rig_transform) = rig_transform_q {
|
||||
let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ);
|
||||
let yaw = euler_rot.0;
|
||||
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, 0.0, 0.0);
|
||||
}
|
||||
let rig_transform = children
|
||||
.iter()
|
||||
.find(|child| rig_transforms.contains(*child))
|
||||
.unwrap();
|
||||
let mut rig_transform = rig_transforms.get_mut(rig_transform).unwrap();
|
||||
|
||||
match controller.0 {
|
||||
// Reset pitch but keep yaw the same
|
||||
let flat_look_dir = actions.look_dir.with_y(0.0).normalize();
|
||||
rig_transform.look_to(flat_look_dir, Dir3::Y);
|
||||
|
||||
match selected_controller.0 {
|
||||
ControllerSet::ApplyControlsFly => {
|
||||
c.entity(*character).insert(FLYING_MOVEMENT_CONFIG);
|
||||
c.entity(controller).insert(FLYING_MOVEMENT_CONFIG);
|
||||
}
|
||||
ControllerSet::ApplyControlsRun => {
|
||||
c.entity(*character).insert(RUNNING_MOVEMENT_CONFIG);
|
||||
c.entity(controller).insert(RUNNING_MOVEMENT_CONFIG);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
@@ -158,10 +168,8 @@ fn add_controller_bundle(
|
||||
(Entity, &ActiveHead),
|
||||
(With<PlayerCharacterController>, Without<Character>),
|
||||
>,
|
||||
db: Option<Res<HeadsDatabase>>,
|
||||
db: If<Res<HeadsDatabase>>,
|
||||
) {
|
||||
let Some(db) = db else { return };
|
||||
|
||||
for (controller, head) in controllers {
|
||||
let controls = db.head_stats(head.0).controls;
|
||||
commands
|
||||
@@ -175,12 +183,12 @@ fn add_controller_bundle(
|
||||
#[derive(Bundle)]
|
||||
pub struct CharacterControllerBundle {
|
||||
character_controller: Character,
|
||||
rigidbody: RigidBody,
|
||||
collider: Collider,
|
||||
move_input: MoveInput,
|
||||
movement_factor: MovementSpeedFactor,
|
||||
collision_events: CollisionEventsEnabled,
|
||||
collision_messages: CollisionEventsEnabled,
|
||||
movement_config: MovementConfig,
|
||||
interpolation: TransformInterpolation,
|
||||
layers: CollisionLayers,
|
||||
}
|
||||
|
||||
@@ -198,12 +206,12 @@ impl CharacterControllerBundle {
|
||||
|
||||
Self {
|
||||
character_controller: Character,
|
||||
rigidbody: RigidBody::Kinematic,
|
||||
collider,
|
||||
move_input: MoveInput::default(),
|
||||
movement_factor: MovementSpeedFactor(1.0),
|
||||
collision_events: CollisionEventsEnabled,
|
||||
collision_messages: CollisionEventsEnabled,
|
||||
movement_config: config,
|
||||
interpolation: TransformInterpolation,
|
||||
layers: CollisionLayers::new(
|
||||
LayerMask(GameLayer::Player.to_bits()),
|
||||
LayerMask::ALL & !GameLayer::CollectiblePhysics.to_bits(),
|
||||
@@ -231,6 +239,7 @@ const RUNNING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
|
||||
step: SteppingConfig {
|
||||
max_vertical: 0.25,
|
||||
max_horizontal: 0.4,
|
||||
max_angle: Some(PI / 4.0),
|
||||
behaviour: SteppingBehaviour::Grounded,
|
||||
max_substeps: 8,
|
||||
},
|
||||
@@ -240,6 +249,7 @@ const RUNNING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
|
||||
snap_to_surface: true,
|
||||
up_direction: Dir3::Y,
|
||||
max_iterations: 2,
|
||||
override_velocity_projection: true,
|
||||
},
|
||||
gravity: CharacterGravity(Some(vec3(0.0, -60.0, 0.0))),
|
||||
friction: GroundFriction(10.0),
|
||||
@@ -258,6 +268,7 @@ const FLYING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
|
||||
step: SteppingConfig {
|
||||
max_vertical: 0.25,
|
||||
max_horizontal: 0.4,
|
||||
max_angle: Some(0.0),
|
||||
behaviour: SteppingBehaviour::Never,
|
||||
max_substeps: 8,
|
||||
},
|
||||
@@ -267,6 +278,7 @@ const FLYING_MOVEMENT_CONFIG: MovementConfig = MovementConfig {
|
||||
snap_to_surface: false,
|
||||
up_direction: Dir3::Y,
|
||||
max_iterations: 2,
|
||||
override_velocity_projection: true,
|
||||
},
|
||||
gravity: CharacterGravity(Some(Vec3::ZERO)),
|
||||
friction: GroundFriction(0.0),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use super::{ControlState, ControllerSet};
|
||||
use crate::{GameState, control::controller_common::MovementSpeedFactor, player::PlayerBodyMesh};
|
||||
use crate::{GameState, control::controller_common::MovementSpeedFactor};
|
||||
use bevy::prelude::*;
|
||||
use happy_feet::prelude::MoveInput;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
pub struct CharacterControllerPlugin;
|
||||
|
||||
@@ -11,56 +10,21 @@ impl Plugin for CharacterControllerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
(rotate_rig, apply_controls)
|
||||
.chain()
|
||||
apply_controls
|
||||
.in_set(ControllerSet::ApplyControlsFly)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn rotate_rig(
|
||||
actions: Query<&ActionState<ControlState>>,
|
||||
mut player: Query<(&mut Transform, &ChildOf), With<PlayerBodyMesh>>,
|
||||
pub fn apply_controls(
|
||||
character: Single<(
|
||||
&mut MoveInput,
|
||||
&ActionState<ControlState>,
|
||||
&MovementSpeedFactor,
|
||||
)>,
|
||||
) {
|
||||
for (mut rig_transform, child_of) in player.iter_mut() {
|
||||
let controls = actions.get(child_of.parent()).unwrap();
|
||||
let (mut char_input, actions, factor) = character.into_inner();
|
||||
|
||||
if controls.view_mode {
|
||||
continue;
|
||||
}
|
||||
|
||||
let look_dir = controls.look_dir;
|
||||
|
||||
// todo: Make consistent with the running controller
|
||||
let sensitivity = 0.001;
|
||||
let max_pitch = 35.0 * PI / 180.0;
|
||||
let min_pitch = -25.0 * PI / 180.0;
|
||||
|
||||
rig_transform.rotate_y(look_dir.x * -sensitivity);
|
||||
|
||||
let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ);
|
||||
let yaw = euler_rot.0;
|
||||
let pitch = euler_rot.1 + look_dir.y * sensitivity;
|
||||
|
||||
let pitch_clamped = pitch.clamp(min_pitch, max_pitch);
|
||||
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch_clamped, 0.0);
|
||||
|
||||
// The following can be used to limit the amount of rotation per frame
|
||||
// let target_rotation = rig_transform.rotation
|
||||
// * Quat::from_rotation_x(controls.keyboard_state.look_dir.y * sensitivity);
|
||||
// let clamped_rotation = rig_transform.rotation.rotate_towards(target_rotation, 0.01);
|
||||
// rig_transform.rotation = clamped_rotation;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_controls(
|
||||
character: Single<(&mut MoveInput, &MovementSpeedFactor)>,
|
||||
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
char_input.set(actions.look_dir * factor.0);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ use crate::{
|
||||
GameState,
|
||||
animation::AnimationFlags,
|
||||
control::{ControllerSettings, controller_common::MovementSpeedFactor},
|
||||
player::PlayerBodyMesh,
|
||||
};
|
||||
#[cfg(feature = "client")]
|
||||
use crate::{control::LookDirMovement, player::PlayerBodyMesh};
|
||||
use bevy::prelude::*;
|
||||
use happy_feet::prelude::{Grounding, KinematicVelocity, MoveInput};
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
@@ -15,7 +16,11 @@ impl Plugin for CharacterControllerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
(rotate_view, apply_controls)
|
||||
(
|
||||
#[cfg(feature = "client")]
|
||||
rotate_view,
|
||||
apply_controls,
|
||||
)
|
||||
.chain()
|
||||
.in_set(ControllerSet::ApplyControlsRun)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
@@ -23,8 +28,10 @@ impl Plugin for CharacterControllerPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn rotate_view(
|
||||
actions: Query<&ActionState<ControlState>>,
|
||||
look_dir: Res<LookDirMovement>,
|
||||
mut player: Query<(&mut Transform, &ChildOf), With<PlayerBodyMesh>>,
|
||||
) {
|
||||
for (mut tr, child_of) in player.iter_mut() {
|
||||
@@ -34,12 +41,12 @@ fn rotate_view(
|
||||
continue;
|
||||
}
|
||||
|
||||
tr.rotate_y(controls.look_dir.x * -0.001);
|
||||
tr.rotate_y(look_dir.0.x * -0.001);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_controls(
|
||||
mut character: Query<(
|
||||
character: Single<(
|
||||
&mut MoveInput,
|
||||
&mut Grounding,
|
||||
&mut KinematicVelocity,
|
||||
@@ -48,29 +55,15 @@ fn apply_controls(
|
||||
&MovementSpeedFactor,
|
||||
&ActionState<ControlState>,
|
||||
)>,
|
||||
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||
) {
|
||||
let Ok((
|
||||
mut move_input,
|
||||
mut grounding,
|
||||
mut velocity,
|
||||
mut flags,
|
||||
settings,
|
||||
move_factor,
|
||||
controls,
|
||||
)) = character.single_mut()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut direction = -controls.move_dir.extend(0.0).xzy();
|
||||
|
||||
if let Some(ref rig_transform) = rig_transform_q {
|
||||
direction = (rig_transform.forward() * direction.z) + (rig_transform.right() * direction.x);
|
||||
}
|
||||
let (mut move_input, mut grounding, mut velocity, mut flags, settings, move_factor, controls) =
|
||||
character.into_inner();
|
||||
|
||||
let ground_normal = *grounding.normal().unwrap_or(Dir3::Y);
|
||||
|
||||
let mut direction = controls.move_dir.extend(0.0).xzy();
|
||||
let look_dir_right = controls.look_dir.cross(Vec3::Y);
|
||||
direction = (controls.look_dir * direction.z) + (look_dir_right * direction.x);
|
||||
let y_projection = direction.project_onto(ground_normal);
|
||||
direction -= y_projection;
|
||||
direction = direction.normalize_or_zero();
|
||||
|
||||
@@ -19,19 +19,18 @@ pub enum ControllerSet {
|
||||
ApplyControlsRun,
|
||||
}
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct ControllerSwitchEvent;
|
||||
|
||||
#[derive(Resource, Debug, Default, PartialEq)]
|
||||
pub struct SelectedController(pub ControllerSet);
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<ControllerSettings>();
|
||||
app.register_type::<LookDirMovement>();
|
||||
|
||||
app.init_resource::<SelectedController>();
|
||||
app.init_resource::<ControlState>();
|
||||
app.init_resource::<LookDirMovement>();
|
||||
app.init_resource::<SelectedController>();
|
||||
|
||||
app.add_event::<ControllerSwitchEvent>();
|
||||
app.add_message::<ControllerSwitchEvent>();
|
||||
|
||||
app.add_plugins(controller_common::plugin);
|
||||
app.add_plugins(controller_flying::CharacterControllerPlugin);
|
||||
@@ -54,11 +53,13 @@ pub fn plugin(app: &mut App) {
|
||||
app.add_systems(Update, head_change.run_if(in_state(GameState::Playing)));
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Reflect)]
|
||||
#[derive(Resource, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
|
||||
pub struct ControlState {
|
||||
/// Movement direction with a maximum length of 1.0
|
||||
pub move_dir: Vec2,
|
||||
pub look_dir: Vec2,
|
||||
/// The current direction that the character is facing
|
||||
/// (i.e. the direction that holding the forward movement key moves)
|
||||
pub look_dir: Dir3,
|
||||
pub jump: bool,
|
||||
/// Determines if the camera can rotate freely around the player
|
||||
pub view_mode: bool,
|
||||
@@ -73,10 +74,40 @@ pub struct ControlState {
|
||||
pub cash_heal: bool,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct LookDirMovement(pub Vec2);
|
||||
|
||||
impl Default for ControlState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
move_dir: Default::default(),
|
||||
look_dir: Dir3::NEG_Z,
|
||||
jump: Default::default(),
|
||||
view_mode: Default::default(),
|
||||
trigger: Default::default(),
|
||||
just_triggered: Default::default(),
|
||||
select_left: Default::default(),
|
||||
select_right: Default::default(),
|
||||
backpack_toggle: Default::default(),
|
||||
backpack_swap: Default::default(),
|
||||
backpack_left: Default::default(),
|
||||
backpack_right: Default::default(),
|
||||
cash_heal: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MapEntities for ControlState {
|
||||
fn map_entities<E: EntityMapper>(&mut self, _entity_mapper: &mut E) {}
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, PartialEq, Eq)]
|
||||
pub enum CharacterInputEnabled {
|
||||
On,
|
||||
Off,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, PartialEq, Reflect, Serialize, Deserialize)]
|
||||
#[reflect(Component)]
|
||||
pub struct ControllerSettings {
|
||||
@@ -84,14 +115,19 @@ pub struct ControllerSettings {
|
||||
pub jump_force: f32,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
pub struct ControllerSwitchEvent {
|
||||
controller: Entity,
|
||||
}
|
||||
|
||||
fn head_change(
|
||||
//TODO: needs a 'LocalPlayer' at some point for multiplayer
|
||||
query: Query<&ActiveHead, (Changed<ActiveHead>, With<Player>)>,
|
||||
query: Query<(Entity, &ActiveHead), (Changed<ActiveHead>, With<Player>)>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
mut selected_controller: ResMut<SelectedController>,
|
||||
mut event_controller_switch: EventWriter<ControllerSwitchEvent>,
|
||||
mut event_controller_switch: MessageWriter<ControllerSwitchEvent>,
|
||||
) {
|
||||
for head in query.iter() {
|
||||
for (entity, head) in query.iter() {
|
||||
let stats = heads_db.head_stats(head.0);
|
||||
let controller = match stats.controls {
|
||||
HeadControls::Plane => ControllerSet::ApplyControlsFly,
|
||||
@@ -99,7 +135,7 @@ fn head_change(
|
||||
};
|
||||
|
||||
if selected_controller.0 != controller {
|
||||
event_controller_switch.write(ControllerSwitchEvent);
|
||||
event_controller_switch.write(ControllerSwitchEvent { controller: entity });
|
||||
|
||||
selected_controller.0 = controller;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn on_start_cutscene(
|
||||
trigger: Trigger<StartCutscene>,
|
||||
trigger: On<StartCutscene>,
|
||||
mut cam_state: ResMut<CameraState>,
|
||||
mut cutscene_state: ResMut<CutsceneState>,
|
||||
cutscenes: Query<(&Transform, &CutsceneCamera, &Target), Without<MainCamera>>,
|
||||
@@ -99,7 +99,7 @@ fn update(
|
||||
|
||||
let _ = cam.single_mut().map(|mut cam| *cam = t);
|
||||
|
||||
if timer.finished() {
|
||||
if timer.is_finished() {
|
||||
cam_state.cutscene = false;
|
||||
*cutscene_state = CutsceneState::None;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_key_collected);
|
||||
}
|
||||
|
||||
fn on_key_collected(trigger: Trigger<KeyCollected>, mut commands: Commands) {
|
||||
fn on_key_collected(trigger: On<KeyCollected>, mut commands: Commands) {
|
||||
match trigger.event().0.as_str() {
|
||||
"fence_gate" => {
|
||||
commands.trigger(StartCutscene("fence_01".to_string()));
|
||||
|
||||
@@ -1,28 +1,12 @@
|
||||
use crate::{
|
||||
GameState,
|
||||
billboards::Billboard,
|
||||
global_observer,
|
||||
heads_database::HeadsDatabase,
|
||||
physics_layers::GameLayer,
|
||||
player::Player,
|
||||
protocol::{GltfSceneRoot, PlaySound},
|
||||
squish_animation::SquishAnimation,
|
||||
tb_entities::SecretHead,
|
||||
};
|
||||
use crate::{GameState, physics_layers::GameLayer, tb_entities::SecretHead};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
|
||||
prelude::*,
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use std::f32::consts::PI;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Event, Reflect)]
|
||||
pub struct HeadDrops {
|
||||
pos: Vec3,
|
||||
head_id: usize,
|
||||
impulse: bool,
|
||||
pub pos: Vec3,
|
||||
pub head_id: usize,
|
||||
pub impulse: bool,
|
||||
}
|
||||
|
||||
impl HeadDrops {
|
||||
@@ -45,20 +29,23 @@ impl HeadDrops {
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
struct HeadDrop {
|
||||
pub struct HeadDrop {
|
||||
pub head_id: usize,
|
||||
}
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
struct HeadDropEnableTime(f32);
|
||||
pub struct HeadDropEnableTime(pub f32);
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
struct SecretHeadMarker;
|
||||
pub struct SecretHeadMarker;
|
||||
|
||||
#[derive(Event, Reflect)]
|
||||
pub struct HeadCollected(pub usize);
|
||||
#[derive(EntityEvent, Reflect)]
|
||||
pub struct HeadCollected {
|
||||
pub entity: Entity,
|
||||
pub head: usize,
|
||||
}
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
@@ -67,8 +54,6 @@ pub fn plugin(app: &mut App) {
|
||||
);
|
||||
|
||||
app.add_systems(OnEnter(GameState::Playing), spawn);
|
||||
|
||||
global_observer!(app, on_head_drop);
|
||||
}
|
||||
|
||||
fn spawn(mut commands: Commands, query: Query<(Entity, &GlobalTransform, &SecretHead)>) {
|
||||
@@ -82,68 +67,6 @@ fn spawn(mut commands: Commands, query: Query<(Entity, &GlobalTransform, &Secret
|
||||
}
|
||||
}
|
||||
|
||||
fn on_head_drop(
|
||||
trigger: Trigger<HeadDrops>,
|
||||
mut commands: Commands,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
time: Res<Time>,
|
||||
) -> Result<(), BevyError> {
|
||||
let drop = trigger.event();
|
||||
|
||||
let angle = rand::random::<f32>() * PI * 2.;
|
||||
let spawn_dir = Quat::from_rotation_y(angle) * Vec3::new(0.5, 0.6, 0.).normalize();
|
||||
|
||||
if drop.impulse {
|
||||
commands.trigger(PlaySound::HeadDrop);
|
||||
}
|
||||
|
||||
let mesh_addr = format!("{:?}", heads_db.head_stats(drop.head_id).ability).to_lowercase();
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
Name::new("headdrop"),
|
||||
Transform::from_translation(drop.pos),
|
||||
Visibility::default(),
|
||||
Collider::sphere(1.5),
|
||||
LockedAxes::ROTATION_LOCKED,
|
||||
RigidBody::Dynamic,
|
||||
CollisionLayers::new(
|
||||
GameLayer::CollectiblePhysics,
|
||||
LayerMask::ALL & !GameLayer::Player.to_bits(),
|
||||
),
|
||||
Restitution::new(0.6),
|
||||
Children::spawn(SpawnWith({
|
||||
let head_id = drop.head_id;
|
||||
let now = time.elapsed_secs();
|
||||
move |parent: &mut RelatedSpawner<ChildOf>| {
|
||||
parent
|
||||
.spawn((
|
||||
Collider::sphere(1.5),
|
||||
CollisionLayers::new(GameLayer::CollectibleSensors, LayerMask::NONE),
|
||||
Sensor,
|
||||
CollisionEventsEnabled,
|
||||
HeadDrop { head_id },
|
||||
HeadDropEnableTime(now + 1.2),
|
||||
))
|
||||
.observe(on_collect_head);
|
||||
}
|
||||
})),
|
||||
#[cfg(feature = "server")]
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
))
|
||||
.insert_if(
|
||||
ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
|
||||
|| drop.impulse,
|
||||
)
|
||||
.with_child((
|
||||
Billboard::All,
|
||||
SquishAnimation(2.6),
|
||||
GltfSceneRoot::HeadDrop(mesh_addr),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enable_collectible(
|
||||
mut commands: Commands,
|
||||
query: Query<(Entity, &HeadDropEnableTime)>,
|
||||
@@ -162,32 +85,3 @@ fn enable_collectible(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_collect_head(
|
||||
trigger: Trigger<OnCollisionStart>,
|
||||
|
||||
mut commands: Commands,
|
||||
query_player: Query<&Player>,
|
||||
query_collectable: Query<(&HeadDrop, &ChildOf)>,
|
||||
query_secret: Query<&SecretHeadMarker>,
|
||||
) {
|
||||
let collectable = trigger.target();
|
||||
let collider = trigger.collider;
|
||||
|
||||
if query_player.contains(collider) {
|
||||
let (drop, child_of) = query_collectable.get(collectable).unwrap();
|
||||
|
||||
let is_secret = query_secret.contains(collectable);
|
||||
|
||||
if is_secret {
|
||||
commands.trigger(PlaySound::SecretHeadCollect);
|
||||
} else {
|
||||
commands.trigger(PlaySound::HeadCollect);
|
||||
}
|
||||
|
||||
commands
|
||||
.entity(collider)
|
||||
.trigger(HeadCollected(drop.head_id));
|
||||
commands.entity(child_of.parent()).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@ use super::HeadsImages;
|
||||
use crate::player::Player;
|
||||
use crate::{GameState, backpack::UiHeadState, loading_assets::UIAssets};
|
||||
use bevy::{ecs::spawn::SpawnIter, prelude::*};
|
||||
#[cfg(feature = "client")]
|
||||
use bevy_ui_gradients::Gradient;
|
||||
use bevy_ui_gradients::{AngularColorStop, BackgroundGradient, ConicGradient, Position};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
@@ -125,7 +122,8 @@ fn spawn_head_ui(
|
||||
AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.9), PI * 1.5),
|
||||
AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.0), PI * 1.5),
|
||||
],
|
||||
position: Position::CENTER,
|
||||
position: UiPosition::CENTER,
|
||||
color_space: InterpolationColorSpace::Srgba,
|
||||
}),
|
||||
ImageNode::default(),
|
||||
Visibility::Hidden,
|
||||
|
||||
@@ -273,7 +273,7 @@ fn on_select_active_head(
|
||||
}
|
||||
|
||||
fn on_swap_backpack(
|
||||
trigger: Trigger<BackbackSwapEvent>,
|
||||
trigger: On<BackbackSwapEvent>,
|
||||
mut commands: Commands,
|
||||
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &mut Backpack), With<Player>>,
|
||||
) {
|
||||
|
||||
@@ -7,11 +7,14 @@ use crate::{
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Event, Reflect)]
|
||||
pub struct Kill;
|
||||
#[derive(EntityEvent, Reflect)]
|
||||
pub struct Kill {
|
||||
pub entity: Entity,
|
||||
}
|
||||
|
||||
#[derive(Event, Reflect)]
|
||||
#[derive(EntityEvent, Reflect)]
|
||||
pub struct Hit {
|
||||
pub entity: Entity,
|
||||
pub damage: u32,
|
||||
}
|
||||
|
||||
@@ -75,14 +78,14 @@ fn on_hp_added(mut commands: Commands, query: Query<Entity, Added<Hitpoints>>) {
|
||||
}
|
||||
|
||||
fn on_hit(
|
||||
trigger: Trigger<Hit>,
|
||||
trigger: On<Hit>,
|
||||
mut commands: Commands,
|
||||
mut query: Query<(&mut Hitpoints, Option<&mut AnimationFlags>)>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let Hit { damage } = trigger.event();
|
||||
let &Hit { damage, entity } = trigger.event();
|
||||
|
||||
let Ok((mut hp, flags)) = query.get_mut(trigger.target()) else {
|
||||
let Ok((mut hp, flags)) = query.get_mut(trigger.event().entity) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -92,11 +95,11 @@ fn on_hit(
|
||||
flags.hit = true;
|
||||
}
|
||||
|
||||
hp.current = hp.current.saturating_sub(*damage);
|
||||
hp.current = hp.current.saturating_sub(damage);
|
||||
hp.last_hit_timestamp = time.elapsed_secs();
|
||||
|
||||
if hp.current == 0 {
|
||||
commands.trigger_targets(Kill, trigger.target());
|
||||
commands.trigger(Kill { entity });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,14 @@ use crate::{
|
||||
player::Player,
|
||||
protocol::{GltfSceneRoot, PlaySound},
|
||||
squish_animation::SquishAnimation,
|
||||
utils::one_shot_force::OneShotForce,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
|
||||
prelude::*,
|
||||
};
|
||||
use lightyear::prelude::DisableReplicateHierarchy;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use std::f32::consts::PI;
|
||||
@@ -29,22 +31,24 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_spawn_key);
|
||||
}
|
||||
|
||||
fn on_spawn_key(trigger: Trigger<KeySpawn>, mut commands: Commands) {
|
||||
fn on_spawn_key(trigger: On<KeySpawn>, mut commands: Commands, time: Res<Time<Fixed>>) {
|
||||
let KeySpawn(position, id) = trigger.event();
|
||||
|
||||
let id = id.clone();
|
||||
|
||||
let angle = rand::random::<f32>() * PI * 2.;
|
||||
let spawn_dir = Quat::from_rotation_y(angle) * Vec3::new(0.5, 0.6, 0.).normalize();
|
||||
let spawn_force = spawn_dir * 180.0 / time.delta_secs();
|
||||
|
||||
commands.spawn((
|
||||
Name::new("key"),
|
||||
Transform::from_translation(*position),
|
||||
Position::new(*position),
|
||||
Visibility::default(),
|
||||
Collider::sphere(1.5),
|
||||
ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
|
||||
LockedAxes::ROTATION_LOCKED,
|
||||
RigidBody::Dynamic,
|
||||
OneShotForce(spawn_force),
|
||||
CollisionLayers::new(GameLayer::CollectiblePhysics, GameLayer::Level),
|
||||
Restitution::new(0.6),
|
||||
Children::spawn((
|
||||
@@ -57,6 +61,7 @@ fn on_spawn_key(trigger: Trigger<KeySpawn>, mut commands: Commands) {
|
||||
Sensor,
|
||||
CollisionEventsEnabled,
|
||||
Key(id),
|
||||
DisableReplicateHierarchy,
|
||||
))
|
||||
.observe(on_collect_key);
|
||||
}),
|
||||
@@ -67,13 +72,13 @@ fn on_spawn_key(trigger: Trigger<KeySpawn>, mut commands: Commands) {
|
||||
}
|
||||
|
||||
fn on_collect_key(
|
||||
trigger: Trigger<OnCollisionStart>,
|
||||
trigger: On<CollisionStart>,
|
||||
mut commands: Commands,
|
||||
query_player: Query<&Player>,
|
||||
query_collectable: Query<(&Key, &ChildOf)>,
|
||||
) {
|
||||
let key = trigger.target();
|
||||
let collider = trigger.collider;
|
||||
let key = trigger.event().collider1;
|
||||
let collider = trigger.event().collider2;
|
||||
|
||||
if query_player.contains(collider) {
|
||||
let (key, child_of) = query_collectable.get(key).unwrap();
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::{GameState, physics_layers::GameLayer};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
use bevy_trenchbroom::physics::SceneCollidersReady;
|
||||
use lightyear::prelude::ReplicationGroup;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{DisableReplicateHierarchy, NetworkTarget, Replicate};
|
||||
|
||||
@@ -14,15 +15,12 @@ pub fn plugin(app: &mut App) {
|
||||
fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands
|
||||
.spawn((
|
||||
Name::new("LevelRoot"),
|
||||
CollisionLayers::new(LayerMask(GameLayer::Level.to_bits()), LayerMask::ALL),
|
||||
SceneRoot(asset_server.load("maps/map1.map#Scene")),
|
||||
#[cfg(feature = "server")]
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
#[cfg(feature = "server")]
|
||||
DisableReplicateHierarchy,
|
||||
))
|
||||
.observe(
|
||||
|t: Trigger<SceneCollidersReady>,
|
||||
|t: On<SceneCollidersReady>,
|
||||
children: Query<&Children>,
|
||||
#[cfg(feature = "server")] map_entities: Query<&TbMapEntityId>,
|
||||
mut commands: Commands,
|
||||
@@ -31,19 +29,28 @@ fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
>| {
|
||||
info!("map loaded");
|
||||
|
||||
for child in children.get(t.target()).unwrap() {
|
||||
for child in children.get(t.event().scene_root_entity).unwrap() {
|
||||
commands.entity(*child).remove::<ChildOf>();
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
if map_entities.contains(*child) {
|
||||
commands.entity(*child).insert((
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
ReplicationGroup::new_id(t.scene_root_entity.to_bits()),
|
||||
DisableReplicateHierarchy,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(cfg!(feature = "client") ^ cfg!(feature = "server"));
|
||||
commands.entity(t.scene_root_entity).insert((
|
||||
#[cfg(feature = "server")]
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
#[cfg(feature = "server")]
|
||||
DisableReplicateHierarchy,
|
||||
ReplicationGroup::new_from_entity(),
|
||||
));
|
||||
|
||||
const { assert!(cfg!(feature = "client") ^ cfg!(feature = "server")) };
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
next_game_state.set(GameState::Connecting);
|
||||
|
||||
@@ -25,7 +25,7 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn on_movable_event(
|
||||
trigger: Trigger<TriggerMovableEvent>,
|
||||
trigger: On<TriggerMovableEvent>,
|
||||
mut commands: Commands,
|
||||
uninit_movables: Query<
|
||||
(Entity, &Target, &Transform, &Movable),
|
||||
|
||||
@@ -15,9 +15,7 @@ use crate::{
|
||||
protocol::PlaySound,
|
||||
tb_entities::EnemySpawn,
|
||||
};
|
||||
use bevy::{pbr::NotShadowCaster, prelude::*};
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use bevy::{light::NotShadowCaster, prelude::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "server")]
|
||||
use std::collections::HashMap;
|
||||
@@ -69,7 +67,7 @@ fn setup(mut commands: Commands, mut spawned: Local<bool>) {
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn on_spawn_check(
|
||||
_trigger: Trigger<OnCheckSpawns>,
|
||||
_trigger: On<OnCheckSpawns>,
|
||||
mut commands: Commands,
|
||||
query: Query<(Entity, &EnemySpawn, &Transform), Without<Npc>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
@@ -102,7 +100,6 @@ fn on_spawn_check(
|
||||
None,
|
||||
None,
|
||||
]),
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
))
|
||||
.insert_if(Ai, || !spawn.disable_ai)
|
||||
.with_child((Name::from("body-rig"), AnimatedCharacter::new(id)))
|
||||
@@ -115,11 +112,11 @@ fn on_spawn_check(
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn on_kill(
|
||||
trigger: Trigger<Kill>,
|
||||
trigger: On<Kill>,
|
||||
mut commands: Commands,
|
||||
query: Query<(&Transform, &EnemySpawn, &ActiveHead)>,
|
||||
) {
|
||||
let Ok((transform, enemy, head)) = query.get(trigger.target()) else {
|
||||
let Ok((transform, enemy, head)) = query.get(trigger.event().entity) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -132,7 +129,7 @@ fn on_kill(
|
||||
commands.trigger(HeadDrops::new(transform.translation, head.0));
|
||||
commands.trigger(OnCheckSpawns);
|
||||
|
||||
commands.entity(trigger.target()).despawn();
|
||||
commands.entity(trigger.event().entity).despawn();
|
||||
|
||||
if !enemy.key.is_empty() {
|
||||
commands.trigger(KeySpawn(transform.translation, enemy.key.clone()));
|
||||
@@ -140,7 +137,7 @@ fn on_kill(
|
||||
}
|
||||
|
||||
fn on_spawn(
|
||||
trigger: Trigger<SpawnCharacter>,
|
||||
trigger: On<SpawnCharacter>,
|
||||
mut commands: Commands,
|
||||
assets: Res<GameAssets>,
|
||||
time: Res<Time>,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use crate::{
|
||||
GameState,
|
||||
tb_entities::{Platform, PlatformTarget},
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
use bevy::math::ops::sin;
|
||||
use bevy::prelude::*;
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
use crate::GameState;
|
||||
use avian3d::prelude::{LinearVelocity, Position};
|
||||
use bevy::{math::ops::sin, prelude::*};
|
||||
use lightyear::prelude::LocalTimeline;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -17,45 +13,25 @@ pub struct ActivePlatform {
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<ActivePlatform>();
|
||||
app.add_systems(OnEnter(GameState::Playing), init);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
move_active.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
fn init(
|
||||
mut commands: Commands,
|
||||
uninit_platforms: Query<
|
||||
(Entity, &Target, &Transform),
|
||||
(Without<ActivePlatform>, With<Platform>),
|
||||
>,
|
||||
targets: Query<(&PlatformTarget, &Transform)>,
|
||||
fn move_active(
|
||||
#[cfg(not(feature = "server"))] time: Single<&LocalTimeline>,
|
||||
#[cfg(feature = "server")] time: Single<&LocalTimeline, With<lightyear::prelude::Server>>,
|
||||
bevy_time: Res<Time<Fixed>>,
|
||||
mut platforms: Query<(&Position, &ActivePlatform, &mut LinearVelocity)>,
|
||||
) {
|
||||
for (e, target, transform) in uninit_platforms.iter() {
|
||||
let Some(target) = targets
|
||||
.iter()
|
||||
.find(|(t, _)| t.targetname == target.target.clone().unwrap_or_default())
|
||||
.map(|(_, t)| t.translation)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for (position, active, mut velocity) in platforms.iter_mut() {
|
||||
let now = time.now.as_duration(bevy_time.delta()).as_secs_f32();
|
||||
let t = (sin(now * 0.4) + 1.) / 2.;
|
||||
|
||||
let platform = ActivePlatform {
|
||||
start: transform.translation,
|
||||
target,
|
||||
};
|
||||
let target = active.start.lerp(active.target, t);
|
||||
let prev = position.0;
|
||||
|
||||
commands.entity(e).insert(platform);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn move_active(time: Res<Time>, mut platforms: Query<(&mut Transform, &mut ActivePlatform)>) {
|
||||
for (mut transform, active) in platforms.iter_mut() {
|
||||
let t = (sin(time.elapsed_secs() * 0.4) + 1.) / 2.;
|
||||
|
||||
transform.translation = active.start.lerp(active.target, t);
|
||||
velocity.0 = (target - prev) / bevy_time.delta_secs();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
input::common_conditions::input_just_pressed,
|
||||
prelude::*,
|
||||
window::{CursorGrabMode, PrimaryWindow},
|
||||
window::{CursorGrabMode, CursorOptions, PrimaryWindow},
|
||||
};
|
||||
use happy_feet::debug::DebugInput;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[require(HedzCharacter)]
|
||||
#[require(HedzCharacter, DebugInput = DebugInput)]
|
||||
pub struct Player;
|
||||
|
||||
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
|
||||
@@ -41,30 +42,35 @@ fn cursor_recenter(q_windows: Single<&mut Window, With<PrimaryWindow>>) {
|
||||
primary_window.set_cursor_position(Some(center));
|
||||
}
|
||||
|
||||
fn toggle_grab_cursor(window: &mut Window) {
|
||||
match window.cursor_options.grab_mode {
|
||||
fn toggle_grab_cursor(options: &mut CursorOptions) {
|
||||
match options.grab_mode {
|
||||
CursorGrabMode::None => {
|
||||
window.cursor_options.grab_mode = CursorGrabMode::Confined;
|
||||
window.cursor_options.visible = false;
|
||||
options.grab_mode = CursorGrabMode::Confined;
|
||||
options.visible = false;
|
||||
}
|
||||
_ => {
|
||||
window.cursor_options.grab_mode = CursorGrabMode::None;
|
||||
window.cursor_options.visible = true;
|
||||
options.grab_mode = CursorGrabMode::None;
|
||||
options.visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_cursor_system(mut window: Single<&mut Window, With<PrimaryWindow>>) {
|
||||
fn toggle_cursor_system(mut window: Single<&mut CursorOptions, With<PrimaryWindow>>) {
|
||||
toggle_grab_cursor(&mut window);
|
||||
}
|
||||
|
||||
fn collect_cash(
|
||||
mut commands: Commands,
|
||||
mut collision_event_reader: EventReader<CollisionStarted>,
|
||||
mut collision_message_reader: MessageReader<CollisionStart>,
|
||||
query_player: Query<&Player>,
|
||||
query_cash: Query<&Cash>,
|
||||
) {
|
||||
for CollisionStarted(e1, e2) in collision_event_reader.read() {
|
||||
for CollisionStart {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
} in collision_message_reader.read()
|
||||
{
|
||||
let collect = if query_player.contains(*e1) && query_cash.contains(*e2) {
|
||||
Some(*e2)
|
||||
} else if query_player.contains(*e2) && query_cash.contains(*e1) {
|
||||
|
||||
@@ -3,9 +3,11 @@ use crate::{
|
||||
protocol::TbMapEntityMapping,
|
||||
};
|
||||
use bevy::{
|
||||
ecs::{component::HookContext, world::DeferredWorld},
|
||||
ecs::{lifecycle::HookContext, world::DeferredWorld},
|
||||
platform::collections::hash_map,
|
||||
prelude::*,
|
||||
};
|
||||
use lightyear::prelude::Replicated;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Component, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
@@ -26,9 +28,18 @@ pub struct TbMapEntityId {
|
||||
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);
|
||||
let entity_is_replicated = world.entity(ctx.entity).contains::<Replicated>();
|
||||
let mut mapping = world.resource_mut::<TbMapEntityMapping>();
|
||||
if let hash_map::Entry::Vacant(e) = mapping.entry(id) {
|
||||
if entity_is_replicated {
|
||||
warn!(
|
||||
"attempted to add a replicated entity to the TbMapEntityMapping; all TbMapEntityIds should be accounted for on the client before the server replicates"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
e.insert(ctx.entity);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_id(mut world: DeferredWorld, ctx: HookContext) {
|
||||
@@ -46,14 +57,14 @@ pub enum GltfSceneRoot {
|
||||
}
|
||||
|
||||
pub fn spawn_gltf_scene_roots(
|
||||
trigger: Trigger<OnAdd, GltfSceneRoot>,
|
||||
trigger: On<Add, 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 root = gltf_roots.get(trigger.event().entity)?;
|
||||
|
||||
let get_scene = |gltf: Handle<Gltf>, index: usize| {
|
||||
let gltf = gltfs.get(&gltf).unwrap();
|
||||
@@ -76,7 +87,9 @@ pub fn spawn_gltf_scene_roots(
|
||||
GltfSceneRoot::Key => assets.mesh_key.clone(),
|
||||
};
|
||||
|
||||
commands.entity(trigger.target()).insert(SceneRoot(scene));
|
||||
commands
|
||||
.entity(trigger.event().entity)
|
||||
.insert(SceneRoot(scene));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
backpack::{Backpack, backpack_ui::BackpackUiState},
|
||||
camera::{CameraArmRotation, CameraTarget},
|
||||
cash::CashResource,
|
||||
character::{self, AnimatedCharacter},
|
||||
character::{AnimatedCharacter, HedzCharacter},
|
||||
control::{
|
||||
ControlState, ControllerSettings,
|
||||
controller_common::{MovementSpeedFactor, PlayerCharacterController},
|
||||
@@ -23,10 +23,13 @@ use crate::{
|
||||
platforms::ActivePlatform,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
protocol::channels::UnorderedReliableChannel,
|
||||
utils::triggers::TriggerAppExt,
|
||||
utils::{
|
||||
auto_rotate::AutoRotation, billboards::Billboard, squish_animation::SquishAnimation,
|
||||
triggers::TriggerAppExt,
|
||||
},
|
||||
};
|
||||
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity};
|
||||
use bevy::prelude::*;
|
||||
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity, Position, Rotation};
|
||||
use bevy::{platform::collections::HashMap, prelude::*};
|
||||
pub use components::*;
|
||||
pub use events::*;
|
||||
use happy_feet::{
|
||||
@@ -38,17 +41,23 @@ use happy_feet::{
|
||||
};
|
||||
use lightyear::prelude::{
|
||||
AppChannelExt, AppComponentExt, AppMessageExt, AppTriggerExt, ChannelMode, ChannelSettings,
|
||||
NetworkDirection, PredictionMode, PredictionRegistrationExt, ReliableSettings,
|
||||
ComponentReplicationConfig, Confirmed, ConfirmedTick, InterpolationRegistrationExt,
|
||||
NetworkDirection, PredictionHistory, PredictionRegistrationExt, ReliableSettings,
|
||||
input::native::InputPlugin,
|
||||
};
|
||||
use lightyear_serde::{
|
||||
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
|
||||
};
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(InputPlugin::<ControlState>::default());
|
||||
|
||||
app.register_type::<ConfirmedTick>();
|
||||
app.register_type::<Confirmed<Position>>();
|
||||
app.register_type::<Confirmed<Rotation>>();
|
||||
app.register_type::<PredictionHistory<Position>>();
|
||||
|
||||
app.register_type::<PlayerId>();
|
||||
app.register_type::<TbMapEntityId>();
|
||||
app.register_type::<TbMapIdCounter>();
|
||||
@@ -64,9 +73,9 @@ pub fn plugin(app: &mut App) {
|
||||
})
|
||||
.add_direction(NetworkDirection::Bidirectional);
|
||||
|
||||
app.add_message::<messages::DespawnTbMapEntity>()
|
||||
app.register_message::<messages::DespawnTbMapEntity>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
app.add_message::<messages::AssignClientPlayer>()
|
||||
app.register_message::<messages::AssignClientPlayer>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
|
||||
app.register_component::<components::GltfSceneRoot>();
|
||||
@@ -75,39 +84,69 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_component::<ActiveHead>();
|
||||
app.register_component::<ActiveHeads>();
|
||||
app.register_component::<ActivePlatform>();
|
||||
app.register_component::<AngularVelocity>();
|
||||
app.register_component::<AnimatedCharacter>();
|
||||
app.register_component::<AnimationFlags>();
|
||||
app.register_component::<AutoRotation>()
|
||||
.with_replication_config(ComponentReplicationConfig {
|
||||
replicate_once: true,
|
||||
..default()
|
||||
});
|
||||
app.register_component::<Backpack>();
|
||||
app.register_component::<BackpackUiState>();
|
||||
app.register_component::<Billboard>();
|
||||
app.register_component::<CameraArmRotation>();
|
||||
app.register_component::<CameraTarget>();
|
||||
app.register_component::<CashResource>();
|
||||
app.register_component::<happy_feet::prelude::Character>();
|
||||
app.register_component::<character::HedzCharacter>();
|
||||
app.register_component::<HedzCharacter>();
|
||||
app.register_component::<Healing>();
|
||||
app.register_component::<Hitpoints>();
|
||||
app.register_component::<Name>();
|
||||
app.register_component::<Player>();
|
||||
app.register_component::<PlayerBodyMesh>()
|
||||
.with_replication_config(ComponentReplicationConfig {
|
||||
replicate_once: true,
|
||||
..default()
|
||||
});
|
||||
app.register_component::<SquishAnimation>();
|
||||
|
||||
// Physics
|
||||
app.register_component::<AngularVelocity>()
|
||||
.add_prediction()
|
||||
.add_should_rollback(|this, that| this.0.distance_squared(that.0) >= 0.01f32.powf(2.0));
|
||||
app.register_component::<CollisionLayers>();
|
||||
app.register_component::<LinearVelocity>()
|
||||
.add_prediction()
|
||||
.add_should_rollback(|this, that| this.0.distance_squared(that.0) >= 0.01f32.powf(2.0));
|
||||
app.register_component::<Position>()
|
||||
.add_prediction()
|
||||
.add_should_rollback(|this, that| this.0.distance_squared(that.0) >= 0.01f32.powf(2.0))
|
||||
.add_linear_correction_fn()
|
||||
.add_linear_interpolation();
|
||||
app.register_component::<Rotation>()
|
||||
.add_prediction()
|
||||
.add_should_rollback(|this, that| this.angle_between(*that) >= 0.01)
|
||||
.add_linear_correction_fn()
|
||||
.add_linear_interpolation();
|
||||
|
||||
// Controller
|
||||
app.register_component::<CharacterDrag>();
|
||||
app.register_component::<CharacterGravity>();
|
||||
app.register_component::<CharacterMovement>();
|
||||
app.register_component::<CollisionLayers>();
|
||||
app.register_component::<ControllerSettings>();
|
||||
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>();
|
||||
app.register_component::<MoveInput>();
|
||||
app.register_component::<MovementSpeedFactor>();
|
||||
app.register_component::<Name>();
|
||||
app.register_component::<Player>();
|
||||
app.register_component::<PlayerBodyMesh>();
|
||||
app.register_component::<PlayerCharacterController>();
|
||||
app.register_component::<PlayerCharacterController>()
|
||||
.with_replication_config(ComponentReplicationConfig {
|
||||
replicate_once: true,
|
||||
..default()
|
||||
});
|
||||
app.register_component::<SteppingConfig>();
|
||||
app.register_component::<Transform>()
|
||||
.add_prediction(PredictionMode::Full)
|
||||
.add_should_rollback(transform_should_rollback);
|
||||
|
||||
app.register_component::<UiActiveHeads>();
|
||||
// `Visibility` isn't `(De)Serialize`, so we have to provide custom serde for it.
|
||||
app.register_component_custom_serde::<Visibility>(SerializeFns {
|
||||
@@ -123,13 +162,13 @@ pub fn plugin(app: &mut App) {
|
||||
},
|
||||
});
|
||||
|
||||
app.replicate_trigger::<BuildExplosionSprite, UnorderedReliableChannel>();
|
||||
app.replicate_trigger::<StartCutscene, UnorderedReliableChannel>();
|
||||
app.replicate_event::<BuildExplosionSprite, UnorderedReliableChannel>();
|
||||
app.replicate_event::<StartCutscene, UnorderedReliableChannel>();
|
||||
|
||||
app.replicate_trigger::<events::ClientHeadChanged, UnorderedReliableChannel>();
|
||||
app.replicate_trigger::<events::PlaySound, UnorderedReliableChannel>();
|
||||
app.replicate_event::<events::ClientHeadChanged, UnorderedReliableChannel>();
|
||||
app.replicate_event::<events::PlaySound, UnorderedReliableChannel>();
|
||||
|
||||
app.add_trigger::<events::ClientEnteredPlaying>()
|
||||
app.register_event::<events::ClientEnteredPlaying>()
|
||||
.add_direction(NetworkDirection::ClientToServer);
|
||||
|
||||
app.add_systems(
|
||||
@@ -140,10 +179,6 @@ pub fn plugin(app: &mut App) {
|
||||
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.)
|
||||
}
|
||||
|
||||
/// A global allocator for `TbMapEntityId` values. Should be reset when a map begins loading.
|
||||
#[derive(Resource, Reflect, Default)]
|
||||
#[reflect(Resource)]
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use crate::{
|
||||
cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer, protocol::TbMapIdCounter,
|
||||
utils::global_observer,
|
||||
GameState, cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer,
|
||||
protocol::TbMapIdCounter, utils::global_observer,
|
||||
};
|
||||
use avian3d::{
|
||||
parry::{na::SVector, shape::SharedShape},
|
||||
prelude::*,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
ecs::{component::HookContext, world::DeferredWorld},
|
||||
ecs::{lifecycle::HookContext, world::DeferredWorld},
|
||||
math::*,
|
||||
prelude::*,
|
||||
};
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
use happy_feet::prelude::PhysicsMover;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[point_class(base(Transform), model({ "path": "models/spawn.glb" }))]
|
||||
#[derive(Default)]
|
||||
#[component(on_add = Self::on_add)]
|
||||
#[model({ "path": "models/spawn.glb" })]
|
||||
pub struct SpawnPoint {}
|
||||
|
||||
impl SpawnPoint {
|
||||
@@ -37,85 +37,70 @@ impl SpawnPoint {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
||||
#[solid_class(
|
||||
hooks(SpawnHooks::new().convex_collider())
|
||||
)]
|
||||
#[derive(Default)]
|
||||
pub struct Worldspawn;
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[spawn_hooks(SpawnHooks::new())]
|
||||
#[base(Transform)]
|
||||
#[solid_class(base(Transform), hooks(SpawnHooks::new()))]
|
||||
#[derive(Default)]
|
||||
pub struct Water;
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
||||
#[solid_class(base(Transform), hooks(SpawnHooks::new().convex_collider()))]
|
||||
#[derive(Default)]
|
||||
pub struct Crates;
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
||||
#[solid_class(base(Transform), hooks(SpawnHooks::new().convex_collider()))]
|
||||
#[derive(Default)]
|
||||
pub struct NamedEntity {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform, Target)]
|
||||
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
||||
#[require(PhysicsMover = PhysicsMover, TransformInterpolation)]
|
||||
#[solid_class(base(Transform, Target), hooks(SpawnHooks::new().convex_collider()))]
|
||||
#[derive(Default)]
|
||||
#[require(RigidBody = RigidBody::Kinematic)]
|
||||
pub struct Platform;
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[point_class(base(Transform))]
|
||||
#[derive(Default)]
|
||||
pub struct PlatformTarget {
|
||||
pub targetname: String,
|
||||
}
|
||||
|
||||
#[derive(SolidClass, Component, Reflect, Default, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform, Target)]
|
||||
#[spawn_hooks(SpawnHooks::new().convex_collider())]
|
||||
#[solid_class(base(Transform, Target), hooks(SpawnHooks::new().convex_collider()))]
|
||||
#[derive(Default, Serialize, Deserialize, PartialEq)]
|
||||
#[require(RigidBody = RigidBody::Kinematic)]
|
||||
pub struct Movable {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[point_class(base(Transform))]
|
||||
#[derive(Default)]
|
||||
pub struct MoveTarget {
|
||||
pub targetname: String,
|
||||
}
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[point_class(base(Transform))]
|
||||
#[derive(Default)]
|
||||
pub struct CameraTarget {
|
||||
pub targetname: String,
|
||||
}
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform, Target)]
|
||||
#[point_class(base(Transform, Target))]
|
||||
#[derive(Default)]
|
||||
pub struct CutsceneCamera {
|
||||
pub name: String,
|
||||
pub targetname: String,
|
||||
}
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform, Target)]
|
||||
#[point_class(base(Transform, Target))]
|
||||
#[derive(Default)]
|
||||
pub struct CutsceneCameraMovementEnd;
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[point_class(base(Transform), model({ "path": "models/alien_naked.glb" }))]
|
||||
#[derive(Default)]
|
||||
#[component(on_add = Self::on_add)]
|
||||
#[model({ "path": "models/alien_naked.glb" })]
|
||||
pub struct EnemySpawn {
|
||||
pub head: String,
|
||||
pub key: String,
|
||||
@@ -151,15 +136,15 @@ impl EnemySpawn {
|
||||
Collider::capsule(0.6, 2.),
|
||||
CollisionLayers::new(LayerMask(GameLayer::Npc.to_bits()), LayerMask::ALL),
|
||||
LockedAxes::new().lock_rotation_z().lock_rotation_x(),
|
||||
#[cfg(feature = "server")]
|
||||
lightyear::prelude::Replicate::to_clients(lightyear::prelude::NetworkTarget::All),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[point_class(base(Transform), model({ "path": "models/cash.glb" }))]
|
||||
#[derive(Default)]
|
||||
#[component(on_add = Self::on_add)]
|
||||
#[model({ "path": "models/cash.glb" })]
|
||||
pub struct CashSpawn {}
|
||||
|
||||
impl CashSpawn {
|
||||
@@ -182,17 +167,34 @@ impl CashSpawn {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PointClass, Component, Reflect, Default)]
|
||||
#[reflect(QuakeClass, Component)]
|
||||
#[base(Transform)]
|
||||
#[model({ "path": "models/head_drop.glb" })]
|
||||
#[point_class(base(Transform), model({ "path": "models/head_drop.glb" }))]
|
||||
#[derive(Default)]
|
||||
pub struct SecretHead {
|
||||
pub head_id: usize,
|
||||
}
|
||||
|
||||
fn fix_target_tb_entities(
|
||||
mut commands: Commands,
|
||||
mut entities: Query<(Entity, &Transform, &Collider), With<Target>>,
|
||||
) {
|
||||
for (entity, tf, coll) in entities.iter_mut() {
|
||||
if let Some(shape) = coll.shape().as_compound() {
|
||||
let mut shapes: Vec<_> = shape.shapes().to_vec();
|
||||
|
||||
for shape in shapes.iter_mut() {
|
||||
shape.0.translation.vector -= SVector::<f32, 3>::from(tf.translation.to_array());
|
||||
}
|
||||
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(Collider::from(SharedShape::compound(shapes)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<SpawnPoint>();
|
||||
app.register_type::<Worldspawn>();
|
||||
app.override_class::<Worldspawn>();
|
||||
app.register_type::<Water>();
|
||||
app.register_type::<Crates>();
|
||||
app.register_type::<NamedEntity>();
|
||||
@@ -207,14 +209,20 @@ pub fn plugin(app: &mut App) {
|
||||
app.register_type::<CashSpawn>();
|
||||
app.register_type::<SecretHead>();
|
||||
|
||||
app.add_systems(OnExit(GameState::MapLoading), fix_target_tb_entities);
|
||||
|
||||
global_observer!(app, tb_component_setup::<CashSpawn>);
|
||||
global_observer!(app, tb_component_setup::<Movable>);
|
||||
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) {
|
||||
fn tb_component_setup<C: Component>(
|
||||
trigger: On<Add, C>,
|
||||
mut commands: Commands,
|
||||
mut world: DeferredWorld,
|
||||
) {
|
||||
let id = world.resource_mut::<TbMapIdCounter>().alloc();
|
||||
|
||||
world.entity_mut(trigger.target()).insert_if_new(id);
|
||||
commands.entity(trigger.event().entity).insert_if_new(id);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
|
||||
#[reflect(Component)]
|
||||
pub struct AutoRotation(pub Quat);
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<AutoRotation>();
|
||||
|
||||
app.add_systems(FixedUpdate, update_auto_rotation);
|
||||
#[cfg(feature = "client")]
|
||||
app.add_systems(Update, update_auto_rotation);
|
||||
}
|
||||
|
||||
fn update_auto_rotation(mut query: Query<(&AutoRotation, &mut Transform)>) {
|
||||
for (auto_rotation, mut transform) in query.iter_mut() {
|
||||
transform.rotate_local(auto_rotation.0);
|
||||
#[cfg(feature = "client")]
|
||||
fn update_auto_rotation(
|
||||
query: Query<(&AutoRotation, &Children)>,
|
||||
mut meshes: Query<&mut Transform>,
|
||||
) {
|
||||
for (auto_rotation, children) in query.iter() {
|
||||
for &child in children {
|
||||
let Ok(mut transform) = meshes.get_mut(child) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
transform.rotate_local(auto_rotation.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::camera::MainCamera;
|
||||
use bevy::prelude::*;
|
||||
use bevy_sprite3d::Sprite3dPlugin;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Reflect, Default, PartialEq, Eq)]
|
||||
#[derive(Component, Reflect, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[reflect(Component)]
|
||||
pub enum Billboard {
|
||||
#[default]
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
use bevy::ecs::{
|
||||
event::Event,
|
||||
entity::Entity,
|
||||
event::{EntityEvent, Event},
|
||||
system::{Commands, EntityCommands},
|
||||
world::{EntityWorldMut, World},
|
||||
};
|
||||
use lightyear::prelude::Disconnected;
|
||||
|
||||
pub trait CommandExt {
|
||||
fn trigger_server(&mut self, event: impl Event) -> &mut Self;
|
||||
fn trigger_server<'a, E: Event<Trigger<'a>: Default>>(&mut self, event: E) -> &mut Self;
|
||||
}
|
||||
|
||||
impl<'w, 's> CommandExt for Commands<'w, 's> {
|
||||
fn trigger_server(&mut self, event: impl Event) -> &mut Self {
|
||||
fn trigger_server<'a, E: Event<Trigger<'a>: Default>>(&mut self, event: E) -> &mut Self {
|
||||
self.queue(|world: &mut World| {
|
||||
let mut query_state = world.query::<&Disconnected>();
|
||||
if cfg!(feature = "server") || !query_state.query(world).is_empty() {
|
||||
@@ -22,11 +23,17 @@ impl<'w, 's> CommandExt for Commands<'w, 's> {
|
||||
}
|
||||
|
||||
pub trait EntityCommandExt {
|
||||
fn trigger_server(&mut self, event: impl Event) -> &mut Self;
|
||||
fn trigger_server<'a, E: EntityEvent<Trigger<'a>: Default>>(
|
||||
&mut self,
|
||||
event: impl FnOnce(Entity) -> E + Send + Sync + 'static,
|
||||
) -> &mut Self;
|
||||
}
|
||||
|
||||
impl<'w> EntityCommandExt for EntityCommands<'w> {
|
||||
fn trigger_server(&mut self, event: impl Event) -> &mut Self {
|
||||
fn trigger_server<'a, E: EntityEvent<Trigger<'a>: Default>>(
|
||||
&mut self,
|
||||
event: impl FnOnce(Entity) -> E + Send + Sync + 'static,
|
||||
) -> &mut Self {
|
||||
self.queue(|mut entity: EntityWorldMut| {
|
||||
let mut query_state = entity.world_scope(|world| world.query::<&Disconnected>());
|
||||
if cfg!(feature = "server") || !query_state.query(entity.world()).is_empty() {
|
||||
|
||||
@@ -13,11 +13,7 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_explosion);
|
||||
}
|
||||
|
||||
fn on_explosion(
|
||||
explosion: Trigger<Explosion>,
|
||||
mut commands: Commands,
|
||||
spatial_query: SpatialQuery,
|
||||
) {
|
||||
fn on_explosion(explosion: On<Explosion>, mut commands: Commands, spatial_query: SpatialQuery) {
|
||||
let explosion = explosion.event();
|
||||
let intersections = {
|
||||
spatial_query.shape_intersections(
|
||||
@@ -32,7 +28,8 @@ fn on_explosion(
|
||||
|
||||
for entity in intersections.iter() {
|
||||
if let Ok(mut e) = commands.get_entity(*entity) {
|
||||
e.trigger(Hit {
|
||||
e.trigger(|entity| Hit {
|
||||
entity,
|
||||
damage: explosion.damage,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod billboards;
|
||||
pub mod commands;
|
||||
pub mod explosions;
|
||||
pub mod observers;
|
||||
pub mod one_shot_force;
|
||||
pub mod run_conditions;
|
||||
pub mod sprite_3d_animation;
|
||||
pub mod squish_animation;
|
||||
@@ -14,4 +15,5 @@ pub(crate) use observers::global_observer;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(observers::plugin);
|
||||
app.add_plugins(one_shot_force::plugin);
|
||||
}
|
||||
|
||||
19
crates/shared/src/utils/one_shot_force.rs
Normal file
19
crates/shared/src/utils/one_shot_force.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use avian3d::prelude::{Forces, RigidBodyForces};
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(FixedUpdate, apply_one_shot_forces);
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct OneShotForce(pub Vec3);
|
||||
|
||||
pub fn apply_one_shot_forces(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(Entity, &OneShotForce, Forces)>,
|
||||
) {
|
||||
for (entity, force, mut forces) in query.iter_mut() {
|
||||
forces.apply_force(force.0);
|
||||
commands.entity(entity).remove::<OneShotForce>();
|
||||
}
|
||||
}
|
||||
@@ -18,16 +18,19 @@ pub fn plugin(app: &mut App) {
|
||||
fn animate_sprite(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
mut query: Query<(Entity, &mut AnimationTimer, &mut Sprite3d)>,
|
||||
mut query: Query<(Entity, &mut AnimationTimer, &Sprite3d, &mut Sprite)>,
|
||||
) {
|
||||
for (e, mut timer, mut sprite_3d) in query.iter_mut() {
|
||||
timer.tick(time.delta());
|
||||
if timer.just_finished() {
|
||||
let length = sprite_3d.texture_atlas_keys.as_ref().unwrap().len();
|
||||
let atlas = sprite_3d.texture_atlas.as_mut().unwrap();
|
||||
for (e, mut timer, sprite_3d, mut sprite) in query.iter_mut() {
|
||||
let length = sprite_3d.texture_atlas_keys.len();
|
||||
let atlas = sprite.texture_atlas.as_mut().unwrap();
|
||||
|
||||
if atlas.index < length - 1 {
|
||||
atlas.index = atlas.index.saturating_add(1) % length;
|
||||
if length > 0 {
|
||||
timer.tick(time.delta());
|
||||
}
|
||||
|
||||
if timer.just_finished() {
|
||||
if atlas.index + 1 < length {
|
||||
atlas.index = (atlas.index + 1) % length;
|
||||
} else {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use bevy::prelude::*;
|
||||
use ops::sin;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Reflect)]
|
||||
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
|
||||
#[reflect(Component)]
|
||||
pub struct SquishAnimation(pub f32);
|
||||
|
||||
|
||||
@@ -1,54 +1,60 @@
|
||||
use crate::utils::global_observer;
|
||||
use bevy::{ecs::system::SystemParam, prelude::*};
|
||||
use lightyear::prelude::{AppTriggerExt, Channel, NetworkDirection, RemoteTrigger, TriggerSender};
|
||||
use lightyear::prelude::{AppTriggerExt, Channel, EventSender, NetworkDirection, RemoteEvent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(SystemParam)]
|
||||
pub struct ServerMultiTriggerSender<'w, 's, M: Event + Clone> {
|
||||
senders: Query<'w, 's, &'static mut TriggerSender<M>>,
|
||||
senders: Query<'w, 's, &'static mut EventSender<M>>,
|
||||
}
|
||||
|
||||
impl<'w, 's, M: Event + Clone> ServerMultiTriggerSender<'w, 's, M> {
|
||||
pub fn server_trigger_targets<C: Channel>(&mut self, trigger: M, target: &[Entity]) {
|
||||
pub fn server_trigger_targets<C: Channel>(&mut self, event: M) {
|
||||
if cfg!(not(feature = "server")) {
|
||||
return;
|
||||
}
|
||||
|
||||
for mut sender in self.senders.iter_mut() {
|
||||
sender.trigger_targets::<C>(trigger.clone(), target.iter().copied());
|
||||
sender.trigger::<C>(event.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TriggerAppExt {
|
||||
fn replicate_trigger<M: Event + Clone + Serialize + for<'de> Deserialize<'de>, C: Channel>(
|
||||
fn replicate_event<
|
||||
'a,
|
||||
M: Event<Trigger<'a>: Default> + Clone + Serialize + for<'de> Deserialize<'de>,
|
||||
C: Channel,
|
||||
>(
|
||||
&mut self,
|
||||
);
|
||||
}
|
||||
|
||||
impl TriggerAppExt for App {
|
||||
fn replicate_trigger<M: Event + Clone + Serialize + for<'de> Deserialize<'de>, C: Channel>(
|
||||
fn replicate_event<
|
||||
'a,
|
||||
M: Event<Trigger<'a>: Default> + Clone + Serialize + for<'de> Deserialize<'de>,
|
||||
C: Channel,
|
||||
>(
|
||||
&mut self,
|
||||
) {
|
||||
self.add_trigger::<M>()
|
||||
self.register_event::<M>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
global_observer!(self, replicate_trigger_to_clients::<M, C>);
|
||||
global_observer!(self, remote_to_local_trigger::<M>);
|
||||
global_observer!(self, remote_to_local_event::<M>);
|
||||
}
|
||||
}
|
||||
|
||||
fn replicate_trigger_to_clients<M: Event + Clone, C: Channel>(
|
||||
trigger: Trigger<M>,
|
||||
on: On<M>,
|
||||
mut sender: ServerMultiTriggerSender<M>,
|
||||
) {
|
||||
let targets: &[Entity] = if trigger.target() == Entity::PLACEHOLDER {
|
||||
&[]
|
||||
} else {
|
||||
&[trigger.target()]
|
||||
};
|
||||
sender.server_trigger_targets::<C>(trigger.event().clone(), targets);
|
||||
sender.server_trigger_targets::<C>(on.event().clone());
|
||||
}
|
||||
|
||||
fn remote_to_local_trigger<M: Event + Clone>(trigger: Trigger<RemoteTrigger<M>>, mut c: Commands) {
|
||||
c.trigger_targets(trigger.event().trigger.clone(), trigger.target());
|
||||
fn remote_to_local_event<'a, M: Event<Trigger<'a>: Default> + Clone>(
|
||||
trigger: On<RemoteEvent<M>>,
|
||||
mut c: Commands,
|
||||
) {
|
||||
c.trigger(trigger.event().trigger.clone());
|
||||
}
|
||||
|
||||
@@ -43,17 +43,25 @@ fn setup(mut commands: Commands, query: Query<(Entity, &Children), With<Water>>)
|
||||
|
||||
fn check_water_collision(
|
||||
mut cmds: Commands,
|
||||
mut collisionstart_events: EventReader<CollisionStarted>,
|
||||
mut collisionend_events: EventReader<CollisionEnded>,
|
||||
mut collisionstart_events: MessageReader<CollisionStart>,
|
||||
mut collisionend_events: MessageReader<CollisionEnd>,
|
||||
query_player: Query<&Player>,
|
||||
query_water: Query<(Entity, &WaterSensor)>,
|
||||
) {
|
||||
let start_events = collisionstart_events
|
||||
.read()
|
||||
.map(|CollisionStarted(e1, e2)| (true, *e1, *e2));
|
||||
let end_events = collisionend_events
|
||||
.read()
|
||||
.map(|CollisionEnded(e1, e2)| (false, *e1, *e2));
|
||||
let start_events = collisionstart_events.read().map(
|
||||
|CollisionStart {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
}| (true, *e1, *e2),
|
||||
);
|
||||
let end_events = collisionend_events.read().map(
|
||||
|CollisionEnd {
|
||||
collider1: e1,
|
||||
collider2: e2,
|
||||
..
|
||||
}| (false, *e1, *e2),
|
||||
);
|
||||
|
||||
for (started, e1, e2) in start_events.chain(end_events) {
|
||||
let entities = [e1, e2];
|
||||
@@ -80,7 +88,7 @@ fn check_water_collision(
|
||||
}
|
||||
|
||||
fn on_player_water(
|
||||
trigger: Trigger<PlayerInWater>,
|
||||
trigger: On<PlayerInWater>,
|
||||
//TODO: use a sparse set component `InWater` that we attach to the player
|
||||
// then we can have a movement factor system that reacts on these components to update the factor
|
||||
// PLUS we can then always adhoc check if a player is `InWater` to play an according sound and such
|
||||
|
||||
@@ -1,7 +1,72 @@
|
||||
@BaseClass base(__transform, __visibility) = __directional_light
|
||||
[
|
||||
color(color1) : "Light Color" : "1 1 1" : ""
|
||||
illuminance(float) : "Light Illuminance" : "10000" : "Illuminance in lux (lumens per square meter), representing the amount of light projected onto surfaces by this light source."
|
||||
shadows_enabled(choices) : "Enable Shadows" : "false" : "" =
|
||||
[
|
||||
"true" : "true"
|
||||
"false" : "false"
|
||||
]
|
||||
affects_lightmapped_mesh_diffuse(choices) : "Affects Lightmapped Mesh Diffuse" : "true" : "Whether this light contributes diffuse lighting to meshes with lightmaps.
|
||||
Note that the specular portion of the light is always considered, because Bevy currently has no means to bake specular light." =
|
||||
[
|
||||
"true" : "true"
|
||||
"false" : "false"
|
||||
]
|
||||
shadow_depth_bias(float) : "Shadow Depth Bias" : "0.02" : "A value that adjusts the tradeoff between self-shadowing artifacts and proximity of shadows to their casters. This value frequently must be tuned to the specific scene; this is normal and a well-known part of the shadow mapping workflow."
|
||||
shadow_normal_bias(float) : "Shadow Normal Bias" : "1.8" : "A bias applied along the direction of the fragment's surface normal. It is scaled to the shadow map's texel size so that it is automatically adjusted to the orthographic projection."
|
||||
]
|
||||
|
||||
@BaseClass base(__transform, __visibility) = __point_light
|
||||
[
|
||||
color(color1) : "Light Color" : "1 1 1" : ""
|
||||
intensity(float) : "Light Intensity" : "1000000" : "Luminous power in lumens, representing the amount of light emitted by this source in all directions."
|
||||
range(float) : "Light Range" : "20" : "Cut-off for the light's area-of-effect. Fragments outside this range will not be affected by this light at all, so it's important to tune this together with `intensity` to prevent hard lighting cut-offs."
|
||||
radius(float) : "Light Radius" : "0" : "Simulates a light source coming from a spherical volume with the given radius. This affects the size of specular highlights created by this light."
|
||||
shadows_enabled(choices) : "Enable Shadows" : "false" : "" =
|
||||
[
|
||||
"true" : "true"
|
||||
"false" : "false"
|
||||
]
|
||||
affects_lightmapped_mesh_diffuse(choices) : "Affects Lightmapped Mesh Diffuse" : "true" : "Whether this light contributes diffuse lighting to meshes with lightmaps.
|
||||
Note that the specular portion of the light is always considered, because Bevy currently has no means to bake specular light." =
|
||||
[
|
||||
"true" : "true"
|
||||
"false" : "false"
|
||||
]
|
||||
shadow_depth_bias(float) : "Shadow Depth Bias" : "0.08" : "A bias used when sampling shadow maps to avoid 'shadow-acne', or false shadow occlusions that happen as a result of shadow-map fragments not mapping 1:1 to screen-space fragments."
|
||||
shadow_normal_bias(float) : "Shadow Normal Bias" : "0.6" : "A bias applied along the direction of the fragment's surface normal. It is scaled to the shadow map's texel size so that it can be small close to the camera and gets larger further away."
|
||||
shadow_map_near_z(float) : "Shadow Map Near Z" : "0.1" : "The distance from the light to near Z plane in the shadow map."
|
||||
]
|
||||
|
||||
@BaseClass base(__transform, __visibility) = __spot_light
|
||||
[
|
||||
color(color1) : "Light Color" : "1 1 1" : ""
|
||||
intensity(float) : "Light Intensity" : "1000000" : "Luminous power in lumens, representing the amount of light emitted by this source in all directions."
|
||||
range(float) : "Light Range" : "20" : "Range in meters that this light illuminates. Note that this value affects resolution of the shadow maps; generally, the higher you set it, the lower-resolution your shadow maps will be."
|
||||
radius(float) : "Light Radius" : "0" : "Simulates a light source coming from a spherical volume with the given radius."
|
||||
shadows_enabled(choices) : "Enable Shadows" : "false" : "" =
|
||||
[
|
||||
"true" : "true"
|
||||
"false" : "false"
|
||||
]
|
||||
affects_lightmapped_mesh_diffuse(choices) : "Affects Lightmapped Mesh Diffuse" : "true" : "Whether this light contributes diffuse lighting to meshes with lightmaps.
|
||||
Note that the specular portion of the light is always considered, because Bevy currently has no means to bake specular light." =
|
||||
[
|
||||
"true" : "true"
|
||||
"false" : "false"
|
||||
]
|
||||
shadow_depth_bias(float) : "Shadow Depth Bias" : "0.02" : "A value that adjusts the tradeoff between self-shadowing artifacts and proximity of shadows to their casters. This value frequently must be tuned to the specific scene; this is normal and a well-known part of the shadow mapping workflow."
|
||||
shadow_normal_bias(float) : "Shadow Normal Bias" : "1.8" : "A bias applied along the direction of the fragment's surface normal. It is scaled to the shadow map's texel size so that it can be small close to the camera and gets larger further away."
|
||||
shadow_map_near_z(float) : "Shadow Map Near Z" : "0.1" : "The distance from the light to near Z plane in the shadow map."
|
||||
outer_angle(float) : "Light Cone Outer Angle" : "45" : "Angle defining the distance from the spot light direction to the outer limit of the light's cone of effect in degrees."
|
||||
inner_angle(float) : "Light Cone Inner Angle" : "0" : "Angle defining the distance from the spot light direction to the inner limit of the light's cone of effect in degrees."
|
||||
]
|
||||
|
||||
@BaseClass = __target
|
||||
[
|
||||
target(string) : "target" : "" : "If [`Some`], when this entity's IO fires, it will activate all entities with its [`Targetable::targetname`] set to this, with whatever input that functionality that entity has set up."
|
||||
killtarget(string) : "killtarget" : "" : "If [`Some`], when this entity's IO fires, it will kill all entities with its [`Targetable::targetname`] set to this."
|
||||
target(string) : "target" : : "If [`Some`], when this entity's IO fires, it will activate all entities with its [`Targetable::targetname`] set to this, with whatever input that functionality that entity has set up."
|
||||
killtarget(string) : "killtarget" : : "If [`Some`], when this entity's IO fires, it will kill all entities with its [`Targetable::targetname`] set to this."
|
||||
]
|
||||
|
||||
@BaseClass = __transform
|
||||
@@ -56,6 +121,26 @@
|
||||
spawn_order(integer) : "spawn_order" : : ""
|
||||
]
|
||||
|
||||
@SolidClass base(__visibility) = func_generic : "Generic brush entity to separate from world geometry. bevy_trenchbroom's version of Quake's `func_wall`."
|
||||
[
|
||||
]
|
||||
|
||||
@SolidClass base(__visibility) = func_group : "Groups a set of brushes together in-editor."
|
||||
[
|
||||
]
|
||||
|
||||
@PointClass base(__directional_light) iconsprite({ path: "sprites/light_directional.png", scale: 0.1 }) = light_directional : "[`LightingWorkflow::DynamicOnly`] implementation."
|
||||
[
|
||||
]
|
||||
|
||||
@PointClass base(__point_light) iconsprite({ path: "sprites/light_point.png", scale: 0.1 }) = light_point : "[`LightingWorkflow::DynamicOnly`] implementation."
|
||||
[
|
||||
]
|
||||
|
||||
@PointClass base(__spot_light) iconsprite({ path: "sprites/light_spot.png", scale: 0.1 }) = light_spot : "[`LightingWorkflow::DynamicOnly`] implementation."
|
||||
[
|
||||
]
|
||||
|
||||
@SolidClass base(__transform, __target) = movable
|
||||
[
|
||||
name(string) : "name" : "" : ""
|
||||
|
||||
Reference in New Issue
Block a user