Cutscene + Items + Target sync (#64)

This commit is contained in:
PROMETHIA-27
2025-09-27 07:58:05 -04:00
committed by GitHub
parent 2f5d154d26
commit d582313013
8 changed files with 122 additions and 124 deletions

View File

@@ -13,13 +13,14 @@ use crate::{
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;
use marker::MarkerEvent; use marker::MarkerEvent;
use serde::{Deserialize, Serialize};
use std::f32::consts::PI; use std::f32::consts::PI;
#[derive(Component, Reflect, Default, Deref)] #[derive(Component, Reflect, Default, Deref, PartialEq, Serialize, Deserialize)]
#[reflect(Component)] #[reflect(Component)]
pub struct AimTarget(pub Option<Entity>); pub struct AimTarget(pub Option<Entity>);
#[derive(Component, Reflect)] #[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
#[reflect(Component)] #[reflect(Component)]
#[require(AimTarget)] #[require(AimTarget)]
pub struct AimState { pub struct AimState {
@@ -39,6 +40,9 @@ impl Default for AimState {
} }
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_type::<AimState>();
app.register_type::<AimTarget>();
app.add_plugins(target_ui::plugin); app.add_plugins(target_ui::plugin);
app.add_plugins(marker::plugin); app.add_plugins(marker::plugin);
@@ -71,65 +75,66 @@ fn update_player_aim(
mut commands: Commands, mut commands: Commands,
potential_targets: Query<(Entity, &Transform), With<Hitpoints>>, potential_targets: Query<(Entity, &Transform), With<Hitpoints>>,
player_rot: Query<(&Transform, &GlobalTransform), With<PlayerBodyMesh>>, player_rot: Query<(&Transform, &GlobalTransform), With<PlayerBodyMesh>>,
mut player_aim: Query<(Entity, &AimState, &mut AimTarget), With<Player>>, mut player_aim: Query<(Entity, &AimState, &mut AimTarget, &Children), With<Player>>,
spatial_query: SpatialQuery, spatial_query: SpatialQuery,
) { ) {
let Some((player, state, mut aim_target)) = player_aim.iter_mut().next() else { for (player, state, mut aim_target, children) in player_aim.iter_mut() {
return; assert_eq!(
}; children.len(),
1,
"expected player to have one direct child"
);
let Some((player_pos, player_forward)) = player_rot let (player_pos, player_forward) = player_rot
.iter() .get(*children.first().unwrap())
.next() .map(|(t, global)| (global.translation(), t.forward()))
.map(|(t, global)| (global.translation(), t.forward())) .unwrap();
else {
return;
};
let mut new_target = None; let mut new_target = None;
let mut target_distance = f32::MAX; let mut target_distance = f32::MAX;
for (e, t) in potential_targets.iter() { for (e, t) in potential_targets.iter() {
if e == player { if e == player {
continue;
}
let delta = player_pos - t.translation;
let distance = delta.length();
if distance > state.range {
continue;
}
let angle = player_forward.angle_between(delta.normalize());
if angle < state.max_angle && distance < target_distance {
if !line_of_sight(&spatial_query, player_pos, delta, distance) {
continue; continue;
} }
new_target = Some(e); let delta = player_pos - t.translation;
target_distance = distance;
}
}
if let Some(e) = &aim_target.0 let distance = delta.length();
&& commands.get_entity(*e).is_err()
{
aim_target.0 = None;
return;
}
if new_target != aim_target.0 { if distance > state.range {
if state.spawn_marker { continue;
if let Some(target) = new_target { }
commands.trigger(MarkerEvent::Spawn(target));
} else { let angle = player_forward.angle_between(delta.normalize());
commands.trigger(MarkerEvent::Despawn);
if angle < state.max_angle && distance < target_distance {
if !line_of_sight(&spatial_query, player_pos, delta, distance) {
continue;
}
new_target = Some(e);
target_distance = distance;
} }
} }
aim_target.0 = new_target;
if let Some(e) = &aim_target.0
&& commands.get_entity(*e).is_err()
{
aim_target.0 = None;
return;
}
if new_target != aim_target.0 {
if state.spawn_marker {
if let Some(target) = new_target {
commands.trigger(MarkerEvent::Spawn(target));
} else {
commands.trigger(MarkerEvent::Despawn);
}
}
aim_target.0 = new_target;
}
} }
} }

View File

@@ -6,8 +6,9 @@ use crate::{
}; };
use bevy::prelude::*; use bevy::prelude::*;
use bevy_trenchbroom::prelude::*; use bevy_trenchbroom::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Event, Debug)] #[derive(Clone, Debug, Event, Serialize, Deserialize)]
pub struct StartCutscene(pub String); pub struct StartCutscene(pub String);
#[derive(Resource, Debug, Default)] #[derive(Resource, Debug, Default)]

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase, GameState, billboards::Billboard, global_observer, heads_database::HeadsDatabase,
loading_assets::HeadDropAssets, physics_layers::GameLayer, player::Player, sounds::PlaySound, physics_layers::GameLayer, player::Player, protocol::GltfSceneRoot, sounds::PlaySound,
squish_animation::SquishAnimation, tb_entities::SecretHead, squish_animation::SquishAnimation, tb_entities::SecretHead,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
@@ -8,6 +8,8 @@ use bevy::{
ecs::{relationship::RelatedSpawner, spawn::SpawnWith}, ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
prelude::*, prelude::*,
}; };
#[cfg(feature = "server")]
use lightyear::prelude::{NetworkTarget, Replicate};
use std::f32::consts::PI; use std::f32::consts::PI;
#[derive(Event, Reflect)] #[derive(Event, Reflect)]
@@ -77,9 +79,7 @@ fn spawn(mut commands: Commands, query: Query<(Entity, &GlobalTransform, &Secret
fn on_head_drop( fn on_head_drop(
trigger: Trigger<HeadDrops>, trigger: Trigger<HeadDrops>,
mut commands: Commands, mut commands: Commands,
assets: Res<HeadDropAssets>,
heads_db: Res<HeadsDatabase>, heads_db: Res<HeadsDatabase>,
gltf_assets: Res<Assets<Gltf>>,
time: Res<Time>, time: Res<Time>,
) -> Result<(), BevyError> { ) -> Result<(), BevyError> {
let drop = trigger.event(); let drop = trigger.event();
@@ -91,13 +91,7 @@ fn on_head_drop(
commands.trigger(PlaySound::HeadDrop); commands.trigger(PlaySound::HeadDrop);
} }
let ability = format!("{:?}.glb", heads_db.head_stats(drop.head_id).ability).to_lowercase(); let mesh_addr = format!("{:?}", heads_db.head_stats(drop.head_id).ability).to_lowercase();
let mesh = if let Some(handle) = assets.meshes.get(ability.as_str()) {
gltf_assets.get(handle)
} else {
gltf_assets.get(&assets.meshes["none.glb"])
}
.ok_or("asset not found")?;
commands commands
.spawn(( .spawn((
@@ -128,6 +122,8 @@ fn on_head_drop(
.observe(on_collect_head); .observe(on_collect_head);
} }
})), })),
#[cfg(feature = "server")]
Replicate::to_clients(NetworkTarget::All),
)) ))
.insert_if( .insert_if(
ExternalImpulse::new(spawn_dir * 180.).with_persistence(false), ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
@@ -136,7 +132,7 @@ fn on_head_drop(
.with_child(( .with_child((
Billboard::All, Billboard::All,
SquishAnimation(2.6), SquishAnimation(2.6),
SceneRoot(mesh.scenes[0].clone()), GltfSceneRoot::HeadDrop(mesh_addr),
)); ));
Ok(()) Ok(())

View File

@@ -5,6 +5,7 @@ use crate::{
sounds::PlaySound, sounds::PlaySound,
}; };
use bevy::prelude::*; use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Event, Reflect)] #[derive(Event, Reflect)]
pub struct Kill; pub struct Kill;
@@ -14,7 +15,7 @@ pub struct Hit {
pub damage: u32, pub damage: u32,
} }
#[derive(Component, Reflect, Debug, Clone, Copy)] #[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Hitpoints { pub struct Hitpoints {
max: u32, max: u32,
current: u32, current: u32,

