gzdoom-gles/src/p_spec.cpp

1650 lines
44 KiB
C++

//-----------------------------------------------------------------------------
//
// Copyright 1993-1996 id Software
// Copyright 1994-1996 Raven Software
// Copyright 1998-1998 Chi Hoang, Lee Killough, Jim Flynn, Rand Phares, Ty Halderman
// Copyright 1999-2016 Randy Heit
// Copyright 2002-2016 Christoph Oelckers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see http://www.gnu.org/licenses/
//
//-----------------------------------------------------------------------------
//
// DESCRIPTION:
// Implements special effects:
// Texture animation, height or lighting changes
// according to adjacent sectors, respective
// utility functions, etc.
// Line Tag handling. Line and Sector triggers.
// Implements donut linedef triggers
// Initializes and implements BOOM linedef triggers for
// Scrollers/Conveyors
// Friction
// Wind/Current
//
//-----------------------------------------------------------------------------
/* For code that originates from ZDoom the following applies:
**
**---------------------------------------------------------------------------
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
** derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
#include <stdlib.h>
#include "templates.h"
#include "doomdef.h"
#include "doomstat.h"
#include "d_event.h"
#include "g_level.h"
#include "gstrings.h"
#include "events.h"
#include "i_system.h"
#include "m_argv.h"
#include "m_random.h"
#include "m_bbox.h"
#include "w_wad.h"
#include "p_local.h"
#include "p_spec.h"
#include "p_blockmap.h"
#include "p_lnspec.h"
#include "p_terrain.h"
#include "p_acs.h"
#include "p_3dmidtex.h"
#include "g_game.h"
#include "s_sound.h"
#include "sc_man.h"
#include "gi.h"
#include "statnums.h"
#include "g_level.h"
#include "v_font.h"
#include "a_sharedglobal.h"
#include "serializer.h"
#include "a_keys.h"
#include "c_dispatch.h"
#include "r_sky.h"
#include "d_player.h"
#include "p_maputl.h"
#include "p_blockmap.h"
#include "g_levellocals.h"
#include "actorinlines.h"
#ifndef NO_EDATA
#include "edata.h"
#endif
#include "vm.h"
// State.
#include "r_state.h"
#include "c_console.h"
#include "r_data/r_interpolate.h"
static FRandom pr_playerinspecialsector ("PlayerInSpecialSector");
EXTERN_CVAR(Bool, cl_predict_specials)
// killough 3/7/98: Initialize generalized scrolling
void P_SpawnScrollers();
static void P_SpawnFriction (); // phares 3/16/98
void P_SpawnPushers (); // phares 3/20/98
// [RH] Check dmflags for noexit and respond accordingly
bool CheckIfExitIsGood (AActor *self, level_info_t *info)
{
cluster_info_t *cluster;
// The world can always exit itself.
if (self == NULL)
return true;
// We must kill all monsters to exit the level.
if ((dmflags2 & DF2_KILL_MONSTERS) && level.killed_monsters != level.total_monsters)
return false;
// Is this a deathmatch game and we're not allowed to exit?
if ((deathmatch || alwaysapplydmflags) && (dmflags & DF_NO_EXIT))
{
P_DamageMobj (self, self, self, TELEFRAG_DAMAGE, NAME_Exit);
return false;
}
// Is this a singleplayer game and the next map is part of the same hub and we're dead?
if (self->health <= 0 &&
!multiplayer &&
info != NULL &&
info->cluster == level.cluster &&
(cluster = FindClusterInfo(level.cluster)) != NULL &&
cluster->flags & CLUSTER_HUB)
{
return false;
}
if (deathmatch && gameaction != ga_completed)
{
Printf ("%s exited the level.\n", self->player->userinfo.GetName());
}
return true;
}
//
// UTILITIES
//
//============================================================================
//
// P_ActivateLine
//
//============================================================================
bool P_ActivateLine (line_t *line, AActor *mo, int side, int activationType, DVector3 *optpos)
{
int lineActivation;
INTBOOL repeat;
INTBOOL buttonSuccess;
uint8_t special;
if (!P_TestActivateLine (line, mo, side, activationType, optpos))
{
return false;
}
// [MK] Use WorldLinePreActivated to decide if activation should continue
bool shouldactivate = true;
E_WorldLinePreActivated(line, mo, &shouldactivate);
if ( !shouldactivate ) return false;
bool remote = (line->special != 7 && line->special != 8 && (line->special < 11 || line->special > 14));
if (line->locknumber > 0 && !P_CheckKeys (mo, line->locknumber, remote)) return false;
lineActivation = line->activation;
repeat = line->flags & ML_REPEAT_SPECIAL;
buttonSuccess = false;
buttonSuccess = P_ExecuteSpecial(line->special,
line, mo, side == 1, line->args[0],
line->args[1], line->args[2],
line->args[3], line->args[4]);
// [MK] Fire up WorldLineActivated
if ( buttonSuccess ) E_WorldLineActivated(line, mo);
special = line->special;
if (!repeat && buttonSuccess)
{ // clear the special on non-retriggerable lines
line->special = 0;
}
if (buttonSuccess)
{
if (activationType == SPAC_Use || activationType == SPAC_Impact || activationType == SPAC_Push)
{
P_ChangeSwitchTexture (line->sidedef[0], repeat, special);
}
}
// some old WADs use this method to create walls that change the texture when shot.
else if (activationType == SPAC_Impact && // only for shootable triggers
(level.flags2 & LEVEL2_DUMMYSWITCHES) && // this is only a compatibility setting for an old hack!
!repeat && // only non-repeatable triggers
(special<Generic_Floor || special>Generic_Crusher) && // not for Boom's generalized linedefs
special && // not for lines without a special
tagManager.LineHasID(line, line->args[0]) && // Safety check: exclude edited UDMF linedefs or ones that don't map the tag to args[0]
line->args[0] && // only if there's a tag (which is stored in the first arg)
P_FindFirstSectorFromTag (line->args[0]) == -1) // only if no sector is tagged to this linedef
{
P_ChangeSwitchTexture (line->sidedef[0], repeat, special);
line->special = 0;
}
// end of changed code
if (developer >= DMSG_SPAMMY && buttonSuccess)
{
Printf ("Line special %d activated on line %i\n", special, line->Index());
}
return true;
}
//============================================================================
//
// P_TestActivateLine
//
//============================================================================
bool P_TestActivateLine (line_t *line, AActor *mo, int side, int activationType, DVector3 *optpos)
{
int lineActivation = line->activation;
if (line->flags & ML_FIRSTSIDEONLY && side == 1)
{
return false;
}
if (lineActivation & SPAC_UseThrough)
{
lineActivation |= SPAC_Use;
}
else if (line->special == Teleport &&
(lineActivation & SPAC_Cross) &&
activationType == SPAC_PCross &&
mo != NULL &&
mo->flags & MF_MISSILE)
{ // Let missiles use regular player teleports
lineActivation |= SPAC_PCross;
}
// BOOM's generalized line types that allow monster use can actually be
// activated by anything except projectiles.
if (lineActivation & SPAC_AnyCross)
{
lineActivation |= SPAC_Cross|SPAC_MCross;
}
if (activationType == SPAC_Use || activationType == SPAC_UseBack)
{
if (!P_CheckSwitchRange(mo, line, side, optpos))
{
return false;
}
}
if (activationType == SPAC_Use && (lineActivation & SPAC_MUse) && !mo->player && mo->flags4 & MF4_CANUSEWALLS)
{
return true;
}
if (activationType == SPAC_Push && (lineActivation & SPAC_MPush) && !mo->player && mo->flags2 & MF2_PUSHWALL)
{
return true;
}
if ((lineActivation & activationType) == 0)
{
if (activationType != SPAC_MCross || lineActivation != SPAC_Cross)
{
return false;
}
}
if (activationType == SPAC_AnyCross && (lineActivation & activationType))
{
return true;
}
if (mo && !mo->player &&
!(mo->flags & MF_MISSILE) &&
!(line->flags & ML_MONSTERSCANACTIVATE) &&
(activationType != SPAC_MCross || (!(lineActivation & SPAC_MCross))))
{
// [RH] monsters' ability to activate this line depends on its type
// In Hexen, only MCROSS lines could be activated by monsters. With
// lax activation checks, monsters can also activate certain lines
// even without them being marked as monster activate-able. This is
// the default for non-Hexen maps in Hexen format.
if (!(level.flags2 & LEVEL2_LAXMONSTERACTIVATION))
{
return false;
}
if ((activationType == SPAC_Use || activationType == SPAC_Push)
&& (line->flags & ML_SECRET))
return false; // never open secret doors
bool noway = true;
switch (activationType)
{
case SPAC_Use:
case SPAC_Push:
switch (line->special)
{
case Door_Raise:
if (line->args[0] == 0 && line->args[1] < 64)
noway = false;
break;
case Teleport:
case Teleport_NoFog:
noway = false;
}
break;
case SPAC_MCross:
if (!(lineActivation & SPAC_MCross))
{
switch (line->special)
{
case Door_Raise:
if (line->args[1] >= 64)
{
break;
}
case Teleport:
case Teleport_NoFog:
case Teleport_Line:
case Plat_DownWaitUpStayLip:
case Plat_DownWaitUpStay:
noway = false;
}
}
else noway = false;
break;
default:
noway = false;
}
return !noway;
}
if (activationType == SPAC_MCross && !(lineActivation & SPAC_MCross) &&
!(line->flags & ML_MONSTERSCANACTIVATE))
{
return false;
}
return true;
}
//============================================================================
//
// P_PredictLine
//
//============================================================================
bool P_PredictLine(line_t *line, AActor *mo, int side, int activationType)
{
int lineActivation;
INTBOOL buttonSuccess;
uint8_t special;
// Only predict a very specifc section of specials
if (line->special != Teleport_Line &&
line->special != Teleport)
{
return false;
}
if (!P_TestActivateLine(line, mo, side, activationType) || !cl_predict_specials)
{
return false;
}
if (line->locknumber > 0) return false;
lineActivation = line->activation;
buttonSuccess = false;
buttonSuccess = P_ExecuteSpecial(line->special,
line, mo, side == 1, line->args[0],
line->args[1], line->args[2],
line->args[3], line->args[4]);
special = line->special;
// end of changed code
if (developer >= DMSG_SPAMMY && buttonSuccess)
{
Printf("Line special %d predicted on line %i\n", special, line->Index());
}
return true;
}
//
// P_PlayerInSpecialSector
// Called every tic frame
// that the player origin is in a special sector
//
void P_PlayerInSpecialSector (player_t *player, sector_t * sector)
{
if (sector == NULL)
{
// Falling, not all the way down yet?
sector = player->mo->Sector;
if (!player->mo->isAtZ(sector->LowestFloorAt(player->mo))
&& !player->mo->waterlevel)
{
return;
}
}
// Has hit ground.
AInventory *ironfeet;
// [RH] Apply any customizable damage
if (sector->damageamount > 0)
{
// Allow subclasses. Better would be to implement it as armor and let that reduce
// the damage as part of the normal damage procedure. Unfortunately, I don't have
// different damage types yet, so that's not happening for now.
auto pitype = PClass::FindActor(NAME_PowerIronFeet);
for (ironfeet = player->mo->Inventory; ironfeet != NULL; ironfeet = ironfeet->Inventory)
{
if (ironfeet->IsKindOf(pitype))
break;
}
if (sector->Flags & SECF_ENDGODMODE) player->cheats &= ~CF_GODMODE;
if ((ironfeet == NULL || pr_playerinspecialsector() < sector->leakydamage))
{
if (sector->Flags & SECF_HAZARD)
{
player->hazardcount += sector->damageamount;
player->hazardtype = sector->damagetype;
player->hazardinterval = sector->damageinterval;
}
else if (level.time % sector->damageinterval == 0)
{
if (!(player->cheats & (CF_GODMODE|CF_GODMODE2))) P_DamageMobj(player->mo, NULL, NULL, sector->damageamount, sector->damagetype);
if ((sector->Flags & SECF_ENDLEVEL) && player->health <= 10 && (!deathmatch || !(dmflags & DF_NO_EXIT)))
{
G_ExitLevel(0, false);
}
if (sector->Flags & SECF_DMGTERRAINFX)
{
P_HitWater(player->mo, player->mo->Sector, player->mo->Pos(), false, true, true);
}
}
}
}
else if (sector->damageamount < 0)
{
if (level.time % sector->damageinterval == 0)
{
P_GiveBody(player->mo, -sector->damageamount, 100);
}
}
if (sector->isSecret())
{
sector->ClearSecret();
P_GiveSecret(player->mo, true, true, sector->Index());
}
}
//============================================================================
//
// P_SectorDamage
//
//============================================================================
static void DoSectorDamage(AActor *actor, sector_t *sec, int amount, FName type, PClassActor *protectClass, int flags)
{
if (!(actor->flags & MF_SHOOTABLE))
return;
if (!(flags & DAMAGE_NONPLAYERS) && actor->player == NULL)
return;
if (!(flags & DAMAGE_PLAYERS) && actor->player != NULL)
return;
if (!(flags & DAMAGE_IN_AIR) && !actor->isAtZ(sec->floorplane.ZatPoint(actor)) && !actor->waterlevel)
return;
if (protectClass != NULL)
{
if (actor->FindInventory(protectClass, !!(flags & DAMAGE_SUBCLASSES_PROTECT)))
return;
}
int dflags = (flags & DAMAGE_NO_ARMOR) ? DMG_NO_ARMOR : 0;
P_DamageMobj (actor, NULL, NULL, amount, type, dflags);
}
void P_SectorDamage(int tag, int amount, FName type, PClassActor *protectClass, int flags)
{
FSectorTagIterator itr(tag);
int secnum;
while ((secnum = itr.Next()) >= 0)
{
AActor *actor, *next;
sector_t *sec = &level.sectors[secnum];
// Do for actors in this sector.
for (actor = sec->thinglist; actor != NULL; actor = next)
{
next = actor->snext;
DoSectorDamage(actor, sec, amount, type, protectClass, flags);
}
// If this is a 3D floor control sector, also do for anything in/on
// those 3D floors.
for (unsigned i = 0; i < sec->e->XFloor.attached.Size(); ++i)
{
sector_t *sec2 = sec->e->XFloor.attached[i];
for (actor = sec2->thinglist; actor != NULL; actor = next)
{
next = actor->snext;
// Only affect actors touching the 3D floor
double z1 = sec->floorplane.ZatPoint(actor);
double z2 = sec->ceilingplane.ZatPoint(actor);
if (z2 < z1)
{
// Account for Vavoom-style 3D floors
double zz = z1;
z1 = z2;
z2 = zz;
}
if (actor->Top() > z1)
{
// If DAMAGE_IN_AIR is used, anything not beneath the 3D floor will be
// damaged (so, anything touching it or above it). Other 3D floors between
// the actor and this one will not stop this effect.
if ((flags & DAMAGE_IN_AIR) || !actor->isAbove(z2))
{
// Here we pass the DAMAGE_IN_AIR flag to disable the floor check, since it
// only works with the real sector's floor. We did the appropriate height checks
// for 3D floors already.
DoSectorDamage(actor, NULL, amount, type, protectClass, flags | DAMAGE_IN_AIR);
}
}
}
}
}
}
//============================================================================
//
// P_GiveSecret
//
//============================================================================
CVAR(Bool, showsecretsector, false, 0)
CVAR(Bool, cl_showsecretmessage, true, CVAR_ARCHIVE)
void P_GiveSecret(AActor *actor, bool printmessage, bool playsound, int sectornum)
{
if (actor != NULL)
{
if (actor->player != NULL)
{
actor->player->secretcount++;
}
if (cl_showsecretmessage && actor->CheckLocalView(consoleplayer))
{
if (printmessage)
{
if (!showsecretsector || sectornum < 0) C_MidPrint(SmallFont, GStrings["SECRETMESSAGE"]);
else
{
FString s = GStrings["SECRETMESSAGE"];
s.AppendFormat(" (Sector %d)", sectornum);
C_MidPrint(SmallFont, s);
}
}
if (playsound) S_Sound (CHAN_AUTO | CHAN_UI, "misc/secret", 1, ATTN_NORM);
}
}
level.found_secrets++;
}
DEFINE_ACTION_FUNCTION(AActor, GiveSecret)
{
PARAM_SELF_PROLOGUE(AActor);
PARAM_BOOL_DEF(printmessage);
PARAM_BOOL_DEF(playsound);
P_GiveSecret(self, printmessage, playsound, -1);
return 0;
}
DEFINE_ACTION_FUNCTION(FLevelLocals, GiveSecret)
{
PARAM_PROLOGUE;
PARAM_OBJECT(activator, AActor);
PARAM_BOOL_DEF(printmessage);
PARAM_BOOL_DEF(playsound);
P_GiveSecret(activator, printmessage, playsound, -1);
return 0;
}
//============================================================================
//
// P_PlayerOnSpecialFlat
//
//============================================================================
void P_PlayerOnSpecialFlat (player_t *player, int floorType)
{
if (Terrains[floorType].DamageAmount &&
!(level.time & Terrains[floorType].DamageTimeMask))
{
AInventory *ironfeet = NULL;
if (Terrains[floorType].AllowProtection)
{
auto pitype = PClass::FindActor(NAME_PowerIronFeet);
for (ironfeet = player->mo->Inventory; ironfeet != NULL; ironfeet = ironfeet->Inventory)
{
if (ironfeet->IsKindOf (pitype))
break;
}
}
int damage = 0;
if (ironfeet == NULL)
{
damage = P_DamageMobj (player->mo, NULL, NULL, Terrains[floorType].DamageAmount,
Terrains[floorType].DamageMOD);
}
if (damage > 0 && Terrains[floorType].Splash != -1)
{
S_Sound (player->mo, CHAN_AUTO,
Splashes[Terrains[floorType].Splash].NormalSplashSound, 1,
ATTN_IDLE);
}
}
}
//
// P_UpdateSpecials
// Animate planes, scroll walls, etc.
//
EXTERN_CVAR (Float, timelimit)
void P_UpdateSpecials ()
{
// LEVEL TIMER
if (deathmatch && timelimit)
{
if (level.maptime >= (int)(timelimit * TICRATE * 60))
{
Printf ("%s\n", GStrings("TXT_TIMELIMIT"));
G_ExitLevel(0, false);
}
}
}
//
// SPECIAL SPAWNING
//
CUSTOM_CVAR (Bool, forcewater, false, CVAR_ARCHIVE|CVAR_SERVERINFO)
{
if (gamestate == GS_LEVEL)
{
for (auto &sec : level.sectors)
{
sector_t *hsec = sec.GetHeightSec();
if (hsec && !(hsec->MoreFlags & SECF_UNDERWATER))
{
if (self)
{
hsec->MoreFlags |= SECF_FORCEDUNDERWATER;
}
else
{
hsec->MoreFlags &= ~SECF_FORCEDUNDERWATER;
}
}
}
}
}
class DLightTransfer : public DThinker
{
DECLARE_CLASS (DLightTransfer, DThinker)
DLightTransfer() {}
public:
DLightTransfer (sector_t *srcSec, int target, bool copyFloor);
void Serialize(FSerializer &arc);
void Tick ();
protected:
static void DoTransfer (int level, int target, bool floor);
sector_t *Source;
int TargetTag;
bool CopyFloor;
short LastLight;
};
IMPLEMENT_CLASS(DLightTransfer, false, false)
void DLightTransfer::Serialize(FSerializer &arc)
{
Super::Serialize (arc);
arc("lastlight", LastLight)
("source", Source)
("targettag", TargetTag)
("copyfloor", CopyFloor);
}
DLightTransfer::DLightTransfer (sector_t *srcSec, int target, bool copyFloor)
{
int secnum;
Source = srcSec;
TargetTag = target;
CopyFloor = copyFloor;
DoTransfer (LastLight = srcSec->lightlevel, target, copyFloor);
if (copyFloor)
{
FSectorTagIterator itr(target);
while ((secnum = itr.Next()) >= 0)
level.sectors[secnum].ChangeFlags(sector_t::floor, 0, PLANEF_ABSLIGHTING);
}
else
{
FSectorTagIterator itr(target);
while ((secnum = itr.Next()) >= 0)
level.sectors[secnum].ChangeFlags(sector_t::ceiling, 0, PLANEF_ABSLIGHTING);
}
ChangeStatNum (STAT_LIGHTTRANSFER);
}
void DLightTransfer::Tick ()
{
int light = Source->lightlevel;
if (light != LastLight)
{
LastLight = light;
DoTransfer (light, TargetTag, CopyFloor);
}
}
void DLightTransfer::DoTransfer (int llevel, int target, bool floor)
{
int secnum;
if (floor)
{
FSectorTagIterator itr(target);
while ((secnum = itr.Next()) >= 0)
level.sectors[secnum].SetPlaneLight(sector_t::floor, llevel);
}
else
{
FSectorTagIterator itr(target);
while ((secnum = itr.Next()) >= 0)
level.sectors[secnum].SetPlaneLight(sector_t::ceiling, llevel);
}
}
class DWallLightTransfer : public DThinker
{
enum
{
WLF_SIDE1=1,
WLF_SIDE2=2,
WLF_NOFAKECONTRAST=4
};
DECLARE_CLASS (DWallLightTransfer, DThinker)
DWallLightTransfer() {}
public:
DWallLightTransfer (sector_t *srcSec, int target, uint8_t flags);
void Serialize(FSerializer &arc);
void Tick ();
protected:
static void DoTransfer (short level, int target, uint8_t flags);
sector_t *Source;
int TargetID;
short LastLight;
uint8_t Flags;
};
IMPLEMENT_CLASS(DWallLightTransfer, false, false)
void DWallLightTransfer::Serialize(FSerializer &arc)
{
Super::Serialize (arc);
arc("lastlight", LastLight)
("source", Source)
("targetid", TargetID)
("flags", Flags);
}
DWallLightTransfer::DWallLightTransfer (sector_t *srcSec, int target, uint8_t flags)
{
int linenum;
int wallflags;
Source = srcSec;
TargetID = target;
Flags = flags;
DoTransfer (LastLight = srcSec->GetLightLevel(), target, Flags);
if (!(flags & WLF_NOFAKECONTRAST))
{
wallflags = WALLF_ABSLIGHTING;
}
else
{
wallflags = WALLF_ABSLIGHTING | WALLF_NOFAKECONTRAST;
}
FLineIdIterator itr(target);
while ((linenum = itr.Next()) >= 0)
{
if (flags & WLF_SIDE1 && level.lines[linenum].sidedef[0] != NULL)
{
level.lines[linenum].sidedef[0]->Flags |= wallflags;
}
if (flags & WLF_SIDE2 && level.lines[linenum].sidedef[1] != NULL)
{
level.lines[linenum].sidedef[1]->Flags |= wallflags;
}
}
ChangeStatNum(STAT_LIGHTTRANSFER);
}
void DWallLightTransfer::Tick ()
{
short light = sector_t::ClampLight(Source->lightlevel);
if (light != LastLight)
{
LastLight = light;
DoTransfer (light, TargetID, Flags);
}
}
void DWallLightTransfer::DoTransfer (short lightlevel, int target, uint8_t flags)
{
int linenum;
FLineIdIterator itr(target);
while ((linenum = itr.Next()) >= 0)
{
line_t *line = &level.lines[linenum];
if (flags & WLF_SIDE1 && line->sidedef[0] != NULL)
{
line->sidedef[0]->SetLight(lightlevel);
}
if (flags & WLF_SIDE2 && line->sidedef[1] != NULL)
{
line->sidedef[1]->SetLight(lightlevel);
}
}
}
//-----------------------------------------------------------------------------
//
// Portals
//
//-----------------------------------------------------------------------------
//---------------------------------------------------------------------------
// Upper stacks go in the top sector. Lower stacks go in the bottom sector.
static void SetupFloorPortal (AActor *point)
{
NActorIterator it (NAME_LowerStackLookOnly, point->tid);
sector_t *Sector = point->Sector;
auto skyv = it.Next();
if (skyv != nullptr)
{
skyv->target = point;
if (Sector->GetAlpha(sector_t::floor) == 1.)
Sector->SetAlpha(sector_t::floor, clamp(point->args[0], 0, 255) / 255.);
Sector->Portals[sector_t::floor] = P_GetStackPortal(skyv, sector_t::floor);
}
}
static void SetupCeilingPortal (AActor *point)
{
NActorIterator it (NAME_UpperStackLookOnly, point->tid);
sector_t *Sector = point->Sector;
auto skyv = it.Next();
if (skyv != nullptr)
{
skyv->target = point;
if (Sector->GetAlpha(sector_t::ceiling) == 1.)
Sector->SetAlpha(sector_t::ceiling, clamp(point->args[0], 0, 255) / 255.);
Sector->Portals[sector_t::ceiling] = P_GetStackPortal(skyv, sector_t::ceiling);
}
}
void P_SetupPortals()
{
TThinkerIterator<AActor> it("StackPoint");
AActor *pt;
TArray<AActor *> points;
while ((pt = it.Next()))
{
FName nm = pt->GetClass()->TypeName;
if (nm == NAME_UpperStackLookOnly)
{
SetupFloorPortal(pt);
}
else if (nm == NAME_LowerStackLookOnly)
{
SetupCeilingPortal(pt);
}
pt->special1 = 0;
points.Push(pt);
}
// the semantics here are incredibly lax so the final setup can only be done once all portals have been created,
// because later stackpoints will happily overwrite info in older ones, if there are multiple links.
for (auto &s : level.sectorPortals)
{
if (s.mType == PORTS_STACKEDSECTORTHING && s.mSkybox)
{
for (auto &ss : level.sectorPortals)
{
if (ss.mType == PORTS_STACKEDSECTORTHING && ss.mSkybox == s.mSkybox->target)
{
s.mPartner = unsigned((&ss) - &level.sectorPortals[0]);
}
}
}
}
// Now we can finally set the displacement and delete the stackpoint reference.
for (auto &s : level.sectorPortals)
{
if (s.mType == PORTS_STACKEDSECTORTHING && s.mSkybox)
{
s.mDisplacement = s.mSkybox->Pos() - s.mSkybox->target->Pos();
s.mSkybox = NULL;
}
}
}
static void SetPortal(sector_t *sector, int plane, unsigned pnum, double alpha)
{
// plane: 0=floor, 1=ceiling, 2=both
if (plane > 0)
{
if (sector->GetPortalType(sector_t::ceiling) == PORTS_SKYVIEWPOINT)
{
sector->Portals[sector_t::ceiling] = pnum;
if (sector->GetAlpha(sector_t::ceiling) == 1.)
sector->SetAlpha(sector_t::ceiling, alpha);
if (level.sectorPortals[pnum].mFlags & PORTSF_SKYFLATONLY)
sector->SetTexture(sector_t::ceiling, skyflatnum);
}
}
if (plane == 2 || plane == 0)
{
if (sector->GetPortalType(sector_t::floor) == PORTS_SKYVIEWPOINT)
{
sector->Portals[sector_t::floor] = pnum;
}
if (sector->GetAlpha(sector_t::floor) == 1.)
sector->SetAlpha(sector_t::floor, alpha);
if (level.sectorPortals[pnum].mFlags & PORTSF_SKYFLATONLY)
sector->SetTexture(sector_t::floor, skyflatnum);
}
}
static void CopyPortal(int sectortag, int plane, unsigned pnum, double alpha, bool tolines)
{
int s;
FSectorTagIterator itr(sectortag);
while ((s = itr.Next()) >= 0)
{
SetPortal(&level.sectors[s], plane, pnum, alpha);
}
for (auto &line : level.lines)
{
// Check if this portal needs to be copied to other sectors
// This must be done here to ensure that it gets done only after the portal is set up
if (line.special == Sector_SetPortal &&
line.args[1] == 1 &&
(line.args[2] == plane || line.args[2] == 3) &&
line.args[3] == sectortag)
{
if (line.args[0] == 0)
{
SetPortal(line.frontsector, plane, pnum, alpha);
}
else
{
FSectorTagIterator itr(line.args[0]);
while ((s = itr.Next()) >= 0)
{
SetPortal(&level.sectors[s], plane, pnum, alpha);
}
}
}
if (tolines && line.special == Sector_SetPortal &&
line.args[1] == 5 &&
line.args[3] == sectortag)
{
if (line.args[0] == 0)
{
line.portaltransferred = pnum;
}
else
{
FLineIdIterator itr(line.args[0]);
while ((s = itr.Next()) >= 0)
{
level.lines[s].portaltransferred = pnum;
}
}
}
}
}
void P_SpawnPortal(line_t *line, int sectortag, int plane, int bytealpha, int linked)
{
if (plane < 0 || plane > 2 || (linked && plane == 2)) return;
for (auto &oline : level.lines)
{
// We must look for the reference line with a linear search unless we want to waste the line ID for it
// which is not a good idea.
if (oline.special == Sector_SetPortal &&
oline.args[0] == sectortag &&
oline.args[1] == linked &&
oline.args[2] == plane &&
oline.args[3] == 1)
{
// beware of overflows.
DVector2 pos1 = line->v1->fPos() + line->Delta() / 2;
DVector2 pos2 = oline.v1->fPos() + oline.Delta() / 2;
unsigned pnum = P_GetPortal(linked ? PORTS_LINKEDPORTAL : PORTS_PORTAL, plane, line->frontsector, oline.frontsector, pos2 - pos1);
CopyPortal(sectortag, plane, pnum, bytealpha / 255., false);
return;
}
}
}
// This searches the viewpoint's sector
// for a skybox line special, gets its tag and transfers the skybox to all tagged sectors.
void P_SpawnSkybox(AActor *origin)
{
sector_t *Sector = origin->Sector;
if (Sector == NULL)
{
Printf("Sector not initialized for SkyCamCompat\n");
origin->Sector = Sector = P_PointInSector(origin->Pos());
}
if (Sector)
{
for(auto refline : Sector->Lines)
{
if (refline->special == Sector_SetPortal && refline->args[1] == 2)
{
// We found the setup linedef for this skybox, so let's use it for our init.
unsigned pnum = P_GetSkyboxPortal(origin);
CopyPortal(refline->args[0], refline->args[2], pnum, 0, true);
return;
}
}
}
}
//
// P_SetSectorDamage
//
// Sets damage properties for one sector. Allows combination of original specials with explicit use of the damage properties
//
static void P_SetupSectorDamage(sector_t *sector, int damage, int interval, int leakchance, FName type, int flags)
{
// Only set if damage is not yet initialized. This ensures that UDMF takes precedence over sector specials.
if (sector->damageamount == 0)
{
sector->damageamount = damage;
sector->damageinterval = MAX(1, interval);
sector->leakydamage = leakchance;
sector->damagetype = type;
sector->Flags = (sector->Flags & ~SECF_DAMAGEFLAGS) | (flags & SECF_DAMAGEFLAGS);
}
}
//
// P_InitSectorSpecial
//
// Sets up everything derived from 'sector->special' for one sector
// ('fromload' is necessary to allow conversion upon savegame load.)
//
void P_SpawnLights(sector_t *sector);
void P_InitSectorSpecial(sector_t *sector, int special)
{
// [RH] All secret sectors are marked with a BOOM-ish bitfield
if (sector->special & SECRET_MASK)
{
sector->Flags |= SECF_SECRET | SECF_WASSECRET;
level.total_secrets++;
}
if (sector->special & FRICTION_MASK)
{
sector->Flags |= SECF_FRICTION;
}
if (sector->special & PUSH_MASK)
{
sector->Flags |= SECF_PUSH;
}
if ((sector->special & DAMAGE_MASK) == 0x100)
{
P_SetupSectorDamage(sector, 5, 32, 0, NAME_Fire, 0);
}
else if ((sector->special & DAMAGE_MASK) == 0x200)
{
P_SetupSectorDamage(sector, 10, 32, 0, NAME_Slime, 0);
}
else if ((sector->special & DAMAGE_MASK) == 0x300)
{
P_SetupSectorDamage(sector, 20, 32, 5, NAME_Slime, 0);
}
sector->special &= 0xff;
// [RH] Normal DOOM special or BOOM specialized?
bool keepspecial = false;
P_SpawnLights(sector);
switch (sector->special)
{
case dLight_Strobe_Hurt:
P_SetupSectorDamage(sector, 20, 32, 5, NAME_Slime, 0);
break;
case dDamage_Hellslime:
P_SetupSectorDamage(sector, 10, 32, 0, NAME_Slime, 0);
break;
case dDamage_Nukage:
P_SetupSectorDamage(sector, 5, 32, 0, NAME_Slime, 0);
break;
case dSector_DoorCloseIn30:
Create<DDoor>(sector, DDoor::doorWaitClose, 2, 0, 0, 30 * TICRATE);
break;
case dDamage_End:
P_SetupSectorDamage(sector, 20, 32, 256, NAME_None, SECF_ENDGODMODE|SECF_ENDLEVEL);
break;
case dSector_DoorRaiseIn5Mins:
Create<DDoor> (sector, DDoor::doorWaitRaise, 2, TICRATE*30/7, 0, 5*60*TICRATE);
break;
case dFriction_Low:
sector->friction = FRICTION_LOW;
sector->movefactor = 0x269/65536.;
sector->Flags |= SECF_FRICTION;
break;
case dDamage_SuperHellslime:
P_SetupSectorDamage(sector, 20, 32, 5, NAME_Slime, 0);
break;
case dDamage_LavaWimpy:
P_SetupSectorDamage(sector, 5, 16, 256, NAME_Fire, SECF_DMGTERRAINFX);
break;
case dDamage_LavaHefty:
P_SetupSectorDamage(sector, 8, 16, 256, NAME_Fire, SECF_DMGTERRAINFX);
break;
case dScroll_EastLavaDamage:
P_SetupSectorDamage(sector, 5, 16, 256, NAME_Fire, SECF_DMGTERRAINFX);
P_CreateScroller(EScroll::sc_floor, -4., 0, -1, sector->Index(), 0);
keepspecial = true;
break;
case hDamage_Sludge:
P_SetupSectorDamage(sector, 4, 32, 0, NAME_Slime, 0);
break;
case sLight_Strobe_Hurt:
P_SetupSectorDamage(sector, 5, 32, 0, NAME_Slime, 0);
break;
case sDamage_Hellslime:
P_SetupSectorDamage(sector, 2, 32, 0, NAME_Slime, SECF_HAZARD);
break;
case Damage_InstantDeath:
// Strife's instant death sector
P_SetupSectorDamage(sector, TELEFRAG_DAMAGE, 1, 256, NAME_InstantDeath, 0);
break;
case sDamage_SuperHellslime:
P_SetupSectorDamage(sector, 4, 32, 0, NAME_Slime, SECF_HAZARD);
break;
case Sector_Hidden:
sector->MoreFlags |= SECF_HIDDEN;
break;
case Sector_Heal:
// CoD's healing sector
P_SetupSectorDamage(sector, -1, 32, 0, NAME_None, 0);
break;
case Sky2:
sector->sky = PL_SKYFLAT;
break;
default:
if (sector->special >= Scroll_North_Slow &&
sector->special <= Scroll_SouthWest_Fast)
{ // Hexen scroll special
static const int8_t hexenScrollies[24][2] =
{
{ 0, 1 }, { 0, 2 }, { 0, 4 },
{ -1, 0 }, { -2, 0 }, { -4, 0 },
{ 0, -1 }, { 0, -2 }, { 0, -4 },
{ 1, 0 }, { 2, 0 }, { 4, 0 },
{ 1, 1 }, { 2, 2 }, { 4, 4 },
{ -1, 1 }, { -2, 2 }, { -4, 4 },
{ -1, -1 }, { -2, -2 }, { -4, -4 },
{ 1, -1 }, { 2, -2 }, { 4, -4 }
};
int i = sector->special - Scroll_North_Slow;
double dx = hexenScrollies[i][0] / 2.;
double dy = hexenScrollies[i][1] / 2.;
P_CreateScroller(EScroll::sc_floor, dx, dy, -1, sector->Index(), 0);
}
else if (sector->special >= Carry_East5 &&
sector->special <= Carry_East35)
{ // Heretic scroll special
// Only east scrollers also scroll the texture
P_CreateScroller(EScroll::sc_floor,
-0.5 * (1 << ((sector->special & 0xff) - Carry_East5)), 0, -1, sector->Index(), 0);
}
keepspecial = true;
break;
}
if (!keepspecial) sector->special = 0;
}
//
// P_SpawnSpecials
//
// After the map has been loaded, scan for specials that spawn thinkers
//
void P_SpawnSpecials (void)
{
P_SetupPortals();
for (auto &sec : level.sectors)
{
if (sec.special == 0)
continue;
P_InitSectorSpecial(&sec, sec.special);
}
#ifndef NO_EDATA
ProcessEDSectors();
#endif
// Init other misc stuff
P_SpawnScrollers(); // killough 3/7/98: Add generalized scrollers
P_SpawnFriction(); // phares 3/12/98: New friction model using linedefs
P_SpawnPushers(); // phares 3/20/98: New pusher model using linedefs
TThinkerIterator<AActor> it2("SkyCamCompat");
AActor *pt2;
while ((pt2 = it2.Next()))
{
P_SpawnSkybox(pt2);
}
for (auto &line : level.lines)
{
switch (line.special)
{
int s;
sector_t *sec;
// killough 3/7/98:
// support for drawn heights coming from different sector
case Transfer_Heights:
{
sec = line.frontsector;
if (line.args[1] & 2)
{
sec->MoreFlags |= SECF_FAKEFLOORONLY;
}
if (line.args[1] & 4)
{
sec->MoreFlags |= SECF_CLIPFAKEPLANES;
}
if (line.args[1] & 8)
{
sec->MoreFlags |= SECF_UNDERWATER;
}
else if (forcewater)
{
sec->MoreFlags |= SECF_FORCEDUNDERWATER;
}
if (line.args[1] & 16)
{
sec->MoreFlags |= SECF_IGNOREHEIGHTSEC;
}
if (line.args[1] & 32)
{
sec->MoreFlags |= SECF_NOFAKELIGHT;
}
FSectorTagIterator itr(line.args[0]);
while ((s = itr.Next()) >= 0)
{
level.sectors[s].heightsec = sec;
sec->e->FakeFloor.Sectors.Push(&level.sectors[s]);
level.sectors[s].AdjustFloorClip();
}
break;
}
// killough 3/16/98: Add support for setting
// floor lighting independently (e.g. lava)
case Transfer_FloorLight:
Create<DLightTransfer> (line.frontsector, line.args[0], true);
break;
// killough 4/11/98: Add support for setting
// ceiling lighting independently
case Transfer_CeilingLight:
Create<DLightTransfer> (line.frontsector, line.args[0], false);
break;
// [Graf Zahl] Add support for setting lighting
// per wall independently
case Transfer_WallLight:
Create<DWallLightTransfer> (line.frontsector, line.args[0], line.args[1]);
break;
case Sector_Attach3dMidtex:
P_Attach3dMidtexLinesToSector(line.frontsector, line.args[0], line.args[1], !!line.args[2]);
break;
case Sector_SetLink:
if (line.args[0] == 0)
{
P_AddSectorLinks(line.frontsector, line.args[1], line.args[2], line.args[3]);
}
break;
case Sector_SetPortal:
// arg 0 = sector tag
// arg 1 = type
// - 0: normal (handled here)
// - 1: copy (handled by the portal they copy)
// - 2: EE-style skybox (handled by the camera object)
// - 3: EE-style flat portal (GZDoom HW renderer only for now)
// - 4: EE-style horizon portal (GZDoom HW renderer only for now)
// - 5: copy portal to line (GZDoom HW renderer only for now)
// - 6: linked portal
// other values reserved for later use
// arg 2 = 0:floor, 1:ceiling, 2:both
// arg 3 = 0: anchor, 1: reference line
// arg 4 = for the anchor only: alpha
if ((line.args[1] == 0 || line.args[1] == 6) && line.args[3] == 0)
{
P_SpawnPortal(&line, line.args[0], line.args[2], line.args[4], line.args[1]);
}
else if (line.args[1] == 3 || line.args[1] == 4)
{
unsigned pnum = P_GetPortal(line.args[1] == 3 ? PORTS_PLANE : PORTS_HORIZON, line.args[2], line.frontsector, NULL, { 0,0 });
CopyPortal(line.args[0], line.args[2], pnum, 0, true);
}
break;
case Line_SetPortal:
P_SpawnLinePortal(&line);
break;
// [RH] ZDoom Static_Init settings
case Static_Init:
switch (line.args[1])
{
case Init_Gravity:
{
double grav = line.Delta().Length() / 100.;
FSectorTagIterator itr(line.args[0]);
while ((s = itr.Next()) >= 0)
level.sectors[s].gravity = grav;
}
break;
//case Init_Color:
// handled in P_LoadSideDefs2()
case Init_Damage:
{
int damage = int(line.Delta().Length());
FSectorTagIterator itr(line.args[0]);
while ((s = itr.Next()) >= 0)
{
sector_t *sec = &level.sectors[s];
sec->damageamount = damage;
sec->damagetype = NAME_None;
if (sec->damageamount < 20)
{
sec->leakydamage = 0;
sec->damageinterval = 32;
}
else if (sec->damageamount < 50)
{
sec->leakydamage = 5;
sec->damageinterval = 32;
}
else
{
sec->leakydamage = 256;
sec->damageinterval = 1;
}
}
}
break;
case Init_SectorLink:
if (line.args[3] == 0)
P_AddSectorLinksByID(line.frontsector, line.args[0], line.args[2]);
break;
// killough 10/98:
//
// Support for sky textures being transferred from sidedefs.
// Allows scrolling and other effects (but if scrolling is
// used, then the same sector tag needs to be used for the
// sky sector, the sky-transfer linedef, and the scroll-effect
// linedef). Still requires user to use F_SKY1 for the floor
// or ceiling texture, to distinguish floor and ceiling sky.
case Init_TransferSky:
{
FSectorTagIterator itr(line.args[0]);
while ((s = itr.Next()) >= 0)
level.sectors[s].sky = (line.Index() + 1) | PL_SKYFLAT;
break;
}
}
break;
}
}
// [RH] Start running any open scripts on this map
FBehavior::StaticStartTypedScripts (SCRIPT_Open, NULL, false);
}
////////////////////////////////////////////////////////////////////////////
//
// FRICTION EFFECTS
//
// phares 3/12/98: Start of friction effects
// As the player moves, friction is applied by decreasing the x and y
// velocity values on each tic. By varying the percentage of decrease,
// we can simulate muddy or icy conditions. In mud, the player slows
// down faster. In ice, the player slows down more slowly.
//
// The amount of friction change is controlled by the length of a linedef
// with type 223. A length < 100 gives you mud. A length > 100 gives you ice.
//
// Also, each sector where these effects are to take place is given a
// new special type _______. Changing the type value at runtime allows
// these effects to be turned on or off.
//
// Sector boundaries present problems. The player should experience these
// friction changes only when his feet are touching the sector floor. At
// sector boundaries where floor height changes, the player can find
// himself still 'in' one sector, but with his feet at the floor level
// of the next sector (steps up or down). To handle this, Thinkers are used
// in icy/muddy sectors. These thinkers examine each object that is touching
// their sectors, looking for players whose feet are at the same level as
// their floors. Players satisfying this condition are given new friction
// values that are applied by the player movement code later.
//
// killough 8/28/98:
//
// Completely redid code, which did not need thinkers, and which put a heavy
// drag on CPU. Friction is now a property of sectors, NOT objects inside
// them. All objects, not just players, are affected by it, if they touch
// the sector's floor. Code simpler and faster, only calling on friction
// calculations when an object needs friction considered, instead of doing
// friction calculations on every sector during every tic.
//
// Although this -might- ruin Boom demo sync involving friction, it's the only
// way, short of code explosion, to fix the original design bug. Fixing the
// design bug in Boom's original friction code, while maintaining demo sync
// under every conceivable circumstance, would double or triple code size, and
// would require maintenance of buggy legacy code which is only useful for old
// demos. Doom demos, which are more important IMO, are not affected by this
// change.
//
// [RH] On the other hand, since I've given up on trying to maintain demo
// sync between versions, these considerations aren't a big deal to me.
//
/////////////////////////////
//
// Initialize the sectors where friction is increased or decreased
static void P_SpawnFriction(void)
{
line_t *l = &level.lines[0];
for (unsigned i = 0 ; i < level.lines.Size() ; i++,l++)
{
if (l->special == Sector_SetFriction)
{
int length;
if (l->args[1])
{ // [RH] Allow setting friction amount from parameter
length = l->args[1] <= 200 ? l->args[1] : 200;
}
else
{
length = int(l->Delta().Length());
}
P_SetSectorFriction (l->args[0], length, false);
l->special = 0;
}
}
}
void P_SetSectorFriction (int tag, int amount, bool alterFlag)
{
int s;
double friction, movefactor;
// An amount of 100 should result in a friction of
// ORIG_FRICTION (0xE800)
friction = ((0x1EB8 * amount) / 0x80 + 0xD001) / 65536.;
// killough 8/28/98: prevent odd situations
friction = clamp(friction, 0., 1.);
// The following check might seem odd. At the time of movement,
// the move distance is multiplied by 'friction/0x10000', so a
// higher friction value actually means 'less friction'.
movefactor = FrictionToMoveFactor(friction);
FSectorTagIterator itr(tag);
while ((s = itr.Next()) >= 0)
{
// killough 8/28/98:
//
// Instead of spawning thinkers, which are slow and expensive,
// modify the sector's own friction values. Friction should be
// a property of sectors, not objects which reside inside them.
// Original code scanned every object in every friction sector
// on every tic, adjusting its friction, putting unnecessary
// drag on CPU. New code adjusts friction of sector only once
// at level startup, and then uses this friction value.
level.sectors[s].friction = friction;
level.sectors[s].movefactor = movefactor;
if (alterFlag)
{
// When used inside a script, the sectors' friction flags
// can be enabled and disabled at will.
if (friction == ORIG_FRICTION)
{
level.sectors[s].Flags &= ~SECF_FRICTION;
}
else
{
level.sectors[s].Flags |= SECF_FRICTION;
}
}
}
}
double FrictionToMoveFactor(double friction)
{
double movefactor;
// [RH] Twiddled these values so that velocity on ice (with
// friction 0xf900) is the same as in Heretic/Hexen.
if (friction >= ORIG_FRICTION) // ice
//movefactor = ((0x10092 - friction)*(0x70))/0x158;
movefactor = (((0x10092 - friction * 65536) * 1024) / 4352 + 568) / 65536.;
else
movefactor = (((friction*65536. - 0xDB34)*(0xA)) / 0x80) / 65536.;
// killough 8/28/98: prevent odd situations
if (movefactor < 1 / 2048.)
movefactor = 1 / 2048.;
return movefactor;
}