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 bevy::prelude::*;
use marker::MarkerEvent;
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
#[derive(Component, Reflect, Default, Deref)]
#[derive(Component, Reflect, Default, Deref, PartialEq, Serialize, Deserialize)]
#[reflect(Component)]
pub struct AimTarget(pub Option<Entity>);
#[derive(Component, Reflect)]
#[derive(Component, Reflect, PartialEq, Serialize, Deserialize)]
#[reflect(Component)]
#[require(AimTarget)]
pub struct AimState {
@@ -39,6 +40,9 @@ impl Default for AimState {
}
pub fn plugin(app: &mut App) {
app.register_type::<AimState>();
app.register_type::<AimTarget>();
app.add_plugins(target_ui::plugin);
app.add_plugins(marker::plugin);
@@ -71,65 +75,66 @@ 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), With<Player>>,
mut player_aim: Query<(Entity, &AimState, &mut AimTarget, &Children), With<Player>>,
spatial_query: SpatialQuery,
) {
let Some((player, state, mut aim_target)) = player_aim.iter_mut().next() else {
return;
};
for (player, state, mut aim_target, children) in player_aim.iter_mut() {
assert_eq!(
children.len(),
1,
"expected player to have one direct child"
);
let Some((player_pos, player_forward)) = player_rot
.iter()
.next()
.map(|(t, global)| (global.translation(), t.forward()))
else {
return;
};
let (player_pos, player_forward) = player_rot
.get(*children.first().unwrap())
.map(|(t, global)| (global.translation(), t.forward()))
.unwrap();
let mut new_target = None;
let mut target_distance = f32::MAX;
let mut new_target = None;
let mut target_distance = f32::MAX;
for (e, t) in potential_targets.iter() {
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) {
for (e, t) in potential_targets.iter() {
if e == player {
continue;
}
new_target = Some(e);
target_distance = distance;
}
}
let delta = player_pos - t.translation;
if let Some(e) = &aim_target.0
&& commands.get_entity(*e).is_err()
{
aim_target.0 = None;
return;
}
let distance = delta.length();
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);
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;
}
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_trenchbroom::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Event, Debug)]
#[derive(Clone, Debug, Event, Serialize, Deserialize)]
pub struct StartCutscene(pub String);
#[derive(Resource, Debug, Default)]

View File

@@ -1,6 +1,6 @@
use crate::{
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,
};
use avian3d::prelude::*;
@@ -8,6 +8,8 @@ use bevy::{
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
prelude::*,
};
#[cfg(feature = "server")]
use lightyear::prelude::{NetworkTarget, Replicate};
use std::f32::consts::PI;
#[derive(Event, Reflect)]
@@ -77,9 +79,7 @@ fn spawn(mut commands: Commands, query: Query<(Entity, &GlobalTransform, &Secret
fn on_head_drop(
trigger: Trigger<HeadDrops>,
mut commands: Commands,
assets: Res<HeadDropAssets>,
heads_db: Res<HeadsDatabase>,
gltf_assets: Res<Assets<Gltf>>,
time: Res<Time>,
) -> Result<(), BevyError> {
let drop = trigger.event();
@@ -91,13 +91,7 @@ fn on_head_drop(
commands.trigger(PlaySound::HeadDrop);
}
let ability = format!("{:?}.glb", 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")?;
let mesh_addr = format!("{:?}", heads_db.head_stats(drop.head_id).ability).to_lowercase();
commands
.spawn((
@@ -128,6 +122,8 @@ fn on_head_drop(
.observe(on_collect_head);
}
})),
#[cfg(feature = "server")]
Replicate::to_clients(NetworkTarget::All),
))
.insert_if(
ExternalImpulse::new(spawn_dir * 180.).with_persistence(false),
@@ -136,7 +132,7 @@ fn on_head_drop(
.with_child((
Billboard::All,
SquishAnimation(2.6),
SceneRoot(mesh.scenes[0].clone()),
GltfSceneRoot::HeadDrop(mesh_addr),
));
Ok(())

View File

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

View File

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

View File

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

View File

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

View File

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