mirror of
https://github.com/vladtrc/iw4L
synced 2026-10-25 18:05:19 -04:00
Let the Predator fly down through the sky ceiling
The Predator script starts the missile 14000 units from its target, or 14000 above the player on maps without Predator spawn points. On Sub Base, Wasteland, Karachi, Invasion, Rundown, Carnival, Storm and Trailer Park that start is above the map's sky ceiling, a slab of sky brushes (contents 0x800; on Sub Base at z 8128-8192), and on Fuel a rooftop Predator starts inside it. The projectile trace uses MASK_SHOT, which includes the sky bit, so the missile hit the top of the slab about 1.75 s after launch and blew up some 7,900 units above Sub Base, with the view still a wash of fog. The Predator works on those maps in MW2, so it has to pass the ceiling there. A missile is now marked as remote-controlled when a player is linked to it as its pilot, and keeps the mark for its whole flight, since the control link is dropped when the pilot dies and the missile must still pass the ceiling then. The mark goes when the projectile is gone and on game shutdown. The marked missile's world trace leaves out sky brushes. A second, sky-only trace over the same tick still stops it on a sky wall or underside (a sky face with normal z of 0.7 or less) through the usual contact detonation, so a Predator steered sideways below the ceiling still ends at the edge of the map. A sky trace that starts inside a sky brush is ignored, which covers a missile inside the slab and the Fuel start. Every other projectile keeps the full mask: an RPG fired straight up still explodes on the ceiling. Checked on the Mac on Sub Base: the RPG still explodes at z 8127.9. The unsteered Predator passes z 8192 and reaches the ground about 5.4 s after launch, with a clear view of the base before impact (before: explosion at z 8192.1 after 1.75 s). A Predator whose pilot is killed about 1.5 s after launch still passes the ceiling and lands. One steered flat below the ceiling explodes on the south sky wall at y -15391.9. On our iPhone build in a Sub Base match, Predators reached the ground with a clear view before impact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user