#![allow(clippy::indexing_slicing)]
use alloc::vec::Vec;
use crate::map::{MapData, SectorId};
use crate::math::*;
pub const VDOORSPEED: Fixed = FRACUNIT * 2;
pub const VDOORWAIT: i32 = 150;
pub const PLATSPEED: Fixed = FRACUNIT;
pub const PLATWAIT: i32 = 3 * 35;
pub const GLOWSPEED: i16 = 8;
pub const STROBEBRIGHT: i32 = 5;
pub const FASTDARK: i32 = 15;
pub const SLOWDARK: i32 = 35;
#[derive(Clone)]
pub enum SpecialThinker {
Door(DoorThinker),
Plat(PlatThinker),
LightFlash(LightFlashThinker),
Strobe(StrobeThinker),
Glow(GlowThinker),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum DoorType {
Normal,
Close30ThenOpen,
Close,
Open,
BlazeRaise,
BlazeOpen,
BlazeClose,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DoorDirection {
Opening,
Waiting,
Closing,
}
#[derive(Clone)]
pub struct DoorThinker {
pub sector: SectorId,
pub door_type: DoorType,
pub top_height: Fixed,
pub speed: Fixed,
pub direction: DoorDirection,
pub top_wait: i32,
pub top_countdown: i32,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PlatType {
DownWaitUpStay,
BlazeDownWaitUpStay,
PerpetualRaise,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PlatState {
Up,
Down,
Waiting,
}
#[derive(Clone)]
pub struct PlatThinker {
pub sector: SectorId,
pub plat_type: PlatType,
pub speed: Fixed,
pub low: Fixed,
pub high: Fixed,
pub wait: i32,
pub count: i32,
pub status: PlatState,
pub tag: i16,
}
#[derive(Clone)]
pub struct LightFlashThinker {
pub sector: SectorId,
pub count: i32,
pub max_light: i16,
pub min_light: i16,
pub max_time: i32,
pub min_time: i32,
}
#[derive(Clone)]
pub struct StrobeThinker {
pub sector: SectorId,
pub count: i32,
pub min_light: i16,
pub max_light: i16,
pub dark_time: i32,
pub bright_time: i32,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GlowDirection {
Brightening,
Dimming,
}
#[derive(Clone)]
pub struct GlowThinker {
pub sector: SectorId,
pub min_light: i16,
pub max_light: i16,
pub direction: GlowDirection,
}
impl SpecialThinker {
pub fn tick(&mut self, sectors: &mut [crate::map::Sector]) -> bool {
match self {
SpecialThinker::Door(door) => door.tick(sectors),
SpecialThinker::Plat(plat) => plat.tick(sectors),
SpecialThinker::LightFlash(flash) => { flash.tick(sectors); false }
SpecialThinker::Strobe(strobe) => { strobe.tick(sectors); false }
SpecialThinker::Glow(glow) => { glow.tick(sectors); false }
}
}
}
impl DoorThinker {
fn tick(&mut self, sectors: &mut [crate::map::Sector]) -> bool {
let sector = &mut sectors[self.sector as usize];
match self.direction {
DoorDirection::Opening => {
sector.ceiling_height += self.speed;
if sector.ceiling_height >= self.top_height {
sector.ceiling_height = self.top_height;
match self.door_type {
DoorType::Normal | DoorType::BlazeRaise => {
self.direction = DoorDirection::Waiting;
self.top_countdown = self.top_wait;
}
DoorType::Open | DoorType::BlazeOpen => return true,
_ => return true,
}
}
}
DoorDirection::Waiting => {
self.top_countdown -= 1;
if self.top_countdown <= 0 {
match self.door_type {
DoorType::Normal | DoorType::BlazeRaise => {
self.direction = DoorDirection::Closing;
}
DoorType::Close30ThenOpen => {
self.direction = DoorDirection::Opening;
}
_ => {}
}
}
}
DoorDirection::Closing => {
sector.ceiling_height -= self.speed;
let floor = sector.floor_height;
if sector.ceiling_height <= floor {
sector.ceiling_height = floor;
match self.door_type {
DoorType::Normal | DoorType::Close | DoorType::BlazeRaise
| DoorType::BlazeClose => return true,
DoorType::Close30ThenOpen => {
self.direction = DoorDirection::Waiting;
self.top_countdown = 35 * 30;
}
_ => return true,
}
}
}
}
false
}
}
impl PlatThinker {
fn tick(&mut self, sectors: &mut [crate::map::Sector]) -> bool {
let sector = &mut sectors[self.sector as usize];
match self.status {
PlatState::Up => {
sector.floor_height += self.speed;
if sector.floor_height >= self.high {
sector.floor_height = self.high;
self.count = self.wait;
self.status = PlatState::Waiting;
match self.plat_type {
PlatType::DownWaitUpStay | PlatType::BlazeDownWaitUpStay => return true,
PlatType::PerpetualRaise => {}
}
}
}
PlatState::Down => {
sector.floor_height -= self.speed;
if sector.floor_height <= self.low {
sector.floor_height = self.low;
if self.wait < 0 {
return true;
}
self.count = self.wait;
self.status = PlatState::Waiting;
}
}
PlatState::Waiting => {
self.count -= 1;
if self.count <= 0 {
self.status = if sector.floor_height <= self.low {
PlatState::Up
} else {
PlatState::Down
};
}
}
}
false
}
}
impl LightFlashThinker {
fn tick(&mut self, sectors: &mut [crate::map::Sector]) {
self.count -= 1;
if self.count > 0 { return; }
let sector = &mut sectors[self.sector as usize];
if sector.light_level == self.max_light {
sector.light_level = self.min_light;
self.count = (self.min_time & 0x1F) + 1;
} else {
sector.light_level = self.max_light;
self.count = (self.max_time & 0x3F) + 1;
}
}
}
impl StrobeThinker {
fn tick(&mut self, sectors: &mut [crate::map::Sector]) {
self.count -= 1;
if self.count > 0 { return; }
let sector = &mut sectors[self.sector as usize];
if sector.light_level == self.min_light {
sector.light_level = self.max_light;
self.count = self.bright_time;
} else {
sector.light_level = self.min_light;
self.count = self.dark_time;
}
}
}
impl GlowThinker {
fn tick(&mut self, sectors: &mut [crate::map::Sector]) {
let sector = &mut sectors[self.sector as usize];
match self.direction {
GlowDirection::Brightening => {
sector.light_level += GLOWSPEED;
if sector.light_level >= self.max_light {
sector.light_level = self.max_light;
self.direction = GlowDirection::Dimming;
}
}
GlowDirection::Dimming => {
sector.light_level -= GLOWSPEED;
if sector.light_level <= self.min_light {
sector.light_level = self.min_light;
self.direction = GlowDirection::Brightening;
}
}
}
}
}
fn find_min_surrounding_light(map: &MapData, sector_id: SectorId) -> i16 {
let mut min_light = map.sectors[sector_id as usize].light_level;
for line in &map.lines {
if line.front_sector == sector_id {
if let Some(back) = line.back_sector {
let bl = map.sectors[back as usize].light_level;
if bl < min_light {
min_light = bl;
}
}
} else if line.back_sector == Some(sector_id) {
let fl = map.sectors[line.front_sector as usize].light_level;
if fl < min_light {
min_light = fl;
}
}
}
min_light
}
pub fn spawn_specials(map: &MapData) -> Vec<SpecialThinker> {
let mut thinkers = Vec::new();
use crate::types::SectorSpecial;
for (i, sector) in map.sectors.iter().enumerate() {
let sid = i as SectorId;
let Some(special) = SectorSpecial::from_repr(sector.special) else { continue };
match special {
SectorSpecial::LightFlicker => {
let min_light = find_min_surrounding_light(map, sid);
thinkers.push(SpecialThinker::LightFlash(LightFlashThinker {
sector: sid,
count: (sector.light_level as i32 & 0x1F) + 1,
max_light: sector.light_level,
min_light,
max_time: 64,
min_time: 7,
}));
}
SectorSpecial::StrobeFast | SectorSpecial::StrobeFastDamage => {
let min_light = find_min_surrounding_light(map, sid);
thinkers.push(SpecialThinker::Strobe(StrobeThinker {
sector: sid,
count: FASTDARK + 1,
min_light,
max_light: sector.light_level,
dark_time: FASTDARK,
bright_time: STROBEBRIGHT,
}));
}
SectorSpecial::StrobeSlow => {
let min_light = find_min_surrounding_light(map, sid);
thinkers.push(SpecialThinker::Strobe(StrobeThinker {
sector: sid,
count: SLOWDARK + 1,
min_light,
max_light: sector.light_level,
dark_time: SLOWDARK,
bright_time: STROBEBRIGHT,
}));
}
SectorSpecial::LightGlow => {
let min_light = find_min_surrounding_light(map, sid);
thinkers.push(SpecialThinker::Glow(GlowThinker {
sector: sid,
min_light,
max_light: sector.light_level,
direction: GlowDirection::Dimming,
}));
}
SectorSpecial::SyncStrobeSlow => {
let min_light = find_min_surrounding_light(map, sid);
thinkers.push(SpecialThinker::Strobe(StrobeThinker {
sector: sid,
count: SLOWDARK,
min_light,
max_light: sector.light_level,
dark_time: SLOWDARK,
bright_time: STROBEBRIGHT,
}));
}
SectorSpecial::SyncStrobeFast => {
let min_light = find_min_surrounding_light(map, sid);
thinkers.push(SpecialThinker::Strobe(StrobeThinker {
sector: sid,
count: FASTDARK,
min_light,
max_light: sector.light_level,
dark_time: FASTDARK,
bright_time: STROBEBRIGHT,
}));
}
_ => {}
}
}
thinkers
}
pub fn update_specials(thinkers: &mut Vec<SpecialThinker>, sectors: &mut [crate::map::Sector]) {
thinkers.retain_mut(|t| !t.tick(sectors));
}
fn find_lowest_ceiling_surrounding(map: &MapData, sector_id: SectorId) -> Fixed {
let mut min_h: Fixed = i32::MAX;
for line in &map.lines {
if line.front_sector == sector_id {
if let Some(back) = line.back_sector {
let h = map.sectors[back as usize].ceiling_height;
if h < min_h { min_h = h; }
}
} else if line.back_sector == Some(sector_id) {
let h = map.sectors[line.front_sector as usize].ceiling_height;
if h < min_h { min_h = h; }
}
}
if min_h == i32::MAX { 0 } else { min_h }
}
fn find_highest_floor_surrounding(map: &MapData, sector_id: SectorId) -> Fixed {
let mut max_h: Fixed = 0;
for line in &map.lines {
if line.front_sector == sector_id {
if let Some(back) = line.back_sector {
let h = map.sectors[back as usize].floor_height;
if h > max_h { max_h = h; }
}
} else if line.back_sector == Some(sector_id) {
let h = map.sectors[line.front_sector as usize].floor_height;
if h > max_h { max_h = h; }
}
}
max_h
}
fn find_lowest_floor_surrounding(map: &MapData, sector_id: SectorId) -> Fixed {
let mut min_h = map.sectors[sector_id as usize].floor_height;
for line in &map.lines {
if line.front_sector == sector_id {
if let Some(back) = line.back_sector {
let h = map.sectors[back as usize].floor_height;
if h < min_h {
min_h = h;
}
}
} else if line.back_sector == Some(sector_id) {
let h = map.sectors[line.front_sector as usize].floor_height;
if h < min_h {
min_h = h;
}
}
}
min_h
}
pub fn use_special_line(
map: &MapData,
thinkers: &mut Vec<SpecialThinker>,
line_idx: usize,
) -> bool {
let line = &map.lines[line_idx];
let special = line.special;
use crate::types::line_special as ls;
match special {
ls::MANUAL_DOOR | ls::BLUE_LOCKED_DOOR | ls::YELLOW_LOCKED_DOOR
| ls::RED_LOCKED_DOOR | ls::MANUAL_OPEN_STAY | ls::BLUE_OPEN_STAY | ls::RED_OPEN_STAY | ls::YELLOW_OPEN_STAY
| ls::BLAZE_RAISE | ls::BLAZE_OPEN => {
let Some(back_sector) = line.back_sector else { return false };
let floor = map.sectors[back_sector as usize].floor_height;
let top = find_lowest_ceiling_surrounding(map, back_sector) - 4 * FRACUNIT;
let speed = if special == ls::BLAZE_RAISE || special == ls::BLAZE_OPEN {
VDOORSPEED * 4
} else {
VDOORSPEED
};
let door_type = match special {
ls::MANUAL_OPEN_STAY | ls::BLUE_OPEN_STAY | ls::RED_OPEN_STAY | ls::YELLOW_OPEN_STAY => DoorType::Open,
ls::BLAZE_RAISE => DoorType::BlazeRaise,
ls::BLAZE_OPEN => DoorType::BlazeOpen,
_ => DoorType::Normal,
};
let cur_ceil = map.sectors[back_sector as usize].ceiling_height;
if cur_ceil > floor {
if door_type == DoorType::Normal || door_type == DoorType::BlazeRaise {
for t in thinkers.iter_mut() {
if let SpecialThinker::Door(d) = t
&& d.sector == back_sector
{
d.direction = DoorDirection::Closing;
return true;
}
}
}
}
thinkers.push(SpecialThinker::Door(DoorThinker {
sector: back_sector,
door_type,
top_height: top,
speed,
direction: DoorDirection::Opening,
top_wait: VDOORWAIT,
top_countdown: 0,
}));
true
}
ls::SWITCH_LIFT => {
let tag = line.tag;
if tag == 0 {
return false;
}
for (i, sector) in map.sectors.iter().enumerate() {
if sector.tag == tag {
let sid = i as SectorId;
let low = find_lowest_floor_surrounding(map, sid);
let high = sector.floor_height;
thinkers.push(SpecialThinker::Plat(PlatThinker {
sector: sid,
plat_type: PlatType::DownWaitUpStay,
speed: PLATSPEED * 4,
low,
high,
wait: PLATWAIT,
count: 0,
status: PlatState::Down,
tag,
}));
}
}
true
}
_ => false,
}
}
pub fn is_exit_line(special: i16) -> bool {
use crate::types::line_special::*;
matches!(special, EXIT_NORMAL | WALK_EXIT | EXIT_SECRET | EXIT_TELEPORT)
}
pub fn cross_special_line(
map: &MapData,
thinkers: &mut Vec<SpecialThinker>,
line_idx: usize,
) -> bool {
let line = &map.lines[line_idx];
let special = line.special;
use crate::types::line_special as ls;
match special {
ls::WALK_OPEN_DOOR => {
do_tagged_door(map, thinkers, line.tag, DoorType::Open, VDOORSPEED);
true
}
ls::WALK_CLOSE_DOOR => {
do_tagged_door(map, thinkers, line.tag, DoorType::Close, VDOORSPEED);
true
}
ls::WALK_RAISE_DOOR => {
do_tagged_door(map, thinkers, line.tag, DoorType::Normal, VDOORSPEED);
true
}
ls::WALK_CLOSE30_OPEN => {
do_tagged_door(map, thinkers, line.tag, DoorType::Close30ThenOpen, VDOORSPEED);
true
}
ls::WALK_LIFT => {
let tag = line.tag;
if tag == 0 {
return false;
}
for (i, sector) in map.sectors.iter().enumerate() {
if sector.tag == tag {
let sid = i as SectorId;
let low = find_lowest_floor_surrounding(map, sid);
let high = sector.floor_height;
thinkers.push(SpecialThinker::Plat(PlatThinker {
sector: sid,
plat_type: PlatType::DownWaitUpStay,
speed: PLATSPEED * 4,
low,
high,
wait: PLATWAIT,
count: 0,
status: PlatState::Down,
tag,
}));
}
}
true
}
ls::W1_FLOOR_LOWER_LOWEST | ls::W1_FLOOR_LOWER_LOWEST_CHANGE
| ls::W1_FLOOR_LOWER_LOWEST_NX => {
let tag = line.tag;
if tag == 0 { return false; }
let extra = if special == ls::W1_FLOOR_LOWER_LOWEST { 8 * FRACUNIT } else { 0 };
for (i, sector) in map.sectors.iter().enumerate() {
if sector.tag == tag {
let sid = i as SectorId;
let low = find_lowest_floor_surrounding(map, sid) + extra;
let high = sector.floor_height;
if low < high {
thinkers.push(SpecialThinker::Plat(PlatThinker {
sector: sid,
plat_type: PlatType::DownWaitUpStay,
speed: PLATSPEED,
low,
high,
wait: -1, count: 0,
status: PlatState::Down,
tag,
}));
}
}
}
true
}
ls::WR_PLAT_DOWN_WAIT_UP => {
let tag = line.tag;
if tag == 0 { return false; }
for (i, sector) in map.sectors.iter().enumerate() {
if sector.tag == tag {
let sid = i as SectorId;
let already_active = thinkers.iter().any(|t| {
matches!(t, SpecialThinker::Plat(p) if p.sector == sid)
});
if already_active { continue; }
let low = find_lowest_floor_surrounding(map, sid);
let high = sector.floor_height.max(
find_highest_floor_surrounding(map, sid)
);
thinkers.push(SpecialThinker::Plat(PlatThinker {
sector: sid,
plat_type: PlatType::DownWaitUpStay,
speed: PLATSPEED * 4,
low,
high,
wait: PLATWAIT,
count: 0,
status: PlatState::Down,
tag,
}));
}
}
true
}
_ => false,
}
}
fn do_tagged_door(
map: &MapData,
thinkers: &mut Vec<SpecialThinker>,
tag: i16,
door_type: DoorType,
speed: Fixed,
) {
if tag == 0 {
return;
}
for (i, sector) in map.sectors.iter().enumerate() {
if sector.tag == tag {
let sid = i as SectorId;
let top = find_lowest_ceiling_surrounding(map, sid) - 4 * FRACUNIT;
let direction = match door_type {
DoorType::Close | DoorType::BlazeClose => DoorDirection::Closing,
_ => DoorDirection::Opening,
};
thinkers.push(SpecialThinker::Door(DoorThinker {
sector: sid,
door_type,
top_height: top,
speed,
direction,
top_wait: VDOORWAIT,
top_countdown: 0,
}));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn door_opens_and_closes() {
let mut sectors = alloc::vec![crate::map::Sector {
floor_height: 0,
ceiling_height: 0, floor_pic: 0,
ceiling_pic: 0,
light_level: 128,
special: 0,
tag: 0,
sound_traversed: 0,
valid_count: 0,
line_count: 0,
first_line: 0,
floor_pic_name: [0; 8],
ceiling_pic_name: [0; 8],
}];
let mut door = DoorThinker {
sector: 0,
door_type: DoorType::Normal,
top_height: 128 * FRACUNIT,
speed: VDOORSPEED,
direction: DoorDirection::Opening,
top_wait: 5,
top_countdown: 0,
};
door.tick(&mut sectors);
assert!(sectors[0].ceiling_height > 0, "door should start opening");
assert_eq!(door.direction, DoorDirection::Opening, "door should be moving up");
for _ in 0..200 {
if door.direction != DoorDirection::Opening {
break;
}
door.tick(&mut sectors);
}
assert_eq!(door.direction, DoorDirection::Waiting, "door should be waiting at top");
assert_eq!(sectors[0].ceiling_height, door.top_height);
for _ in 0..door.top_wait + 1 {
door.tick(&mut sectors);
}
assert_eq!(door.direction, DoorDirection::Closing, "door should be closing");
}
#[test]
fn glow_oscillates() {
let mut sectors = alloc::vec![crate::map::Sector {
floor_height: 0,
ceiling_height: 128 * FRACUNIT,
floor_pic: 0,
ceiling_pic: 0,
light_level: 200,
special: 8,
tag: 0,
sound_traversed: 0,
valid_count: 0,
line_count: 0,
first_line: 0,
floor_pic_name: [0; 8],
ceiling_pic_name: [0; 8],
}];
let mut glow = GlowThinker {
sector: 0,
min_light: 100,
max_light: 200,
direction: GlowDirection::Dimming,
};
glow.tick(&mut sectors);
assert!(sectors[0].light_level < 200, "glow should dim");
for _ in 0..100 {
glow.tick(&mut sectors);
}
assert!(glow.direction == GlowDirection::Brightening || sectors[0].light_level > 100);
}
}