#![allow(clippy::indexing_slicing)]
use core::ops::ControlFlow;
use crate::game_data::{ActionId, EntityType, MobjFlag, StateNum};
use crate::map::MapData;
use crate::math::*;
use crate::rules::PlayerAction;
use crate::specials;
use crate::types::AmmoType;
use crate::world::{Entity, EntityId, World};
pub fn advance_state(entity: &mut Entity) -> ActionId {
if entity.tics == -1 {
return ActionId::NONE; }
entity.tics -= 1;
if entity.tics > 0 {
return ActionId::NONE;
}
let Some(cur) = entity.state.get() else { return ActionId::NONE };
set_entity_state(entity, cur.next_state)
}
pub fn set_entity_state(entity: &mut Entity, state_num: StateNum) -> ActionId {
let Some(state) = state_num.get() else {
entity.state = StateNum::NULL;
return ActionId::NONE;
};
entity.state = state_num;
entity.tics = state.tics;
entity.sprite = state.sprite;
entity.frame = state.frame;
state.action
}
#[inline]
#[allow(clippy::too_many_arguments)]
pub fn segments_cross(
ax1: Fixed, ay1: Fixed, ax2: Fixed, ay2: Fixed,
bx1: Fixed, by1: Fixed, bx2: Fixed, by2: Fixed,
) -> bool {
let d1 = cross2d(bx2 - bx1, by2 - by1, ax1 - bx1, ay1 - by1);
let d2 = cross2d(bx2 - bx1, by2 - by1, ax2 - bx1, ay2 - by1);
if (d1 > 0 && d2 > 0) || (d1 < 0 && d2 < 0) { return false; }
let d3 = cross2d(ax2 - ax1, ay2 - ay1, bx1 - ax1, by1 - ay1);
let d4 = cross2d(ax2 - ax1, ay2 - ay1, bx2 - ax1, by2 - ay1);
if (d3 > 0 && d4 > 0) || (d3 < 0 && d4 < 0) { return false; }
true
}
#[inline]
pub fn cross2d(ax: Fixed, ay: Fixed, bx: Fixed, by: Fixed) -> i64 {
ax as i64 * by as i64 - ay as i64 * bx as i64
}
#[inline]
fn within_angle(angle_diff: u32, tolerance: u32) -> bool {
angle_diff <= tolerance || angle_diff >= 0u32.wrapping_sub(tolerance)
}
fn for_each_special_line(
map: &MapData,
x: Fixed,
y: Fixed,
mut f: impl FnMut(usize, &crate::map::Line) -> ControlFlow<()>,
) {
let bm = &map.blockmap;
let bx = (x - bm.origin_x) >> crate::map::MAPBLOCKSHIFT;
let by = (y - bm.origin_y) >> crate::map::MAPBLOCKSHIFT;
for dx in -1..=1 {
for dy in -1..=1 {
let cx = bx + dx;
let cy = by + dy;
if cx < 0 || cy < 0 || cx as usize >= bm.width || cy as usize >= bm.height {
continue;
}
let block_idx = cy as usize * bm.width + cx as usize;
if block_idx >= bm.offsets.len() { continue; }
let mut offset = bm.offsets[block_idx] as usize;
if offset < bm.lists.len() && bm.lists[offset] == 0 {
offset += 1;
}
while offset < bm.lists.len() {
let val = bm.lists[offset];
if val == 0xFFFF { break; }
let line_idx = val as usize;
if line_idx < map.lines.len() {
let line = &map.lines[line_idx];
if line.special != 0
&& f(line_idx, line).is_break()
{
return;
}
}
offset += 1;
}
}
}
}
#[inline]
fn line_crosses(map: &MapData, line: &crate::map::Line, x1: Fixed, y1: Fixed, x2: Fixed, y2: Fixed) -> bool {
let v1 = &map.vertexes[line.v1 as usize];
let v2 = &map.vertexes[line.v2 as usize];
segments_cross(x1, y1, x2, y2, v1.x, v1.y, v2.x, v2.y)
}
pub fn apply_player_input(world: &mut World, map: &MapData, player_id: EntityId, action: &PlayerAction) {
let Some(entity) = world.get_mut(player_id) else { return };
entity.angle = entity.angle.wrapping_add((action.angle_turn as u32) << 16);
if action.forward_move != 0 {
let move_amt = (action.forward_move as Fixed) * 2048;
let fine = (entity.angle >> ANGLETOFINESHIFT) as usize;
entity.momx += fixed_mul(move_amt, finecosine(fine));
entity.momy += fixed_mul(move_amt, finesine(fine));
}
if action.side_move != 0 {
let move_amt = (action.side_move as Fixed) * 2048;
let fine = (entity.angle.wrapping_sub(ANG90) >> ANGLETOFINESHIFT) as usize;
entity.momx += fixed_mul(move_amt, finecosine(fine & FINEMASK));
entity.momy += fixed_mul(move_amt, finesine(fine));
}
let old_x = entity.x;
let old_y = entity.y;
crate::physics::xy_movement(entity, map);
crate::physics::z_movement(entity);
let new_x = entity.x;
let new_y = entity.y;
check_crossed_lines(world, map, old_x, old_y, new_x, new_y);
}
pub fn check_crossed_lines(world: &mut World, map: &MapData, old_x: Fixed, old_y: Fixed, new_x: Fixed, new_y: Fixed) {
if old_x == new_x && old_y == new_y { return; }
let mut exit_hit: Option<crate::world::LevelExit> = None;
for_each_special_line(map, new_x, new_y, |line_idx, line| {
if line_crosses(map, line, old_x, old_y, new_x, new_y) {
if specials::is_exit_line(line.special) {
exit_hit = Some(crate::world::exit_kind(line.special));
} else {
specials::cross_special_line(map, &mut world.specials, line_idx);
}
}
ControlFlow::Continue(())
});
if let Some(kind) = exit_hit {
world.level_exit = kind;
}
}
pub fn use_lines(world: &mut World, map: &MapData, player_id: EntityId) {
let Some(entity) = world.get(player_id) else { return };
let fine = (entity.angle >> ANGLETOFINESHIFT) as usize;
let use_range = 64 * FRACUNIT;
let px = entity.x;
let py = entity.y;
let x2 = px + fixed_mul(use_range, finecosine(fine));
let y2 = py + fixed_mul(use_range, finesine(fine));
let mut exit_hit: Option<crate::world::LevelExit> = None;
for_each_special_line(map, px, py, |line_idx, line| {
if line_crosses(map, line, px, py, x2, y2) {
if specials::is_exit_line(line.special) {
exit_hit = Some(crate::world::exit_kind(line.special));
return ControlFlow::Break(());
}
specials::use_special_line(map, &mut world.specials, line_idx);
}
ControlFlow::Continue(())
});
if let Some(kind) = exit_hit {
world.level_exit = kind;
}
}
pub fn hitscan_attack(
world: &mut World,
map: &MapData,
shooter_id: EntityId,
damage: i32,
angle_spread: i32,
puff_type: EntityType,
) {
let Some(shooter) = world.get(shooter_id) else { return };
let sx = shooter.x;
let sy = shooter.y;
let sz = shooter.z;
let s_height = shooter.height;
let angle = shooter.angle.wrapping_add(angle_spread as u32);
let range = 2048 * FRACUNIT;
let tolerance = ANG90 / 18;
let mut best_dist = i64::MAX;
let mut best_id = None;
for e in world.iter() {
if e.id == shooter_id || !e.flags.contains(MobjFlag::Shootable) { continue; }
let dx = e.x - sx;
let dy = e.y - sy;
let dist = dx.abs() as i64 + dy.abs() as i64;
if dist <= 0 || dist > range as i64 { continue; }
let to_angle = crate::map::point_to_angle(dx, dy);
if !within_angle(to_angle.wrapping_sub(angle), tolerance) {
continue;
}
if dist < best_dist {
let eye_z = sz + s_height - (s_height >> 2);
if crate::physics::check_sight(map, sx, sy, eye_z, e.x, e.y, e.z, e.height) {
best_dist = dist;
best_id = Some(e.id);
}
}
}
if let Some(target_id) = best_id {
if let Some(target) = world.get(target_id) {
spawn_puff(world, puff_type, target.x, target.y, target.z + (target.height >> 1));
}
apply_damage_to(world, target_id, damage, Some(shooter_id));
} else {
let fine = (angle >> ANGLETOFINESHIFT) as usize & FINEMASK;
let cos = finecosine(fine);
let sin = finesine(fine);
let eye_z = sz + s_height - (s_height >> 2);
let step = 64 * FRACUNIT;
let max_steps = (range / step).min(32);
for i in 1..=max_steps {
let px = sx + fixed_mul(step * i, cos);
let py = sy + fixed_mul(step * i, sin);
if !crate::physics::check_sight(map, sx, sy, eye_z, px, py, eye_z, 0) {
let half = step / 2;
let px = sx + fixed_mul(step * i - half, cos);
let py = sy + fixed_mul(step * i - half, sin);
spawn_puff(world, puff_type, px, py, eye_z);
break;
}
}
}
}
pub fn apply_damage_to(
world: &mut World,
target_id: EntityId,
damage: i32,
source: Option<EntityId>,
) {
let (shootable, painchance, painstate_null, is_player, seestate_null, spawnstate, cur_state) =
match world.get(target_id) {
Some(t) => {
let info = t.entity_type.info();
(
t.flags.contains(MobjFlag::Shootable),
info.map(|i| i.painchance).unwrap_or(0),
info.map(|i| i.painstate.is_null()).unwrap_or(true),
t.entity_type == crate::world::EntityType(0),
info.map(|i| i.seestate.is_null()).unwrap_or(true),
info.map(|i| i.spawnstate).unwrap_or(crate::game_data::StateNum::NULL),
t.state,
)
}
None => return,
};
if !shootable {
return;
}
let damage = if is_player {
absorb_with_armor(world, target_id, damage)
} else {
damage
};
if let Some(src_id) = source
&& src_id != target_id
{
let src_shootable = world
.get(src_id)
.is_some_and(|s| s.flags.contains(MobjFlag::Shootable));
if src_shootable
&& let Some(target) = world.get_mut(target_id)
{
target.target = Some(src_id);
target.reaction_time = 0;
let should_wake = !seestate_null && cur_state == spawnstate;
if should_wake
&& let Some(info) = target.entity_type.info()
{
set_entity_state(target, info.seestate);
}
}
}
let pain_roll = world.p_random();
let Some(target) = world.get_mut(target_id) else { return };
target.health -= damage;
if target.health <= 0 {
if let Some(info) = target.entity_type.info() {
target.flags.remove(MobjFlag::Shootable | MobjFlag::Solid);
set_entity_state(target, info.deathstate);
}
} else if !painstate_null
&& pain_roll < painchance
&& let Some(info) = target.entity_type.info()
{
set_entity_state(target, info.painstate);
}
}
fn absorb_with_armor(world: &mut World, pid: EntityId, damage: i32) -> i32 {
let Some(ps) = world.player_state_mut(pid) else { return damage };
let armor_type = ps.armor_type as u8;
if armor_type == 0 || ps.armor_points <= 0 {
return damage;
}
let saved_want = match armor_type {
1 => damage / 3, _ => damage / 2, };
let saved = saved_want.min(ps.armor_points);
if saved >= ps.armor_points {
ps.armor_type = crate::types::ArmorType::None;
}
ps.armor_points -= saved;
damage - saved
}
pub fn radius_damage(world: &mut World, origin: EntityId, radius: Fixed, max_damage: i32) {
let Some(entity) = world.get(origin) else { return };
let ox = entity.x;
let oy = entity.y;
let mut targets = alloc::vec::Vec::new();
for e in world.iter() {
if e.id == origin { continue; }
if !e.flags.contains(MobjFlag::Shootable) { continue; }
let dist = (e.x - ox).abs().max((e.y - oy).abs());
if dist >= radius { continue; }
let damage = max_damage - (dist >> FRACBITS).min(max_damage);
if damage > 0 {
targets.push((e.id, damage));
}
}
for (tid, damage) in targets {
apply_damage_to(world, tid, damage, Some(origin));
}
}
pub fn explode_missile(world: &mut World, id: EntityId) {
let Some(missile) = world.get_mut(id) else { return };
missile.momx = 0;
missile.momy = 0;
missile.momz = 0;
missile.flags.remove(MobjFlag::Missile); if let Some(info) = missile.entity_type.info() {
if !info.deathstate.is_null() {
set_entity_state(missile, info.deathstate);
} else {
missile.state = StateNum::NULL;
}
} else {
missile.state = StateNum::NULL;
}
}
pub fn check_missile_collision(world: &World, missile_id: EntityId) -> Option<EntityId> {
let missile = world.get(missile_id)?;
if !missile.flags.contains(MobjFlag::Missile) { return None; }
let mx = missile.x;
let my = missile.y;
let mradius = missile.radius;
let source_id = missile.target;
world.iter().find_map(|e| {
if e.id == missile_id { return None; }
if Some(e.id) == source_id { return None; }
if !e.flags.contains(MobjFlag::Shootable) { return None; }
let dx = (e.x - mx).abs();
let dy = (e.y - my).abs();
let touch = mradius + e.radius;
(dx < touch && dy < touch).then_some(e.id)
})
}
pub fn melee_attack(world: &mut World, pid: EntityId, damage: i32, range: Fixed) {
let Some(shooter) = world.get(pid) else { return };
let sx = shooter.x;
let sy = shooter.y;
let angle = shooter.angle;
let tolerance = ANG90 / 6;
let mut best_dist = i64::MAX;
let mut best_id = None;
for e in world.iter() {
if e.id == pid || !e.flags.contains(MobjFlag::Shootable) { continue; }
let dx = e.x - sx;
let dy = e.y - sy;
let dist = dx.abs() as i64 + dy.abs() as i64;
if dist <= 0 || dist > range as i64 { continue; }
let to_angle = crate::map::point_to_angle(dx, dy);
if !within_angle(to_angle.wrapping_sub(angle), tolerance) { continue; }
if dist < best_dist {
best_dist = dist;
best_id = Some(e.id);
}
}
if let Some(target_id) = best_id {
apply_damage_to(world, target_id, damage, Some(pid));
}
}
pub fn tick_entities(
world: &mut World,
map: &MapData,
mut dispatch: impl FnMut(ActionId, &mut World, &MapData, EntityId),
) {
let ids = world.take_entity_ids();
for &id in &ids {
let mut action_id = if let Some(entity) = world.get_mut(id) {
advance_state(entity)
} else {
ActionId::NONE
};
let mut chain = 0;
while action_id != ActionId::NONE && chain < 8 {
let state_before = world.get(id).map(|e| e.state);
dispatch(action_id, world, map, id);
let state_after = world.get(id).map(|e| e.state);
action_id = if state_after != state_before {
state_after
.and_then(|s| s.get())
.map_or(ActionId::NONE, |st| st.action)
} else {
ActionId::NONE
};
chain += 1;
}
if !world.is_controlled(id) {
if let Some(entity) = world.get_mut(id) {
let old_x = entity.x;
let old_y = entity.y;
crate::physics::xy_movement(entity, map);
crate::physics::z_movement(entity);
let new_x = entity.x;
let new_y = entity.y;
check_crossed_lines(world, map, old_x, old_y, new_x, new_y);
}
if let Some(entity) = world.get(id)
&& entity.flags.contains(MobjFlag::Missile)
&& entity.momx == 0 && entity.momy == 0
{
explode_missile(world, id);
}
if let Some(hit_id) = check_missile_collision(world, id) {
let (damage, shooter) = world
.get(id)
.and_then(|e| e.entity_type.info().map(|info| (info.damage, e.target)))
.unwrap_or((0, None));
if damage > 0 {
apply_damage_to(world, hit_id, damage, shooter);
}
explode_missile(world, id);
}
}
}
for &id in &ids {
if world.get(id).is_some_and(|e| e.state.is_null()) {
world.remove(id);
}
}
world.return_id_scratch(ids);
}
pub fn spawn_puff(world: &mut World, puff_type: EntityType, x: Fixed, y: Fixed, z: Fixed) {
let Some(info) = puff_type.info() else { return };
let mut puff = Entity {
entity_type: puff_type,
x, y, z,
flags: info.flags(),
..Entity::default()
};
if let Some(st) = info.spawnstate.get() {
puff.sprite = st.sprite;
puff.frame = st.frame;
puff.state = info.spawnstate;
puff.tics = st.tics;
}
world.spawn(puff);
}
pub fn spawn_projectile(world: &mut World, source_id: EntityId, target_id: EntityId, missile_type: EntityType) {
let Some(source) = world.get(source_id) else { return };
let Some(target) = world.get(target_id) else { return };
let Some(info) = missile_type.info() else { return };
let sx = source.x;
let sy = source.y;
let sz = source.z + (source.height >> 1);
let dx = target.x - sx;
let dy = target.y - sy;
let dz = (target.z + (target.height >> 1)) - sz;
let dist = ((dx.abs() as i64 + dy.abs() as i64) >> FRACBITS).max(1) as i32;
let speed = info.speed;
let angle = crate::map::point_to_angle(dx, dy);
let mut missile = Entity {
entity_type: missile_type,
x: sx, y: sy, z: sz,
angle,
radius: info.radius,
height: info.height,
health: info.spawnhealth,
flags: info.flags(),
target: Some(source_id),
momx: fixed_div(fixed_mul(dx >> FRACBITS, speed), dist),
momy: fixed_div(fixed_mul(dy >> FRACBITS, speed), dist),
momz: fixed_div(fixed_mul(dz >> FRACBITS, speed), dist),
..Entity::default()
};
if let Some(st) = info.spawnstate.get() {
missile.sprite = st.sprite;
missile.frame = st.frame;
missile.state = info.spawnstate;
missile.tics = st.tics;
}
world.spawn(missile);
}
pub fn fire_player_projectile(world: &mut World, pid: EntityId, missile_type: EntityType) {
let Some(shooter) = world.get(pid) else { return };
let Some(info) = missile_type.info() else { return };
let sx = shooter.x;
let sy = shooter.y;
let sz = shooter.z + (shooter.height >> 1) + 8 * FRACUNIT;
let angle = shooter.angle;
let fine = (angle >> ANGLETOFINESHIFT) as usize & FINEMASK;
let speed = info.speed;
let mut missile = Entity {
entity_type: missile_type,
x: sx, y: sy, z: sz,
angle,
radius: info.radius,
height: info.height,
health: info.spawnhealth,
flags: info.flags(),
target: Some(pid),
momx: fixed_mul(speed, finecosine(fine)),
momy: fixed_mul(speed, finesine(fine)),
momz: 0,
..Entity::default()
};
if let Some(st) = info.spawnstate.get() {
missile.sprite = st.sprite;
missile.frame = st.frame;
missile.state = info.spawnstate;
missile.tics = st.tics;
}
world.spawn(missile);
}
pub const DIR_SPEED: [(Fixed, Fixed); 8] = [
(FRACUNIT, 0),
(47000, 47000),
(0, FRACUNIT),
(-47000, 47000),
(-FRACUNIT, 0),
(-47000, -47000),
(0, -FRACUNIT),
(47000, -47000),
];
const DI_EAST: u8 = 0;
const DI_SOUTHEAST: u8 = 7;
const DI_NODIR: u8 = 8;
const OPPOSITE_DIR: [u8; 9] = [4, 5, 6, 7, 0, 1, 2, 3, 8];
const DIAG_DIRS: [u8; 4] = [3, 1, 5, 7];
fn gradual_turn_to_movedir(world: &mut World, id: EntityId) {
if let Some(e) = world.get_mut(id) {
if e.move_dir >= 8 {
return;
}
e.angle &= 7u32 << 29;
let target = (e.move_dir as u32) << 29;
let delta = e.angle.wrapping_sub(target) as i32;
if delta > 0 {
e.angle = e.angle.wrapping_sub(ANG90 / 2);
} else if delta < 0 {
e.angle = e.angle.wrapping_add(ANG90 / 2);
}
}
}
fn monster_move(world: &mut World, map: &MapData, id: EntityId) -> bool {
let (dir, speed, x, y) = {
let Some(e) = world.get(id) else { return false };
if e.move_dir >= 8 {
return false;
}
let Some(info) = e.entity_type.info() else {
return false;
};
(e.move_dir as usize, info.speed, e.x, e.y)
};
let (sx, sy) = DIR_SPEED[dir];
let tryx = x + speed * sx;
let tryy = y + speed * sy;
match world.get_mut(id) {
Some(e) => crate::physics::try_move(e, map, tryx, tryy),
None => false,
}
}
fn monster_try_walk(world: &mut World, map: &MapData, id: EntityId) -> bool {
if !monster_move(world, map, id) {
return false;
}
let r = world.p_random();
if let Some(e) = world.get_mut(id) {
e.move_count = r & 15;
}
true
}
fn monster_new_chase_dir(world: &mut World, map: &MapData, id: EntityId) {
let Some(target_id) = world.get(id).and_then(|e| e.target) else {
return;
};
let (olddir, sx, sy) = match world.get(id) {
Some(e) => (e.move_dir, e.x, e.y),
None => return,
};
let turnaround = OPPOSITE_DIR[olddir.min(8) as usize];
let (tx, ty) = match world.get(target_id) {
Some(t) => (t.x, t.y),
None => return,
};
let deltax = tx - sx;
let deltay = ty - sy;
let mut d1 = if deltax > 10 * FRACUNIT {
0 } else if deltax < -10 * FRACUNIT {
4 } else {
DI_NODIR
};
let mut d2 = if deltay < -10 * FRACUNIT {
6 } else if deltay > 10 * FRACUNIT {
2 } else {
DI_NODIR
};
if d1 != DI_NODIR && d2 != DI_NODIR {
let idx = ((deltay < 0) as usize) << 1 | ((deltax > 0) as usize);
let diag = DIAG_DIRS[idx];
if diag != turnaround {
if let Some(e) = world.get_mut(id) {
e.move_dir = diag;
}
if monster_try_walk(world, map, id) {
return;
}
}
}
if world.p_random() > 200 || deltay.abs() > deltax.abs() {
(d1, d2) = (d2, d1);
}
if d1 == turnaround {
d1 = DI_NODIR;
}
if d2 == turnaround {
d2 = DI_NODIR;
}
if d1 != DI_NODIR {
if let Some(e) = world.get_mut(id) {
e.move_dir = d1;
}
if monster_try_walk(world, map, id) {
return;
}
}
if d2 != DI_NODIR {
if let Some(e) = world.get_mut(id) {
e.move_dir = d2;
}
if monster_try_walk(world, map, id) {
return;
}
}
if olddir != DI_NODIR {
if let Some(e) = world.get_mut(id) {
e.move_dir = olddir;
}
if monster_try_walk(world, map, id) {
return;
}
}
if world.p_random() & 1 != 0 {
for tdir in DI_EAST..=DI_SOUTHEAST {
if tdir == turnaround {
continue;
}
if let Some(e) = world.get_mut(id) {
e.move_dir = tdir;
}
if monster_try_walk(world, map, id) {
return;
}
}
} else {
for tdir in (DI_EAST..=DI_SOUTHEAST).rev() {
if tdir == turnaround {
continue;
}
if let Some(e) = world.get_mut(id) {
e.move_dir = tdir;
}
if monster_try_walk(world, map, id) {
return;
}
}
}
if turnaround != DI_NODIR {
if let Some(e) = world.get_mut(id) {
e.move_dir = turnaround;
}
if monster_try_walk(world, map, id) {
return;
}
}
if let Some(e) = world.get_mut(id) {
e.move_dir = DI_NODIR;
}
}
pub fn check_entity_sight(world: &World, map: &MapData, id: EntityId, target_id: EntityId) -> bool {
let Some(entity) = world.get(id) else { return false };
let Some(target) = world.get(target_id) else { return false };
let eye_z = entity.z + entity.height - (entity.height >> 2);
crate::physics::check_sight(
map,
entity.x, entity.y, eye_z,
target.x, target.y, target.z, target.height,
)
}
pub fn a_look(world: &mut World, map: &MapData, id: EntityId) {
let Some(entity) = world.get(id) else { return };
let my_x = entity.x;
let my_y = entity.y;
let my_angle = entity.angle;
let eye_z = entity.z + entity.height - (entity.height >> 2);
let etype = entity.entity_type;
const MELEE_CONE_RANGE: Fixed = 64 * FRACUNIT;
let mut best_id = None;
let mut best_dist = i64::MAX;
for pid in world.controlled_entities() {
let Some(player) = world.get(pid) else { continue };
let dx = player.x - my_x;
let dy = player.y - my_y;
let manhattan = dx.abs() as i64 + dy.abs() as i64;
if manhattan >= best_dist {
continue;
}
let approx_dist = dx.abs().max(dy.abs()) + (dx.abs().min(dy.abs()) >> 1);
if approx_dist > MELEE_CONE_RANGE {
let to_angle = crate::map::point_to_angle(dx, dy);
let diff = to_angle.wrapping_sub(my_angle);
if diff > ANG90 && diff < ANG90.wrapping_mul(3) {
continue;
}
}
if crate::physics::check_sight(
map,
my_x, my_y, eye_z,
player.x, player.y, player.z, player.height,
) {
best_dist = manhattan;
best_id = Some(pid);
}
}
if let Some(target_id) = best_id {
let Some(info) = etype.info() else { return };
if let Some(entity) = world.get_mut(id) {
entity.target = Some(target_id);
if !info.seestate.is_null() {
set_entity_state(entity, info.seestate);
}
}
}
}
pub fn a_face_target(world: &mut World, id: EntityId) {
let (target_x, target_y, target_shadow) = {
let Some(entity) = world.get(id) else { return };
let Some(tid) = entity.target else { return };
let Some(target) = world.get(tid) else { return };
(
target.x,
target.y,
target.flags.contains(MobjFlag::Shadow),
)
};
let (sx, sy) = match world.get(id) {
Some(e) => (e.x, e.y),
None => return,
};
let base_angle = crate::map::point_to_angle(target_x - sx, target_y - sy);
let shadow_offset: i32 = if target_shadow {
(world.p_random() - world.p_random()) << 21
} else {
0
};
if let Some(entity) = world.get_mut(id) {
entity.flags.remove(MobjFlag::Ambush);
entity.angle = base_angle.wrapping_add(shadow_offset as u32);
}
}
pub fn a_chase(world: &mut World, map: &MapData, id: EntityId) {
let (etype, target, reaction, move_count, just_attacked) = {
let Some(e) = world.get(id) else { return };
(
e.entity_type,
e.target,
e.reaction_time,
e.move_count,
e.flags.contains(MobjFlag::JustAttacked),
)
};
if just_attacked {
if let Some(e) = world.get_mut(id) {
e.flags.remove(MobjFlag::JustAttacked);
}
return;
}
if reaction > 0
&& let Some(e) = world.get_mut(id)
{
e.reaction_time -= 1;
}
let Some(target_id) = target else {
let Some(info) = etype.info() else { return };
if let Some(e) = world.get_mut(id) { set_entity_state(e, info.spawnstate); }
return;
};
gradual_turn_to_movedir(world, id);
let Some(info) = etype.info() else { return };
if !info.meleestate.is_null() && check_melee_range(world, id, target_id) {
if let Some(e) = world.get_mut(id) { set_entity_state(e, info.meleestate); }
return;
}
let missile_ready = !info.missilestate.is_null()
&& reaction <= 0
&& move_count == 0
&& check_missile_range(world, map, id, target_id);
if missile_ready {
if let Some(e) = world.get_mut(id) {
set_entity_state(e, info.missilestate);
e.flags.insert(MobjFlag::JustAttacked);
}
return;
}
let expired = match world.get_mut(id) {
Some(e) => {
e.move_count -= 1;
e.move_count < 0
}
None => return,
};
if expired || !monster_move(world, map, id) {
monster_new_chase_dir(world, map, id);
}
if etype.info().is_some_and(|i| i.activesound != 0) {
let _ = world.p_random();
}
}
#[inline]
pub fn check_melee_range(world: &World, id: EntityId, target_id: EntityId) -> bool {
let Some(entity) = world.get(id) else { return false };
let Some(target) = world.get(target_id) else { return false };
let dist = (target.x - entity.x).abs() + (target.y - entity.y).abs();
dist < 64 * FRACUNIT + target.radius
}
pub fn check_missile_range(
world: &mut World,
map: &MapData,
id: EntityId,
target_id: EntityId,
) -> bool {
if !check_entity_sight(world, map, id, target_id) {
return false;
}
let (ex, ey, has_melee, just_hit) = {
let Some(e) = world.get(id) else { return false };
let Some(info) = e.entity_type.info() else {
return false;
};
(
e.x,
e.y,
!info.meleestate.is_null(),
e.flags.contains(MobjFlag::JustHit),
)
};
let (tx, ty) = match world.get(target_id) {
Some(t) => (t.x, t.y),
None => return false,
};
if just_hit {
if let Some(e) = world.get_mut(id) {
e.flags.remove(MobjFlag::JustHit);
}
return true;
}
let dx = (tx - ex).abs();
let dy = (ty - ey).abs();
let approx = dx.max(dy) + (dx.min(dy) >> 1);
let mut dist = (approx - 64 * FRACUNIT).max(0);
if !has_melee {
dist = (dist - 128 * FRACUNIT).max(0);
}
let dist = (dist >> FRACBITS).min(200);
world.p_random() >= dist
}
pub const MAXBOB: Fixed = 0x10_0000; pub const WEAPONTOP: Fixed = 32 * FRACUNIT;
pub const WEAPONBOTTOM: Fixed = 128 * FRACUNIT;
pub const RAISESPEED: Fixed = 6 * FRACUNIT;
pub fn update_weapon_bob(world: &mut World, pid: EntityId) {
let Some(entity) = world.get(pid) else { return };
let momx = entity.momx;
let momy = entity.momy;
let bob = ((fixed_mul(momx, momx) + fixed_mul(momy, momy)) >> 2).min(MAXBOB);
if let Some(ps) = world.player_state_mut(pid) {
ps.bob = bob;
}
}
pub fn psp_set_state(world: &mut World, pid: EntityId, state: StateNum) {
if let Some(ps) = world.player_state_mut(pid) {
ps.psp_state = state;
if let Some(st) = state.get() {
ps.psp_tics = st.tics;
}
}
}
pub fn use_ammo(world: &mut World, pid: EntityId, ammo: AmmoType, count: i32) -> bool {
let Some(ps) = world.player_state_mut(pid) else { return false };
let idx = ammo as usize;
if ps.ammo[idx] < count { return false; }
ps.ammo[idx] -= count;
true
}
pub fn check_player_sector(world: &mut World, map: &MapData, pid: EntityId) {
let Some(entity) = world.get(pid) else { return };
if entity.z != entity.floor_z { return; }
let ssect = crate::physics::find_subsector(map, entity.x, entity.y);
if ssect >= map.subsectors.len() { return; }
let sector_idx = map.subsectors[ssect].sector as usize;
if sector_idx >= map.sectors.len() { return; }
let special = map.sectors[sector_idx].special;
match special {
9 => {
if let Some(ps) = world.player_state_mut(pid) {
ps.secret_count += 1;
}
if sector_idx < world.sectors.len() {
world.sectors[sector_idx].special = 0;
}
}
5 => {
if world.tick.is_multiple_of(32) {
apply_damage_to(world, pid, 10, None);
}
}
7 => {
if world.tick.is_multiple_of(32) {
apply_damage_to(world, pid, 5, None);
}
}
16 => {
if world.tick.is_multiple_of(32) {
apply_damage_to(world, pid, 20, None);
}
}
_ => {}
}
}