Cutscene + Items + Target sync (#64)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user