mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-26 02:11:39 -04:00
Merge treuenten gameplay, effects and map-loading fixes
Includes PRs #100, #101, #102, #103, #104, #105, #106, #107 and #108.
This commit is contained in:
@@ -274,6 +274,8 @@ pub struct OwnedFxElemDef {
|
||||
pub trail_def: Option<OwnedFxTrailDef>,
|
||||
|
||||
pub spark_fountain_def: Option<OwnedFxSparkFountainDef>,
|
||||
|
||||
pub phys_pieces: Vec<Option<fx_iw4::FxPhysPiece>>,
|
||||
}
|
||||
|
||||
impl OwnedFxElemDef {
|
||||
@@ -338,6 +340,11 @@ impl OwnedFxElemDef {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn phys_piece(&self, random_seed: u64) -> Option<fx_iw4::FxPhysPiece> {
|
||||
let idx = fx_iw4::elem_visual_index(self.view.visual_count, random_seed);
|
||||
self.phys_pieces.get(idx).copied().flatten()
|
||||
}
|
||||
|
||||
pub fn decal_mark_pair(&self, random_seed: u64) -> Option<&OwnedFxVisual> {
|
||||
if self.view.elem_type != elem_type::DECAL {
|
||||
return None;
|
||||
@@ -953,6 +960,15 @@ impl FxDefinitions {
|
||||
*edge = model_hint_edge(ns, hint.as_deref(), models);
|
||||
}
|
||||
}
|
||||
elem.phys_pieces =
|
||||
if fx_iw4::elem_skips_position_update(elem.view.elem_type, elem.view.flags) {
|
||||
elem.visuals
|
||||
.iter()
|
||||
.map(|vis| model_phys_piece(vis, models))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1446,6 +1462,7 @@ fn capture_elem(
|
||||
has_extended,
|
||||
trail_def,
|
||||
spark_fountain_def,
|
||||
phys_pieces: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1695,6 +1712,17 @@ fn model_hint_edge(
|
||||
}
|
||||
}
|
||||
|
||||
fn model_phys_piece(
|
||||
vis: &OwnedFxVisual,
|
||||
models: &crate::FxModelCatalog,
|
||||
) -> Option<fx_iw4::FxPhysPiece> {
|
||||
let OwnedFxVisual::Model { edge, .. } = vis else {
|
||||
return None;
|
||||
};
|
||||
let (mid, half) = models.get_at(edge.bound_index()?)?.skel.bounds?;
|
||||
fx_iw4::FxPhysPiece::from_bounds(mid, half)
|
||||
}
|
||||
|
||||
fn resolve_material_visual(
|
||||
s: &impl FxRead,
|
||||
materials: &MaterialCatalog,
|
||||
@@ -2056,6 +2084,7 @@ fn leftover_capture_elem_t5(
|
||||
has_extended,
|
||||
trail_def: None,
|
||||
spark_fountain_def: None,
|
||||
phys_pieces: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2544,5 +2573,6 @@ fn capture_elem_t6(
|
||||
has_extended: false,
|
||||
trail_def: None,
|
||||
spark_fountain_def: None,
|
||||
phys_pieces: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -552,6 +552,23 @@ pub const HUD_CHROME_MENUS: &[&str] = &[
|
||||
"perks_info_hd",
|
||||
];
|
||||
|
||||
fn cover_wide_hud_overlays(catalog: &mut MenuCatalog) {
|
||||
for name in HUD_CHROME_MENUS {
|
||||
let Some(menu) = catalog.menus.get_mut(*name) else {
|
||||
continue;
|
||||
};
|
||||
for item in &mut menu.items {
|
||||
let rect = &item.rect;
|
||||
if !item.background.is_empty()
|
||||
&& (rect.x, rect.y, rect.w, rect.h) == (-427.0, -240.0, 854.0, 480.0)
|
||||
&& (rect.horz_align, rect.vert_align) == (2, 2)
|
||||
{
|
||||
item.cover = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui_games_root(games: &GamesRoot) -> Result<GamesRoot, String> {
|
||||
let anchor = find_zone_file(games, "iw4:common_mp")?;
|
||||
game_root_for_zone(&anchor.path).map(GamesRoot)
|
||||
@@ -1025,6 +1042,7 @@ impl MenuSink {
|
||||
|
||||
fn finish_and_take(mut self, games: Option<&Path>) -> MenuCatalog {
|
||||
self.finish_ui_images(games);
|
||||
cover_wide_hud_overlays(&mut self.catalog);
|
||||
self.catalog
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ pub struct MatchLoadRequest {
|
||||
struct MatchLoadTask {
|
||||
task: Task<Result<Option<PreparedMatchReady>, MapLoadFailed>>,
|
||||
request_id: u64,
|
||||
load_key: frame::LocalLoadKey,
|
||||
zone: String,
|
||||
progress: LoadProgress,
|
||||
}
|
||||
|
||||
@@ -141,6 +143,7 @@ fn start_match_load(
|
||||
let load_key = request.load_key;
|
||||
let zone = request.zone.clone();
|
||||
let zone_accepted = zone.clone();
|
||||
let zone_task = zone.clone();
|
||||
let cancel_handle = progress.clone();
|
||||
// The process outlives the overlay and every task this spawns: it is what
|
||||
// the table reads, what admission finishes, and what the bench still has
|
||||
@@ -224,6 +227,8 @@ fn start_match_load(
|
||||
MatchLoadTask {
|
||||
task,
|
||||
request_id,
|
||||
load_key,
|
||||
zone: zone_task,
|
||||
progress: cancel_handle,
|
||||
},
|
||||
frame::MatchScope::Loading,
|
||||
@@ -356,8 +361,13 @@ fn poll_match_load(
|
||||
task.request_id
|
||||
);
|
||||
}
|
||||
let Some(outcome) = future::block_on(future::poll_once(&mut task.task)) else {
|
||||
return;
|
||||
let polled = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
future::block_on(future::poll_once(&mut task.task))
|
||||
}));
|
||||
let outcome = match polled {
|
||||
Ok(None) => return,
|
||||
Ok(Some(outcome)) => outcome,
|
||||
Err(panic) => Err(walk_panic_failure(task, panic.as_ref())),
|
||||
};
|
||||
let request_id = task.request_id;
|
||||
inflight.0 = false;
|
||||
@@ -418,6 +428,26 @@ fn poll_match_load(
|
||||
});
|
||||
}
|
||||
|
||||
fn walk_panic_failure(task: &MatchLoadTask, panic: &(dyn std::any::Any + Send)) -> MapLoadFailed {
|
||||
let message = panic
|
||||
.downcast_ref::<&str>()
|
||||
.copied()
|
||||
.or_else(|| panic.downcast_ref::<String>().map(String::as_str))
|
||||
.unwrap_or("no message");
|
||||
diag::error!(
|
||||
World,
|
||||
"match load: walk request #{} (`{}`) panicked: {message} — reported as a failed map load",
|
||||
task.request_id,
|
||||
task.zone
|
||||
);
|
||||
MapLoadFailed {
|
||||
request_id: task.request_id,
|
||||
load_key: task.load_key,
|
||||
zone: task.zone.clone(),
|
||||
error: format!("The load stopped on an internal error: {message}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn expire_match_load_abort(
|
||||
mut commands: Commands,
|
||||
abort: Option<Res<MatchLoadAbort>>,
|
||||
|
||||
@@ -205,7 +205,7 @@ pub struct FxElemLightInstance {
|
||||
pub is_spot: bool,
|
||||
pub origin: [f32; 3],
|
||||
pub radius: f32,
|
||||
pub color_bgr: [f32; 3],
|
||||
pub color_rgb: [f32; 3],
|
||||
|
||||
pub axis: [[f32; 3]; 3],
|
||||
}
|
||||
|
||||
@@ -37,7 +37,9 @@ pub use marks::{
|
||||
FxMarksSystemHost, GfxMarkMeshCensus, GfxMarkMeshSurf, MarkImpactRequest, MarkImpactResult,
|
||||
MarkReceiverEnable, MarkTraceRecord,
|
||||
};
|
||||
pub use motion::{evaluate_elem_collide_motion, evaluate_elem_motion};
|
||||
pub use motion::{
|
||||
evaluate_elem_collide_motion, evaluate_elem_motion, evaluate_elem_physics_motion,
|
||||
};
|
||||
pub use play::{
|
||||
FxPlayPose, FxPlayRequest, PlayResult, axis_from_hit_normal, axis_from_impact_velocity,
|
||||
play_at_origin, play_bolted, play_oriented, spawn_impact_or_death_effect, spawn_oriented,
|
||||
|
||||
+145
-7
@@ -1,10 +1,11 @@
|
||||
use fx_iw4::{
|
||||
FX_ELEM_AT_REST_NONE, collide_marks_at_rest, collide_substep_schedule,
|
||||
collision_reflect_base_vel_delta, elem_dies_on_touch, elem_gravity_accel_z,
|
||||
elem_update_has_velocity_graph, elem_uses_collision, elem_uses_vel_local, elem_uses_vel_world,
|
||||
get_at_rest_fraction, get_velocity_at_time_sampled, impact_child_speed_allows,
|
||||
integrate_velocity_graph_sampled, orientation_pos_from_world, orientation_pos_to_world,
|
||||
sample_reflection_factor, trace_mask, vec3_length_sq,
|
||||
FX_ELEM_AT_REST_NONE, FX_PHYS_PIECE_HALF, collide_marks_at_rest, collide_on_ground,
|
||||
collide_substep_schedule, collision_reflect_base_vel_delta, elem_dies_on_touch,
|
||||
elem_gravity_accel_z, elem_launch_velocity, elem_update_has_velocity_graph,
|
||||
elem_uses_collision, elem_uses_vel_local, elem_uses_vel_world, get_at_rest_fraction,
|
||||
get_velocity_at_time_sampled, impact_child_speed_allows, integrate_velocity_graph_sampled,
|
||||
orientation_pos_from_world, orientation_pos_to_world, sample_reflection_factor, trace_mask,
|
||||
vec3_length_sq,
|
||||
};
|
||||
|
||||
use crate::update::{FxElemMotionQuery, FxElemMotionResult, FxElemTraceHit, FxImpactSpawn};
|
||||
@@ -41,6 +42,7 @@ pub fn evaluate_elem_collide_motion(
|
||||
coll_maxs: [f32; 3],
|
||||
use_item_clip: bool,
|
||||
has_effect_on_impact: bool,
|
||||
leaves_start_solid: bool,
|
||||
vel_graph_local: &[fx_iw4::FxElemVec3Range],
|
||||
vel_graph_world: &[fx_iw4::FxElemVec3Range],
|
||||
q: FxElemMotionQuery<'_>,
|
||||
@@ -125,7 +127,7 @@ pub fn evaluate_elem_collide_motion(
|
||||
orientation_pos_to_world(q.orient.origin, q.orient.axis, end_local),
|
||||
);
|
||||
let hit = trace(start, end, coll_mins, coll_maxs, mask);
|
||||
if hit.startsolid || hit.allsolid {
|
||||
if hit.allsolid || (hit.startsolid && !leaves_start_solid) {
|
||||
base_vel = [0.0; 3];
|
||||
break;
|
||||
}
|
||||
@@ -224,6 +226,142 @@ pub fn evaluate_elem_collide_motion(
|
||||
})
|
||||
}
|
||||
|
||||
const FX_PHYS_GRAVITY: f32 = 800.0;
|
||||
const FX_PHYS_BOUNCE: f32 = 0.2;
|
||||
const FX_PHYS_FRICTION: f32 = 0.5;
|
||||
const FX_PHYS_REST_SPEED: f32 = 20.0;
|
||||
const FX_PHYS_UNSTICK_MAX_LIFT: f32 = 16.0;
|
||||
const FX_PHYS_CONTACT_SLOP: f32 = 0.125;
|
||||
|
||||
pub fn evaluate_elem_physics_motion(
|
||||
flags: i32,
|
||||
use_item_clip: bool,
|
||||
vel_graph_local: &[fx_iw4::FxElemVec3Range],
|
||||
vel_graph_world: &[fx_iw4::FxElemVec3Range],
|
||||
q: FxElemMotionQuery<'_>,
|
||||
mut trace: impl FnMut([f32; 3], [f32; 3], [f32; 3], [f32; 3], u32) -> FxElemTraceHit,
|
||||
) -> FxElemMotionResult {
|
||||
let mut landed = (q.at_rest_fraction != FX_ELEM_AT_REST_NONE).then_some(q.at_rest_fraction);
|
||||
let mut result = FxElemMotionResult {
|
||||
origin_delta: [0.0; 3],
|
||||
base_vel: q.base_vel,
|
||||
remove: false,
|
||||
at_rest_fraction: landed,
|
||||
spawn_impact: None,
|
||||
};
|
||||
if landed.is_some() && q.base_vel == [0.0; 3] {
|
||||
return result;
|
||||
}
|
||||
let from = q.prev_msec.max(q.msec_begin);
|
||||
if q.msec_now <= from {
|
||||
return result;
|
||||
}
|
||||
let mut vel = if q.prev_msec <= q.msec_begin {
|
||||
elem_launch_velocity(
|
||||
flags,
|
||||
vel_graph_local,
|
||||
vel_graph_world,
|
||||
q.orient.axis,
|
||||
q.motion_random.velocity,
|
||||
)
|
||||
} else {
|
||||
q.base_vel
|
||||
};
|
||||
let mask = trace_mask(use_item_clip);
|
||||
let (mins, maxs) = ([-FX_PHYS_PIECE_HALF; 3], [FX_PHYS_PIECE_HALF; 3]);
|
||||
let mut pos = orientation_pos_to_world(q.orient.origin, q.orient.axis, q.origin);
|
||||
for step in collide_substep_schedule(from, q.msec_now) {
|
||||
let dt = (step.msec_end.saturating_sub(step.msec_start)).max(0) as f32 * 0.001;
|
||||
vel[2] -= FX_PHYS_GRAVITY * dt;
|
||||
let end = [
|
||||
pos[0] + vel[0] * dt,
|
||||
pos[1] + vel[1] * dt,
|
||||
pos[2] + vel[2] * dt,
|
||||
];
|
||||
let hit = trace(pos, end, mins, maxs, mask);
|
||||
if hit.allsolid {
|
||||
vel = [0.0; 3];
|
||||
match lift_out_of_solid(pos, mins, maxs, mask, &mut trace) {
|
||||
Some(lifted) => {
|
||||
pos = lifted;
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
landed.get_or_insert(at_rest_byte(step.msec_start, q.msec_begin, q.life_ms));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if hit.fraction >= 1.0 {
|
||||
pos = end;
|
||||
continue;
|
||||
}
|
||||
pos = [
|
||||
pos[0] + (end[0] - pos[0]) * hit.fraction,
|
||||
pos[1] + (end[1] - pos[1]) * hit.fraction,
|
||||
pos[2] + (end[2] - pos[2]) * hit.fraction,
|
||||
];
|
||||
let n = hit.normal;
|
||||
let into = dot(vel, n);
|
||||
if into < 0.0 {
|
||||
let push = (1.0 + FX_PHYS_BOUNCE) * into;
|
||||
for (v, n) in vel.iter_mut().zip(n) {
|
||||
*v -= push * n;
|
||||
}
|
||||
let along = dot(vel, n);
|
||||
for (v, n) in vel.iter_mut().zip(n) {
|
||||
let normal_part = along * n;
|
||||
*v = normal_part + (*v - normal_part) * (1.0 - FX_PHYS_FRICTION);
|
||||
}
|
||||
}
|
||||
if collide_on_ground(n[2]) {
|
||||
landed.get_or_insert(at_rest_byte(step.msec_end, q.msec_begin, q.life_ms));
|
||||
if vec3_length_sq(vel) < FX_PHYS_REST_SPEED * FX_PHYS_REST_SPEED {
|
||||
vel = [0.0; 3];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let stored = orientation_pos_from_world(q.orient.origin, q.orient.axis, pos);
|
||||
result.origin_delta = [
|
||||
stored[0] - q.origin[0],
|
||||
stored[1] - q.origin[1],
|
||||
stored[2] - q.origin[2],
|
||||
];
|
||||
result.base_vel = vel;
|
||||
result.at_rest_fraction = landed;
|
||||
result
|
||||
}
|
||||
|
||||
fn lift_out_of_solid(
|
||||
pos: [f32; 3],
|
||||
mins: [f32; 3],
|
||||
maxs: [f32; 3],
|
||||
mask: u32,
|
||||
trace: &mut impl FnMut([f32; 3], [f32; 3], [f32; 3], [f32; 3], u32) -> FxElemTraceHit,
|
||||
) -> Option<[f32; 3]> {
|
||||
let mut dz = -mins[2] + FX_PHYS_CONTACT_SLOP;
|
||||
let cap = dz.max(FX_PHYS_UNSTICK_MAX_LIFT);
|
||||
while dz <= cap {
|
||||
let lifted = [pos[0], pos[1], pos[2] + dz];
|
||||
let probe = trace(lifted, lifted, mins, maxs, mask);
|
||||
if !probe.startsolid && !probe.allsolid {
|
||||
return Some(lifted);
|
||||
}
|
||||
dz *= 2.0;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn at_rest_byte(msec: i32, msec_begin: i32, life_ms: f32) -> u8 {
|
||||
let frac = get_at_rest_fraction(msec as f32, msec_begin as f32, 1.0 / life_ms.max(1.0));
|
||||
frac.clamp(0.0, 254.0) as u8
|
||||
}
|
||||
|
||||
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
fn integrate_free_flight(
|
||||
flags: i32,
|
||||
gravity_base: f32,
|
||||
|
||||
+15
-3
@@ -860,6 +860,14 @@ impl FxSystemHost {
|
||||
Some(idx as u8)
|
||||
}
|
||||
|
||||
fn free_bolt_record(&mut self, index: u8) {
|
||||
let Some(next) = self.bolt_next.get_mut(usize::from(index)) else {
|
||||
return;
|
||||
};
|
||||
*next = self.bolt_first_free;
|
||||
self.bolt_first_free = i32::from(index);
|
||||
}
|
||||
|
||||
pub fn spawn_effect(
|
||||
&mut self,
|
||||
def_name: &str,
|
||||
@@ -873,6 +881,9 @@ impl FxSystemHost {
|
||||
if wants_spotlight && self.spotlight_count >= FX_SPOT_LIGHT_LIMIT {
|
||||
return Err(SpawnFail::TooManySpotlights);
|
||||
}
|
||||
if self.ring_occupancy() >= FX_EFFECT_HANDLE_RING_SIZE as i32 {
|
||||
return Err(SpawnFail::RingFull);
|
||||
}
|
||||
let bolt_index = if bolt == FX_SPAWN_BOLT_NONE {
|
||||
0xff
|
||||
} else {
|
||||
@@ -883,9 +894,6 @@ impl FxSystemHost {
|
||||
};
|
||||
|
||||
let alloc = self.first_free_effect;
|
||||
if self.ring_occupancy() >= FX_EFFECT_HANDLE_RING_SIZE as i32 {
|
||||
return Err(SpawnFail::RingFull);
|
||||
}
|
||||
self.first_free_effect = alloc.wrapping_add(1);
|
||||
|
||||
let ring_i = (alloc as u32 & (FX_EFFECT_HANDLE_RING_SIZE - 1)) as usize;
|
||||
@@ -986,6 +994,10 @@ impl FxSystemHost {
|
||||
self.set_handle_at_ring(active_index as u32, handle);
|
||||
active_index = active_index.wrapping_add(1);
|
||||
if let Some(slot) = handle_to_slot(handle) {
|
||||
let bolt = self.effects[slot].bolt;
|
||||
if bolt != 0xff {
|
||||
self.free_bolt_record(bolt);
|
||||
}
|
||||
self.effects[slot] = FxEffectSlot::default();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ use fx_iw4::{
|
||||
FX_ELEM_TYPE_SPARK_CLOUD, FX_ELEM_TYPE_SPARK_FOUNTAIN, FX_STATUS_HAS_PENDING_LOOP_ELEMS,
|
||||
FX_STATUS_REF_COUNT_MASK_IW4, FxUpdateEffectBolt, axis_to_quat,
|
||||
begin_iterating_over_effects_exclusive, bolt_compose_orientation, bolt_mark_lost,
|
||||
elem_norm_time, elem_uses_collision, end_iterating_over_effects, end_iterating_runs_gc,
|
||||
unit_quat_to_axis, update_effect_bolt, vector_vectors,
|
||||
elem_norm_time, elem_skips_position_update, elem_uses_collision, end_iterating_over_effects,
|
||||
end_iterating_runs_gc, unit_quat_to_axis, update_effect_bolt, vector_vectors,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -384,7 +384,7 @@ fn pending_collide_for_elem(
|
||||
if msec_now >= death {
|
||||
return None;
|
||||
}
|
||||
if !elem_uses_collision(elem.flags) {
|
||||
if !elem_uses_collision(elem.flags) && !elem_skips_position_update(elem.elem_type, elem.flags) {
|
||||
return None;
|
||||
}
|
||||
let def_index = elem.def_index;
|
||||
|
||||
@@ -119,3 +119,59 @@ pub fn collision_reflect_base_vel_delta(
|
||||
reflected[2] - pre_impact_vel[2],
|
||||
]
|
||||
}
|
||||
|
||||
pub const FX_PHYS_PIECE_HALF: f32 = 1.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct FxPhysPiece {
|
||||
flat_axis: usize,
|
||||
flat_mid: f32,
|
||||
flat_half: f32,
|
||||
}
|
||||
|
||||
impl FxPhysPiece {
|
||||
pub fn from_bounds(mid: [f32; 3], half: [f32; 3]) -> Option<Self> {
|
||||
if !mid.iter().chain(&half).all(|v| v.is_finite()) || half.iter().any(|v| *v <= 0.0) {
|
||||
return None;
|
||||
}
|
||||
let flat_axis = (0..3).min_by(|&a, &b| half[a].total_cmp(&half[b]))?;
|
||||
Some(Self {
|
||||
flat_axis,
|
||||
flat_mid: mid[flat_axis],
|
||||
flat_half: half[flat_axis],
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rest_pose(&self, axis: [[f32; 3]; 3], scale: f32) -> ([[f32; 3]; 3], f32) {
|
||||
let a = axis[self.flat_axis];
|
||||
let up = if a[2] >= 0.0 { 1.0 } else { -1.0 };
|
||||
let v = [a[1] * up, -a[0] * up, 0.0];
|
||||
let k = 1.0 / (1.0 + a[2] * up);
|
||||
let turn = |p: [f32; 3]| {
|
||||
let vxp = cross(v, p);
|
||||
let vxvxp = cross(v, vxp);
|
||||
[
|
||||
p[0] + vxp[0] + vxvxp[0] * k,
|
||||
p[1] + vxp[1] + vxvxp[1] * k,
|
||||
p[2] + vxp[2] + vxvxp[2] * k,
|
||||
]
|
||||
};
|
||||
let lowest = if up > 0.0 {
|
||||
self.flat_mid - self.flat_half
|
||||
} else {
|
||||
-(self.flat_mid + self.flat_half)
|
||||
};
|
||||
(
|
||||
[turn(axis[0]), turn(axis[1]), turn(axis[2])],
|
||||
-FX_PHYS_PIECE_HALF - lowest * scale,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
||||
[
|
||||
a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0],
|
||||
]
|
||||
}
|
||||
|
||||
@@ -94,11 +94,8 @@ pub fn cull_elem_light(
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn elem_light_color_bgr(color_rgba: [u8; 4], scale: f32) -> [f32; 3] {
|
||||
pub fn elem_light_color_rgb(color_rgba: [u8; 4], scale: f32) -> [f32; 3] {
|
||||
let s = scale * (crate::FX_RECIP_255 as f32);
|
||||
[
|
||||
color_rgba[2] as f32 * s,
|
||||
color_rgba[1] as f32 * s,
|
||||
color_rgba[0] as f32 * s,
|
||||
]
|
||||
let [r, g, b, _a] = color_rgba;
|
||||
[r as f32 * s, g as f32 * s, b as f32 * s]
|
||||
}
|
||||
|
||||
@@ -88,14 +88,15 @@ pub use bolt::{
|
||||
update_effect_bolt,
|
||||
};
|
||||
pub use collide::{
|
||||
FX_COLLIDE_SUBSTEP_MS, FX_COLLISION_REFLECT_SCALE, FX_IMPACT_CHILD_MIN_SPEED_SQ, FX_TRACE_MASK,
|
||||
FX_TRACE_MASK_ITEM_CLIP, FxCollideSubstep, collide_marks_at_rest, collide_on_ground,
|
||||
collide_substep_schedule, collision_reflect_base_vel_delta, impact_child_speed_allows,
|
||||
sample_reflection_factor, trace_mask,
|
||||
FX_COLLIDE_SUBSTEP_MS, FX_COLLISION_REFLECT_SCALE, FX_IMPACT_CHILD_MIN_SPEED_SQ,
|
||||
FX_PHYS_PIECE_HALF, FX_TRACE_MASK, FX_TRACE_MASK_ITEM_CLIP, FxCollideSubstep, FxPhysPiece,
|
||||
collide_marks_at_rest, collide_on_ground, collide_substep_schedule,
|
||||
collision_reflect_base_vel_delta, impact_child_speed_allows, sample_reflection_factor,
|
||||
trace_mask,
|
||||
};
|
||||
pub use cull::{
|
||||
FX_ELEM_FLAG_CULL_DRAW_5_PLANES, cull_cloud, cull_cloud_plane_count, cull_cloud_radius,
|
||||
cull_elem_for_spawn_allows, cull_elem_light, cull_sphere, elem_light_color_bgr,
|
||||
cull_elem_for_spawn_allows, cull_elem_light, cull_sphere, elem_light_color_rgb,
|
||||
};
|
||||
pub use draw::{FX_DRAW_ELEM_HANDLER_PRESENT, FxElemType, draw_elem_handler_present};
|
||||
pub use effect_def::FxEffectDef;
|
||||
@@ -341,7 +342,9 @@ pub use vec::{
|
||||
FX_PERP_VECTOR_UNIT, effect_orient_arc, perpendicular_vector, vec3_distance, vec3_length_sq,
|
||||
vec3_normalize, vector_vectors,
|
||||
};
|
||||
pub use velocity::{FX_VEL_AT_TIME_SCALE, get_velocity_at_time, get_velocity_at_time_sampled};
|
||||
pub use velocity::{
|
||||
FX_VEL_AT_TIME_SCALE, elem_launch_velocity, get_velocity_at_time, get_velocity_at_time_sampled,
|
||||
};
|
||||
pub use view::{
|
||||
FX_EFFECT_DEF_SIZE, FX_ELEM_DEF_STRIDE, FxEffectDefView, FxElemDefView, effect_def_view,
|
||||
elem_def_gravity_accel_z, elem_def_view, elem_def_view_x64,
|
||||
|
||||
@@ -75,3 +75,36 @@ pub fn get_velocity_at_time_sampled(
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn elem_launch_velocity(
|
||||
flags: i32,
|
||||
local_samples: &[crate::FxElemVec3Range],
|
||||
world_samples: &[crate::FxElemVec3Range],
|
||||
orient_axis: [[f32; 3]; 3],
|
||||
random: [f32; 3],
|
||||
) -> [f32; 3] {
|
||||
let mut out = [0.0f32; 3];
|
||||
if elem_uses_vel_world(flags) && world_samples.len() >= 2 {
|
||||
let s = sample_vel_graph_at_age_sampled(world_samples, 0.0, random);
|
||||
let scale = graph_speed_scale(world_samples.len());
|
||||
for (out, s) in out.iter_mut().zip(s) {
|
||||
*out += s * scale;
|
||||
}
|
||||
}
|
||||
if elem_uses_vel_local(flags) && local_samples.len() >= 2 {
|
||||
let s = sample_vel_graph_at_age_sampled(local_samples, 0.0, random);
|
||||
let scale = graph_speed_scale(local_samples.len());
|
||||
for (axis, out) in out.iter_mut().enumerate() {
|
||||
let w = s[0] * orient_axis[0][axis]
|
||||
+ s[1] * orient_axis[1][axis]
|
||||
+ s[2] * orient_axis[2][axis];
|
||||
*out += w * scale;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn graph_speed_scale(fenceposts: usize) -> f32 {
|
||||
(fenceposts - 1) as f32 * FX_VEL_AT_TIME_SCALE as f32
|
||||
}
|
||||
|
||||
@@ -948,10 +948,12 @@ fn push_stretch(
|
||||
);
|
||||
if rest.w > f32::EPSILON && rest.w < surface.width() {
|
||||
let grow = surface.width() / rest.w;
|
||||
let centre = applied.x + applied.w * 0.5;
|
||||
let centre_x = applied.x + applied.w * 0.5;
|
||||
let centre_y = applied.y + applied.h * 0.5;
|
||||
applied.w *= grow;
|
||||
applied.h *= grow;
|
||||
applied.x = centre - applied.w * 0.5;
|
||||
applied.x = centre_x - applied.w * 0.5;
|
||||
applied.y = centre_y - applied.h * 0.5;
|
||||
}
|
||||
}
|
||||
let color = style.fill_color(item);
|
||||
|
||||
@@ -106,7 +106,14 @@ impl WeaponbarExprHost<'_> {
|
||||
|
||||
impl ExprHost for WeaponbarExprHost<'_> {
|
||||
fn binding_label(&self, command: &str) -> Option<&str> {
|
||||
self.input?.binding_keys.get(command).map(String::as_str)
|
||||
let keys = &self.input?.binding_keys;
|
||||
keys.get(command)
|
||||
.or_else(|| match command {
|
||||
"+activate" => keys.get("+usereload"),
|
||||
"+usereload" => keys.get("+activate"),
|
||||
_ => None,
|
||||
})
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
fn ui_active(&self) -> Result<i32, ExprError> {
|
||||
|
||||
@@ -212,10 +212,10 @@ pub fn add_omni_light_to_scene_allows(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn omni_light_pack(origin: [f32; 3], radius: f32, color_bgr: [f32; 3]) -> GfxLightPack {
|
||||
pub fn omni_light_pack(origin: [f32; 3], radius: f32, color_rgb: [f32; 3]) -> GfxLightPack {
|
||||
GfxLightPack {
|
||||
light_type: GFX_LIGHT_TYPE_OMNI,
|
||||
color: color_bgr,
|
||||
color: color_rgb,
|
||||
direction: [0.0; 3],
|
||||
origin,
|
||||
radius,
|
||||
@@ -281,7 +281,7 @@ pub fn spot_light_pack(
|
||||
origin: [f32; 3],
|
||||
forward: [f32; 3],
|
||||
radius: f32,
|
||||
color_bgr: [f32; 3],
|
||||
color_rgb: [f32; 3],
|
||||
cone: SpotLightConeDvars,
|
||||
) -> GfxLightPack {
|
||||
let end = spot_light_clamp_end(cone.start_radius, cone.end_radius, radius);
|
||||
@@ -301,9 +301,9 @@ pub fn spot_light_pack(
|
||||
GfxLightPack {
|
||||
light_type: GFX_LIGHT_TYPE_SPOT,
|
||||
color: [
|
||||
color_bgr[0] * cone.brightness,
|
||||
color_bgr[1] * cone.brightness,
|
||||
color_bgr[2] * cone.brightness,
|
||||
color_rgb[0] * cone.brightness,
|
||||
color_rgb[1] * cone.brightness,
|
||||
color_rgb[2] * cone.brightness,
|
||||
],
|
||||
direction: dir,
|
||||
origin,
|
||||
|
||||
@@ -83,9 +83,9 @@ pub fn vec_to_yaw(x: f32, y: f32) -> f32 {
|
||||
|
||||
const VECT_TO_ANGLES_PITCH_SCALE: f32 = -57.2957763671875_f32;
|
||||
|
||||
const VECT_TO_ANGLES_PITCH_DOWN: f32 = 270.0;
|
||||
const VECT_TO_ANGLES_PITCH_DOWN: f32 = 90.0;
|
||||
|
||||
const VECT_TO_ANGLES_PITCH_UP: f32 = 90.0;
|
||||
const VECT_TO_ANGLES_PITCH_UP: f32 = 270.0;
|
||||
|
||||
pub fn vect_to_angles(forward: [f32; 3]) -> [f32; 3] {
|
||||
let [x, y, z] = forward;
|
||||
@@ -118,7 +118,7 @@ fn axis_to_angles_roll_atan(v: [f32; 3]) -> f32 {
|
||||
let [x, y, z] = v;
|
||||
if y == 0.0 && x == 0.0 {
|
||||
return if 0.0 <= -z {
|
||||
VECT_TO_ANGLES_PITCH_UP
|
||||
VECT_TO_ANGLES_PITCH_DOWN
|
||||
} else {
|
||||
AXIS_TO_ANGLES_ROLL_PITCH_NEG
|
||||
};
|
||||
|
||||
@@ -64,9 +64,11 @@ impl CEntityRuntime {
|
||||
return;
|
||||
};
|
||||
if !self.in_next_snap()
|
||||
|| !matches!(self.pose_e_type as i32, ET_PLAYER | ET_PLAYER_CORPSE)
|
||||
|| previous.pos.tr_type != TR_INTERPOLATE
|
||||
|| self.current.pos.tr_type != TR_INTERPOLATE
|
||||
|| !lerps_between_snapshots(
|
||||
self.pose_e_type as i32,
|
||||
previous.pos.tr_type,
|
||||
self.current.pos.tr_type,
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -92,6 +94,19 @@ impl CEntityRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
fn lerps_between_snapshots(e_type: i32, previous_tr_type: i32, current_tr_type: i32) -> bool {
|
||||
match e_type {
|
||||
ET_PLAYER_CORPSE => true,
|
||||
ET_PLAYER => previous_tr_type == TR_INTERPOLATE && current_tr_type == TR_INTERPOLATE,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn another_body(current: &EntityState, next: &EntityState) -> bool {
|
||||
next.e_type == ET_PLAYER_CORPSE
|
||||
&& (current.time2 != next.time2 || current.client_num != next.client_num)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PlayerDrawGate {
|
||||
pub eyes_entity_num: i32,
|
||||
@@ -246,8 +261,9 @@ pub fn apply_existing(rt: &mut CEntityRuntime, next: EntityState, at_time_ms: i3
|
||||
let was_in = rt.in_next_snap();
|
||||
let e_type_changed = rt.next_state.e_type != next.e_type;
|
||||
let teleport = (rt.next_state.e_flags ^ next.e_flags) & EFLAGS_TELEPORT != 0;
|
||||
if !was_in || e_type_changed || teleport {
|
||||
let new_entity = !was_in || e_type_changed;
|
||||
let other_body = another_body(&rt.next_state, &next);
|
||||
if !was_in || e_type_changed || teleport || other_body {
|
||||
let new_entity = !was_in || e_type_changed || other_body;
|
||||
reset_entity(rt, next, at_time_ms, new_entity);
|
||||
return;
|
||||
}
|
||||
@@ -335,6 +351,7 @@ pub fn corpse_slot_to_entity_state(slot: &PlayerCorpseSlot) -> EntityState {
|
||||
es.number = slot.entnum;
|
||||
es.e_type = ET_PLAYER_CORPSE;
|
||||
es.client_num = i32::try_from(slot.victim.0).unwrap_or(0);
|
||||
es.time2 = slot.spawn_time_ms;
|
||||
es.e_flags = slot.e_flags;
|
||||
es.tr_type = slot.tr_type;
|
||||
es.tr_time = slot.tr_time;
|
||||
|
||||
@@ -194,6 +194,10 @@ impl PresentedSnapshot {
|
||||
self.trajectory_time_ms.unwrap_or(render_time_ms)
|
||||
}
|
||||
|
||||
pub fn is_archived_replay(&self) -> bool {
|
||||
self.trajectory_time_ms.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn set_trajectory_sample(
|
||||
&mut self,
|
||||
time_ms: Option<i32>,
|
||||
|
||||
@@ -175,6 +175,6 @@ pub use transport::wire::{WireError, WireReader, WireWriter};
|
||||
|
||||
pub use svc_script_audio::SvcScriptAudio;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 116;
|
||||
pub const PROTOCOL_VERSION: u32 = 118;
|
||||
|
||||
pub use client::fire_verdict::{FireVerdictState, PredictedFireStatus};
|
||||
|
||||
@@ -86,7 +86,7 @@ pub(crate) fn play_script_seats(
|
||||
focus_life,
|
||||
focus,
|
||||
ends_at_ms: i32::MAX,
|
||||
final_kill: false,
|
||||
final_kill: seat.final_kill && seat.archive_ms > 0,
|
||||
started_at_ms,
|
||||
killcamoffset_ms: seat.archive_ms,
|
||||
predelay_ms: 0,
|
||||
|
||||
@@ -273,6 +273,13 @@ fn seat_snapshot(
|
||||
return (live.clone(), None);
|
||||
}
|
||||
let archived = lookup.tick.and_then(|tick| archive.frame(tick));
|
||||
overlay_archived_cameras(
|
||||
&mut out,
|
||||
live,
|
||||
archived.map(|f| &f.snapshot),
|
||||
viewer,
|
||||
session.focus_client,
|
||||
);
|
||||
overlay_killcam_hud(
|
||||
&mut out,
|
||||
live,
|
||||
@@ -341,6 +348,31 @@ fn overlay_archived_world(
|
||||
out
|
||||
}
|
||||
|
||||
fn overlay_archived_cameras(
|
||||
out: &mut Snapshot,
|
||||
live: &Snapshot,
|
||||
archived: Option<&Snapshot>,
|
||||
viewer: ClientId,
|
||||
focus: ClientId,
|
||||
) {
|
||||
let focus_row = archived.and_then(|snap| snap.meta.for_client(focus));
|
||||
let delta_ms = archived.map_or(0, |snap| {
|
||||
sim::level_time_ms(live.tick).wrapping_sub(sim::level_time_ms(snap.tick))
|
||||
});
|
||||
let linked_weapon_view = focus_row.and_then(|meta| meta.linked_weapon_view);
|
||||
let remote_missile =
|
||||
focus_row
|
||||
.and_then(|meta| meta.remote_missile)
|
||||
.map(|link| sim::RemoteMissile {
|
||||
unlink_at_ms: link.unlink_at_ms.map(|ms| ms.wrapping_add(delta_ms)),
|
||||
..link
|
||||
});
|
||||
if let Some((_, meta)) = out.meta.clients.iter_mut().find(|(id, _)| *id == viewer) {
|
||||
meta.linked_weapon_view = linked_weapon_view;
|
||||
meta.remote_missile = remote_missile;
|
||||
}
|
||||
}
|
||||
|
||||
fn rebase_entity_state(es: &mut entity_iw4::EntityState, delta_ms: i32) {
|
||||
if es.tr_time != 0 {
|
||||
es.tr_time = es.tr_time.wrapping_add(delta_ms);
|
||||
@@ -377,6 +409,15 @@ fn rebase_archived_world(out: &mut Snapshot, viewer: ClientId, delta_ms: i32) {
|
||||
p.detonate_at_ms = p.detonate_at_ms.map(|t| t.wrapping_add(delta_ms));
|
||||
p.cleanup_at_ms = p.cleanup_at_ms.wrapping_add(delta_ms);
|
||||
}
|
||||
let bodies = out.meta.corpses.slots.iter_mut();
|
||||
for body in bodies.filter(|body| body.occupied) {
|
||||
if body.tr_time != 0 {
|
||||
body.tr_time = body.tr_time.wrapping_add(delta_ms);
|
||||
}
|
||||
if body.spawn_time_ms != 0 {
|
||||
body.spawn_time_ms = body.spawn_time_ms.wrapping_add(delta_ms);
|
||||
}
|
||||
}
|
||||
for (id, ps) in &mut out.players {
|
||||
if *id != viewer {
|
||||
rebase_archived_timers(ps, delta_ms);
|
||||
|
||||
@@ -2210,6 +2210,9 @@ fn encode_corpse_pool(out: &mut WireWriter, pool: &PlayerCorpsePool) {
|
||||
}
|
||||
out.put_u8(u8::from(slot.falling));
|
||||
out.put_i32(slot.ground_entity_num);
|
||||
out.put_i32(slot.spawn_time_ms);
|
||||
out.put_u32(slot.shield.map_or(0, |shield| shield.weapon));
|
||||
out.put_u8(u8::from(slot.shield.is_some_and(|shield| shield.on_back)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2243,6 +2246,8 @@ fn decode_corpse_pool(input: &mut WireReader<'_>) -> Result<PlayerCorpsePool, Wi
|
||||
tr_base: [input.get_f32()?, input.get_f32()?, input.get_f32()?],
|
||||
falling: input.get_u8()? != 0,
|
||||
ground_entity_num: input.get_i32()?,
|
||||
spawn_time_ms: input.get_i32()?,
|
||||
shield: decode_corpse_shield(input)?,
|
||||
};
|
||||
}
|
||||
_ => return Err(WireError::Malformed("bad corpse slot tag")),
|
||||
@@ -2251,6 +2256,18 @@ fn decode_corpse_pool(input: &mut WireReader<'_>) -> Result<PlayerCorpsePool, Wi
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
fn decode_corpse_shield(
|
||||
input: &mut WireReader<'_>,
|
||||
) -> Result<Option<sim::ShieldAttachment>, WireError> {
|
||||
let weapon = input.get_u32()?;
|
||||
let on_back = match input.get_u8()? {
|
||||
0 => false,
|
||||
1 => true,
|
||||
_ => return Err(WireError::Malformed("bad corpse shield tag")),
|
||||
};
|
||||
Ok((weapon != 0).then_some(sim::ShieldAttachment { weapon, on_back }))
|
||||
}
|
||||
|
||||
fn encode_entity_states(out: &mut WireWriter, rows: &[entity_iw4::EntityState]) {
|
||||
debug_assert!(rows.len() <= u16::MAX as usize);
|
||||
out.put_u16(rows.len() as u16);
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct PersistentRemoteTree {
|
||||
pub(crate) legs_restart: bool,
|
||||
pub(crate) torso_restart: bool,
|
||||
|
||||
pub(crate) occupation_tr_time: Option<i32>,
|
||||
pub(crate) occupation_key: Option<CorpseOccupationKey>,
|
||||
|
||||
pub(crate) legs_rate_sample: ClientAnimSample,
|
||||
|
||||
@@ -88,16 +88,36 @@ pub fn skip_frozen_corpse_dobj(
|
||||
is_corpse && tree_reused && last_was_cache_hit && has_cache
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct CorpseOccupationKey {
|
||||
pub spawn_time_ms: i32,
|
||||
pub victim: i32,
|
||||
}
|
||||
|
||||
pub fn corpse_occupation_key(es: &entity_iw4::EntityState) -> CorpseOccupationKey {
|
||||
CorpseOccupationKey {
|
||||
spawn_time_ms: es.time2,
|
||||
victim: es.client_num,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fresh_corpse_clip_time(age_ms: i32, clip_length_ms: i32) -> f32 {
|
||||
if clip_length_ms <= 0 || age_ms <= 0 {
|
||||
return 0.0;
|
||||
}
|
||||
(age_ms as f32 / clip_length_ms as f32).min(1.0)
|
||||
}
|
||||
|
||||
pub fn clone_corpse_tree_from_victim(
|
||||
trees: &mut RemoteBodyTrees,
|
||||
corpse_ent: u32,
|
||||
victim: u32,
|
||||
tr_time: i32,
|
||||
key: CorpseOccupationKey,
|
||||
) {
|
||||
if trees
|
||||
.by_ent
|
||||
.get(&corpse_ent)
|
||||
.is_some_and(|slot| slot.occupation_tr_time == Some(tr_time))
|
||||
.is_some_and(|slot| slot.occupation_key == Some(key))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +128,7 @@ pub fn clone_corpse_tree_from_victim(
|
||||
let mut dst = src;
|
||||
dst.dobj = None;
|
||||
dst.reuse_key = None;
|
||||
dst.occupation_tr_time = Some(tr_time);
|
||||
dst.occupation_key = Some(key);
|
||||
trees.by_ent.insert(corpse_ent, dst);
|
||||
}
|
||||
|
||||
@@ -129,6 +149,7 @@ pub fn advance_remote_tree(
|
||||
dt: f32,
|
||||
origin: [f32; 3],
|
||||
pose_time_ms: i32,
|
||||
corpse_age_ms: Option<i32>,
|
||||
trees: &mut RemoteBodyTrees,
|
||||
) -> Result<AdvancedRemoteTree, String> {
|
||||
let tree = binding.tree();
|
||||
@@ -139,7 +160,7 @@ pub fn advance_remote_tree(
|
||||
let legs_restart = legs.restart_toggle();
|
||||
let torso_restart = torso.restart_toggle();
|
||||
let previous = trees.by_ent.remove(&persist_key);
|
||||
let occupation_tr_time = previous.as_ref().and_then(|slot| slot.occupation_tr_time);
|
||||
let occupation_key = previous.as_ref().and_then(|slot| slot.occupation_key);
|
||||
let prev = previous.filter(|slot| Arc::ptr_eq(&slot.binding, binding));
|
||||
let prev_restart_changed_legs = prev
|
||||
.as_ref()
|
||||
@@ -178,6 +199,7 @@ pub fn advance_remote_tree(
|
||||
let overlayed = overlay_legs_clip(&clip, &torso_clip);
|
||||
(Arc::new(overlayed), Some(torso_clip))
|
||||
};
|
||||
let fresh = prev.is_none();
|
||||
let old_legs = prev.as_ref().map(|slot| slot.legs).unwrap_or(0);
|
||||
let old_torso = prev.as_ref().map(|slot| slot.torso).unwrap_or(0);
|
||||
let old_legs_moving = prev
|
||||
@@ -225,7 +247,7 @@ pub fn advance_remote_tree(
|
||||
torso: 0,
|
||||
legs_restart: false,
|
||||
torso_restart: false,
|
||||
occupation_tr_time,
|
||||
occupation_key,
|
||||
legs_rate_sample: ClientAnimSample::default(),
|
||||
torso_rate_sample: ClientAnimSample::default(),
|
||||
},
|
||||
@@ -292,6 +314,23 @@ pub fn advance_remote_tree(
|
||||
.set_state(id, state)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
if fresh && let Some(age_ms) = corpse_age_ms {
|
||||
for (index, leaf_clip) in [
|
||||
(legs_index, Some(&clip)),
|
||||
(torso_index, torso_clip.as_ref()),
|
||||
] {
|
||||
let Some(leaf_clip) = leaf_clip else {
|
||||
continue;
|
||||
};
|
||||
let time = fresh_corpse_clip_time(age_ms, leaf_clip.length_msec());
|
||||
let mut state = slot.runtime.states()[index as usize];
|
||||
state.time = time;
|
||||
state.old_time = time;
|
||||
slot.runtime
|
||||
.set_state(xmodel_runtime::XAnimNodeId(index), state)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
}
|
||||
clips = Some(PoseClips {
|
||||
clip,
|
||||
torso_clip,
|
||||
|
||||
@@ -17,6 +17,10 @@ const MSEC_STEP: i32 = 17;
|
||||
const MAX_ITERS: i32 = 2;
|
||||
|
||||
const CONTENTS_SOLID: u32 = 1;
|
||||
const CONTENTS_ITEMCLIP: u32 = 0x400;
|
||||
const WAKE_MASKS: [u32; 2] = [CONTENTS_SOLID | CONTENTS_ITEMCLIP, CONTENTS_SOLID];
|
||||
const WAKE_LIFTS: [f32; 6] = [0.0, CONTACT_SLOP, 0.25, 0.5, 1.0, 2.0];
|
||||
const WAKE_FOOTPRINTS: [f32; 3] = [1.0, 0.75, 0.5];
|
||||
const SLEEP_SPEED: f32 = 4.0;
|
||||
const GROUND_NZ: f32 = 0.7;
|
||||
const CONTACT_SLOP: f32 = 0.125;
|
||||
@@ -56,6 +60,7 @@ struct DynEntPhysBody {
|
||||
maxs: [f32; 3],
|
||||
bounce: f32,
|
||||
friction: f32,
|
||||
mask: u32,
|
||||
on_ground: bool,
|
||||
asleep_ms: i32,
|
||||
}
|
||||
@@ -99,6 +104,7 @@ pub fn step_phys_world0(
|
||||
}
|
||||
let (mins, maxs) = xmodel_phys_hull(catalog, &inst.current_model);
|
||||
let add_vel = impulse.impulse / preset.mass;
|
||||
let phys_clip = clip.0.as_deref();
|
||||
world
|
||||
.bodies
|
||||
.entry(entity)
|
||||
@@ -106,15 +112,22 @@ pub fn step_phys_world0(
|
||||
body.vel += add_vel;
|
||||
body.asleep_ms = 0;
|
||||
})
|
||||
.or_insert_with(|| DynEntPhysBody {
|
||||
origin: transform.translation,
|
||||
vel: add_vel,
|
||||
mins,
|
||||
maxs,
|
||||
bounce: preset.bounce,
|
||||
friction: preset.friction,
|
||||
on_ground: false,
|
||||
asleep_ms: 0,
|
||||
.or_insert_with(|| {
|
||||
let pose = phys_clip
|
||||
.and_then(|clip| clear_wake_pose(clip, transform.translation, mins, maxs));
|
||||
let (origin, mins, maxs, mask) =
|
||||
pose.unwrap_or((transform.translation, mins, maxs, CONTENTS_SOLID));
|
||||
DynEntPhysBody {
|
||||
origin,
|
||||
vel: add_vel,
|
||||
mins,
|
||||
maxs,
|
||||
bounce: preset.bounce,
|
||||
friction: preset.friction,
|
||||
mask,
|
||||
on_ground: false,
|
||||
asleep_ms: 0,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -185,6 +198,39 @@ pub fn step_phys_world0(
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_wake_pose(
|
||||
clip: &ClipCollision,
|
||||
origin: Vec3,
|
||||
mins: [f32; 3],
|
||||
maxs: [f32; 3],
|
||||
) -> Option<(Vec3, [f32; 3], [f32; 3], u32)> {
|
||||
for mask in WAKE_MASKS {
|
||||
for scale in WAKE_FOOTPRINTS {
|
||||
let (mins, maxs) = narrowed_footprint(mins, maxs, scale);
|
||||
for lift in WAKE_LIFTS {
|
||||
let at = (origin + Vec3::Z * lift).to_array();
|
||||
let probe = clip.sweep_box(at, at, mins, maxs, mask);
|
||||
if !probe.startsolid && !probe.allsolid {
|
||||
return Some((Vec3::from_array(at), mins, maxs, mask));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn narrowed_footprint(mins: [f32; 3], maxs: [f32; 3], scale: f32) -> ([f32; 3], [f32; 3]) {
|
||||
let mut lo = mins;
|
||||
let mut hi = maxs;
|
||||
for axis in 0..2 {
|
||||
let mid = (mins[axis] + maxs[axis]) * 0.5;
|
||||
let half = (maxs[axis] - mins[axis]) * 0.5 * scale;
|
||||
lo[axis] = mid - half;
|
||||
hi[axis] = mid + half;
|
||||
}
|
||||
(lo, hi)
|
||||
}
|
||||
|
||||
fn integrate_free(body: &mut DynEntPhysBody, dt: f32) {
|
||||
body.vel.z += GRAVITY_Z * dt;
|
||||
body.origin += body.vel * dt;
|
||||
@@ -200,7 +246,7 @@ fn integrate_body(body: &mut DynEntPhysBody, clip: &ClipCollision, dt: f32) {
|
||||
end.to_array(),
|
||||
body.mins,
|
||||
body.maxs,
|
||||
CONTENTS_SOLID,
|
||||
body.mask,
|
||||
);
|
||||
if hit.allsolid || hit.startsolid {
|
||||
body.vel = Vec3::ZERO;
|
||||
|
||||
@@ -3,10 +3,10 @@ use crate::anim::remote_body::{
|
||||
RemoteBodySkinnedItem, RemoteBodySkinnedQueue, RemoteBodyTrees, RemoteModelLods,
|
||||
RemoteSkinModels, RemoteSkinPoseHashes, SkinAfterPose, WorldGunGap, advance_remote_tree,
|
||||
bind_remote_skin_models, clone_corpse_tree_from_victim, commit_assembled_body,
|
||||
ensure_remote_dobj, hash_skin_matrices, occupy_lod_byte, occupy_remote_kit_dobj, packed_anim,
|
||||
pose_remote_dobj, push_cached_surfaces, radii, remote_player_controller, select_remote_lods,
|
||||
select_remote_models, skin_after_pose, skin_slot_need, skip_frozen_corpse_dobj,
|
||||
take_unique_geom, validate_remote_tracks, zero_anim,
|
||||
corpse_occupation_key, ensure_remote_dobj, hash_skin_matrices, occupy_lod_byte,
|
||||
occupy_remote_kit_dobj, packed_anim, pose_remote_dobj, push_cached_surfaces, radii,
|
||||
remote_player_controller, select_remote_lods, select_remote_models, skin_after_pose,
|
||||
skin_slot_need, skip_frozen_corpse_dobj, take_unique_geom, validate_remote_tracks, zero_anim,
|
||||
};
|
||||
use crate::anim::scene_submission::{AnimDObjSceneSkels, AnimDObjSceneSubmission, AnimSceneSubmit};
|
||||
use crate::anim::world_weapon::PreparedItemCompositions;
|
||||
@@ -52,6 +52,26 @@ pub struct RemotePlayer {
|
||||
pub shield: Option<sim::ShieldAttachment>,
|
||||
}
|
||||
|
||||
fn drawn_shield(
|
||||
snapshot: &sim::Snapshot,
|
||||
number: u16,
|
||||
client: sim::ClientId,
|
||||
) -> Option<sim::ShieldAttachment> {
|
||||
let body = snapshot
|
||||
.meta
|
||||
.corpses
|
||||
.slots
|
||||
.iter()
|
||||
.find(|slot| slot.occupied && slot.entnum == i32::from(number));
|
||||
match body {
|
||||
Some(body) => body.shield,
|
||||
None => snapshot
|
||||
.meta
|
||||
.for_client(client)
|
||||
.and_then(|meta| meta.shield),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct RemoteFxBolts {
|
||||
pub flash: Option<fx::FxBoltTarget>,
|
||||
@@ -384,7 +404,9 @@ fn occupy_remote_scene_ents(
|
||||
if !soldier.owns_bodies(&bodies.0) {
|
||||
continue;
|
||||
}
|
||||
let shield = meta.and_then(|meta| meta.shield);
|
||||
let shield = presented
|
||||
.snapshot()
|
||||
.and_then(|snap| drawn_shield(snap, identity.number(), client));
|
||||
let held = remote_pose_sample(runtime).weapon;
|
||||
let (kit_models, radius, hide_part_bits) = match live_kits.filter(|_| shield.is_none()) {
|
||||
Some((kits, _world)) => {
|
||||
@@ -556,7 +578,9 @@ fn sync_remote_bodies(
|
||||
let marker = RemotePlayer {
|
||||
ffa_team,
|
||||
client_state_team,
|
||||
shield: meta.and_then(|meta| meta.shield),
|
||||
shield: presented
|
||||
.snapshot()
|
||||
.and_then(|snap| drawn_shield(snap, identity.number(), client)),
|
||||
};
|
||||
if remote.is_some() {
|
||||
commands.entity(entity).insert((pose, marker));
|
||||
@@ -868,16 +892,13 @@ impl<'a> RemotePoseFrame<'a> {
|
||||
let persist_key = u32::from(identity.number());
|
||||
let is_corpse = runtime.next_state.e_type == ET_PLAYER_CORPSE
|
||||
|| runtime.pose_e_type == ET_PLAYER_CORPSE as u8;
|
||||
let occupation_tr_time = is_corpse.then_some(runtime.next_state.tr_time);
|
||||
if is_corpse {
|
||||
let occupation_key = is_corpse.then(|| corpse_occupation_key(&runtime.next_state));
|
||||
if let Some(key) = occupation_key {
|
||||
let victim = u32::try_from(runtime.next_state.client_num).unwrap_or(0);
|
||||
clone_corpse_tree_from_victim(
|
||||
self.trees,
|
||||
persist_key,
|
||||
victim,
|
||||
runtime.next_state.tr_time,
|
||||
);
|
||||
clone_corpse_tree_from_victim(self.trees, persist_key, victim, key);
|
||||
}
|
||||
let corpse_age_ms =
|
||||
occupation_key.map(|key| runtime.pose_time_ms.wrapping_sub(key.spawn_time_ms));
|
||||
|
||||
if runtime.previous_pose.is_none() {
|
||||
if let Some(slot) = self.trees.get_mut(persist_key) {
|
||||
@@ -955,6 +976,7 @@ impl<'a> RemotePoseFrame<'a> {
|
||||
dt,
|
||||
sample.rate_origin,
|
||||
sample.rate_time_ms,
|
||||
corpse_age_ms,
|
||||
trees,
|
||||
)?;
|
||||
|
||||
@@ -1051,11 +1073,11 @@ impl<'a> RemotePoseFrame<'a> {
|
||||
}
|
||||
Ok(PoseOneOutcome::Posed)
|
||||
})();
|
||||
if let Some(tr_time) = occupation_tr_time
|
||||
if let Some(key) = occupation_key
|
||||
&& let Some(slot) = trees.get_mut(persist_key)
|
||||
&& slot.occupation_tr_time.is_none()
|
||||
&& slot.occupation_key.is_none()
|
||||
{
|
||||
slot.occupation_tr_time = Some(tr_time);
|
||||
slot.occupation_key = Some(key);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use asset_world::ClipCollision;
|
||||
use net::PresentedSnapshot;
|
||||
use net::{AUTHORITY_MS, PresentedProjectile, PresentedSnapshot};
|
||||
use playerstate_iw4::{
|
||||
CG_CAMERA_PULLBACK_BOX_HALF, CG_CAMERA_PULLBACK_CLIPMASK, CG_THIRD_PERSON_RANGE_DEFAULT,
|
||||
KillCamMode, OffsetThirdPersonViewInputs, ThirdPersonViewInputs, is_third_person_view,
|
||||
offset_third_person_view,
|
||||
};
|
||||
use sim::ClientId;
|
||||
use sim::{ClientId, LifeSequence, ProjectileId, RemoteMissile};
|
||||
|
||||
use render_scene::WorldCameraPose;
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn presented_is_third_person(
|
||||
if in_killcam && ps.kill_cam_entity != playerstate_iw4::ENTITYNUM_NONE {
|
||||
return true;
|
||||
}
|
||||
if remote_missile_camera(presented, local, 0).is_some() {
|
||||
if remote_missile_link(presented, local).is_some() {
|
||||
return true;
|
||||
}
|
||||
is_third_person_view(ThirdPersonViewInputs {
|
||||
@@ -42,25 +42,160 @@ pub fn presented_is_third_person(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remote_missile_camera(
|
||||
fn remote_missile_link(
|
||||
presented: &PresentedSnapshot,
|
||||
local: ClientId,
|
||||
) -> Option<(RemoteMissile, LifeSequence)> {
|
||||
let meta = presented.snapshot()?.meta.for_client(local)?;
|
||||
Some((meta.remote_missile?, meta.life_sequence))
|
||||
}
|
||||
|
||||
// The missile leaves the snapshot on impact, before the link goes and the
|
||||
// static starts. The last pose is held until then so that neither the
|
||||
// player's own view nor a black frame shows in between.
|
||||
#[derive(Default)]
|
||||
pub struct RemoteMissileView {
|
||||
held: Option<HeldMissileView>,
|
||||
turn: MissileTurn,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct HeldMissileView {
|
||||
life: LifeSequence,
|
||||
projectile: ProjectileId,
|
||||
pose: WorldCameraPose,
|
||||
}
|
||||
|
||||
const TURN_SAMPLES: usize = 4;
|
||||
|
||||
#[derive(Default)]
|
||||
struct MissileTurn {
|
||||
missile: Option<(LifeSequence, ProjectileId)>,
|
||||
archived: bool,
|
||||
samples: Vec<(i32, [f32; 3])>,
|
||||
}
|
||||
|
||||
impl MissileTurn {
|
||||
fn record(
|
||||
&mut self,
|
||||
missile: (LifeSequence, ProjectileId),
|
||||
archived: bool,
|
||||
snapshot_ms: i32,
|
||||
angles: [f32; 3],
|
||||
) {
|
||||
// The final killcam of the pilot's own kill replays the same missile
|
||||
// of the same life from seat snapshots stamped with the live tick,
|
||||
// later than every sample of the flight, so a switch between live and
|
||||
// archived presentation starts a fresh history.
|
||||
if self.missile != Some(missile) || self.archived != archived {
|
||||
self.missile = Some(missile);
|
||||
self.archived = archived;
|
||||
self.samples.clear();
|
||||
}
|
||||
match self.samples.last_mut() {
|
||||
Some(last) if last.0 == snapshot_ms => last.1 = angles,
|
||||
Some(last) if last.0 > snapshot_ms => {
|
||||
self.samples.clear();
|
||||
self.samples.push((snapshot_ms, angles));
|
||||
}
|
||||
_ => {
|
||||
if self.samples.len() == TURN_SAMPLES {
|
||||
self.samples.remove(0);
|
||||
}
|
||||
self.samples.push((snapshot_ms, angles));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn angles_at(&self, at_time: i32) -> Option<[f32; 3]> {
|
||||
let shown = at_time.saturating_sub(AUTHORITY_MS);
|
||||
let &(first_ms, first) = self.samples.first()?;
|
||||
if shown <= first_ms {
|
||||
return Some(first);
|
||||
}
|
||||
let Some(pair) = self.samples.windows(2).find(|pair| shown < pair[1].0) else {
|
||||
return self.samples.last().map(|&(_, newest)| newest);
|
||||
};
|
||||
let ((from_ms, from), (to_ms, to)) = (pair[0], pair[1]);
|
||||
let fraction = (shown - from_ms) as f32 / (to_ms - from_ms) as f32;
|
||||
Some(std::array::from_fn(|i| {
|
||||
from[i] + math_iw4::angle_subtract(to[i], from[i]) * fraction
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteMissileView {
|
||||
pub fn camera(
|
||||
&mut self,
|
||||
presented: &PresentedSnapshot,
|
||||
local: ClientId,
|
||||
render_time: i32,
|
||||
clip: Option<&ClipCollision>,
|
||||
) -> Option<WorldCameraPose> {
|
||||
let at_time = presented.trajectory_time_ms(render_time);
|
||||
let Some((link, life)) = remote_missile_link(presented, local) else {
|
||||
self.held = None;
|
||||
self.turn = MissileTurn::default();
|
||||
return None;
|
||||
};
|
||||
self.held = self
|
||||
.held
|
||||
.filter(|held| held.life == life && held.projectile == link.projectile);
|
||||
let flying = link
|
||||
.unlink_at_ms
|
||||
.is_none()
|
||||
.then(|| {
|
||||
presented
|
||||
.presented_projectiles()
|
||||
.iter()
|
||||
.find(|p| p.authoritative_id() == Some(link.projectile))
|
||||
})
|
||||
.flatten();
|
||||
let snapshot_ms = presented.tick().map_or(at_time, sim::level_time_ms);
|
||||
if let Some(missile) = flying {
|
||||
self.turn.record(
|
||||
(life, link.projectile),
|
||||
presented.is_archived_replay(),
|
||||
snapshot_ms,
|
||||
link.angles,
|
||||
);
|
||||
let pose = WorldCameraPose {
|
||||
origin: missile_eye(missile, snapshot_ms, at_time, clip),
|
||||
angles: self.turn.angles_at(at_time).unwrap_or(link.angles),
|
||||
};
|
||||
self.held = Some(HeldMissileView {
|
||||
life,
|
||||
projectile: link.projectile,
|
||||
pose,
|
||||
});
|
||||
}
|
||||
self.held.map(|held| held.pose)
|
||||
}
|
||||
}
|
||||
|
||||
fn missile_eye(
|
||||
missile: &PresentedProjectile,
|
||||
snapshot_ms: i32,
|
||||
at_time: i32,
|
||||
) -> Option<WorldCameraPose> {
|
||||
let link = presented
|
||||
.snapshot()?
|
||||
.meta
|
||||
.for_client(local)?
|
||||
.remote_missile
|
||||
.filter(|link| link.unlink_at_ms.is_none())?;
|
||||
let missile = presented
|
||||
.presented_projectiles()
|
||||
.iter()
|
||||
.find(|p| p.authoritative_id() == Some(link.projectile))?;
|
||||
Some(WorldCameraPose {
|
||||
origin: missile.origin_at(at_time),
|
||||
angles: link.angles,
|
||||
})
|
||||
clip: Option<&ClipCollision>,
|
||||
) -> [f32; 3] {
|
||||
let known = missile.origin_at(at_time.min(snapshot_ms));
|
||||
if at_time <= snapshot_ms {
|
||||
return known;
|
||||
}
|
||||
let ahead = missile.origin_at(at_time.min(snapshot_ms.saturating_add(AUTHORITY_MS)));
|
||||
let fraction = clip.map_or(1.0, |clip| {
|
||||
let half = CG_CAMERA_PULLBACK_BOX_HALF;
|
||||
let hit = clip.sweep_box(
|
||||
known,
|
||||
ahead,
|
||||
[-half; 3],
|
||||
[half; 3],
|
||||
CG_CAMERA_PULLBACK_CLIPMASK,
|
||||
);
|
||||
if hit.allsolid { 0.0 } else { hit.fraction }
|
||||
});
|
||||
std::array::from_fn(|i| known[i] + (ahead[i] - known[i]) * fraction.clamp(0.0, 1.0))
|
||||
}
|
||||
|
||||
pub fn linked_weapon_camera(
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::anim::view_kick_state::{KickParams, ViewKickState, add_kick_to_viewan
|
||||
use crate::anim::view_sway::ViewSwayState;
|
||||
use crate::occupancy::remote_body::RemotePlayer;
|
||||
use crate::occupancy::third_person::{
|
||||
linked_weapon_camera, presented_is_third_person, remote_missile_camera, third_person_camera,
|
||||
RemoteMissileView, linked_weapon_camera, presented_is_third_person, third_person_camera,
|
||||
};
|
||||
use render_scene::{FlyCamera, FpvLens, SimCamera, transform_from_iw_view};
|
||||
use render_scene::{WorldCameraPose, WorldScriptModelInstance};
|
||||
@@ -228,6 +228,7 @@ pub fn sync_camera_from_presented(
|
||||
mut lenses: Query<&mut Projection, With<FpvLens>>,
|
||||
view: Res<ViewSubject>,
|
||||
death_cam_clip: Res<crate::occupancy::dyn_ent::DynEntPhysClip>,
|
||||
mut missile_view: Local<RemoteMissileView>,
|
||||
) {
|
||||
kick.killcam_focus_distance = None;
|
||||
if let Some(view) = sim_cam.view_override {
|
||||
@@ -283,7 +284,12 @@ pub fn sync_camera_from_presented(
|
||||
kick.killcam_focus_distance = focus_distance;
|
||||
return;
|
||||
}
|
||||
if let Some(pose) = remote_missile_camera(&presented, local.0, clock.time()) {
|
||||
if let Some(pose) = missile_view.camera(
|
||||
&presented,
|
||||
local.0,
|
||||
clock.time(),
|
||||
death_cam_clip.0.as_deref(),
|
||||
) {
|
||||
let pose = earthquake_pose(pose, &presented, clock.time());
|
||||
let eye = transform_from_iw_view(pose);
|
||||
for mut transform in &mut q {
|
||||
|
||||
@@ -52,6 +52,14 @@ pub struct DynEntWakeBroadphase {
|
||||
pub query_ms: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Message)]
|
||||
pub struct DynEntPelletHit {
|
||||
pub world: frame::WorldStamp,
|
||||
pub weapon: u32,
|
||||
pub start: Vec3,
|
||||
pub end: Vec3,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct DynEntWakeEntry {
|
||||
entity: Entity,
|
||||
@@ -787,6 +795,59 @@ pub(crate) fn on_entity_bullet_hit(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn hit_dyn_ents_from_pellets(
|
||||
mut hits: MessageReader<DynEntPelletHit>,
|
||||
weapons: Option<Res<PreparedWeapons>>,
|
||||
catalog: Option<Res<asset_world::MapXModelSceneCatalog>>,
|
||||
mut broadphase: ResMut<DynEntWakeBroadphase>,
|
||||
mut instances: Query<
|
||||
(
|
||||
Entity,
|
||||
&mut WorldDynEntInstance,
|
||||
&Transform,
|
||||
&mut Visibility,
|
||||
),
|
||||
With<DynEntModelEntity>,
|
||||
>,
|
||||
mut impulses: MessageWriter<DynEntPhysImpulse>,
|
||||
mut phys: ResMut<DynEntPhysWorld>,
|
||||
mut host: Option<ResMut<HostFxSystem>>,
|
||||
fx: Option<Res<PreparedFxCatalog>>,
|
||||
scene: Option<Res<WorldScene>>,
|
||||
entity_marks: Res<EntityMarks>,
|
||||
) {
|
||||
let catalog = catalog.as_deref();
|
||||
let fx = fx.as_deref();
|
||||
let scene = scene.as_deref();
|
||||
let marks = entity_marks.as_ref();
|
||||
for hit in hits.read() {
|
||||
let Some((entity, pose, bullet_scale)) =
|
||||
closest_segment_hit(hit.start, hit.end, catalog, &mut broadphase, &instances)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let weapons = weapons
|
||||
.as_deref()
|
||||
.and_then(|weapons| weapons.for_event(hit.world).ok());
|
||||
hit_one_dyn_ent(
|
||||
entity,
|
||||
pose,
|
||||
hit.start,
|
||||
hit.end,
|
||||
bullet_scale,
|
||||
weapon_hit_damage(weapons.as_ref(), hit.weapon),
|
||||
&mut instances,
|
||||
&mut impulses,
|
||||
phys.as_mut(),
|
||||
host.as_deref_mut(),
|
||||
fx,
|
||||
scene,
|
||||
marks,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_entity_event_sound(
|
||||
sound: On<EntityEventSound>,
|
||||
weapons: Option<Res<PreparedWeapons>>,
|
||||
@@ -917,6 +978,7 @@ fn on_physics_sphere(
|
||||
|
||||
pub fn register_dyn_ent_wake(app: &mut App) {
|
||||
app.scoped::<DynEntWakeBroadphase>(frame::MatchScope::Live)
|
||||
.scoped_message::<DynEntPelletHit>(frame::MatchScope::Live)
|
||||
.add_observer(on_entity_explosion)
|
||||
.add_observer(on_physics_sphere)
|
||||
.add_observer(on_entity_bullet_hit)
|
||||
@@ -927,6 +989,12 @@ pub fn register_dyn_ent_wake(app: &mut App) {
|
||||
.in_set(frame::InMatch)
|
||||
.in_set(net::ClientSet::Receive),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
hit_dyn_ents_from_pellets
|
||||
.in_set(frame::InMatch)
|
||||
.in_set(net::ClientSet::Present),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
wake_player_overlap
|
||||
|
||||
@@ -47,6 +47,11 @@ pub(crate) fn generate(
|
||||
models: &asset_world::MapXModelSceneCatalog,
|
||||
) {
|
||||
let mut state = marks.lock();
|
||||
state.attached.retain(|a| {
|
||||
host.marks
|
||||
.constructed(a.handle)
|
||||
.is_some_and(|m| m.context as u8 == 3 && (m.context >> 16) as u16 == a.entity)
|
||||
});
|
||||
let pending = std::mem::take(&mut state.pending);
|
||||
let before = state.unsupported_triangles;
|
||||
let before_receivers = state.unsupported_receivers;
|
||||
@@ -378,6 +383,7 @@ fn stage(
|
||||
host.marks
|
||||
.set_material_name(handle, Some(&request.material));
|
||||
host.note_models_go_fire();
|
||||
state.attached.retain(|a| a.handle != handle);
|
||||
state.attached.push(EntityMarkAttachment {
|
||||
entity: request.entity,
|
||||
model: model.to_string(),
|
||||
|
||||
@@ -707,7 +707,7 @@ fn commit_fx_transaction(
|
||||
light.origin,
|
||||
light.axis[0],
|
||||
light.radius,
|
||||
light.color_bgr,
|
||||
light.color_rgb,
|
||||
spot_cone,
|
||||
)),
|
||||
Err(lighting_iw4::AddOmniLightRefuse::Cap) => {
|
||||
@@ -725,7 +725,7 @@ fn commit_fx_transaction(
|
||||
Ok(()) => env.dlights.scene.push(lighting_iw4::omni_light_pack(
|
||||
light.origin,
|
||||
light.radius,
|
||||
light.color_bgr,
|
||||
light.color_rgb,
|
||||
)),
|
||||
Err(lighting_iw4::AddOmniLightRefuse::Cap) => {
|
||||
env.dlights.cap_full = env.dlights.cap_full.saturating_add(1);
|
||||
@@ -3261,12 +3261,13 @@ fn drain_pellet_fx(
|
||||
tracers: Option<Res<PreparedTracers>>,
|
||||
mut tracer_world: ResMut<TracerWorld>,
|
||||
mut gate: ResMut<TracerDrawGate>,
|
||||
(local, generation, timeline, mut occurrences, verdicts): (
|
||||
(local, generation, timeline, mut occurrences, verdicts, mut dyn_ent_hits): (
|
||||
Res<LocalPresentClient>,
|
||||
Res<frame::WorldGeneration>,
|
||||
Res<net::EntityEventCursor>,
|
||||
ResMut<PresentedFireFx>,
|
||||
Res<net::FireVerdictState>,
|
||||
MessageWriter<crate::adapters::anim::dyn_ent_wake::DynEntPelletHit>,
|
||||
),
|
||||
mut host: ResMut<HostFxSystem>,
|
||||
mut cursor: ResMut<FxJournalCursor>,
|
||||
@@ -3280,6 +3281,14 @@ fn drain_pellet_fx(
|
||||
if world != generation.stamp() {
|
||||
continue;
|
||||
}
|
||||
if domain != net::EntityEventDomain::Predicted && record.normal != [0.0; 3] {
|
||||
dyn_ent_hits.write(crate::adapters::anim::dyn_ent_wake::DynEntPelletHit {
|
||||
world,
|
||||
weapon: record.weapon,
|
||||
start: Vec3::from_array(record.start),
|
||||
end: Vec3::from_array(record.end),
|
||||
});
|
||||
}
|
||||
let now = host.0.msec_now;
|
||||
let request = FireFxRequest {
|
||||
world,
|
||||
|
||||
@@ -12,16 +12,17 @@ use fx::{
|
||||
FxModelInstance, FxPackedLightingSrc, FxPlayPose, FxPlayRequest, FxSparkDrawQuery,
|
||||
FxSparkFillQuery, FxSparkFillVisual, FxSpriteInstance, FxSystemHost, FxTrailCollideHit,
|
||||
FxTrailDrawDef, FxTrailSampleVisual, PendingCollide, PlayResult, axis_from_hit_normal,
|
||||
evaluate_elem_collide_motion, evaluate_elem_motion, generate_verts_with_trails, play_bolted,
|
||||
play_oriented, spawn_impact_or_death_effect, spawn_looping_partial, spawn_oriented,
|
||||
update as fx_update,
|
||||
evaluate_elem_collide_motion, evaluate_elem_motion, evaluate_elem_physics_motion,
|
||||
generate_verts_with_trails, play_bolted, play_oriented, spawn_impact_or_death_effect,
|
||||
spawn_looping_partial, spawn_oriented, update as fx_update,
|
||||
};
|
||||
use fx_iw4::{
|
||||
FX_ELEM_ATLAS_OFF, FX_ELEM_ATLAS_SIZE, FxElemType, FxRandomChannel, apply_lighting_frac_bgra,
|
||||
build_cloud, clamp_elem_rotation_time, cull_cloud, cull_elem_light,
|
||||
effect_def_needs_lighting_sample, elem_light_color_bgr, elem_uses_lighting_frac,
|
||||
FX_ELEM_AT_REST_NONE, FX_ELEM_ATLAS_OFF, FX_ELEM_ATLAS_SIZE, FxElemType, FxRandomChannel,
|
||||
apply_lighting_frac_bgra, build_cloud, clamp_elem_rotation_time, cull_cloud, cull_elem_light,
|
||||
effect_def_needs_lighting_sample, elem_light_color_rgb, elem_uses_lighting_frac,
|
||||
evaluate_rotation_total, evaluate_scale, evaluate_size0, evaluate_size1, get_elem_angles_axis,
|
||||
get_velocity_at_time, sample_f32, sprite_atlas_uv, vec3_normalize, vis_blocker_add_prepared,
|
||||
get_velocity_at_time, msec_for_sampling_axis, sample_f32, sprite_atlas_uv, vec3_normalize,
|
||||
vis_blocker_add_prepared,
|
||||
};
|
||||
use math_iw4::angle_vectors;
|
||||
use sim::SimWorld;
|
||||
@@ -658,6 +659,14 @@ fn eval_pending_collide(
|
||||
let Some(elem) = effect.elems.get(q.def_index as usize) else {
|
||||
return out;
|
||||
};
|
||||
if fx_iw4::elem_skips_position_update(elem.view.elem_type, elem.view.flags) {
|
||||
let Some(world) = clip_world else {
|
||||
out.gap = Some(q.def_index);
|
||||
return out;
|
||||
};
|
||||
out.result = Some(evaluate_physics_piece(elem, world, q));
|
||||
return out;
|
||||
}
|
||||
if fx_iw4::elem_uses_collision(elem.view.flags) {
|
||||
let Some(world) = clip_world else {
|
||||
out.gap = Some(q.def_index);
|
||||
@@ -673,6 +682,7 @@ fn eval_pending_collide(
|
||||
elem.view.coll_maxs,
|
||||
elem.view.use_item_clip != 0,
|
||||
!elem.effect_on_impact.is_absent(),
|
||||
elem.view.elem_type == FxElemType::Model as u8,
|
||||
elem.vel_graph_local.as_slice(),
|
||||
elem.vel_graph_world.as_slice(),
|
||||
q,
|
||||
@@ -699,6 +709,29 @@ fn eval_pending_collide(
|
||||
out
|
||||
}
|
||||
|
||||
fn evaluate_physics_piece(
|
||||
elem: &asset_game::OwnedFxElemDef,
|
||||
world: &SimWorld,
|
||||
q: fx::FxElemMotionQuery<'_>,
|
||||
) -> FxElemMotionResult {
|
||||
evaluate_elem_physics_motion(
|
||||
elem.view.flags,
|
||||
elem.view.use_item_clip != 0,
|
||||
elem.vel_graph_local.as_slice(),
|
||||
elem.vel_graph_world.as_slice(),
|
||||
q,
|
||||
|start, end, mins, maxs, mask| {
|
||||
let t = world.trace_world(start, end, mins, maxs, mask);
|
||||
FxElemTraceHit {
|
||||
fraction: t.fraction,
|
||||
normal: t.normal,
|
||||
startsolid: t.startsolid != 0,
|
||||
allsolid: t.allsolid != 0,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const COLLIDE_JOBS_PER_WORKER: usize = 16;
|
||||
|
||||
fn collide_worker_count(jobs: usize) -> usize {
|
||||
@@ -830,6 +863,13 @@ fn tick_fx_pass(
|
||||
let out = (|| {
|
||||
let effect = catalog_lookup(catalog, q.catalog_index)?;
|
||||
let elem = effect.elems.get(q.def_index as usize)?;
|
||||
if fx_iw4::elem_skips_position_update(elem.view.elem_type, elem.view.flags) {
|
||||
let Some(world) = clip_world else {
|
||||
collide_gap_indices.borrow_mut().push(q.def_index);
|
||||
return None;
|
||||
};
|
||||
return Some(evaluate_physics_piece(elem, world, q));
|
||||
}
|
||||
if fx_iw4::elem_uses_collision(elem.view.flags) {
|
||||
let Some(world) = clip_world else {
|
||||
collide_gap_indices.borrow_mut().push(q.def_index);
|
||||
@@ -845,6 +885,7 @@ fn tick_fx_pass(
|
||||
elem.view.coll_maxs,
|
||||
elem.view.use_item_clip != 0,
|
||||
!elem.effect_on_impact.is_absent(),
|
||||
elem.view.elem_type == FxElemType::Model as u8,
|
||||
elem.vel_graph_local.as_slice(),
|
||||
elem.vel_graph_world.as_slice(),
|
||||
q,
|
||||
@@ -1467,7 +1508,7 @@ pub fn build_fx_verts(
|
||||
is_spot: ctx.elem_type == FxElemType::SpotLight as u8,
|
||||
origin: ctx.origin,
|
||||
radius: size0,
|
||||
color_bgr: elem_light_color_bgr(color, scale),
|
||||
color_rgb: elem_light_color_rgb(color, scale),
|
||||
axis,
|
||||
})
|
||||
};
|
||||
@@ -1521,24 +1562,36 @@ fn evaluate_fx_model_instance(
|
||||
if scale == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let rot_t = clamp_elem_rotation_time(
|
||||
ctx.age_msec as f32,
|
||||
ctx.life_msec as f32,
|
||||
ctx.at_rest_fraction,
|
||||
);
|
||||
let axis = get_elem_angles_axis(
|
||||
let landed_piece = ctx.at_rest_fraction != FX_ELEM_AT_REST_NONE
|
||||
&& fx_iw4::elem_skips_position_update(elem.view.elem_type, elem.view.flags);
|
||||
let rot_t = if landed_piece {
|
||||
msec_for_sampling_axis(ctx.at_rest_fraction, ctx.life_msec as f32).min(ctx.age_msec as f32)
|
||||
} else {
|
||||
clamp_elem_rotation_time(
|
||||
ctx.age_msec as f32,
|
||||
ctx.life_msec as f32,
|
||||
ctx.at_rest_fraction,
|
||||
)
|
||||
};
|
||||
let mut axis = get_elem_angles_axis(
|
||||
elem.view.spawn_angles,
|
||||
elem.view.angular_velocity,
|
||||
ctx.elem_random_seed,
|
||||
rot_t,
|
||||
ctx.axis,
|
||||
);
|
||||
let mut origin = ctx.origin;
|
||||
if landed_piece && let Some(piece) = elem.phys_piece(ctx.elem_random_seed) {
|
||||
let (flat, lift) = piece.rest_pose(axis, scale);
|
||||
axis = flat;
|
||||
origin[2] += lift;
|
||||
}
|
||||
Some(FxModelInstance {
|
||||
def_name: ctx.def_name.to_owned(),
|
||||
def_index: ctx.def_index,
|
||||
model_index,
|
||||
elem_handle: ctx.elem_handle,
|
||||
origin: ctx.origin,
|
||||
origin,
|
||||
axis,
|
||||
scale,
|
||||
flags: ctx.flags,
|
||||
|
||||
@@ -9,6 +9,8 @@ use weapon_iw4::{
|
||||
|
||||
pub const CONTENTS_SOLID: u32 = 0x0000_0001;
|
||||
|
||||
pub const CONTENTS_SKY: u32 = 0x0000_0800;
|
||||
|
||||
pub const MASK_PLAYER_SOLID: u32 = 0x0281_0011;
|
||||
|
||||
pub const MASK_SHOT: u32 = 0x0280_6831;
|
||||
|
||||
+220
-115
@@ -1,6 +1,8 @@
|
||||
use crate::frame::FrameWorld;
|
||||
use crate::world::ClientId;
|
||||
use entity_iw4::{TR_GRAVITY, TR_INTERPOLATE, Trajectory, evaluate_trajectory};
|
||||
use entity_iw4::{
|
||||
TR_GRAVITY, TR_INTERPOLATE, Trajectory, evaluate_trajectory, evaluate_trajectory_delta,
|
||||
};
|
||||
use playerstate_iw4::{
|
||||
AnimPair, ENTITYNUM_NONE, LINK_FLAGS_WEAPON_VIEW_ONLY, MAX_CLIENT_CORPSES,
|
||||
PLAYER_CORPSE_ENTITY_BASE, PlayerState,
|
||||
@@ -34,6 +36,10 @@ pub struct PlayerCorpseSlot {
|
||||
pub falling: bool,
|
||||
|
||||
pub ground_entity_num: i32,
|
||||
|
||||
pub spawn_time_ms: i32,
|
||||
|
||||
pub shield: Option<crate::ShieldAttachment>,
|
||||
}
|
||||
|
||||
impl Default for PlayerCorpseSlot {
|
||||
@@ -55,6 +61,8 @@ impl Default for PlayerCorpseSlot {
|
||||
tr_base: [0.0; 3],
|
||||
falling: false,
|
||||
ground_entity_num: ENTITYNUM_NONE,
|
||||
spawn_time_ms: 0,
|
||||
shield: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +163,10 @@ pub(crate) fn occupy_player_clone(
|
||||
ps: &PlayerState,
|
||||
time_ms: i32,
|
||||
) -> u8 {
|
||||
let shield = world
|
||||
.client_meta(victim)
|
||||
.and_then(|meta| meta.shield)
|
||||
.filter(|shield| shield.on_back);
|
||||
let pool = world.corpses_mut();
|
||||
let entnum = pool.alloc_clone_entity();
|
||||
let slot = pool.get_free(ps.origin);
|
||||
@@ -176,6 +188,8 @@ pub(crate) fn occupy_player_clone(
|
||||
tr_delta: clamp_clone_tr_delta(ps.velocity),
|
||||
tr_base: ps.origin,
|
||||
falling: true,
|
||||
spawn_time_ms: time_ms,
|
||||
shield,
|
||||
..PlayerCorpseSlot::default()
|
||||
},
|
||||
);
|
||||
@@ -209,11 +223,8 @@ fn corpse_land_angles(yaw: f32, normal: [f32; 3]) -> [f32; 3] {
|
||||
}
|
||||
|
||||
fn clamp_clone_tr_delta(mut delta: [f32; 3]) -> [f32; 3] {
|
||||
if delta[0] > G_CLONE_PLAYER_MAX_VELOCITY {
|
||||
delta[0] = G_CLONE_PLAYER_MAX_VELOCITY;
|
||||
}
|
||||
if delta[1] > G_CLONE_PLAYER_MAX_VELOCITY {
|
||||
delta[1] = G_CLONE_PLAYER_MAX_VELOCITY;
|
||||
for speed in &mut delta[..2] {
|
||||
*speed = speed.clamp(-G_CLONE_PLAYER_MAX_VELOCITY, G_CLONE_PLAYER_MAX_VELOCITY);
|
||||
}
|
||||
delta
|
||||
}
|
||||
@@ -224,7 +235,9 @@ const CORPSE_ANIM_AXIS_Y_SCALE: f32 = -1.0;
|
||||
|
||||
const CORPSE_GROUND_PROBE_DROP: f32 = 1.0;
|
||||
|
||||
const CORPSE_ANIM_DELTA_TO_VELOCITY: f32 = 20.0;
|
||||
const CORPSE_STEP_DOWN: f32 = 18.0;
|
||||
|
||||
const CORPSE_WALL_SLIDE_GAP: f32 = 0.125;
|
||||
|
||||
const CORPSE_UNSTICK_Z_LIFT: f32 = 32.0;
|
||||
|
||||
@@ -245,109 +258,210 @@ pub(crate) fn phase_run_corpse_move(world: &mut FrameWorld, time_ms: i32) {
|
||||
if !slot.occupied {
|
||||
continue;
|
||||
}
|
||||
let ragdoll = is_ragdoll_tr_type(slot.tr_type);
|
||||
|
||||
let anim_delta = world
|
||||
.corpse_dobj_tree_delta(slot.entnum, crate::MATCH_TICK_MS as i32)
|
||||
.filter(|d| {
|
||||
d[0] * d[0] + d[1] * d[1] + d[2] * d[2] > CORPSE_ANIM_DELTA_LEN_SQ_MIN
|
||||
&& (!slot.falling || !ragdoll)
|
||||
});
|
||||
if !slot.falling && anim_delta.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut desired = evaluate_trajectory(&slot.trajectory(), time_ms);
|
||||
let world_delta = anim_delta.map(|delta| corpse_anim_world_delta(slot.viewangles, delta));
|
||||
if let Some(world_delta) = world_delta {
|
||||
for axis in 0..3 {
|
||||
desired[axis] += world_delta[axis];
|
||||
}
|
||||
}
|
||||
|
||||
let hit = world.trace_clip(
|
||||
slot.origin,
|
||||
desired,
|
||||
PLAYER_MINS,
|
||||
PLAYER_MAXS,
|
||||
MASK_PLAYER_SOLID,
|
||||
);
|
||||
let mut fraction = hit.fraction;
|
||||
let endpos = if fraction >= 1.0 { desired } else { hit.endpos };
|
||||
world.corpses_mut().slots[i].origin = endpos;
|
||||
if hit.startsolid != 0 {
|
||||
fraction = 0.0;
|
||||
}
|
||||
|
||||
if fraction >= 1.0 {
|
||||
let Some(world_delta) = world_delta else {
|
||||
continue;
|
||||
};
|
||||
|
||||
{
|
||||
let slot = &mut world.corpses_mut().slots[i];
|
||||
slot.tr_type = if ragdoll {
|
||||
TR_RAGDOLL_SETTLED
|
||||
} else {
|
||||
TR_INTERPOLATE
|
||||
};
|
||||
slot.tr_base = endpos;
|
||||
slot.tr_time = 0;
|
||||
slot.tr_duration = 0;
|
||||
slot.tr_delta = [0.0; 3];
|
||||
}
|
||||
if ragdoll {
|
||||
let mut probe = desired;
|
||||
probe[2] -= CORPSE_GROUND_PROBE_DROP;
|
||||
let ground =
|
||||
world.trace_clip(endpos, probe, PLAYER_MINS, PLAYER_MAXS, MASK_PLAYER_SOLID);
|
||||
if ground.fraction < 1.0 || ground.startsolid != 0 {
|
||||
let slot = &mut world.corpses_mut().slots[i];
|
||||
slot.falling = true;
|
||||
let mut vel = slot.tr_delta;
|
||||
for axis in 0..3 {
|
||||
vel[axis] = (vel[axis] + world_delta[axis]) * CORPSE_ANIM_DELTA_TO_VELOCITY;
|
||||
}
|
||||
slot.tr_delta = vel;
|
||||
slot.tr_type = TR_BOUNCE_GRAVITY;
|
||||
slot.tr_time = time_ms;
|
||||
slot.tr_duration = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
world.corpses_mut().slots[i].falling = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if !slot.falling {
|
||||
continue;
|
||||
}
|
||||
|
||||
if hit.allsolid != 0 {
|
||||
let mut lifted = world.corpses().slots[i].origin;
|
||||
lifted[2] += CORPSE_UNSTICK_Z_LIFT;
|
||||
let retry = world.trace_clip(
|
||||
lifted,
|
||||
desired,
|
||||
PLAYER_MINS,
|
||||
PLAYER_MAXS,
|
||||
MASK_PLAYER_SOLID & !CORPSE_UNSTICK_CLIPMASK_DROP,
|
||||
);
|
||||
if retry.allsolid == 0 {
|
||||
let f = retry.fraction;
|
||||
world.corpses_mut().slots[i].origin = [
|
||||
lifted[0] + (desired[0] - lifted[0]) * f,
|
||||
lifted[1] + (desired[1] - lifted[1]) * f,
|
||||
lifted[2] + (desired[2] - lifted[2]) * f,
|
||||
];
|
||||
}
|
||||
}
|
||||
corpse_land(world, i, hit, time_ms);
|
||||
let anim_delta = world.corpse_dobj_tree_delta(slot.entnum, crate::MATCH_TICK_MS as i32);
|
||||
let frame = &*world;
|
||||
let stepped = step_corpse(slot, anim_delta, time_ms, |start, end, mask| {
|
||||
frame.trace_clip(start, end, PLAYER_MINS, PLAYER_MAXS, mask)
|
||||
});
|
||||
world.corpses_mut().slots[i] = stepped;
|
||||
}
|
||||
let live = world.corpses().entnums();
|
||||
world.corpse_dobj_tree_retain(&live);
|
||||
}
|
||||
|
||||
fn step_corpse(
|
||||
mut slot: PlayerCorpseSlot,
|
||||
anim_delta: Option<[f32; 3]>,
|
||||
time_ms: i32,
|
||||
mut trace: impl FnMut([f32; 3], [f32; 3], u32) -> trace_iw4::Trace,
|
||||
) -> PlayerCorpseSlot {
|
||||
let was_falling = slot.falling;
|
||||
let ragdoll = is_ragdoll_tr_type(slot.tr_type);
|
||||
let anim_delta = anim_delta.filter(|d| {
|
||||
d[0] * d[0] + d[1] * d[1] + d[2] * d[2] > CORPSE_ANIM_DELTA_LEN_SQ_MIN
|
||||
&& (!was_falling || !ragdoll)
|
||||
});
|
||||
if !was_falling && anim_delta.is_none() {
|
||||
return slot;
|
||||
}
|
||||
let world_delta = anim_delta.map(|delta| {
|
||||
let step = corpse_anim_world_delta(slot.viewangles, delta);
|
||||
if was_falling {
|
||||
step
|
||||
} else {
|
||||
[step[0], step[1], 0.0]
|
||||
}
|
||||
});
|
||||
|
||||
let mut desired = evaluate_trajectory(&slot.trajectory(), time_ms);
|
||||
if let Some(step) = world_delta {
|
||||
for axis in 0..3 {
|
||||
desired[axis] += step[axis];
|
||||
}
|
||||
}
|
||||
let hit = trace(slot.origin, desired, MASK_PLAYER_SOLID);
|
||||
let endpos = if hit.fraction >= 1.0 {
|
||||
desired
|
||||
} else {
|
||||
hit.endpos
|
||||
};
|
||||
slot.origin = endpos;
|
||||
|
||||
if hit.fraction >= 1.0 && hit.startsolid == 0 {
|
||||
let Some(step) = world_delta else {
|
||||
return slot;
|
||||
};
|
||||
if was_falling {
|
||||
for (base, step) in slot.tr_base.iter_mut().zip(step) {
|
||||
*base += step;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
slot.tr_type = if ragdoll {
|
||||
TR_RAGDOLL_SETTLED
|
||||
} else {
|
||||
TR_INTERPOLATE
|
||||
};
|
||||
slot.tr_base = endpos;
|
||||
slot.tr_time = 0;
|
||||
slot.tr_duration = 0;
|
||||
slot.tr_delta = [0.0; 3];
|
||||
follow_ground_or_fall(&mut slot, time_ms, ragdoll, &mut trace);
|
||||
return slot;
|
||||
}
|
||||
|
||||
if !was_falling {
|
||||
slot.tr_base = endpos;
|
||||
if hit.fraction > 0.0 && hit.startsolid == 0 {
|
||||
follow_ground_or_fall(&mut slot, time_ms, ragdoll, &mut trace);
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
if hit.allsolid != 0 {
|
||||
let mut lifted = slot.origin;
|
||||
lifted[2] += CORPSE_UNSTICK_Z_LIFT;
|
||||
let retry = trace(
|
||||
lifted,
|
||||
desired,
|
||||
MASK_PLAYER_SOLID & !CORPSE_UNSTICK_CLIPMASK_DROP,
|
||||
);
|
||||
if retry.allsolid == 0 {
|
||||
let f = retry.fraction;
|
||||
slot.origin = [
|
||||
lifted[0] + (desired[0] - lifted[0]) * f,
|
||||
lifted[1] + (desired[1] - lifted[1]) * f,
|
||||
lifted[2] + (desired[2] - lifted[2]) * f,
|
||||
];
|
||||
}
|
||||
land_corpse(&mut slot, &hit);
|
||||
return slot;
|
||||
}
|
||||
if hit.normal[2] <= 0.0 {
|
||||
if hit.startsolid != 0 {
|
||||
for axis in 0..3 {
|
||||
slot.origin[axis] += hit.normal[axis];
|
||||
}
|
||||
slot.tr_base = slot.origin;
|
||||
slot.tr_time = time_ms;
|
||||
slot.tr_delta = [0.0; 3];
|
||||
return slot;
|
||||
}
|
||||
slide_down_wall(&mut slot, &hit, desired, time_ms, &mut trace);
|
||||
return slot;
|
||||
}
|
||||
land_corpse(&mut slot, &hit);
|
||||
slot
|
||||
}
|
||||
|
||||
fn follow_ground_or_fall(
|
||||
slot: &mut PlayerCorpseSlot,
|
||||
time_ms: i32,
|
||||
ragdoll: bool,
|
||||
trace: &mut impl FnMut([f32; 3], [f32; 3], u32) -> trace_iw4::Trace,
|
||||
) {
|
||||
let at = slot.origin;
|
||||
let mut probe = at;
|
||||
probe[2] -= CORPSE_GROUND_PROBE_DROP;
|
||||
let ground = trace(at, probe, MASK_PLAYER_SOLID);
|
||||
if ground.fraction < 1.0 || ground.startsolid != 0 {
|
||||
slot.falling = false;
|
||||
return;
|
||||
}
|
||||
let mut step_probe = at;
|
||||
step_probe[2] -= CORPSE_STEP_DOWN;
|
||||
let step = trace(at, step_probe, MASK_PLAYER_SOLID);
|
||||
if step.fraction < 1.0 && step.startsolid == 0 && step.allsolid == 0 && step.normal[2] > 0.0 {
|
||||
slot.origin = step.endpos;
|
||||
slot.tr_base = step.endpos;
|
||||
slot.falling = false;
|
||||
slot.ground_entity_num = i32::from(trace_iw4::trace_get_entity_hit_id(
|
||||
step.hit_type,
|
||||
step.hit_id,
|
||||
));
|
||||
return;
|
||||
}
|
||||
slot.falling = true;
|
||||
slot.tr_type = if ragdoll {
|
||||
TR_BOUNCE_GRAVITY
|
||||
} else {
|
||||
TR_GRAVITY
|
||||
};
|
||||
slot.tr_base = at;
|
||||
slot.tr_time = time_ms;
|
||||
slot.tr_duration = 0;
|
||||
slot.tr_delta = [0.0; 3];
|
||||
}
|
||||
|
||||
fn slide_down_wall(
|
||||
slot: &mut PlayerCorpseSlot,
|
||||
hit: &trace_iw4::Trace,
|
||||
desired: [f32; 3],
|
||||
time_ms: i32,
|
||||
trace: &mut impl FnMut([f32; 3], [f32; 3], u32) -> trace_iw4::Trace,
|
||||
) {
|
||||
let n = hit.normal;
|
||||
let mut start = hit.endpos;
|
||||
let mut rest = [0.0f32; 3];
|
||||
for axis in 0..3 {
|
||||
start[axis] += n[axis] * CORPSE_WALL_SLIDE_GAP;
|
||||
rest[axis] = desired[axis] - hit.endpos[axis];
|
||||
}
|
||||
let into_wall = dot(rest, n);
|
||||
let mut end = start;
|
||||
for axis in 0..3 {
|
||||
end[axis] += rest[axis] - into_wall * n[axis];
|
||||
}
|
||||
let slid = trace(start, end, MASK_PLAYER_SOLID);
|
||||
if slid.allsolid == 0 && slid.startsolid == 0 {
|
||||
slot.origin = if slid.fraction >= 1.0 {
|
||||
end
|
||||
} else {
|
||||
slid.endpos
|
||||
};
|
||||
if slid.fraction < 1.0 && slid.normal[2] > 0.0 {
|
||||
land_corpse(slot, &slid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let speed = evaluate_trajectory_delta(&slot.trajectory(), time_ms);
|
||||
let mut velocity = [0.0, 0.0, speed[2]];
|
||||
let into = dot(velocity, n);
|
||||
if into < 0.0 {
|
||||
for axis in 0..3 {
|
||||
velocity[axis] -= into * n[axis];
|
||||
}
|
||||
}
|
||||
slot.tr_base = slot.origin;
|
||||
slot.tr_time = time_ms;
|
||||
slot.tr_duration = 0;
|
||||
slot.tr_delta = velocity;
|
||||
}
|
||||
|
||||
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
fn corpse_anim_world_delta(angles: [f32; 3], delta: [f32; 3]) -> [f32; 3] {
|
||||
let (mut fwd, right, up) = math_iw4::angle_vectors(angles);
|
||||
let mut left = [
|
||||
@@ -364,18 +478,9 @@ fn corpse_anim_world_delta(angles: [f32; 3], delta: [f32; 3]) -> [f32; 3] {
|
||||
out
|
||||
}
|
||||
|
||||
fn corpse_land(world: &mut FrameWorld, i: usize, hit: trace_iw4::Trace, time_ms: i32) {
|
||||
let slot = &mut world.corpses_mut().slots[i];
|
||||
fn land_corpse(slot: &mut PlayerCorpseSlot, hit: &trace_iw4::Trace) {
|
||||
let ragdoll = is_ragdoll_tr_type(slot.tr_type);
|
||||
slot.tr_delta = [0.0; 3];
|
||||
if hit.allsolid == 0 && hit.normal[2] <= 0.0 {
|
||||
for axis in 0..3 {
|
||||
slot.origin[axis] += hit.normal[axis];
|
||||
}
|
||||
slot.tr_base = slot.origin;
|
||||
slot.tr_time = time_ms;
|
||||
return;
|
||||
}
|
||||
slot.falling = false;
|
||||
slot.tr_type = if ragdoll {
|
||||
TR_RAGDOLL_SETTLED
|
||||
|
||||
@@ -647,6 +647,8 @@ pub(crate) fn think_projectile(world: &mut FrameWorld, tick: Tick, entnum: i32)
|
||||
}
|
||||
return;
|
||||
}
|
||||
let remote = crate::remote_missile::is_remote(world, projectile.id);
|
||||
let mask = crate::remote_missile::projectile_mask(remote);
|
||||
let mut trace_start = start;
|
||||
let mut hops = 0u32;
|
||||
let mut hop_capped = false;
|
||||
@@ -665,7 +667,7 @@ pub(crate) fn think_projectile(world: &mut FrameWorld, tick: Tick, entnum: i32)
|
||||
&BulletTraceQuery {
|
||||
start: trace_start,
|
||||
end,
|
||||
mask: MASK_BULLET_WORLD,
|
||||
mask,
|
||||
ignore: Some(projectile.owner),
|
||||
ignore_hit: None,
|
||||
ignore_model: None,
|
||||
@@ -737,6 +739,22 @@ pub(crate) fn think_projectile(world: &mut FrameWorld, tick: Tick, entnum: i32)
|
||||
}
|
||||
break outcome;
|
||||
};
|
||||
let outcome = if remote {
|
||||
let sky = crate::world::clip_trace(
|
||||
brushes,
|
||||
bsp,
|
||||
mesh,
|
||||
start,
|
||||
end,
|
||||
[0.0; 3],
|
||||
[0.0; 3],
|
||||
crate::bullet_collision::CONTENTS_SKY,
|
||||
&|_| true,
|
||||
);
|
||||
crate::remote_missile::sky_wall_outcome(outcome, start, &sky)
|
||||
} else {
|
||||
outcome
|
||||
};
|
||||
if world.publishes_snapshot()
|
||||
&& projectile.live
|
||||
&& crate::script_player::means(
|
||||
|
||||
@@ -608,6 +608,8 @@ impl FrameWorld<'_> {
|
||||
pub fn shutdown_game(&mut self) {
|
||||
*self.deref_mut() = SimState::new();
|
||||
clear_payload_rows(self.ecs);
|
||||
self.ecs
|
||||
.remove_resource::<crate::remote_missile::RemoteMissiles>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ pub struct ScriptSeat {
|
||||
pub archive_ms: i32,
|
||||
pub ps_offset_ms: i32,
|
||||
pub length_ms: i32,
|
||||
pub final_kill: bool,
|
||||
}
|
||||
|
||||
impl Default for ScriptSeat {
|
||||
@@ -17,6 +18,7 @@ impl Default for ScriptSeat {
|
||||
archive_ms: 0,
|
||||
ps_offset_ms: 0,
|
||||
length_ms: 0,
|
||||
final_kill: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use crate::ProjectileId;
|
||||
use crate::bullet_collision::{CONTENTS_SKY, ColliderId, MASK_BULLET_WORLD, TraceOutcome};
|
||||
use crate::frame::FrameWorld;
|
||||
use crate::match_state::ClientLifecycle;
|
||||
use crate::world::{ClientId, Tick};
|
||||
use bevy_ecs::prelude::{Resource, World};
|
||||
|
||||
const PITCH_RANGE: [f32; 2] = [1.0, 87.0];
|
||||
const PITCH_RATE: f32 = 15.0;
|
||||
@@ -9,6 +12,87 @@ const SPEED_RANGE: [f32; 2] = [3_000.0, 6_000.0];
|
||||
const SPEED_UP: f32 = 2_000.0;
|
||||
const SPEED_DOWN: f32 = 500.0;
|
||||
|
||||
const SKY_CEILING_MIN_NORMAL_Z: f32 = 0.7;
|
||||
|
||||
// The mark is kept for the missile's whole flight. The pilot's control link is
|
||||
// dropped when the pilot dies, and the missile must still pass the sky ceiling then.
|
||||
#[derive(Resource, Debug, Default)]
|
||||
pub(crate) struct RemoteMissiles(Vec<ProjectileId>);
|
||||
|
||||
pub(crate) fn mark_remote(world: &mut World, id: ProjectileId) {
|
||||
let mut marked = world.get_resource_or_insert_with(RemoteMissiles::default);
|
||||
if !marked.0.contains(&id) {
|
||||
marked.0.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_remote(world: &mut FrameWorld, id: ProjectileId) -> bool {
|
||||
world
|
||||
.ecs()
|
||||
.get_resource::<RemoteMissiles>()
|
||||
.is_some_and(|marked| marked.0.contains(&id))
|
||||
}
|
||||
|
||||
fn forget_gone_missiles(world: &mut FrameWorld) {
|
||||
let marked = match world.ecs().get_resource::<RemoteMissiles>() {
|
||||
Some(marked) if !marked.0.is_empty() => marked.0.clone(),
|
||||
_ => return,
|
||||
};
|
||||
let mut flying = Vec::with_capacity(marked.len());
|
||||
world.visit_projectiles(|p| {
|
||||
if marked.contains(&p.id) {
|
||||
flying.push(p.id);
|
||||
}
|
||||
});
|
||||
if flying.len() != marked.len() {
|
||||
world.ecs().resource_mut::<RemoteMissiles>().0 = flying;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn projectile_mask(remote: bool) -> u32 {
|
||||
if remote {
|
||||
MASK_BULLET_WORLD & !CONTENTS_SKY
|
||||
} else {
|
||||
MASK_BULLET_WORLD
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sky_wall_outcome(
|
||||
outcome: TraceOutcome,
|
||||
start: [f32; 3],
|
||||
sky: &trace_iw4::Trace,
|
||||
) -> TraceOutcome {
|
||||
if sky.fraction >= 1.0
|
||||
|| sky.startsolid != 0
|
||||
|| sky.allsolid != 0
|
||||
|| sky.normal[2] > SKY_CEILING_MIN_NORMAL_Z
|
||||
{
|
||||
return outcome;
|
||||
}
|
||||
let reach = match outcome {
|
||||
TraceOutcome::Hit { end, .. } | TraceOutcome::Miss { end } => distance(start, end),
|
||||
TraceOutcome::StartSolid { .. } | TraceOutcome::Invalid { .. } => return outcome,
|
||||
};
|
||||
if distance(start, sky.endpos) >= reach {
|
||||
return outcome;
|
||||
}
|
||||
TraceOutcome::Hit {
|
||||
fraction: sky.fraction,
|
||||
end: sky.endpos,
|
||||
normal: sky.normal,
|
||||
collider: ColliderId::World {
|
||||
surface_flags: sky.surface_flags,
|
||||
contents: sky.contents,
|
||||
glass_encoded: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn distance(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||||
let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
|
||||
}
|
||||
|
||||
pub(crate) fn steer(
|
||||
world: &mut FrameWorld,
|
||||
id: ClientId,
|
||||
@@ -49,6 +133,7 @@ pub(crate) fn advance(world: &mut FrameWorld, tick: Tick) {
|
||||
if !world.publishes_snapshot() {
|
||||
return;
|
||||
}
|
||||
forget_gone_missiles(world);
|
||||
let now = crate::level_time_ms(tick);
|
||||
for id in world.client_ids_sorted() {
|
||||
let Some(mut link) = world.client_meta(id).and_then(|m| m.remote_missile) else {
|
||||
|
||||
@@ -419,7 +419,7 @@ pub(crate) fn select_location(
|
||||
pub(crate) fn command_buttons(command: &str) -> u32 {
|
||||
match command {
|
||||
"+attack" | "mouse1" => buttons::ATTACK,
|
||||
"+activate" | "+usereload" => buttons::USE,
|
||||
"+activate" | "+usereload" => buttons::USE | buttons::USE_RELOAD,
|
||||
"+melee" => buttons::MELEE_CHARGE,
|
||||
"+frag" => buttons::FRAG,
|
||||
"+smoke" => buttons::SMOKE,
|
||||
|
||||
@@ -1061,19 +1061,34 @@ impl PlayerSlot {
|
||||
}
|
||||
|
||||
pub(crate) fn script_seats(world: &World) -> Vec<(ClientId, crate::ScriptSeat)> {
|
||||
world
|
||||
.resource::<RoundScript>()
|
||||
let runtime = world.resource::<RoundScript>();
|
||||
let final_kill = level_flag(runtime, "showingfinalkillcam");
|
||||
runtime
|
||||
.players
|
||||
.iter()
|
||||
.filter(|(_, slot)| &*slot.sessionstate == "spectator" && slot.spectator.target.is_some())
|
||||
.map(|(client, slot)| {
|
||||
let mut seat = slot.seat;
|
||||
seat.spectator_client = slot.spectator.target.unwrap() as i32;
|
||||
seat.final_kill = final_kill;
|
||||
(ClientId(*client), seat)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn level_flag(runtime: &RoundScript, name: &str) -> bool {
|
||||
let symbol = runtime
|
||||
.program
|
||||
.as_ref()
|
||||
.and_then(|program| program.symbol_ids.get(name))
|
||||
.or_else(|| runtime.dynamic_symbols.get(name));
|
||||
match symbol.and_then(|symbol| runtime.objects.get(&0)?.get(symbol)) {
|
||||
Some(Value::Int(n)) => *n != 0,
|
||||
Some(Value::Float(f)) => *f != 0.0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
const SEAT_FIELDS: [&str; 6] = [
|
||||
"forcespectatorclient",
|
||||
"killcamentity",
|
||||
|
||||
@@ -313,25 +313,54 @@ fn present(world: &mut World, now: i32) {
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_lookup(world: &mut World, object: u64, tag: &str) -> Option<Option<[f32; 3]>> {
|
||||
fn with_tag_dobj<T>(
|
||||
world: &mut World,
|
||||
object: u64,
|
||||
read: impl FnOnce(&AuthorityDObjState) -> Option<T>,
|
||||
) -> Option<T> {
|
||||
let presence = world
|
||||
.resource::<RoundScript>()
|
||||
.entities
|
||||
.get(&object)?
|
||||
.presence?;
|
||||
let frame = settled(world);
|
||||
let dobj = frame
|
||||
if let Some(dobj) = frame
|
||||
.entity_collision_capabilities()
|
||||
.iter()
|
||||
.find(|row| row.owner.script_model() == Some(presence))?
|
||||
.dobj
|
||||
.as_ref()?;
|
||||
Some(dobj.tag_world_pose(tag).map(|(at, _)| {
|
||||
dobj.world_from_model
|
||||
.inverse()
|
||||
.transform_point3(glam::Vec3::from(at))
|
||||
.to_array()
|
||||
}))
|
||||
.as_ref()
|
||||
{
|
||||
return read(dobj);
|
||||
}
|
||||
let (model, origin, angles) = {
|
||||
let mut runtime = world.resource_mut::<RoundScript>();
|
||||
(
|
||||
model_field(&mut runtime, object)?,
|
||||
vector(&mut runtime, object, "origin"),
|
||||
vector(&mut runtime, object, "angles"),
|
||||
)
|
||||
};
|
||||
let capability = FrameWorld::from_world(world)
|
||||
.model_capability(&model)
|
||||
.flatten()?;
|
||||
read(&AuthorityDObjState::at_pose(
|
||||
&model,
|
||||
Some(capability),
|
||||
origin,
|
||||
angles,
|
||||
))
|
||||
}
|
||||
|
||||
fn tag_lookup(world: &mut World, object: u64, tag: &str) -> Option<Option<[f32; 3]>> {
|
||||
with_tag_dobj(world, object, |dobj| {
|
||||
Some(dobj.tag_world_pose(tag).map(|(at, _)| {
|
||||
dobj.world_from_model
|
||||
.inverse()
|
||||
.transform_point3(glam::Vec3::from(at))
|
||||
.to_array()
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn tag_world(
|
||||
@@ -339,19 +368,7 @@ pub(crate) fn tag_world(
|
||||
object: u64,
|
||||
tag: &str,
|
||||
) -> Option<([f32; 3], [[f32; 3]; 3])> {
|
||||
let presence = world
|
||||
.resource::<RoundScript>()
|
||||
.entities
|
||||
.get(&object)?
|
||||
.presence?;
|
||||
let frame = settled(world);
|
||||
let matrix = frame
|
||||
.entity_collision_capabilities()
|
||||
.iter()
|
||||
.find(|row| row.owner.script_model() == Some(presence))?
|
||||
.dobj
|
||||
.as_ref()?
|
||||
.tag_world_matrix(tag)?;
|
||||
let matrix = with_tag_dobj(world, object, |dobj| dobj.tag_world_matrix(tag))?;
|
||||
let axis = |v: glam::Vec4| v.truncate().normalize_or_zero().to_array();
|
||||
Some((
|
||||
matrix.w_axis.truncate().to_array(),
|
||||
|
||||
@@ -97,6 +97,32 @@ pub(crate) fn advance(world: &mut World) {
|
||||
update(world, true);
|
||||
}
|
||||
|
||||
pub(crate) fn hold_killcam_viewer(
|
||||
world: &mut World,
|
||||
client: ClientId,
|
||||
cmd: &mut playerstate_iw4::UserCmd,
|
||||
) {
|
||||
let seated = world
|
||||
.get_resource::<RoundScript>()
|
||||
.and_then(|runtime| runtime.players.get(&client.0))
|
||||
.is_some_and(|slot| {
|
||||
&*slot.sessionstate == "spectator"
|
||||
&& slot.seat.archive_ms > 0
|
||||
&& slot.spectator.target.is_some()
|
||||
});
|
||||
if !seated {
|
||||
return;
|
||||
}
|
||||
use playerstate_iw4::buttons;
|
||||
let held = FrameWorld::from_world(world)
|
||||
.player(client)
|
||||
.map_or(0, |ps| ps.weapon);
|
||||
cmd.forwardmove = 0;
|
||||
cmd.rightmove = 0;
|
||||
cmd.buttons &= buttons::PRONE | buttons::CROUCH | buttons::STANCE_HELD;
|
||||
cmd.weapon = held as u16;
|
||||
}
|
||||
|
||||
fn update(world: &mut World, input: bool) {
|
||||
let replacement = RoundScript::new(world.resource::<crate::script::MatchScript>());
|
||||
let mut runtime = std::mem::replace(&mut *world.resource_mut::<RoundScript>(), replacement);
|
||||
@@ -148,7 +174,9 @@ fn update(world: &mut World, input: bool) {
|
||||
&& !archived
|
||||
&& pressed & (playerstate_iw4::buttons::ATTACK | playerstate_iw4::buttons::ADS) != 0;
|
||||
if let Some(forced) = forced {
|
||||
slot.spectator.target = allowed.contains(&forced).then_some(forced);
|
||||
let own_replay =
|
||||
archived && forced == client && targets.iter().any(|(id, ..)| *id == client);
|
||||
slot.spectator.target = (own_replay || allowed.contains(&forced)).then_some(forced);
|
||||
slot.spectator.free = false;
|
||||
} else if input
|
||||
&& pressed & playerstate_iw4::buttons::USE != 0
|
||||
|
||||
@@ -180,14 +180,45 @@ fn aim_point(world: &mut World, aim: TurretAim) -> Option<[f32; 3]> {
|
||||
}
|
||||
}
|
||||
|
||||
fn gunner_input(world: &mut World, client: u32) -> Option<([f32; 3], bool)> {
|
||||
let mut frame = crate::frame::FrameWorld::from_world(world);
|
||||
let angles = frame.player(crate::ClientId(client))?.viewangles;
|
||||
let held = crate::script_player::buttons(&mut frame, crate::ClientId(client));
|
||||
Some((
|
||||
math_iw4::angle_vectors(angles).0,
|
||||
held & playerstate_iw4::buttons::ATTACK != 0,
|
||||
))
|
||||
fn gunner_input(world: &mut World, heli: u64, client: u32) -> Option<([f32; 3], bool)> {
|
||||
let id = crate::ClientId(client);
|
||||
let (eye, angles, held) = {
|
||||
let mut frame = crate::frame::FrameWorld::from_world(world);
|
||||
let ps = *frame.player(id)?;
|
||||
let eye = frame
|
||||
.client_meta(id)
|
||||
.and_then(|meta| meta.linked_weapon_view)
|
||||
.map_or(
|
||||
[
|
||||
ps.origin[0],
|
||||
ps.origin[1],
|
||||
ps.origin[2] + ps.view_height_current,
|
||||
],
|
||||
|view| view.origin,
|
||||
);
|
||||
(
|
||||
eye,
|
||||
ps.viewangles,
|
||||
crate::script_player::buttons(&mut frame, id),
|
||||
)
|
||||
};
|
||||
let dir = math_iw4::angle_vectors(angles).0;
|
||||
let end = std::array::from_fn(|i| eye[i] + dir[i] * GUNNER_RANGE);
|
||||
let ignore = super::natives::engine::TraceIgnore {
|
||||
client: Some(id),
|
||||
..super::turrets::ignore_self(world, heli)
|
||||
};
|
||||
let point = match super::natives::engine::entity_trace(
|
||||
world,
|
||||
eye,
|
||||
end,
|
||||
crate::bullet_collision::MASK_SHOT,
|
||||
ignore,
|
||||
) {
|
||||
crate::bullet_collision::TraceOutcome::Hit { end, .. } => end,
|
||||
_ => end,
|
||||
};
|
||||
Some((point, held & playerstate_iw4::buttons::ATTACK != 0))
|
||||
}
|
||||
|
||||
fn fire_weapon(world: &mut World, receiver: &Value, args: &[Value]) -> Result<Value, String> {
|
||||
@@ -992,7 +1023,7 @@ pub(crate) fn advance(world: &mut World) {
|
||||
.map_or((None, None), |heli| (heli.gunner, heli.weapon));
|
||||
let control = gunner
|
||||
.filter(|client| world.resource::<RoundScript>().players.contains_key(client))
|
||||
.and_then(|client| gunner_input(world, client));
|
||||
.and_then(|client| gunner_input(world, id, client));
|
||||
let fire_ms = weapon
|
||||
.and_then(|weapon| crate::frame::FrameWorld::from_world(world).combat_facts_for(weapon))
|
||||
.map_or(100, |facts| facts.fire_time_ms.max(1));
|
||||
@@ -1111,10 +1142,8 @@ pub(crate) fn advance(world: &mut World) {
|
||||
}
|
||||
let [pitch, yaw, roll] = angles;
|
||||
match control {
|
||||
Some((view, attack)) => {
|
||||
heli.turret = Some(TurretAim::Point(std::array::from_fn(|i| {
|
||||
next[i] + view[i] * GUNNER_RANGE
|
||||
})));
|
||||
Some((point, attack)) => {
|
||||
heli.turret = Some(TurretAim::Point(point));
|
||||
heli.on_target = true;
|
||||
if attack && now >= heli.next_fire_ms {
|
||||
heli.next_fire_ms = now + fire_ms;
|
||||
|
||||
@@ -170,6 +170,7 @@ pub(crate) fn register(registry: &mut NativeRegistry) {
|
||||
angles,
|
||||
..Default::default()
|
||||
});
|
||||
crate::remote_missile::mark_remote(frame.ecs(), id);
|
||||
}
|
||||
Ok(Value::Undefined)
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ pub(crate) use host::players::{
|
||||
pub(crate) use host::presence::sync_presence;
|
||||
pub use host::registry::{Native, NativeRegistry};
|
||||
pub(crate) use host::restart::restart_level;
|
||||
pub(crate) use host::spectators::hold_killcam_viewer;
|
||||
pub(crate) use host::weapons::{publish_projectile_launches, sync_engine_events};
|
||||
pub use ir::IR_VERSION;
|
||||
pub(crate) use ir::{Binary, Callee, Function, Global, Op, Unary};
|
||||
|
||||
@@ -49,6 +49,7 @@ pub(crate) fn spawn(
|
||||
};
|
||||
world.unlink_player_area(id);
|
||||
world.client_meta_mut(id).shield = None;
|
||||
let prev_health = world.player(id).map(|p| p.health);
|
||||
let mut ps = spawn_player_state(origin, angles);
|
||||
if let Some(max) = world
|
||||
.client_meta(id)
|
||||
@@ -56,8 +57,12 @@ pub(crate) fn spawn(
|
||||
.filter(|&n| n > 0)
|
||||
{
|
||||
ps.max_health = max;
|
||||
ps.health = max;
|
||||
}
|
||||
ps.health = match sessionstate {
|
||||
"playing" => ps.max_health,
|
||||
"dead" | "intermission" => prev_health.unwrap_or(0),
|
||||
_ => 0,
|
||||
};
|
||||
let cmd = spawn_command_angles(world, id);
|
||||
ps.delta_angles = std::array::from_fn(|axis| angles[axis] - cmd[axis] as f32 * SHORT2ANGLE);
|
||||
let prev_teleport = world
|
||||
|
||||
@@ -393,6 +393,7 @@ fn run_players_system(ecs: &mut World) {
|
||||
crate::script::player_commands(world.ecs(), id.0, cmd.buttons, old_buttons);
|
||||
let mut cmd = *cmd;
|
||||
crate::script_player::constrain_cmd(&mut world, *id, &mut cmd);
|
||||
crate::script::hold_killcam_viewer(world.ecs(), *id, &mut cmd);
|
||||
crate::script::select_location(world.ecs(), id.0, &mut cmd, old_buttons);
|
||||
if world
|
||||
.client_meta(*id)
|
||||
|
||||
Reference in New Issue
Block a user