View File

@@ -1,12 +1,14 @@
use crate::{ use crate::{
billboards::Billboard, global_observer, loading_assets::GameAssets, physics_layers::GameLayer, billboards::Billboard, global_observer, physics_layers::GameLayer, player::Player,
player::Player, sounds::PlaySound, squish_animation::SquishAnimation, protocol::GltfSceneRoot, sounds::PlaySound, squish_animation::SquishAnimation,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
use bevy::{ use bevy::{
ecs::{relationship::RelatedSpawner, spawn::SpawnWith}, ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
prelude::*, prelude::*,
}; };
#[cfg(feature = "server")]
use lightyear::prelude::{NetworkTarget, Replicate};
use std::f32::consts::PI; use std::f32::consts::PI;
#[derive(Event, Reflect)] #[derive(Event, Reflect)]
@@ -23,7 +25,7 @@ pub fn plugin(app: &mut App) {
global_observer!(app, on_spawn_key); global_observer!(app, on_spawn_key);
} }
fn on_spawn_key(trigger: Trigger<KeySpawn>, mut commands: Commands, assets: Res<GameAssets>) { fn on_spawn_key(trigger: Trigger<KeySpawn>, mut commands: Commands) {
let KeySpawn(position, id) = trigger.event(); let KeySpawn(position, id) = trigger.event();
let id = id.clone(); let id = id.clone();
@@ -42,11 +44,7 @@ fn on_spawn_key(trigger: Trigger<KeySpawn>, mut commands: Commands, assets: Res<
CollisionLayers::new(GameLayer::CollectiblePhysics, GameLayer::Level), CollisionLayers::new(GameLayer::CollectiblePhysics, GameLayer::Level),
Restitution::new(0.6), Restitution::new(0.6),
Children::spawn(( Children::spawn((
Spawn(( Spawn((Billboard::All, SquishAnimation(2.6), GltfSceneRoot::Key)),
Billboard::All,
SquishAnimation(2.6),
SceneRoot(assets.mesh_key.clone()),
)),
SpawnWith(|parent: &mut RelatedSpawner<ChildOf>| { SpawnWith(|parent: &mut RelatedSpawner<ChildOf>| {
parent parent
.spawn(( .spawn((
@@ -59,6 +57,8 @@ fn on_spawn_key(trigger: Trigger<KeySpawn>, mut commands: Commands, assets: Res<
.observe(on_collect_key); .observe(on_collect_key);
}), }),
)), )),
#[cfg(feature = "server")]
Replicate::to_clients(NetworkTarget::All),
)); ));
} }

View File

@@ -1,26 +1,25 @@
use crate::{ use crate::{
GameState, GameState, character::Character, global_observer, loading_assets::GameAssets,
utils::billboards::Billboard,
};
#[cfg(feature = "server")]
use crate::{
ai::Ai, ai::Ai,
character::{AnimatedCharacter, Character}, character::AnimatedCharacter,
global_observer,
head::ActiveHead, head::ActiveHead,
head_drop::HeadDrops, head_drop::HeadDrops,
heads::{ActiveHeads, HEAD_COUNT, HeadState}, heads::{ActiveHeads, HEAD_COUNT, HeadState},
heads_database::HeadsDatabase, heads_database::HeadsDatabase,
hitpoints::{Hitpoints, Kill}, hitpoints::{Hitpoints, Kill},
keys::KeySpawn, keys::KeySpawn,
loading_assets::GameAssets,
sounds::PlaySound, sounds::PlaySound,
tb_entities::EnemySpawn, tb_entities::EnemySpawn,
utils::billboards::Billboard,
}; };
use bevy::{pbr::NotShadowCaster, prelude::*}; use bevy::{pbr::NotShadowCaster, prelude::*};
use lightyear::prelude::Disconnected;
#[cfg(feature = "client")]
use lightyear::prelude::{Client, Connected};
#[cfg(feature = "server")] #[cfg(feature = "server")]
use lightyear::prelude::{NetworkTarget, Replicate}; use lightyear::prelude::{NetworkTarget, Replicate};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "server")]
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)] #[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
@@ -38,67 +37,44 @@ struct NpcSpawning {
#[reflect(Component)] #[reflect(Component)]
pub struct SpawningBeam(pub f32); pub struct SpawningBeam(pub f32);
#[cfg(feature = "server")]
#[derive(Event)] #[derive(Event)]
struct OnCheckSpawns { struct OnCheckSpawns;
on_client: bool,
}
#[derive(Event)] #[derive(Event)]
pub struct SpawnCharacter(pub Vec3); pub struct SpawnCharacter(pub Vec3);
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.init_resource::<NpcSpawning>(); app.init_resource::<NpcSpawning>();
#[cfg(feature = "server")]
app.add_systems(FixedUpdate, setup.run_if(in_state(GameState::Playing))); app.add_systems(FixedUpdate, setup.run_if(in_state(GameState::Playing)));
app.add_systems(Update, update_beams.run_if(in_state(GameState::Playing))); app.add_systems(Update, update_beams.run_if(in_state(GameState::Playing)));
#[cfg(feature = "server")]
global_observer!(app, on_spawn_check); global_observer!(app, on_spawn_check);
global_observer!(app, on_spawn); global_observer!(app, on_spawn);
} }
fn setup( #[cfg(feature = "server")]
mut commands: Commands, fn setup(mut commands: Commands, mut spawned: Local<bool>) {
#[cfg(feature = "client")] client: Query<
(Option<&Connected>, Option<&Disconnected>),
With<Client>,
>,
mut spawned: Local<bool>,
) {
if *spawned { if *spawned {
return; return;
} }
#[cfg(feature = "server")] commands.init_resource::<NpcSpawning>();
{ commands.trigger(OnCheckSpawns);
commands.init_resource::<NpcSpawning>();
commands.trigger(OnCheckSpawns { on_client: false });
*spawned = true; *spawned = true;
}
#[cfg(feature = "client")]
if let Ok((connected, disconnected)) = client.single()
&& (connected.is_some() || disconnected.is_some())
{
commands.init_resource::<NpcSpawning>();
commands.trigger(OnCheckSpawns {
on_client: disconnected.is_some(),
});
*spawned = true;
}
} }
#[cfg(feature = "server")]
fn on_spawn_check( fn on_spawn_check(
trigger: Trigger<OnCheckSpawns>, _trigger: Trigger<OnCheckSpawns>,
mut commands: Commands, mut commands: Commands,
query: Query<(Entity, &EnemySpawn, &Transform), Without<Npc>>, query: Query<(Entity, &EnemySpawn, &Transform), Without<Npc>>,
heads_db: Res<HeadsDatabase>, heads_db: Res<HeadsDatabase>,
spawning: Res<NpcSpawning>, spawning: Res<NpcSpawning>,
) { ) {
if cfg!(not(feature = "server")) && !trigger.event().on_client {
return;
}
//TODO: move into HeadsDatabase //TODO: move into HeadsDatabase
let mut names: HashMap<String, usize> = HashMap::default(); let mut names: HashMap<String, usize> = HashMap::default();
for i in 0..HEAD_COUNT { for i in 0..HEAD_COUNT {
@@ -126,6 +102,8 @@ fn on_spawn_check(
None, None,
None, None,
]), ]),
#[cfg(feature = "server")]
Replicate::to_clients(NetworkTarget::All),
)) ))
.insert_if(Ai, || !spawn.disable_ai) .insert_if(Ai, || !spawn.disable_ai)
.with_child((Name::from("body-rig"), AnimatedCharacter::new(id))) .with_child((Name::from("body-rig"), AnimatedCharacter::new(id)))
@@ -138,9 +116,9 @@ fn on_spawn_check(
} }
} }
#[cfg(feature = "server")]
fn on_kill( fn on_kill(
trigger: Trigger<Kill>, trigger: Trigger<Kill>,
disconnected: Option<Single<&Disconnected>>,
mut commands: Commands, mut commands: Commands,
query: Query<(&Transform, &EnemySpawn, &ActiveHead)>, query: Query<(&Transform, &EnemySpawn, &ActiveHead)>,
) { ) {
@@ -155,9 +133,7 @@ fn on_kill(
} }
commands.trigger(HeadDrops::new(transform.translation, head.0)); commands.trigger(HeadDrops::new(transform.translation, head.0));
commands.trigger(OnCheckSpawns { commands.trigger(OnCheckSpawns);
on_client: cfg!(feature = "server") || disconnected.is_some(),
});
commands.entity(trigger.target()).despawn(); commands.entity(trigger.target()).despawn();

View File

@@ -9,10 +9,12 @@ use crate::{
controller_common::{MovementSpeedFactor, PlayerCharacterController}, controller_common::{MovementSpeedFactor, PlayerCharacterController},
controls::ControllerSettings, controls::ControllerSettings,
}, },
cutscene::StartCutscene,
global_observer, global_observer,
head::ActiveHead, head::ActiveHead,
heads::ActiveHeads, heads::ActiveHeads,
loading_assets::GameAssets, hitpoints::Hitpoints,
loading_assets::{GameAssets, HeadDropAssets},
player::{Player, PlayerBodyMesh}, player::{Player, PlayerBodyMesh},
utils::triggers::TriggerAppExt, utils::triggers::TriggerAppExt,
}; };
@@ -57,6 +59,7 @@ pub fn plugin(app: &mut App) {
app.register_component::<Grounding>(); app.register_component::<Grounding>();
app.register_component::<GroundingConfig>(); app.register_component::<GroundingConfig>();
app.register_component::<GroundingState>(); app.register_component::<GroundingState>();
app.register_component::<Hitpoints>();
app.register_component::<KinematicVelocity>(); app.register_component::<KinematicVelocity>();
app.register_component::<LinearVelocity>(); app.register_component::<LinearVelocity>();
app.register_component::<MoveInput>(); app.register_component::<MoveInput>();
@@ -84,6 +87,7 @@ pub fn plugin(app: &mut App) {
}); });
app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>(); app.replicate_trigger::<BuildExplosionSprite, ActionsChannel>();
app.replicate_trigger::<StartCutscene, ActionsChannel>();
global_observer!(app, spawn_gltf_scene_roots); global_observer!(app, spawn_gltf_scene_roots);
} }
@@ -96,26 +100,40 @@ fn transform_should_rollback(this: &Transform, that: &Transform) -> bool {
#[reflect(Component)] #[reflect(Component)]
pub enum GltfSceneRoot { pub enum GltfSceneRoot {
Projectile(String), Projectile(String),
HeadDrop(String),
Key,
} }
fn spawn_gltf_scene_roots( fn spawn_gltf_scene_roots(
trigger: Trigger<OnAdd, GltfSceneRoot>, trigger: Trigger<OnAdd, GltfSceneRoot>,
mut commands: Commands, mut commands: Commands,
gltf_roots: Query<&GltfSceneRoot>, gltf_roots: Query<&GltfSceneRoot>,
head_drop_assets: Res<HeadDropAssets>,
assets: Res<GameAssets>, assets: Res<GameAssets>,
gltfs: Res<Assets<Gltf>>, gltfs: Res<Assets<Gltf>>,
) -> Result { ) -> Result {
let root = gltf_roots.get(trigger.target())?; let root = gltf_roots.get(trigger.target())?;
let (gltf, index) = match root { let get_scene = |gltf: Handle<Gltf>, index: usize| {
GltfSceneRoot::Projectile(addr) => ( let gltf = gltfs.get(&gltf).unwrap();
gltf.scenes[index].clone()
};
let scene = match root {
GltfSceneRoot::Projectile(addr) => get_scene(
assets.projectiles[format!("{addr}.glb").as_str()].clone(), assets.projectiles[format!("{addr}.glb").as_str()].clone(),
0, 0,
), ),
GltfSceneRoot::HeadDrop(addr) => {
let gltf = head_drop_assets
.meshes
.get(format!("{addr}.glb").as_str())
.cloned();
let gltf = gltf.unwrap_or_else(|| head_drop_assets.meshes["none.glb"].clone());
get_scene(gltf, 0)
}
GltfSceneRoot::Key => assets.mesh_key.clone(),
}; };
let gltf = gltfs.get(&gltf).unwrap();
let scene = gltf.scenes[index].clone();
commands.entity(trigger.target()).insert(SceneRoot(scene)); commands.entity(trigger.target()).insert(SceneRoot(scene));

View File

@@ -15,8 +15,9 @@ run *args:
server: server:
RUST_BACKTRACE=1 cargo r {{server_args}} RUST_BACKTRACE=1 cargo r {{server_args}}
dbg: dbg *args:
RUST_BACKTRACE=1 cargo r {{client_args}},dbg cargo b {{server_args}},dbg
RUST_BACKTRACE=1 cargo r {{client_args}},dbg -- {{args}}
dbg-server: dbg-server:
RUST_BACKTRACE=1 cargo r {{server_args}},dbg RUST_BACKTRACE=1 cargo r {{server_args}},dbg