//************************************************************************** //** //** ## ## ## ## ## #### #### ### ### //** ## ## ## ## ## ## ## ## ## ## #### #### //** ## ## ## ## ## ## ## ## ## ## ## ## ## ## //** ## ## ######## ## ## ## ## ## ## ## ### ## //** ### ## ## ### ## ## ## ## ## ## //** # ## ## # #### #### ## ## //** //** $Id: Entity.vc 1755 2006-10-03 19:24:11Z dj_jl $ //** //** Copyright (C) 1999-2006 Jānis Legzdiņš //** //** 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 2 //** 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. //** //************************************************************************** class EntityEx : Entity abstract; const int GAME_Doom = 0x01, GAME_Heretic = 0x02, GAME_Hexen = 0x04, GAME_Strife = 0x08, GAME_Raven = GAME_Heretic | GAME_Hexen; const float ONFLOORZ = -99999.0; const float ONCEILINGZ = 99999.0; const float FLOATRANDZ = 99998.0; const float FRICTION_NORMAL = 3.28125; const float FRICTION_LOW = 0.95703125; const float FRICTION_FLY = 2.87109375; const float FRICTION_WATER = 3.0; const float MAXMOVE = 1050.0; const float MAXMOVESTEP = MAXMOVE / (35.0 * 2.0); const float STOPSPEED = 2.1875; const float BOUNCE_VAL = 1.5; // follow a player exlusively for 3 seconds const int BASETHRESHOLD = 100; const float MELEERANGE = 64.0; const float MISSILERANGE = (32.0 * 64.0); const float MONS_LOOK_RANGE = (20.0 * 64.0); const int MONS_LOOK_LIMIT = 64; enum { BOUNCE_None, BOUNCE_Doom, BOUNCE_Heretic, BOUNCE_Hexen }; // States state IdleState; state SeeState; state MeleeState; state MissileState; // Sounds name SightSound; name ActiveSound; name AttackSound; name PainSound; name DeathSound; name HowlSound; name CrunchSound; // Flags bool bSpecial; // call P_SpecialThing when touched bool bShootable; // Can be hit. bool bAmbush; bool bJustHit; // try to attack right back bool bJustAttacked; // take at least one step before attacking bool bSpawnCeiling; // hang from ceiling instead of floor // movement flags bool bPickUp; // for players to pick up items bool bMissile; // don't hit same species, explode on block bool bDropped; // dropped by a demon, not level spawned bool bNoBlood; // don't bleed when shot (use puff) bool bInFloat; // floating to a height for a move, don't // auto float to target's height bool bSkullFly; // skull in flight bool bCountKill; // count towards intermission kill total bool bCountItem; // count towards intermission item total bool bWindThrust; // gets pushed around by the wind specials bool bActivateImpact; // an MF_MISSILE mobj can activate // SPAC_IMPACT bool bActivatePushWall; // mobj can push walls bool bActivateMCross; // can activate monster cross lines bool bActivatePCross; // can activate projectile cross lines bool bDormant; // thing is dormant bool bWaterJump; bool bNoTeleport; // does not teleport bool bTelestomp; // mobj can stomp another bool bCannotPush; // cannot push other pushable mobjs bool bSmallSplash; // Always use small splash bool bNoSplash; // Things that don't splash bool bStaticLight; // Static light source. bool bDynamicLight; // Dynamic light source. bool bMuzzleFlash; // Muzzle flash effect. bool bLeaveTrail; // Leave particles trail. bool bTriggerHappy; bool bBlaster; bool bFloatBob; // use float bobbing z movement bool bOnmobjCopyVel; bool bNoBounceSound; // Don't make sound when bouncing bool bNoWallBounceSnd; // Don't make sound when bouncing off a wall bool bBounceSky; // Bounce when hitting the sky bool bExplodeOnSky; // Explodes when hits the sky bool bBounceOnActors; // Bounces against other actors bool bSlide; // slides against walls bool bReflective; // reflects missiles bool bSeekerMissile; // is a seeker (for reflection) bool bNoExplodeFloor; // Don't explode when hitting the floor bool bIceCorpse; // a frozen corpse (for blasting) bool bExplodeOnWater; // Explode on water surfaces bool bCanBounceWater; // Bounce on water surfaces bool bFallingFriction; // Apply friction while falling bool bNoRadiusDamage; // Does not take radius damage bool bCantAutoAim; // Can't auto aim at this actor bool bPuffOnActors; // Spawn this puff when hitting actors bool bInvulnerable; // mobj is invulnerable bool bAxeBlood; // Use axe's blood splatter bool bFriendly; // Will fight on player's side bool bRandomise; // Randomise initial state time bool bFullVolDeath; // Play missile death sound at full volume bool bExploCount; // Use explosion counters. bool bSpectral; // Can be killed only with Sigil bool bDamageInvulnerable; // These inflictors aren't foiled by invulnerability bool bSkullFlyInvulnerable; // Invulnerable during skull fly attack bool bNoDamageThrust; // does not thrust target when damaging bool bConditionalFireDamage; bool bNoExtremeDeath; // Does never gib enemies. bool bExtremeDeath; // Always gibs enemies. bool bLightning; // Electrocutes victims bool bHowlVictims; // Make victims play howl sound when damaged bool bNoGrudge; bool bNeverTarget; // Neve switch target to this actor bool bNoTargetSwitch; // Never switches target until current one is dead bool bNoGravKill; // Doesn't set NoGravity to false when killed bool bFaster; bool bFastMelee; bool bStanding; // Don't walk around. bool bBoss; // mobj is a major boss bool bNonShootable; // mobj is totally non-shootable, // but still considered solid bool bThruGhost; // missile will pass through ghosts bool bRip; // missile rips through solid targets bool bPushable; // can be pushed by other moving mobjs bool bBloodlessImpact; // Don't spawn blood when hitting a thing bool bMonster; bool bNoDeathmatch; bool bTeleport; // don't cross lines or look at heights bool bSpawnFloat; // spawn random float z bool bNoMorph; // Don't morph into chicken/pig. bool bNoBlockMonst; // Can cross ML_BLOCKMONSTERS lines bool bLookAllAround; // Actor can see all around. bool bNeutral; // Neutral characters (peasants and beggars) bool bStandMustSeeTarget; //COUNTITEM 800000 bool bInCombat; // Actors in combat won't talk bool bFullVolActive; // Play active sound at full volume bool bDehackedSpecial; // Old style special handling bool bUnknown2; //8000000 bool bPuffParticles; // Puff spawns particles bool bExplodeParticles; // Use particle explosion // Params float Speed; float StepSpeed; float FloatSpeed; float PainChance; int GibsHealth; float MissileChance; float MissileMinRange; float MissileMaxRange; byte BounceType; byte BounceCount; float BounceFactor; // Thing being chased/attacked (or NULL). // Also the originator for missiles. EntityEx Target; EntityEx Tracer; // Player number last looked for. int LastLook; int MissileDamage; // For missiles // For nightmare respawn. mthing_t SpawnPoint; // Reaction time: if non 0, don't attack yet. int ReactionCount; // Used by player to freeze a bit after teleporting. float ReactionTime; // If >0, the target will be chased // no matter what (even if shot) int Threshold; // Movement direction, movement generation (zig-zagging). int MoveDir; // 0-7 int MoveCount; // when 0, select a new dir float RDFactor; // Static light parameters. TVec LightOffset; int LightColour; float LightRadius; // Dynamic light parameters. int DLightColour; float DLightRadius; int ExplodeEffect; float MeleeRange; int Special1; // Special info int Special2; // Special info float Special1f; float Special2f; class SpecialCID; name DamageType; float DeathHeight; float BurnHeight; name ClassName; // For Strife // Identifier in conversation scripts, originaly index into mobjinfo. int ConversationID; int CurrentSpeech; // Current speech index. // Actor's inventory Inventory Inventory; float FloatBobPhase; float PuffVelZ; replication { reliable if (Role == ROLE_Authority && bNetOwner) Inventory; } //========================================================================== // // Destroyed // //========================================================================== void Destroyed() { if (Role == ROLE_Authority) { // Remove inventory. while (Inventory) { Inventory.Destroy(); } } ::Destroyed(); } //========================================================================== // // SetOrigin2 // //========================================================================== final void SetOrigin2(TVec origin) { Origin = origin; // Set subsector and/or block links. LinkToWorld(); if (origin.z == ONFLOORZ) { Origin.z = FloorZ; } else if (origin.z == ONCEILINGZ) { Origin.z = CeilingZ - Height; } else if (origin.z == FLOATRANDZ) { float space = CeilingZ - Height - FloorZ; if (space > 48.0) { space -= 40.0; Origin.z = space * Random() + FloorZ + 40.0; } else { Origin.z = FloorZ; } } else if (bFloatBob) { Origin.z = FloorZ + Origin.z; // artifact z passed in as height } VTerrainInfo* TInfo = GetFloorType(); if (bFloorClip && TInfo->bLiquid && Origin.z == FloorZ) { FloorClip = TInfo->FootClip; } else { FloorClip = 0.0; } } //************************************************************************** // // OBJECT MOVEMENT // //************************************************************************** //========================================================================== // // Physics // //========================================================================== bool Physics(float DeltaTime) { EntityEx onmo; SectorThinker SecThink; float scrollx; float scrolly; float height; float waterheight; // killough 4/4/98: add waterheight if (bBlaster) { return BlasterPhysics(DeltaTime); } if (Sector->AffectorData && bColideWithWorld) { // killough 3/7/98: Carry things on floor // killough 3/20/98: use new sector list which reflects true members // killough 3/27/98: fix carrier bug // killough 4/4/98: Underwater, carry things even w/o gravity // Move objects only if on floor or underwater, // non-floating, and clipped. for (SecThink = SectorThinker(Sector->AffectorData); SecThink; SecThink = SecThink.NextAffector) { if (!Scroller(SecThink)) { continue; } scrollx = Scroller(SecThink).CarryScrollX; scrolly = Scroller(SecThink).CarryScrollY; if (!scrollx && !scrolly) { continue; } if (bNoGravity && (!Sector->heightsec || (Sector->heightsec->bIgnoreHeightSec))) { continue; } height = GetPlanePointZ(&Sector->floor, Origin); if (Origin.z > height) { if (!Sector->heightsec || (Sector->heightsec->bIgnoreHeightSec)) { continue; } waterheight = GetPlanePointZ(&Sector->heightsec->floor, Origin); if (waterheight > height && Origin.z >= waterheight) { continue; } } Velocity.x += scrollx; Velocity.y += scrolly; } } CheckWater(); if (!bFloatBob) { UpdateVelocity(); } // momentum movement if (Velocity.x || Velocity.y) { // Handle X and Y momentums XYMovement(DeltaTime); if (IsDestroyed()) { // mobj was removed return false; } } else if (bSkullFly && Health > 0) { // A flying mobj slammed into something bSkullFly = false; Velocity = vector(0.0, 0.0, 0.0); if (!SetState(SeeState ? SeeState : IdleState)) { // mobj was removed return false; } } else if (bBlasted) { // Reset to not blasted when momentums are gone ResetBlasted(); } if (bFloatBob) { // Floating item bobbing motion (special1 is height) if (Sector->bHasExtrafloors) { // Make sure FloorZ is from bottom region. Origin.z = ONFLOORZ; LinkToWorld(); } FloatBobPhase += DeltaTime; Origin.z = FloorZ + Special1f + Level.Game.FloatBobOffsets[ftoi(FloatBobPhase * 35.0) & 63]; } else if (!bNoPassMobj && !bMissile) { // Handle Z momentum and gravity onmo = EntityEx(CheckOnmobj()); if (onmo) { if (bIsPlayer) { if (Velocity.z < -DEFAULT_GRAVITY * 0.25 && !bFly) { PlayerLandedOnThing(); } } if (onmo.Origin.z + onmo.Height - Origin.z <= MaxStepHeight) { if (bIsPlayer) { PlayerEx(Player).ViewHeight -= onmo.Origin.z + onmo.Height - Origin.z; PlayerEx(Player).DeltaViewHeight = (GetBaseViewHeight() - PlayerEx(Player).ViewHeight) * 4.0; } Origin.z = onmo.Origin.z + onmo.Height; } bOnMobj = true; Velocity.z = 0.0; if (onmo.bOnmobjCopyVel) { Velocity.x = onmo.Velocity.x; Velocity.y = onmo.Velocity.y; if (onmo.Origin.z < onmo.FloorZ) { Origin.z += onmo.FloorZ - onmo.Origin.z; if (onmo.bIsPlayer) { PlayerEx(onmo.Player).ViewHeight -= onmo.FloorZ - onmo.Origin.z; PlayerEx(onmo.Player).DeltaViewHeight = (GetBaseViewHeight() - PlayerEx(onmo.Player).ViewHeight) * 4.0; } onmo.Origin.z = onmo.FloorZ; } } } else if ((Origin.z != FloorZ) || Velocity.z) { ZMovement(DeltaTime); bOnMobj = false; } if (IsDestroyed()) { // entity was removed return false; } } else if ((Origin.z != FloorZ) || Velocity.z) { // Handle Z momentum and gravity ZMovement(DeltaTime); if (IsDestroyed()) { // entity was removed return false; } } return true; } //========================================================================== // // XYMovement // //========================================================================== final void XYMovement(float DeltaTime) { float ptryx; float ptryy; float xmove; float ymove; int special; if (bWindThrust) { special = Sector->special & SECSPEC_BASE_MASK; switch (special) { case SECSPEC_WindEastSlow: case SECSPEC_WindEastMedium: case SECSPEC_WindEastFast: Thrust(0.0, LineSpecialGameInfo(Level.Game).windTab[special - SECSPEC_WindEastSlow]); break; case SECSPEC_WindNorthSlow: case SECSPEC_WindNorthMedium: case SECSPEC_WindNorthFast: Thrust(90.0, LineSpecialGameInfo(Level.Game).windTab[special - SECSPEC_WindNorthSlow]); break; case SECSPEC_WindSouthSlow: case SECSPEC_WindSouthMedium: case SECSPEC_WindSouthFast: Thrust(270.0, LineSpecialGameInfo(Level.Game).windTab[special - SECSPEC_WindSouthSlow]); break; case SECSPEC_WindWestSlow: case SECSPEC_WindWestMedium: case SECSPEC_WindWestFast: Thrust(180.0, LineSpecialGameInfo(Level.Game).windTab[special - SECSPEC_WindWestSlow]); break; } } if (Velocity.x > MAXMOVE) { Velocity.x = MAXMOVE; } else if (Velocity.x < -MAXMOVE) { Velocity.x = -MAXMOVE; } if (Velocity.y > MAXMOVE) { Velocity.y = MAXMOVE; } else if (Velocity.y < -MAXMOVE) { Velocity.y = -MAXMOVE; } xmove = Velocity.x * DeltaTime; ymove = Velocity.y * DeltaTime; do { if (xmove > MAXMOVESTEP || ymove > MAXMOVESTEP) { ptryx = Origin.x + xmove / 2.0; ptryy = Origin.y + ymove / 2.0; xmove /= 2.0; ymove /= 2.0; } else { ptryx = Origin.x + xmove; ptryy = Origin.y + ymove; xmove = 0.0; ymove = 0.0; } tmtrace_t tmtrace; if (!TryMoveEx(&tmtrace, vector(ptryx, ptryy, Origin.z))) { // blocked move if (tmtrace.BlockingMobj) { HitMobj(tmtrace.BlockingMobj); } else { HitLine(&tmtrace); } return; } } while (xmove || ymove); } //========================================================================== // // ZMovement // //========================================================================== final void ZMovement(float DeltaTime) { float dist; float delta; // check for smooth step up if (bIsPlayer && Origin.z < FloorZ) { PlayerEx(Player).ViewHeight -= FloorZ - Origin.z; PlayerEx(Player).DeltaViewHeight = (GetBaseViewHeight() - PlayerEx(Player).ViewHeight) * 4.0; } // adjust height Origin.z += Velocity.z * DeltaTime; if (bFloat && Target) { // float down towards enemy if too close if (!bSkullFly && !bInFloat) { dist = DistTo2(Target); delta = Target.Origin.z + Height / 2.0 - Origin.z; if (delta < 0.0 && dist < -delta * 3.0) Origin.z -= FloatSpeed * DeltaTime; else if (delta > 0.0 && dist < delta * 3.0) Origin.z += FloatSpeed * DeltaTime; } } if (bIsPlayer && bFly && !(Origin.z <= FloorZ) && XLevel.TicTime & 2) { Origin.z += sin(90.0 * 35.0 / 20.0 * XLevel.Time); } // clip movement if (Origin.z <= FloorZ + 0.1) { // hit the floor if (!HitFloor(DeltaTime)) { return; } } if (Origin.z + Height > CeilingZ) { // hit the ceiling if (!HitCeiling()) { return; } } } //========================================================================== // // HitLine // //========================================================================== final void HitLine(tmtrace_t* tmtrace) { if (bMissile) { if (BounceType == BOUNCE_Doom || BounceType == BOUNCE_Hexen) { // If number of bounces is limited. if (BounceCount > 0 && --BounceCount <= 0) { ExplodeMissile(); return; } // Struck a wall BounceWall(BOUNCE_VAL); if (!bNoBounceSound && !bNoWallBounceSnd && SightSound) { PlaySound(SightSound, CHAN_VOICE); } return; } // explode a missile if (tmtrace->CeilingLine && tmtrace->CeilingLine->backsector && tmtrace->CeilingLine->backsector->ceiling.pic == Level.Game.skyflatnum) { // Hack to prevent missiles exploding against the sky. // Does not handle sky floors. if (bBounceSky) { Velocity = vector(0.0, 0.0, -1.0 * 35.0); } else if (bExplodeOnSky) { ExplodeMissile(); } else { Destroy(); } return; } ExplodeMissile(); } else if (bSlide) { // try to slide along it SlideMove(); } else { Velocity.x = 0.0; Velocity.y = 0.0; } } //========================================================================== // // HitMobj // //========================================================================== final void HitMobj(Entity Other) { float angle; float speed; if (bMissile) { if (BounceType == BOUNCE_Doom || BounceType == BOUNCE_Hexen) { // Bounce against walls and non-killable ofjects if (bBounceOnActors || EntityEx(Other).bReflective || (!Other.bIsPlayer && !EntityEx(Other).bCountKill)) { angle = AngleMod360(atan2(Origin.y - Other.Origin.y, Origin.x - Other.Origin.x) + (Random() * 16.0 - 8.0)); speed = Length(Velocity); speed = speed * 0.75; Angles.yaw = angle; Velocity.x = speed * cos(angle); Velocity.y = speed * sin(angle); if (!bNoBounceSound && SightSound) { PlaySound(SightSound, CHAN_VOICE); } } else { // Struck a player/creature ExplodeMissile(); } return; } if (EntityEx(Other).bReflective) { angle = EntityEx(Other).GetReflectedAngle(self); if (angle != -1.0) { // Reflect the missile along angle Angles.yaw = angle; Velocity.x = (Speed / 2.0) * cos(angle); Velocity.y = (Speed / 2.0) * sin(angle); Velocity.z = -Velocity.z * 0.5; if (bSeekerMissile) { Tracer = Target; } Target = EntityEx(Other); return; } } // Explode a missile ExplodeMissile(); } else if (bSlide) { // Try to slide along it // Slide against mobj // if (TryMove(vector(Origin.x, ptryy, Origin.z))) if (TryMove(vector(Origin.x, Origin.y + Velocity.y * Level.Game.frametime, Origin.z))) { Velocity.x = 0.0; } // else if (TryMove(vector(ptryx, Origin.y, Origin.z))) else if (TryMove(vector(Origin.x + Velocity.x * Level.Game.frametime, Origin.y, Origin.z))) { Velocity.y = 0.0; } else { Velocity.x = 0.0; Velocity.y = 0.0; } } else { Velocity.x = 0.0; Velocity.y = 0.0; } } //========================================================================== // // HitFloor // //========================================================================== final bool HitFloor(float DeltaTime) { float vdot; if (bMissile && (bColideWithWorld || !LineSpecialGameInfo(Level.Game).bNoClipIgnoreFloor)) { Origin.z = FloorZ; if (BounceType != BOUNCE_None) { FloorBounceMissile(); return false; } if (bNoExplodeFloor) { // The spirit struck the ground Velocity.z = 0.0; HitFloorType(); return false; } if (bIgnoreFloorStep) { // Minotaur floor fire can go up steps return false; } HitFloorType(); ExplodeMissile(); return false; } vdot = DotProduct(Velocity, Floor->normal); if (bCountKill) // Blasted mobj falling { if (vdot < -23.0 * 35.0) { MonsterFallingDamage(); } } Origin.z = FloorZ; if (vdot < -0.1) { // Spawn splashes, etc. HitFloorType(); if (DamageType == 'Ice' && vdot < -DEFAULT_GRAVITY * 0.25) { StateTime = 0.1; Velocity = vector(0.0, 0.0, 0.0); return false; } // Do some special action when hitting the floor. OnHitFloor(); if (bIsPlayer) { PlayerEx(Player).JumpTime = 0.2; // delay any jumping for a short time if (vdot < -DEFAULT_GRAVITY * 0.25 && !bNoGravity) { // Squat down. // Decrease ViewHeight for a moment after hitting the ground // (hard), and utter appropriate sound. PlayerLandedOnThing(); } } //Velocity -= vdot * Floor->normal; Velocity.z = 0.0; } if (bSkullFly) { // The skull slammed into something Velocity.z = -Velocity.z; } Crash(); return true; } //========================================================================== // // HitCeiling // //========================================================================== final bool HitCeiling() { float vdot; vdot = DotProduct(Velocity, Ceiling->normal); if (vdot < 0.0) { Velocity -= vdot * Ceiling->normal; } Origin.z = CeilingZ - Height; if (bMissile && (bColideWithWorld || !LineSpecialGameInfo(Level.Game).bNoClipIgnoreFloor)) { if (BounceType != BOUNCE_None) { CeilingBounceMissile(); return false; } if (bIgnoreCeilingStep) { return false; } if (Ceiling->pic == Level.Game.skyflatnum) { if (bBounceSky) { Velocity = vector(0.0, 0.0, -1.0 * 35.0); } else if (bExplodeOnSky) { ExplodeMissile(); } else { Destroy(); } return false; } ExplodeMissile(); return false; } if (bSkullFly) { // the skull slammed into something Velocity.z = -Velocity.z; } return true; } //========================================================================== // // FloorBounceMissile // //========================================================================== void FloorBounceMissile() { float vdot; if (HitFloorType()) { // Landed on some kind of liquid. if (bExplodeOnWater) { ExplodeMissile(); return; } if (!bCanBounceWater) { Destroy(); return; } } // If number of bounces is limited. if (BounceCount > 0 && --BounceCount <= 0) { ExplodeMissile(); return; } vdot = DotProduct(Velocity, Floor->normal); if (BounceType == BOUNCE_Heretic) { Velocity -= 2.0 * vdot * Floor->normal; Angles.yaw = atan2(Velocity.y, Velocity.x); SetState(FindState('Death')); return; } Velocity = (Velocity - 2.0 * vdot * Floor->normal) * BounceFactor; Angles.yaw = atan2(Velocity.y, Velocity.x); if (!bNoBounceSound && SightSound) { PlaySound(SightSound, CHAN_VOICE); } if (BounceType == BOUNCE_Doom) { if (!bNoGravity && Velocity.z < 3.0 * 35.0) { BounceType = BOUNCE_None; } } } //========================================================================== // // CeilingBounceMissile // //========================================================================== final void CeilingBounceMissile() { float vdot; // If number of bounces is limited. if (BounceCount > 0 && --BounceCount <= 0) { ExplodeMissile(); return; } vdot = DotProduct(Velocity, Ceiling->normal); if (BounceType == BOUNCE_Heretic) { Velocity -= 2.0 * vdot * Ceiling->normal; Angles.yaw = atan2(Velocity.y, Velocity.x); SetState(FindState('Death')); return; } // Reverse momentum here for ceiling bounce Velocity = (Velocity - 2.0 * vdot * Ceiling->normal) * BounceFactor; Angles.yaw = atan2(Velocity.y, Velocity.x); if (!bNoBounceSound && SightSound) { PlaySound(SightSound, CHAN_VOICE); } } //========================================================================== // // GetReflectedAngle // //========================================================================== float GetReflectedAngle(EntityEx Other) { float angle = atan2(Other.Origin.y - Origin.y, Other.Origin.x - Origin.x); // Change angle for reflection angle = AngleMod360(angle + Random() * 16.0 - 8.0); return angle; } //========================================================================== // // Crash // //========================================================================== final void Crash() { state CrashState = FindState('Crash'); if (CrashState && bCorpse && DamageType != 'Ice') { SetState(CrashState); } } //=========================================================================== // // PlayerLandedOnThing // //=========================================================================== final void PlayerLandedOnThing() { PlayerEx(Player).DeltaViewHeight = Velocity.z / 8.0; FallingDamage(); if (Health > 0 && !PlayerIsMorphed()) { if (Velocity.z < -DEFAULT_GRAVITY * 0.375) { PlaySound('*grunt', CHAN_VOICE); } if ((Origin.z > FloorZ || !GetFloorType()->bLiquid) && !AreSoundsEquivalent('*grunt', '*land')) { PlaySound('*land', CHAN_BODY); } } //FIXME Player.centreing = true; } //========================================================================== // // FallingDamage // //========================================================================== final void FallingDamage() { int damage; float mom; float dist; mom = fabs(Velocity.z); if (Level.bFallingDamage) { // Hexen style falling damage. if (mom <= 23.0 * 35.0) { // Not fast enough. return; } if (mom >= 63.0 * 35.0) { // Automatic death. damage = 10000; } else { dist = mom / 35.0 * 16.0 / 23.0; damage = ftoi(dist * dist / 10.0) - 24; if (Velocity.z > -39.0 * 35.0 && damage > Health && Health != 1) { // No-death threshold. damage = Health - 1; } } } else if (Level.bOldFallingDamage) { // ZDoom style falling damage, less damaging. if (mom <= 19.0 * 35.0) { // Not fast enough. return; } if (mom >= 84.0 * 35.0) { // Automatic death. damage = 10000; } else { mom = mom / 35.0; damage = (ftoi(mom * mom * 11.0 / 128.0) - 30) / 2; if (damage < 1) { damage = 1; } } } else if (Level.bStrifeFallingDamage) { // Strife style falling damage, very strong. if (mom <= 20.0 * 35.0) { // Not fast enough. return; } damage = ftoi(mom * (8192.0 / 3125.0 / 35.0)); } else { return; } PlaySound('*land', CHAN_BODY); Damage(none, none, damage, 'Falling'); LineSpecialLevelInfo(Level).NoiseAlert(self, self); } //========================================================================== // // MonsterFallingDamage // //========================================================================== final void MonsterFallingDamage() { int damage; float mom; if (!Level.bMonsterFallingDamage) { return; } mom = fabs(Velocity.z) / 35.0; if (mom > 35.0) { // automatic death damage = 10000; } else { damage = ftoi((mom - 23.0) * 6.0); } damage = 10000; // always kill 'em Damage(none, none, damage, 'Falling'); } //========================================================================== // // BlasterPhysics // // Thinker for the ultra-fast blaster PL2 ripper-spawning missile. // //========================================================================== final bool BlasterPhysics(float DeltaTime) { int i; float xfrac; float yfrac; float zfrac; bool changexy; tmtrace_t tmtrace; // Handle movement if (Velocity.x || Velocity.y || (Origin.z != FloorZ) || Velocity.z) { xfrac = Velocity.x * DeltaTime / 8.0; yfrac = Velocity.y * DeltaTime / 8.0; zfrac = Velocity.z * DeltaTime / 8.0; changexy = xfrac || yfrac; for (i = 0; i < 8; i++) { if (changexy) { if (!TryMoveEx(&tmtrace, vector(Origin.x + xfrac, Origin.y + yfrac, Origin.z))) { // Blocked move if (tmtrace.BlockingMobj) { BlasterHitMobj(tmtrace.BlockingMobj); } else { ExplodeMissile(); } return !IsDestroyed(); } } Origin.z += zfrac; if (Origin.z <= FloorZ) { // Hit the floor Origin.z = FloorZ; HitFloorType(); ExplodeMissile(); return !IsDestroyed(); } if (Origin.z + Height > CeilingZ) { // Hit the ceiling Origin.z = CeilingZ - Height; ExplodeMissile(); return !IsDestroyed(); } if (changexy) { SpawnBlasterEffects(); } } } return true; } //========================================================================== // // ResetBlasted // //========================================================================== final void ResetBlasted() { bBlasted = false; if (!bIceCorpse) { bSlide = false; } } //========================================================================== // // OnHitFloor // //========================================================================== void OnHitFloor() { } //========================================================================== // // BlasterHitMobj // //========================================================================== bool BlasterHitMobj(Entity Other) { return ExplodeMissile(); } //========================================================================== // // SpawnBlasterEffects // //========================================================================== void SpawnBlasterEffects() { } //========================================================================== // // HitFloorType // //========================================================================== final bool HitFloorType() { EntityEx A; TVec org; bool smallsplash = false; VTerrainInfo* TInfo; VSplashInfo* SInfo; if (FloorZ != GetPlanePointZ(&Sector->floor, Origin)) { // don't splash if landing on the edge above water/lava/etc.... return false; } // Things that don't splash go here if (bNoSplash) { return false; } TInfo = GetFloorType(); // Small splash for small masses if (Mass < 10.0 || bSmallSplash) smallsplash = true; if (TInfo->DamageAmount && bIsPlayer && XLevel.TicTime & TInfo->DamageTimeMask) { Damage(none, none, TInfo->DamageAmount, TInfo->DamageType); } SInfo = GetSplashInfo(TInfo->Splash); if (!SInfo) { return TInfo->bLiquid; } org = Origin; org.z = FloorZ; if (smallsplash) { if (SInfo->SmallClass) { A = Spawn(class(SInfo->SmallClass), org); A.FloorClip += SInfo->SmallClip; if (SInfo->SmallSound) { A.PlaySound(SInfo->SmallSound, CHAN_VOICE); } } } else { if (SInfo->BaseClass) { Spawn(class(SInfo->BaseClass), org); if (SInfo->Sound && !SInfo->ChunkClass) { A.PlaySound(SInfo->Sound, CHAN_VOICE); } } if (SInfo->ChunkClass) { A = Spawn(class(SInfo->ChunkClass), org); A.Target = self; A.Velocity.x = (Random() - Random()) * SInfo->ChunkXVelMul * 35.0; A.Velocity.y = (Random() - Random()) * SInfo->ChunkYVelMul * 35.0; A.Velocity.z = (SInfo->ChunkBaseZVel + Random() * SInfo->ChunkZVelMul) * 35.0; if (SInfo->Sound) { A.PlaySound(SInfo->Sound, CHAN_VOICE); } } if (SInfo->Sound && !SInfo->BaseClass && !SInfo->ChunkClass) { PlaySound(SInfo->Sound, CHAN_BODY); } if (!SInfo->bNoAlert && bIsPlayer) { LineSpecialLevelInfo(Level).NoiseAlert(self, self); } } return TInfo->bLiquid; } //=========================================================================== // // GetFloorType // //=========================================================================== final VTerrainInfo* GetFloorType() { return TerrainType(Floor->pic); } //========================================================================== // // HandleFloorclip // //========================================================================== final void HandleFloorclip() { if (bFloorClip) { VTerrainInfo* TInfo = GetFloorType(); if (Origin.z == FloorZ && TInfo->bLiquid) { FloorClip = TInfo->FootClip; } else { FloorClip = 0.0; } } } //========================================================================== // // ApplyFriction // //========================================================================== final void ApplyFriction() { float dot; if (bMissile || bSkullFly) { // no friction for missiles ever return; } if (Origin.z > FloorZ && !bOnMobj && WaterLevel < 2 && !bFly && !bFallingFriction) { // no friction when airborne return; } // Clip velocity if (Origin.z <= FloorZ) { dot = DotProduct(Velocity, Floor->normal); if (dot < 0.0) { Velocity -= dot * Floor->normal; } } if (bCorpse) { // Don't stop sliding if halfway off a step with some momentum if (Velocity.x > 0.25 * 35.0 || Velocity.x < -0.25 * 35.0 || Velocity.y > 0.25 * 35.0 || Velocity.y < -0.25 * 35.0) { if (FloorZ != GetPlanePointZ(&Sector->floor, Origin)) { return; } } } if (Velocity.x > -STOPSPEED && Velocity.x < STOPSPEED && Velocity.y > -STOPSPEED && Velocity.y < STOPSPEED && Velocity.z > -STOPSPEED && Velocity.z < STOPSPEED && (!bIsPlayer || (!Player.ForwardMove && !Player.SideMove))) { if (bIsPlayer) { // if in a walking frame, stop moving if (StateIsInRange(State, SeeState, none, 4)) { SetState(IdleState); } } Velocity = vector(0.0, 0.0, 0.0); } else { // slow down Velocity -= Velocity * (GetFriction() * Level.Game.frametime); } } //=========================================================================== // // GetFriction // //=========================================================================== final float GetFriction() { if (WaterLevel > 1) { return FRICTION_WATER; } if (bFly && Origin.z > FloorZ && !bOnMobj) { return FRICTION_FLY; } if ((Sector->special & SECSPEC_BASE_MASK) == SECSPEC_FrictionLow) { return FRICTION_LOW; } VTerrainInfo* TInfo = GetFloorType(); if (TInfo->Friction) { return TInfo->Friction; } if (Sector->special & SECSPEC_FRICTION_MASK) { return Sector->Friction; } return FRICTION_NORMAL; } //************************************************************************** // // RADIUS ATTACK // //************************************************************************** //========================================================================== // // RadiusAttack // // Source is the creature that caused the explosion at spot. // //========================================================================== final void RadiusAttack(EntityEx Source, int BombDamage, float BombDistance, bool DamageSource, optional name BombDmgType) { EntityEx Other; float dist; int damage; foreach RadiusThings(Other, Origin, BombDistance) { if (!Other.bShootable) { continue; } // Check for actors that take no damage from concussion. if (bNoRadiusDamage) { continue; } if (!DamageSource && Other == Source) { // don't damage the source of the explosion continue; } dist = Length(Other.GetCentre() - GetCentre()) - Other.Radius; if (dist < 0.0) { dist = 0.0; } if (dist >= BombDistance) { // out of range continue; } if (Other.CanSee(self)) { // OK to damage, target is in direct path damage = ftoi(itof(BombDamage) * RDFactor * (BombDistance - dist) / BombDistance); Other.Damage(self, Source, damage, BombDmgType); } } } //************************************************************************** // // AIMING // //************************************************************************** //========================================================================== // // AimLineAttack // // Sets linetaget and aim_slope when a target is aimed at. // //========================================================================== final EntityEx AimLineAttack(out TVec OutDir, TAVec angles, float distance) { float x2; float y2; float topangle; float botangle; TVec dir; intercept_t* in; EntityEx linetarget; // who got hit (or NULL) // Height if not aiming up or down // ???: use slope for monsters? float aim_z; float aim_range; float aim_slope; TVec aim_dir; float aim_topslope; float aim_bottomslope; // slopes to top and bottom of target float aim_range2d; AngleVector(&angles, &aim_dir); x2 = Origin.x + distance * aim_dir.x; y2 = Origin.y + distance * aim_dir.y; aim_z = Origin.z + Height / 2.0 + 8.0 - FloorClip; aim_range2d = Length(vector(distance * aim_dir.x, distance * aim_dir.y, 0.0)); // can't shoot outside view angles topangle = AngleMod180(-angles.pitch + 30.0); botangle = AngleMod180(-angles.pitch - 30.0); if (topangle > 89.0) topangle = 89.0; if (botangle < -89.0) botangle = -89.0; aim_topslope = tan(topangle); aim_bottomslope = tan(botangle); aim_range = distance; linetarget = none; foreach PathTraverse(in, Origin.x, Origin.y, x2, y2, PT_ADDLINES | PT_ADDTHINGS) { line_t* li; EntityEx th; float thingtopslope; float thingbottomslope; float dist; float slope; opening_t* open; if (in->bIsALine) { TVec hit_point; li = in->line; if (!(li->flags & ML_TWOSIDED)) break; // stop // Crosses a two sided line. // A two sided line will restrict // the possible target ranges. dist = aim_range * in->frac; hit_point = Origin + dist * aim_dir; open = LineOpenings(li, hit_point); open = FindOpening(open, hit_point.z, hit_point.z); if (!open || open->bottom >= open->top) break; // stop dist = aim_range2d * in->frac; if (li->frontsector->floorheight != li->backsector->floorheight) { slope = (open->bottom - aim_z) / dist; if (slope > aim_bottomslope) aim_bottomslope = slope; } if (li->frontsector->ceilingheight != li->backsector->ceilingheight) { slope = (open->top - aim_z) / dist; if (slope < aim_topslope) aim_topslope = slope; } if (aim_topslope <= aim_bottomslope) break; // stop continue; // shot continues } // shoot a thing th = EntityEx(in->Thing); if (th == self) continue; // can't shoot self if (!th.bShootable) continue; // corpse or something if (th.bCantAutoAim) { // Can't auto-aim at pods continue; } if (IsTeammate(th)) { // don't aim at fellow co-op players continue; } // check angles to see if the thing can be aimed at dist = aim_range2d * in->frac; thingtopslope = (th.Origin.z + th.Height - aim_z) / dist; if (thingtopslope < aim_bottomslope) continue; // shot over the thing thingbottomslope = (th.Origin.z - aim_z) / dist; if (thingbottomslope > aim_topslope) continue; // shot under the thing // this thing can be hit! if (thingtopslope > aim_topslope) thingtopslope = aim_topslope; if (thingbottomslope < aim_bottomslope) thingbottomslope = aim_bottomslope; aim_slope = (thingtopslope + thingbottomslope) / 2.0; linetarget = th; break; // don't go any farther } if (linetarget) { angles.pitch = -atan(aim_slope); } AngleVector(&angles, &dir); OutDir = dir; return linetarget; } //=========================================================================== // // Aim // // Sets a slope so a near miss is at aproximately the height of the // intended target // //=========================================================================== final EntityEx Aim(out TVec OutDir, float distance, optional float yaw) { TAVec ang; TVec dir; EntityEx LineTarget; // see which target is to be aimed at ang = Angles; if (specified_yaw) { ang.yaw = yaw; } LineTarget = AimLineAttack(OutDir, ang, distance); if (!LineTarget) { ang.yaw = AngleMod360(ang.yaw + 45.0 / 8.0); LineTarget = AimLineAttack(OutDir, ang, distance); if (!LineTarget) { ang.yaw = AngleMod360(ang.yaw - 45.0 / 4.0); LineTarget = AimLineAttack(OutDir, ang, distance); if (!LineTarget) { ang.yaw = AngleMod360(ang.yaw + 45.0 / 8.0); AngleVector(&ang, &dir); OutDir = dir; } } } return LineTarget; } //========================================================================== // // AimEx // //========================================================================== final EntityEx AimEx(out TVec OutDir, float Range, float AngleInc, int NumSteps, optional float FinalRange) { int i; TAVec angles; TVec vforward; EntityEx LineTarget; for (i = 0; i < NumSteps; i++) { // Try to the left angles = Angles; angles.yaw = AngleMod360(angles.yaw + itof(i) * AngleInc); LineTarget = AimLineAttack(OutDir, angles, Range); if (LineTarget) { return LineTarget; } // Try to the right angles = Angles; angles.yaw = AngleMod360(angles.yaw - itof(i) * AngleInc); LineTarget = AimLineAttack(OutDir, angles, Range); if (LineTarget) { return LineTarget; } } if (FinalRange) { // Didn't find any creatures, so try to strike any walls angles = Angles; LineTarget = AimLineAttack(OutDir, angles, FinalRange); } else { AngleVector(&Angles, &vforward); OutDir = vforward; } return LineTarget; } //************************************************************************** // // SHOOTING // //************************************************************************** //========================================================================== // // ShootHitPlane // //========================================================================== final bool ShootHitPlane(sec_plane_t* plane, TVec linestart, TVec lineend, float range, class PuffType) { float org_dist; float hit_dist; if (plane->flags & SPF_NOBLOCKSHOOT) { // Doesn't block shooting return true; } org_dist = DotProduct(linestart, plane->normal) - plane->dist; if (org_dist < 0.0) { // Don't shoot back side return true; } hit_dist = DotProduct(lineend, plane->normal) - plane->dist; if (hit_dist >= 0.0) { // Didn't hit plane return true; } // Hit plane if (plane->pic == Level.Game.skyflatnum) { // don't shoot the sky! return false; } // If we are shooting floor or ceiling we are adjusting position // to spawn puff on floor or ceiling, not on wall lineend -= (lineend - linestart) * hit_dist / (hit_dist - org_dist); // position a bit closer lineend += 4.0 * plane->normal; // Spawn bullet puffs. SpawnPuff(lineend, range, PuffType, false); // don't go any farther return false; } //========================================================================== // // ShootCheckPlanes // //========================================================================== final bool ShootCheckPlanes(sector_t* sec, TVec linestart, TVec lineend, float range, class PuffType) { sec_region_t *reg; sec_region_t *startreg; startreg = PointInRegion(sec, linestart); for (reg = startreg; reg; reg = reg->next) { if (!ShootHitPlane(reg->floor, linestart, lineend, range, PuffType)) { // Hit floor return false; } if (!ShootHitPlane(reg->ceiling, linestart, lineend, range, PuffType)) { // Hit ceiling return false; } } for (reg = startreg->prev; reg; reg = reg->prev) { if (!ShootHitPlane(reg->floor, linestart, lineend, range, PuffType)) { // Hit floor return false; } if (!ShootHitPlane(reg->ceiling, linestart, lineend, range, PuffType)) { // Hit ceiling return false; } } return true; } //========================================================================== // // LineAttack // // If damage == 0, it is just a test trace that will leave linetarget set. // //========================================================================== final int LineAttack(TVec Dir, float Distance, int LADamage, class PuffType, optional bool NoAttackGhosts, optional TVec* OutHitPoint, optional name DmgType) { TVec Dst; intercept_t* in; TVec LineStart; TVec LineEnd; TVec ShootOrigin; ShootOrigin = Origin; ShootOrigin.z += Height * 0.5 + 8.0 - FloorClip; Dst = ShootOrigin + Distance * Dir; LineStart = ShootOrigin; foreach PathTraverse(in, Origin.x, Origin.y, Dst.x, Dst.y, PT_ADDLINES | PT_ADDTHINGS) { TVec hit_point; line_t* li; EntityEx th; if (in->bIsALine) { sector_t *sec; li = in->line; hit_point = ShootOrigin + (Distance * in->frac) * Dir; if (li->flags & ML_TWOSIDED && PointOnPlaneSide(ShootOrigin, li)) { sec = li->backsector; } else { sec = li->frontsector; } LineEnd = hit_point; // Check for shooting floor or ceiling if (!ShootCheckPlanes(sec, LineStart, LineEnd, Distance, PuffType)) { return false; } LineStart = LineEnd; // Execute line special after checking for hitting floor or ceiling // when we know that it actally hits line if (li->special) { LineSpecialLevelInfo(Level).ActivateLine(li, self, 0, SPAC_IMPACT); } if (li->flags & ML_TWOSIDED) { // crosses a two sided line opening_t *open; float opentop = 0.0; open = LineOpenings(li, hit_point); if (open) { opentop = open->top; } while (open) { if (open->bottom <= hit_point.z && open->top >= hit_point.z) { // shot continues break; } open = open->next; } if (open) { continue; } if (li->frontsector->ceiling.pic == Level.Game.skyflatnum && li->backsector->ceiling.pic == Level.Game.skyflatnum && hit_point.z > opentop) { // it's a sky hack wall return false; } } // Hit line // position a bit closer hit_point -= 4.0 * Dir; // Spawn bullet puffs. SpawnPuff(hit_point, Distance, PuffType, false); // don't go any farther return false; } // shoot a thing th = EntityEx(in->Thing); if (th == self) continue; // can't shoot self if (!th.bShootable) continue; // corpse or something // check angles to see if the thing can be aimed at hit_point = ShootOrigin + (Distance * in->frac) * Dir; if (th.Origin.z + th.Height < hit_point.z) continue; // shot over the thing if (th.Origin.z > hit_point.z) continue; // shot under the thing // hit thing // position a bit closer hit_point -= 10.0 * Dir; // check for physical attacks on a ghost if (th.Alpha < 1.0 && NoAttackGhosts) { continue; } if (OutHitPoint) { *OutHitPoint = hit_point; } // Spawn bullet puffs or blod spots, depending on target type. if (PuffType.default.bPuffOnActors || th.bNoBlood || th.bInvulnerable || th.bDormant) { SpawnPuff(hit_point, Distance, PuffType, true); } if (!LineSpecialGameInfo(Level.Game).bBloodSplatter && !th.bNoBlood && !th.bInvulnerable && !th.bDormant) { SpawnBlood(hit_point, LADamage); } if (LADamage && LineSpecialGameInfo(Level.Game).bBloodSplatter) { if (!th.bNoBlood && !th.bInvulnerable && !th.bDormant) { if (PuffType.default.bAxeBlood) { th.SpawnBloodSplatter2(hit_point); } if (P_Random() < 192) { th.SpawnBloodSplatter(hit_point, LADamage); } } } if (LADamage) { th.Damage(self, self, LADamage, DmgType); } // don't go any farther return false; } LineEnd = Dst; return ShootCheckPlanes(XLevel.PointInSector(Dst), LineStart, LineEnd, Distance, PuffType); } //========================================================================== // // TeleportMove // //========================================================================== final bool TeleportMove(TVec org) { EntityEx Other; float blockdist; // kill anything occupying the position // stomp on any things contacted foreach RadiusThings(Other, org, Radius) { if (!Other.bShootable) { continue; } blockdist = Other.Radius + Radius; if (fabs(Other.Origin.x - org.x) >= blockdist || fabs(Other.Origin.y - org.y) >= blockdist) { // didn't hit it continue; } // don't clip against self if (Other == self) { continue; } // Check if allowed to stomp things if (!bTelestomp && !Level.bAllowMonsterTelefrags) { return false; } Other.Damage(self, self, 10000, 'Telefrag'); } // the move is ok, // so link the thing into its new position UnlinkFromWorld(); Origin = org; LinkToWorld(); if (org.z == ONFLOORZ) { Origin.z = FloorZ; } return true; } //========================================================================== // // Teleport // //========================================================================== final bool Teleport(TVec Dst, float angle, bool DstFog, bool SrcFog, bool KeepDir) { TVec oldOrg; float aboveFloor; float fogDelta; EntityEx fog; oldOrg = Origin; aboveFloor = Origin.z - FloorZ; if (!TeleportMove(Dst)) { return false; } if (bIsPlayer) { if (FindInventory(PowerFlight) && aboveFloor) { Origin.z = Origin.z + aboveFloor; if (Origin.z + Height > CeilingZ) { Origin.z = CeilingZ - Height; } Player.ViewOrg.z = Origin.z + PlayerEx(Player).ViewHeight; } else { Player.ViewOrg.z = Origin.z + PlayerEx(Player).ViewHeight; if (!KeepDir) { Angles.pitch = 0.0; } } } else if (bMissile) { Origin.z = Origin.z + aboveFloor; if (Origin.z + Height > CeilingZ) { Origin.z = CeilingZ - Height; } } // spawn teleport fog at source and destination fogDelta = bMissile ? 0.0 : LineSpecialGameInfo(Level.Game).TeleFogHeight; if (SrcFog) { fog = Spawn(LineSpecialGameInfo(Level.Game).TeleportFogClass, oldOrg + vector(0.0, 0.0, fogDelta)); fog.PlaySound('misc/teleport', CHAN_VOICE); } if (DstFog) { fog = Spawn(LineSpecialGameInfo(Level.Game).TeleportFogClass, Origin + vector(20.0 * cos(angle), 20.0 * sin(angle), fogDelta)); fog.PlaySound('misc/teleport', CHAN_VOICE); } if (!KeepDir) { if (bIsPlayer && !FindInventory(PowerWeaponLevel2) && !FindInventory(PowerSpeed)) { // Don't move for a bit, freeze player for about .5 sec ReactionTime = 0.5; } Angles.yaw = angle; if (bIsPlayer) { Player.bFixAngle = true; } } HandleFloorclip(); if (bMissile) { Velocity.x = Speed * cos(angle); Velocity.y = Speed * sin(angle); } else if (!KeepDir) // no fog doesn't alter the player's momentums { Velocity = vector(0.0, 0.0, 0.0); } return true; } //=========================================================================== // // MoveThing // //=========================================================================== final bool MoveThing(TVec Pos, bool Fog) { EntityEx fogAct; TVec OldOrg = Origin; UnlinkFromWorld(); Origin = Pos; LinkToWorld(); if (TestLocation()) { if (Fog) { fogAct = Spawn(LineSpecialGameInfo(Level.Game).TeleportFogClass, Pos + vector(0.0, 0.0, LineSpecialGameInfo(Level.Game).TeleFogHeight)); fogAct.PlaySound('misc/teleport', CHAN_VOICE); fogAct = Spawn(LineSpecialGameInfo(Level.Game).TeleportFogClass, OldOrg + vector(0.0, 0.0, LineSpecialGameInfo(Level.Game).TeleFogHeight)); fogAct.PlaySound('misc/teleport', CHAN_VOICE); } return true; } else { UnlinkFromWorld(); Origin = OldOrg; LinkToWorld(); return false; } } //========================================================================== // // TestLocation // // Returns true if the mobj is not blocked by anything at its current // location, otherwise returns false. // //========================================================================== final bool TestLocation() { if (!bColideWithThings && !bColideWithWorld) return true; if (CheckPosition(Origin)) { // XY is ok, now check Z if ((Origin.z < FloorZ) || (Origin.z + Height > CeilingZ)) { // Bad Z return false; } return true; } return false; } //========================================================================== // // Thrust // //========================================================================== final void Thrust(float angle, float move) { Velocity.x += move * cos(angle) * 35.0; Velocity.y += move * sin(angle) * 35.0; } //========================================================================== // // FaceActor // // Returns 1 if 'source' needs to turn clockwise, or 0 if 'source' needs // to turn counter clockwise. 'delta' is set to the amount 'source' // needs to turn. // //========================================================================== int FaceActor(EntityEx target, out float delta) { float diff; float angle1; float angle2; angle1 = Angles.yaw; angle2 = atan2(target.Origin.y - Origin.y, target.Origin.x - Origin.x); if (angle2 > angle1) { diff = AngleMod360(angle2 - angle1); if (diff > 180.0) { delta = 360.0 - diff; return 0; } else { delta = diff; return 1; } } else { diff = AngleMod360(angle1 - angle2); if (diff > 180.0) { delta = 360.0 - diff; return 1; } else { delta = diff; return 0; } } } //========================================================================== // // HeightClip // // Takes a valid thing and adjusts the thing->FloorZ, thing->CeilingZ, and // possibly thing->z. This is called for all nearby monsters whenever a // sector changes height. If the thing doesn't fit, the z will be set to the // lowest value and false will be returned. // //========================================================================== final bool HeightClip() { bool onfloor; onfloor = (Origin.z == FloorZ); tmtrace_t tmtrace; CheckRelPosition(&tmtrace, Origin); // what about stranding a monster partially off an edge? Floor = tmtrace.Floor; Ceiling = tmtrace.Ceiling; FloorZ = tmtrace.FloorZ; CeilingZ = tmtrace.CeilingZ; if (onfloor) { // walking monsters rise and fall with the floor if ((Origin.z - FloorZ < 9.0) || bNoGravity || !XLevel.bExtended) { Origin.z = FloorZ; } } else { // don't adjust a floating monster unless forced to if (Origin.z + Height > CeilingZ) Origin.z = CeilingZ - Height; } if (CeilingZ - FloorZ < Height) return false; return true; } //========================================================================== // // CheckMeleeRange // //========================================================================== bool CheckMeleeRange() { float dist; if (!Target) { return false; } dist = DistTo(Target); if (dist >= MeleeRange + Target.Radius) { return false; } // Don't melee things too far above or below actor. if (Target.Origin.z > Origin.z + Height) { return false; } if (Target.Origin.z + Target.Height < Origin.z) { return false; } if (!CanSee(Target)) { return false; } return true; } //========================================================================== // // CheckMeleeRange2 // //========================================================================== final bool CheckMeleeRange2() { float dist; if (!Target) { return false; } dist = DistTo(Target); if (dist >= MELEERANGE * 2.0 || dist < MELEERANGE) { return false; } if (Target.Origin.z > Origin.z + Height) { // Enemy is higher than the attacker return false; } if (Origin.z > Target.Origin.z + Target.Height) { // Attacker is higher return false; } if (!CanSee(Target)) { return false; } return true; } //========================================================================== // // CheckMissileRange // //========================================================================== final bool CheckMissileRange() { float dist; if (!CanSee(Target)) { return false; } if (bJustHit) { // The target just hit the enemy, so fight back! bJustHit = false; return true; } if (ReactionCount) { // Don't attack yet return false; } dist = DistTo(Target) - 64.0; if (!MeleeState) { // No melee attack, so fire more frequently dist -= 128.0; } if (MissileMaxRange && dist > MissileMaxRange) { return false; // too far away } if (MissileMinRange && dist < MissileMinRange) { return false; // close for fist attack } if (bTriggerHappy) { // Attack from far away dist /= 2.0; } if (dist > MissileChance) { dist = MissileChance; } if (Random() * 256.0 < dist) { return false; } return true; } //========================================================================== // // LookForMonsters // //========================================================================== final bool LookForMonsters() { int count; EntityEx mo; if (!Level.Game.Players[0].MO.CanSee(self)) { // Player can't see monster return false; } count = 0; foreach AllThinkers(EntityEx, mo) { if (!mo.bCountKill || (mo == self) || (mo.Health <= 0)) { // Not a valid monster continue; } if (DistTo(mo) > MONS_LOOK_RANGE) { // Out of range continue; } if (P_Random() < 16) { // Skip continue; } if (count++ > MONS_LOOK_LIMIT) { // Stop searching return false; } if (!CanSee(mo)) { // Out of sight continue; } if (IsNotAttackingMaster(mo)) { continue; } // Found a target monster Target = mo; return true; } return false; } //========================================================================== // // LookForMonsters2 // //========================================================================== final bool LookForMonsters2(bool allaround) { EntityEx mo; float an; float dist; foreach AllThinkers(EntityEx, mo) { if (mo.Health <= 0) continue; // dead if (mo == self) continue; // self if (!mo.bCountKill) continue; // not a monster if (bFriendly == mo.bFriendly) continue; // fiendly. if (!CanSee(mo)) continue; // out of sight if (P_Random() > 128) continue; // sometimes skip if (!allaround) { an = AngleMod360(atan2(mo.Origin.y - Origin.y, mo.Origin.x - Origin.x) - Angles.yaw); if (an > 90.0 && an < 270.0) { dist = DistTo(mo); // if real close, react anyway if (dist > MELEERANGE) continue; // behind back } } Target = mo; return true; } return false; } //========================================================================== // // LookForPlayers // // If allaround is false, only look 180 degrees in front. // Returns true if a player is targeted. // //========================================================================== final bool LookForPlayers(bool allaround) { int c; int stop; BasePlayer P; float an; float dist; if (bFriendly) { return LookForMonsters2(allaround); } if (!Level.Game.netgame && Level.Game.Players[0] && Level.Game.Players[0].bSpawned && Level.Game.Players[0].Health <= 0) { // Single player game and player is dead, look for monsters return LookForMonsters(); } c = 0; stop = (LastLook - 1) & (MAXPLAYERS - 1); for (;; LastLook = (LastLook + 1) & (MAXPLAYERS - 1)) { if (LastLook == stop) { // done looking return false; } P = Level.Game.Players[LastLook]; if (!P) continue; if (c++ == 2) { // done looking return false; } if (!P.bSpawned || !P.MO) continue; // not spawned yet if (P.Health <= 0) continue; // dead if (!CanSee(P.MO)) continue; // out of sight if (!allaround) { an = AngleMod360(atan2(P.MO.Origin.y - Origin.y, P.MO.Origin.x - Origin.x) - Angles.yaw); if (an > 90.0 && an < 270.0) { dist = DistTo(P.MO); // if real close, react anyway if (dist > MELEERANGE) continue; // behind back } } if (P.MO.Alpha < 1.0) { // Player is invisible if (DistTo(P.MO) > 2.0 * MELEERANGE && Length(P.MO.Velocity) < 5.0 * 35.0) { // Player is sneaking - can't detect return false; } if (P_Random() < 225) { // Player isn't sneaking, but still didn't detect return false; } } if (IsMaster(EntityEx(P.MO))) { continue; // Don't target master } Target = EntityEx(P.MO); return true; } return false; } //========================================================================== // // StepMove // // Move in the current direction, returns false if the move is blocked. // //========================================================================== final bool StepMove() { float tryx, deltax, origx; float tryy, deltay, origy; float maxmove; int steps; float xspeed; float yspeed; int i; bool try_ok; line_t *ld; int good; if (bBlasted) return true; if (MoveDir == DI_NODIR) return false; // Instead of yanking non-floating monsters to the ground, // let gravity drop them down, unless they're moving down a step. if (!bNoGravity && Origin.z > FloorZ && !bOnMobj) { if (Origin.z > FloorZ + MaxStepHeight) { return false; } else { Origin.z = FloorZ; } } #ifdef RANGECHECK if (MoveDir >= 8 || MoveDir < 0) Error("Weird MoveDir!"); #endif origx = Origin.x; origy = Origin.y; deltax = StepSpeed * LineSpecialGameInfo(Level.Game).xspeed[MoveDir]; deltay = StepSpeed * LineSpecialGameInfo(Level.Game).yspeed[MoveDir]; tryx = origx + deltax; tryy = origy + deltay; // Like P_XYMovement this should do multiple moves if the step size is too large maxmove = Radius; steps = 1; if (maxmove > 0.0) { xspeed = fabs(deltax); yspeed = fabs(deltay); if (xspeed > yspeed) { if (xspeed > maxmove) { steps = 1 + ftoi(xspeed / maxmove); } } else { if (yspeed > maxmove) { steps = 1 + ftoi(yspeed / maxmove); } } } try_ok = true; tmtrace_t tmtrace; for (i = 1; i < steps; i++) { try_ok = TryMoveEx(&tmtrace, vector(origx + (deltax / itof(steps * i)), origy + (deltay / itof(steps * i)), Origin.z)); if (!try_ok) break; } // killough 3/15/98: don't jump over dropoffs: if (try_ok) try_ok = TryMoveEx(&tmtrace, vector(tryx, tryy, Origin.z)); if (!try_ok) { // open any specials if (bFloat && tmtrace.bFloatOk) { // must adjust height if (Origin.z < tmtrace.FloorZ) Origin.z += FloatSpeed * Level.Game.frametime; else Origin.z -= FloatSpeed * Level.Game.frametime; // Check to make sure there's nothing in the way of the float if (TestMobjZ()) { bInFloat = true; return true; } } if (!tmtrace.NumSpecHit) return false; MoveDir = DI_NODIR; // if the special is not a door that can be opened, return false // // killough 8/9/98: this is what caused monsters to get stuck in // doortracks, because it thought that the monster freed itself // by opening a door, even if it was moving towards the doortrack, // and not the door itself. // // killough 9/9/98: If a line blocking the monster is activated, // return true 90% of the time. If a line blocking the monster is // not activated, but some other line is, return false 90% of the // time. A bit of randomness is needed to ensure it's free from // lockups, but for most cases, it returns the correct result. // // Do NOT simply return false 1/4th of the time (causes monsters to // back out when they shouldn't, and creates secondary stickiness). good = 0; while (tmtrace.NumSpecHit--) { ld = tmtrace.SpecHit[tmtrace.NumSpecHit]; // if the special is not a door // that can be opened, // return false if (LineSpecialLevelInfo(Level).ActivateLine(ld, self, 0, SPAC_USE) || (bActivatePushWall && LineSpecialLevelInfo(Level).ActivateLine(ld, self, 0, SPAC_PUSH))) { good |= ld == tmtrace.BlockingLine ? 1 : 2;//true; } } return good && ((Random() >= 203.0) ^ (good & 1)); } else { bInFloat = false; } if (!bFloat && !bNoGravity) { if (Origin.z > FloorZ) { HitFloorType(); } Origin.z = FloorZ; } return true; } //========================================================================== // // TryWalk // // Attempts to move actor in its current (ob->moveangle) direction. // If blocked by either a wall or an actor returns FALSE. // If move is either clear of block only by a door, returns TRUE and sets. // If a door is in the way, an OpenDoor call is made to start it opening. // //========================================================================== final bool TryWalk() { if (!StepMove()) { return false; } MoveCount = P_Random() & 15; return true; } //========================================================================== // // DO_NewChaseDir // //========================================================================== final void DO_NewChaseDir(float deltax, float deltay) { int d[3]; int tdir; int olddir; int turnaround; olddir = MoveDir; turnaround = LineSpecialGameInfo(Level.Game).opposite[olddir]; if (deltax > 10.0) d[1] = DI_EAST; else if (deltax < -10.0) d[1] = DI_WEST; else d[1] = DI_NODIR; if (deltay < -10.0) d[2] = DI_SOUTH; else if (deltay > 10.0) d[2] = DI_NORTH; else d[2] = DI_NODIR; // try direct route if (d[1] != DI_NODIR && d[2] != DI_NODIR) { MoveDir = LineSpecialGameInfo(Level.Game).diags[((deltay < 0.0) << 1) + (deltax > 0.0)]; if (MoveDir != turnaround && TryWalk()) return; } // try other directions if (P_Random() > 200 || fabs(deltay) > fabs(deltax)) { tdir = d[1]; d[1] = d[2]; d[2] = tdir; } if (d[1] == turnaround) d[1] = DI_NODIR; if (d[2] == turnaround) d[2] = DI_NODIR; if (d[1] != DI_NODIR) { MoveDir = d[1]; if (TryWalk()) { // either moved forward or attacked return; } } if (d[2] != DI_NODIR) { MoveDir = d[2]; if (TryWalk()) return; } // there is no direct path to the player, // so pick another direction. if (olddir != DI_NODIR) { MoveDir = olddir; if (TryWalk()) return; } // randomly determine direction of search if (P_Random() & 1) { for (tdir = DI_EAST; tdir <= DI_SOUTHEAST; tdir++) { if (tdir != turnaround) { MoveDir = tdir; if (TryWalk()) return; } } } else { for (tdir = DI_SOUTHEAST; tdir != (DI_EAST - 1); tdir--) { if (tdir != turnaround) { MoveDir = tdir; if (TryWalk()) return; } } } if (turnaround != DI_NODIR) { MoveDir = turnaround; if (TryWalk()) return; } MoveDir = DI_NODIR; // can not move } //============================================================================= // // NewChaseDir // // killough 9/8/98: Split into two functions // //============================================================================= final void NewChaseDir() { float deltax; float deltay; if (!Target) Error("NewChaseDir: called with no Target"); deltax = Target.Origin.x - Origin.x; deltay = Target.Origin.y - Origin.y; tmtrace_t tmtrace; CheckRelPosition(&tmtrace, Origin); // Try to move away from a dropoff if (FloorZ - tmtrace.DropOffZ > MaxDropoffHeight && Origin.z <= FloorZ && !bDropOff && !bOnMobj && !bFloat) { avoiddropoff_t a; // We call CheckDropoff here to determine if the // bounding box actually needs to be used below CheckDropOff(&a); if (a.deltax || a.deltay) { // [Graf Zahl] I have changed TryMove to only apply this logic when // being called from here. bAavoidingDropoff activates the code that // allows monsters to move away from a dropoff. This is different from // MBF which requires unconditional use of the altered logic and therefore // forcing a massive change in the monster behavior to use this. // use different dropoff movement logic in TryMove bAvoidingDropoff = true; DO_NewChaseDir(a.deltax, a.deltay); bAvoidingDropoff = false; // If moving away from dropoff, set movecount to 1 so that // small steps are taken to get monster away from dropoff. MoveCount = 1; return; } } DO_NewChaseDir(deltax, deltay); } //========================================================================== // // RandomChaseDir // //========================================================================== final void RandomChaseDir() { int olddir; int turnaround; int tdir; int turndir; olddir = MoveDir; turnaround = LineSpecialGameInfo(Level.Game).opposite[olddir]; if (P_Random() & 1) { for (tdir = DI_WEST; tdir <= DI_NORTHWEST; tdir++) { if (tdir != turnaround) { MoveDir = tdir; if (TryWalk()) return; } } } else { for (tdir = DI_NORTHWEST; tdir >= DI_WEST; tdir--) { if (tdir != turnaround) { MoveDir = tdir; if (TryWalk()) return; } } } // If the actor elects to continue in its current direction, let it do // so unless the way is blocked. Then it must turn. turndir = (P_Random() & 1) ? -1 : 1; if (olddir == DI_NODIR) { olddir = (P_Random() & 7); } for (tdir = (olddir + turndir) & 7; tdir != olddir; tdir = (tdir + turndir) & 7) { if (tdir != turnaround) { MoveDir = tdir; if (TryWalk ()) return; } } if (turnaround != DI_NODIR) { MoveDir = turnaround; if (TryWalk()) { MoveCount = P_Random() & 15; return; } } MoveDir = DI_NODIR; // can not move } //========================================================================== // // CheckMissileSpawn // // Moves the missile forward a bit and possibly explodes it right there. // //========================================================================== final bool CheckMissileSpawn() { if (bRandomise && StateTime > 0.0) { StateTime -= Random() * 0.1; if (StateTime < 1.0 / 35.0) StateTime = 1.0 / 35.0; } // move a little forward so an angle can be computed if it // immediately explodes if (Speed > 100.0 * 35.0) { // Ultra-fast missile Origin += Velocity * 0.0142857143 / 8.0; } else { // Normal missile Origin += Velocity * 0.0142857143; } if (!TryMove(Origin)) { ExplodeMissile(); return false; } return true; } //========================================================================== // // ExplodeMissile // //========================================================================== final bool ExplodeMissile() { if (bExploCount) { Special2++; if (Special2 < Special1) { return true; } } Velocity = vector(0.0, 0.0, 0.0); if (!SetState(FindState('Death'))) { return false; } if (bRandomise) { StateTime -= Random() * 0.1; if (StateTime < 1.0 / 35.0) StateTime = 1.0 / 35.0; } bMissile = false; if (DeathSound) { PlaySound(DeathSound, CHAN_VOICE, 1.0, bFullVolDeath ? ATTN_NONE : ATTN_NORMAL); } if (!bLeaveTrail && bDynamicLight) { SendExplosion(DLightColour, Origin); bDynamicLight = false; } bLeaveTrail = false; return true; } //========================================================================== // // SpawnMissile // // Returns NULL if the missile exploded immediately, otherwise returns // a Actor reference to the missile. // //========================================================================== final EntityEx SpawnMissile(EntityEx dest, class type, optional float SpawnHeight) { EntityEx A; TVec org; TVec dir; org = Origin; if (specified_SpawnHeight) { org.z = Origin.z + SpawnHeight; } else if (type.default.bIgnoreFloorStep) { org.z = ONFLOORZ + FloorClip; } else { org.z = Origin.z + 32.0; } org.z -= FloorClip; A = Spawn(type, org); if (A.SightSound) { A.PlaySound(A.SightSound, CHAN_VOICE); } A.Target = self; // where it came from if (dest) { dir = dest.Origin - Origin; if (dest.Alpha < 1.0) { // Invisible target VectorRotateAroundZ(&dir, (Random() - Random()) * 45.0 / 2.0); } } else { dir = Velocity; } dir = Normalise(dir); A.Velocity = dir * A.Speed; VectorAngles(&dir, &A.Angles); return A.CheckMissileSpawn() ? A : none; } //========================================================================== // // SpawnMissileXYZ // // Returns NULL if the missile exploded immediately, otherwise returns // a Actor reference to the missile. // //========================================================================== final EntityEx SpawnMissileXYZ(TVec org, EntityEx dest, class type) { TVec dir; EntityEx A; org.z -= FloorClip; A = Spawn(type, org); if (A.SightSound) { A.PlaySound(A.SightSound, CHAN_VOICE); } A.Target = self; // Originator dir = dest.Origin - Origin; if (dest.Alpha < 1.0) { // Invisible target VectorRotateAroundZ(&dir, (Random() - Random()) * 45.0); } dir = Normalise(dir); A.Velocity = dir * A.Speed; VectorAngles(&dir, &A.Angles); return A.CheckMissileSpawn() ? A : none; } //========================================================================== // // SpawnMissileZAimed // //========================================================================== final EntityEx SpawnMissileZAimed(float z, EntityEx dest, class type) { EntityEx A; TVec dir; float dist; A = Spawn(type, Origin + vector(0.0, 0.0, z - FloorClip)); if (A.SightSound) A.PlaySound(A.SightSound, CHAN_VOICE); A.Target = self; // where it came from dist = DistTo2(dest); dir.x = cos(Angles.yaw); dir.y = sin(Angles.yaw); dir.z = dist != 0.0 ? (dest.Origin.z - Origin.z) / dist : A.Speed; // fuzzy player if (dest.Alpha < 1.0) { VectorRotateAroundZ(&dir, (Random() - Random()) * 45.0 / 2.0); } dir = Normalise(dir); A.Velocity = dir * A.Speed; VectorAngles(&dir, &A.Angles); return A.CheckMissileSpawn() ? A : none; } //========================================================================== // // SpawnMissileAngles // // Returns NULL if the missile exploded immediately, otherwise returns // a Actor reference to the missile. // //========================================================================== final EntityEx SpawnMissileAngles(class type, float yaw, float pitch, optional float SpawnHeight) { TVec org; EntityEx A; TVec dir; org = Origin; if (specified_SpawnHeight) { org.z = Origin.z + SpawnHeight; } else if (type.default.bIgnoreFloorStep) { org.z = ONFLOORZ + FloorClip; } else { org.z = Origin.z + 32.0; } org.z -= FloorClip; A = Spawn(type, org); if (A.SightSound) { A.PlaySound(A.SightSound, CHAN_VOICE); } A.Target = self; // Originator A.Angles.yaw = yaw; A.Angles.pitch = pitch; AngleVector(&A.Angles, &dir); A.Velocity = A.Speed * dir; return A.CheckMissileSpawn() ? A : none; } //========================================================================== // // SpawnMissileAngle // // Returns NULL if the missile exploded immediately, otherwise returns // a Actor reference to the missile. // //========================================================================== final EntityEx SpawnMissileAngle(class type, float angle, float momz, optional float SpawnHeight) { TVec org; EntityEx mo; org = Origin; if (specified_SpawnHeight) { org.z = Origin.z + SpawnHeight; } else if (type.default.bIgnoreFloorStep) { // Minotaur floor fire missile org.z = ONFLOORZ + FloorClip; } else { org.z = Origin.z + 32.0; } org.z -= FloorClip; mo = Spawn(type, org); if (mo.SightSound) { mo.PlaySound(mo.SightSound, CHAN_VOICE); } mo.Target = self; // Originator mo.Angles.yaw = angle; mo.Velocity.x = mo.Speed * cos(angle); mo.Velocity.y = mo.Speed * sin(angle); mo.Velocity.z = momz; return mo.CheckMissileSpawn() ? mo : none; } //========================================================================== // // SpawnMissileAngleSpeed // // Returns NULL if the missile exploded immediately, otherwise returns // a Actor reference to the missile. // //========================================================================== final EntityEx SpawnMissileAngleSpeed(class type, float angle, float momz, float speed) { TVec org; EntityEx mo; org = Origin; org.z -= FloorClip; mo = Spawn(type, org); mo.Tracer = self; // Originator mo.Angles.yaw = angle; mo.Velocity.x = speed * cos(angle); mo.Velocity.y = speed * sin(angle); mo.Velocity.z = momz; return mo.CheckMissileSpawn() ? mo : none; } //========================================================================== // // SpawnSubMissile // //========================================================================== final EntityEx SpawnSubMissile(class type, EntityEx AInstigator) { EntityEx other; TVec dir; other = Spawn(type, Origin); if (!other) { return none; } other.Target = AInstigator; other.Angles = Angles; other.Velocity.x = other.Speed * cos(Angles.yaw); other.Velocity.y = other.Speed * sin(Angles.yaw); AimLineAttack(dir, Angles, 1024.0); other.Velocity.z = other.Speed * dir.z; other.CheckMissileSpawn(); return other; } //========================================================================== // // SpawnPlayerMissile // //========================================================================== final EntityEx SpawnPlayerMissile(class type) { EntityEx AimTarget; return SpawnPlayerMissileEx(type, false, AimTarget); } //========================================================================== // // SpawnPlayerMissileAngle // //========================================================================== final EntityEx SpawnPlayerMissileAngle(class type, float angle) { EntityEx AimTarget; return SpawnPlayerMissileEx(type, false, AimTarget, angle); } //=========================================================================== // // SpawnPlayerMissileAngleXYZ // //=========================================================================== final EntityEx SpawnPlayerMissileAngleXYZ(TVec org, class type, float angle) { EntityEx AimTarget; return SpawnPlayerMissileEx(type, false, AimTarget, angle, org); } //========================================================================== // // SpawnPlayerMissileEx // // Tries to aim at a nearby monster // //========================================================================== final EntityEx SpawnPlayerMissileEx(class type, bool RetExploded, out EntityEx AimTarget, optional float angle, optional TVec SpawnOrg) { EntityEx A; TVec dir; TVec org; // Try to find a target if (specified_angle) { AimTarget = Aim(dir, 16.0 * 64.0, angle); } else { AimTarget = Aim(dir, 16.0 * 64.0); } org = specified_SpawnOrg ? SpawnOrg : Origin; if (type.default.bIgnoreFloorStep) { org.z = ONFLOORZ; dir.z = 0.0; dir = Normalise(dir); } else if (type.default.bIgnoreCeilingStep) { org.z = ONCEILINGZ; dir.z = 0.0; dir = Normalise(dir); } else { org.z += 32.0 - tan(Angles.pitch); org.z -= FloorClip; } A = Spawn(type, org); if (A.SightSound) { A.PlaySound(A.SightSound, CHAN_VOICE); } A.Target = self; A.Velocity = dir * A.Speed; VectorAngles(&dir, &A.Angles); return A.CheckMissileSpawn() || RetExploded ? A : none; } //========================================================================== // // Damage // // Damages both enemies and players. // "inflictor" is the thing that caused the damage, creature or missile, // can be NULL (slime, etc). // "source" is the thing to target after taking damage, creature or NULL. // Source and inflictor are the same for melee attacks. Source can be NULL // for slime, barrel explosions and other environmental stuff. // //========================================================================== final void Damage(EntityEx inflictor, EntityEx source, int damage, optional name DmgType) { if (!bShootable) { // shouldn't happen... return; } if (Health <= 0) { if (inflictor && DmgType == 'Ice') { return; } else if (bIceCorpse) // frozen { StateTime = 0.1; Velocity.x = 0.0; Velocity.y = 0.0; } return; } if (bInvulnerable && damage < 10000) { // Actor is invulnerable if (bIsPlayer) { if (LineSpecialGameInfo(Level.Game).bNoDamageThrustInvulnerable) { // For player, no exceptions return; } } else if (!inflictor || !inflictor.bDamageInvulnerable) { return; } } // Spectral targets only take damage from spectral projectiles. if (bSpectral) { if (!inflictor || !inflictor.bSpectral) { if (MissileState) { SetState(MissileState); } return; } } if (bSkullFly) { if (bSkullFlyInvulnerable) { // Invulnerable during charge attack return; } Velocity = vector(0.0, 0.0, 0.0); SetState(SeeState); } if (bDormant) { // Invulnerable, and won't wake up return; } if (bIsPlayer && Level.Game.gameskill == sk_baby) { // Take half damage in trainer mode damage >>= 1; } // Special damage types if (inflictor) { damage = inflictor.DoSpecialDamage(self, source, damage); if (damage == -1) { return; } } damage = TakeSpecialDamage(inflictor, source, damage); if (damage == -1) { return; } // Push the target unless using a weapon that should not inflict // thrust if (inflictor && inflictor != self && bColideWithThings && !inflictor.bNoDamageThrust) { float kickback; if (!source || !source.bIsPlayer) { kickback = LineSpecialGameInfo(Level.Game).DefaultKickBack; } else { kickback = PlayerEx(source.Player).ReadyWeapon.Kickback; } if (kickback) { TVec dir; float thrust; thrust = kickback / 8.0 * itof(damage) / Mass; if (thrust < 0.0 || thrust > 10.0) { thrust = 10.0; } dir = GetCentre() - inflictor.GetCentre(); if (Length(dir) < 0.001) { // Zero length. In this case Doom would use angle 0 dir = vector(1.0, 0.0, 0.0); } if (source && source.bIsPlayer && (source == inflictor) && PlayerEx(source.Player).ReadyWeapon.bStaff2Kickback) { // Staff power level 2 dir.z = 0.0; Velocity += 35.0 * 10.0 * Normalise(dir); Velocity.z += 35.0 * 5.0; } else { Velocity += 35.0 * thrust * Normalise(dir); } } } // player specific if (bIsPlayer) { if (PlayerEx(Player).CheckFriendlyFire(source, damage)) { return; } // End of game hell hack. if ((Sector->special & SECSPEC_BASE_MASK) == SECSPEC_DamageSuperHellslimeExit && damage >= Health) { damage = Health - 1; } // Below certain threshold, ignore damage in GOD mode, or with INVUL power. if (damage < 1000 && (bInvulnerable || (PlayerEx(Player).Cheats & PlayerEx::CF_GODMODE))) { return; } if (Inventory) { int NewDmg = damage; Inventory.AbsorbDamage(damage, DmgType, NewDmg); damage = NewDmg; if (damage <= 0) { return; } } damage = PlayerEx(Player).ArmorAbsorbDamage(inflictor, source, damage, DmgType); if (damage == -1) { return; } Player.Health -= damage; // mirror mobj health here for Dave if (Player.Health < 0) { Player.Health = 0; } PlayerEx(Player).Attacker = source; PlayerEx(Player).DamageFlash += itof(damage) / 35.0; // add damage after armor / invuln if (PlayerEx(Player).DamageFlash > 3.0) { PlayerEx(Player).DamageFlash = 3.0; // teleport stomp does 10k points... } } // do the damage Health -= damage; if (Health <= 0) { // Death Special1 = damage; // check for special fire damage or ice damage deaths if (DmgType == 'Fire') { if (bIsPlayer && !PlayerIsMorphed()) { // Check for flame death if (!inflictor || !inflictor.bConditionalFireDamage || (Health > -50 && damage > 25)) { DamageType = 'Fire'; } } else { DamageType = 'Fire'; } } else { DamageType = DmgType; } if (source && source.IsServant()) { // Minotaur's kills go to his master EntityEx master = source.Tracer; // Make sure still alive and not a pointer to fighter head if (master && master.bIsPlayer && (master.Player.MO == master)) { source = master; } } Died(source, inflictor); return; } if ((Random() < PainChance) && !bSkullFly) { if (inflictor && inflictor.bLightning) { if (P_Random() < 96) { bJustHit = true; // fight back! SetState(FindState('Pain')); } else { // "electrocute" the target bFullBright = true; if (HowlSound && bCountKill && P_Random() < 128 && !GetSoundPlayingInfo(self, GetSoundID(HowlSound))) { PlaySound(HowlSound, CHAN_VOICE); } } } else { bJustHit = true; // fight back! SetState(FindState('Pain')); if (inflictor && inflictor.bHowlVictims) { if (HowlSound && bCountKill && P_Random() < 128 && !GetSoundPlayingInfo(self, GetSoundID(HowlSound))) { PlaySound(HowlSound, CHAN_VOICE); } } } } ReactionCount = 0; // we're awake now... if (source) { if (source == Target) { Threshold = BASETHRESHOLD; if (State == IdleState && SeeState) { SetState(SeeState); } } else if (OkayToSwitchTarget(source)) { // Target actor is not intent on another actor, // so make him chase after source Target = source; Threshold = BASETHRESHOLD; if (State == IdleState && SeeState) { SetState(SeeState); } } } } //========================================================================== // // DoSpecialDamage // //========================================================================== int DoSpecialDamage(EntityEx victim, EntityEx source, int damage) { return damage; } //========================================================================== // // TakeSpecialDamage // //========================================================================== int TakeSpecialDamage(EntityEx inflictor, EntityEx source, int damage) { return damage; } //========================================================================== // // OkayToSwitchTarget // //========================================================================== bool OkayToSwitchTarget(EntityEx source) { return (!Threshold || bNoGrudge) && !source.bNeverTarget && !bNoTargetSwitch; } //========================================================================== // // Died // //========================================================================== void Died(EntityEx source, EntityEx inflictor) { Inventory Item; // Notify actor's items. for (Item = Inventory; Item;) { Inventory Next = Item.Inventory; Item.OwnerDied(); Item = Next; } if (source) { // Set Target to the thing that killed it. It's needed for Strife's // special dropped items. Target = source; } if (inflictor && inflictor.bNoExtremeDeath) { // Prevent gibing animation. Health = -1; } if (source && (source.bIsPlayer) && PlayerEx(source.Player).IsWeaponAlwaysExtremeDeath()) { // Always extreme death. Health = -5000; } if (bIsPlayer && Level.bDeathSlideShow) { // Start sad finale. PlayerEx(Player).StartDeathSlideShow(); } bShootable = false; bFloat = false; bSkullFly = false; if (!bNoGravKill) { bNoGravity = false; } bDropOff = true; bCorpse = true; bNoPassMobj = true; float NewHeight = 0.0; if (DamageType == 'Fire') { NewHeight = BurnHeight; } if (!NewHeight) { NewHeight = DeathHeight; } if (NewHeight < 0.0) { Height = 0.0; } else if (NewHeight) { Height = NewHeight; } else { Height /= 4.0; } if (Special && (!bSpecial || bCountKill)) { // Initiate monster death actions Level.ExecuteActionSpecial(Special, Args[0], Args[1], Args[2], Args[3], Args[4], NULL, 0, self); } if (bCountKill) { Level.CurrentKills++; } if (source && source.bIsPlayer) { if (bCountKill) { // count for intermission source.Player.KillCount++; } KilledByPlayer(source); } else if (!Level.Game.netgame && bCountKill) { // Count all monster deaths, // even those caused by other monsters Level.Game.Players[0].KillCount++; } if (bIsPlayer) { PlayerKilled(source, inflictor); bSolid = false; bFly = false; Player.PlayerState = PST_DEAD; PlayerEx(Player).DropWeapon(); #ifdef FIXME if (Player == Level.Game.Players[consoleplayer] && automapactive) { // Don't die in auto map, switch view prior to dying AM_Stop(); } #endif } if (DamageType == 'Fire' && FindState('Burn')) { // Flame death SetState(FindState('Burn')); } else if (DamageType && FindState(DamageType)) { // Specialised death state for this damage type (ice, etc). SetState(FindState(DamageType)); } else if (Health < GibsHealth && FindState('XDeath')) { // Extreme death SetState(FindState('XDeath')); } else { // Normal death SetState(FindState('Death')); } StateTime -= Random() * 0.1; if (StateTime < 1.0 / 35.0) { StateTime = 1.0 / 35.0; } } //========================================================================== // // GetStateTime // //========================================================================== final float GetStateTime(state AState, float AStateTime) { if (Level.Game.fastparm) { if (bFaster && StateIsInRange(AState, SeeState, none, 8)) { AStateTime /= 2.0; } if (bFastMelee && StateIsInRange(AState, MeleeState, none, 3)) { AStateTime /= 2.0; } } return AStateTime; } //========================================================================== // // NightmareRespawn // //========================================================================== final void NightmareRespawn() { float x; float y; float z; EntityEx A; x = SpawnPoint.x; y = SpawnPoint.y; // somthing is occupying it's position? if (!CheckPosition(vector(x, y, ONFLOORZ))) return; // no respwan // spawn a teleport fog at old spot A = Spawn(LineSpecialGameInfo(Level.Game).TeleportFogClass, vector(Origin.x, Origin.y, ONFLOORZ)); A.Origin.z += LineSpecialGameInfo(Level.Game).TeleFogHeight; A.PlaySound('misc/teleport', CHAN_BODY); // spawn a teleport fog at the new spot A = Spawn(LineSpecialGameInfo(Level.Game).TeleportFogClass, vector(x, y, ONFLOORZ)); A.Origin.z += LineSpecialGameInfo(Level.Game).TeleFogHeight; A.PlaySound('misc/teleport', CHAN_BODY); // spawn the new monster // spawn it if (bSpawnCeiling) z = ONCEILINGZ; else z = ONFLOORZ; // inherit attributes from deceased one A = Spawn(class(Class), vector(x, y, z)); Level.CopyMThing(&SpawnPoint, &A.SpawnPoint); A.Angles.yaw = itof(45 * (SpawnPoint.angle / 45)); if (SpawnPoint.options & LineSpecialLevelInfo::MTF_AMBUSH) A.bAmbush = true; if (SpawnPoint.options & LineSpecialLevelInfo::MTF_STANDSTILL) A.bStanding = true; A.ReactionCount = 18; // remove the old monster Destroy(); } //========================================================================== // // Touch // //========================================================================== bool Touch(Entity InOther) { bool solid; int damage; EntityEx Other; Other = EntityEx(InOther); // For Korax Arena if (Other.IsTouched(self)) return !Other.bSolid && !Other.bSpecial && !Other.bShootable; if (!Other.bSolid && !Other.bSpecial && !Other.bShootable) return true; // check for skulls slamming into things if (bSkullFly) { return Slam(Other); } // Check for blasted thing running into another if (bBlasted && Other.bShootable) { if (!Other.bBoss && Other.bCountKill) { Other.Velocity.x += Velocity.x; Other.Velocity.y += Velocity.y; if ((Other.Velocity.x + Other.Velocity.y) > 3.0 * 35.0) { damage = (ftoi(Mass) / 100) + 1; Other.Damage(self, self, damage); damage = (ftoi(Other.Mass) / 100) + 1; Damage(Other, Other, damage >> 2); } return false; } } // missiles can hit other things if (bMissile) { // Check for a non-shootable mobj if (Other.bNonShootable) { return true; } // Check for passing through a ghost if (Other.Alpha < 1.0 && bThruGhost) { return true; } if ((BounceType == BOUNCE_Doom || BounceType == BOUNCE_Hexen) && MissileDamage == 0) { return Target == Other || !Other.bSolid; } switch (SpecialMissileHit(Other)) { case 0: return false; case 1: return true; } if (Target && Target.GetSpecies() == Other.GetSpecies()) { // Don't hit same species as originator. if (Other == Target) { // Don't missile self return true; } if (!Other.bIsPlayer) { // Explode, but do no damage. // Let players missile other players. return false; } } if (!Other.bShootable) { // didn't do any damage return !Other.bSolid; } // Don't hit spectres with non-sigil weapons. if (Other.bSpectral && !bSpectral) { return true; } if (bRip) { if (!Other.bNoBlood && !Other.bReflective && !Other.bInvulnerable) { // Ok to spawn some blood SpawnRipperBlood(); } PlaySound('misc/ripslop', CHAN_BODY); damage = ((P_Random() & 3) + 2) * MissileDamage; Other.Damage(self, Target, damage, DamageType); if (Other.bPushable && !bCannotPush) { // Push thing Other.Velocity.x += Velocity.x / 4.0; Other.Velocity.y += Velocity.y / 4.0; } //WHAT A FUCK IS THIS??????? numspechit = 0; return true; } // damage / explode damage = ((P_Random() % 8) + 1) * MissileDamage; if (damage > 0) { if (LineSpecialGameInfo(Level.Game).bBloodSplatter && !Other.bNoBlood && !Other.bReflective && !Other.bInvulnerable && !Other.bDormant && !bBloodlessImpact && P_Random() < 192) { Other.SpawnBloodSplatter(Origin, damage); } Other.Damage(self, Target, damage, DamageType); } // don't traverse any more return false; } if (Other.bPushable && !bCannotPush) { // Push thing Other.Velocity.x += Velocity.x / 4.0; Other.Velocity.y += Velocity.y / 4.0; } solid = Other.bSolid; // check for special pickup if (Other.bSpecial) { if (Other.bDehackedSpecial) { Other.TouchDehackedSpecial(self); } else { Other.TouchSpecial(self); // Can remove thing } } return !solid; } //========================================================================== // // IsTouched // // For Korax Arena // //========================================================================== bool IsTouched(Entity Toucher) { return false; } //=========================================================================== // // Slam // //=========================================================================== bool Slam(EntityEx Other) { bSkullFly = false; Velocity = vector(0.0, 0.0, 0.0); if (!bDormant) { int damage = ((P_Random() % 8) + 1) * MissileDamage; Other.Damage(self, self, damage, 'Melee'); SetState(SeeState ? SeeState : IdleState); } else { SetState(IdleState); StateTime = -1.0; } return false; // stop moving } //========================================================================== // // SpecialMissileHit // //========================================================================== int SpecialMissileHit(EntityEx Other) { return -1; } //========================================================================== // // GetSpecies // //========================================================================== class GetSpecies() { return class(Class); } //=========================================================================== // // CheckForPushSpecial // //=========================================================================== final void CheckForPushSpecial(line_t* line, int side) { if (line->special) { if (bActivatePushWall) { LineSpecialLevelInfo(Level).ActivateLine(line, self, side, SPAC_PUSH); } else if (bActivateImpact) { LineSpecialLevelInfo(Level).ActivateLine(line, bMissile ? Target : self, side, SPAC_IMPACT); } } } //========================================================================== // // BlockedByLine // //========================================================================== final void BlockedByLine(line_t* ld) { if (bBlasted) { Damage(none, none, ftoi(Mass) >> 5); } CheckForPushSpecial(ld, 0); } //========================================================================== // // PushLine // //========================================================================== final void PushLine(tmtrace_t* tmtrace) { if (bColideWithWorld) { int numSpecHitTemp; line_t *ld; int side; if (bBlasted) { Damage(none, none, ftoi(Mass) >> 5); } numSpecHitTemp = tmtrace->NumSpecHit; while (numSpecHitTemp > 0) { numSpecHitTemp--; // see if the line was crossed ld = tmtrace->SpecHit[numSpecHitTemp]; side = PointOnPlaneSide(Origin, ld); CheckForPushSpecial(ld, side); } } } //========================================================================== // // CrossSpecialLine // //========================================================================== final void CrossSpecialLine(line_t *ld, int side) { if (bIsPlayer) { LineSpecialLevelInfo(Level).ActivateLine(ld, self, side, SPAC_CROSS); } else if (bActivateMCross) { LineSpecialLevelInfo(Level).ActivateLine(ld, self, side, SPAC_MCROSS); } else if (bActivatePCross) { LineSpecialLevelInfo(Level).ActivateLine(ld, self, side, SPAC_PCROSS); } else if (ld->special == LNSPEC_Teleport || ld->special == LNSPEC_TeleportNoFog || ld->special == LNSPEC_TeleportLine) { // Teleport hack LineSpecialLevelInfo(Level).ActivateLine(ld, self, side, SPAC_MCROSS); } } //========================================================================== // // Activate // //========================================================================== bool Activate() { if (bMonster) { // Monster if (bDormant) { bDormant = false; StateTime = 0.1; return true; } } return false; } //========================================================================== // // Deactivate // //========================================================================== bool Deactivate() { if (bMonster) { // Monster if (!bDormant) { bDormant = true; StateTime = -1.0; return true; } } return false; } //========================================================================== // // FaceMovementDirection // //========================================================================== final void FaceMovementDirection() { switch (MoveDir) { case DI_EAST: Angles.yaw = 0.0; break; case DI_NORTHEAST: Angles.yaw = 45.0; break; case DI_NORTH: Angles.yaw = 90.0; break; case DI_NORTHWEST: Angles.yaw = 135.0; break; case DI_WEST: Angles.yaw = 180.0; break; case DI_SOUTHWEST: Angles.yaw = 225.0; break; case DI_SOUTH: Angles.yaw = 270.0; break; case DI_SOUTHEAST: Angles.yaw = 315.0; break; } } //========================================================================== // // OnMapSpawn // //========================================================================== void OnMapSpawn(mthing_t* mthing) { if (bNoDeathmatch && Level.Game.deathmatch) { Destroy(); return; } if (Level.Game.gameskill == sk_nightmare) { ReactionCount = 0; } LastLook = P_Random() % MAXPLAYERS; bCheckLineBlocking = false; bCheckLineBlockMonsters = false; if (!bMissile) { bCheckLineBlocking = true; if (!bNoBlockMonst) { bCheckLineBlockMonsters = true; } } if (!IdleState) { IdleState = FindState('Spawn'); } if (!SeeState) { SeeState = FindState('See'); } if (!MeleeState) { MeleeState = FindState('Melee'); } if (!MissileState) { MissileState = FindState('Missile'); } // Set the state, but do not use SetState, because action routines can't // be called yet. If the spawnstate has an action routine, it will not // be called. SetInitialState(IdleState); if (mthing) { float x; float y; float z; Level.CopyMThing(mthing, &SpawnPoint); x = mthing->x; y = mthing->y; if (bSpawnCeiling) { z = ONCEILINGZ; } else if (bSpawnFloat) { z = FLOATRANDZ; } else if (bFloatBob) { z = mthing->height; } else { z = ONFLOORZ; } SetOrigin2(vector(x, y, z)); if (z == ONFLOORZ) { Origin.z += mthing->height; } else if (z == ONCEILINGZ) { Origin.z -= mthing->height; } LinkToWorld(); TID = mthing->tid; Special = mthing->special; Args[0] = mthing->arg1; Args[1] = mthing->arg2; Args[2] = mthing->arg3; Args[3] = mthing->arg4; Args[4] = mthing->arg5; if (bFloatBob) { // Seed random starting index for bobbing motion FloatBobPhase = Random() * 256.0 / 35.0; Special1f = mthing->height; } if (StateTime > 0.0) { StateTime = 0.1 + Random() * StateTime; } if (bCountKill) { Level.TotalKills++; } if (bCountItem) { Level.TotalItems++; } Angles.yaw = itof(AngleIncrements() * (mthing->angle / AngleIncrements())); if (mthing->options & LineSpecialLevelInfo::MTF_AMBUSH) { bAmbush = true; } if (mthing->options & LineSpecialLevelInfo::MTF_STANDSTILL) { bStanding = true; } if (mthing->options & LineSpecialLevelInfo::MTF_DORMANT) { SetDormant(); } } else { SetOrigin2(Origin); } if (bStaticLight && mthing && !TID) { if (LightColour == 0xffffffff) { Level.AddStaticLight(Origin + LightOffset, mthing->arg1 ? itof(mthing->arg1) * 8.0 : LightRadius); } else { Level.AddStaticLightRGB(Origin + LightOffset, mthing->arg1 ? itof(mthing->arg1) * 8.0 : LightRadius, LightColour); } } } //========================================================================== // // SeekerMissile // // The missile Tracer field must be Actor target. Returns true if // target was tracked, false if not. // //========================================================================== final bool SeekerMissile(float thresh, float turnMax) { int dir; float dist; float delta; float angle; if (!Tracer) { return false; } if (!Tracer.bShootable) { // Target died Tracer = none; return false; } dir = FaceActor(Tracer, delta); if (delta > thresh) { delta /= 2.0; if (delta > turnMax) { delta = turnMax; } } if (dir) { // Turn clockwise Angles.yaw = AngleMod360(Angles.yaw + delta); } else { // Turn counter clockwise Angles.yaw = AngleMod360(Angles.yaw - delta); } angle = Angles.yaw; Velocity.x = Speed * cos(angle); Velocity.y = Speed * sin(angle); if (Origin.z + Height < Tracer.Origin.z || Tracer.Origin.z + Tracer.Height < Origin.z) { // Need to seek vertically dist = DistTo2(Tracer); dist = dist / Speed; if (dist < 1.0) { dist = 1.0; } Velocity.z = (Tracer.Origin.z + Tracer.Height / 2.0 - Origin.z - Height / 2.0) / dist; } return true; } //========================================================================== // // PlayActiveSound // //========================================================================== void PlayActiveSound() { if (ActiveSound) { PlaySound(ActiveSound, CHAN_VOICE, 1.0, bFullVolActive ? ATTN_NONE : ATTN_NORMAL); } } //========================================================================== // // DropItem // //========================================================================== final void DropItem(class type, int amount, float chance) { EntityEx A; if (Random() > chance) { return; } A = Spawn(type, Origin + vector(0.0, 0.0, Height / 2.0)); A.bDropped = true; // special versions of items if (Inventory(A)) { Inventory Item = Inventory(A); if (amount > 0) { Item.Amount = amount; } else if (Ammo(Item)) { // Half ammo when dropped by bad guys. if (Item.DropAmount) { Item.Amount = Item.DropAmount; } else { Item.Amount = Item.Amount / 2; if (Item.Amount < 1) { Item.Amount = 1; } } } else if (Weapon(Item)) { // The same fror dropped weapons. Weapon(Item).AmmoGive1 = Weapon(Item).AmmoGive1 / 2; if (Weapon(Item).AmmoGive1 < 1) { Weapon(Item).AmmoGive1 = 1; } Weapon(Item).AmmoGive2 = Weapon(Item).AmmoGive2 / 2; if (Weapon(Item).AmmoGive2 < 1) { Weapon(Item).AmmoGive2 = 1; } } if (Item.SpecialDropAction(self)) { return; } } A.Velocity.x = (Random() - Random()) * 35.0; A.Velocity.y = (Random() - Random()) * 35.0; A.Velocity.z = (5.0 + Random() * 4.0) * 35.0; } //========================================================================== // // NoBlockingSet // //========================================================================== void NoBlockingSet() { } //========================================================================== // // Tick // //========================================================================== void Tick(float deltaTime) { Inventory Item; // Handle powerup effects. for (Item = Inventory; Item && Item.Owner == self; Item = Item.Inventory) { Item.DoEffect(); } if (!Physics(deltaTime)) { return; } // Cycle through states, calling action functions at transitions if (StateTime != -1.0) { if (!AdvanceState(deltaTime)) { // freed itself return; } } else { // Check for nightmare respawn if (!bMonster) { return; } if (!LineSpecialGameInfo(Level.Game).respawnmonsters) { return; } MoveCount++; if (MoveCount < 12 * 35) { return; } if (XLevel.TicTime & 31) { return; } if (P_Random() > 4) { return; } NightmareRespawn(); } } //========================================================================== // // SectorChanged // //========================================================================== final bool SectorChanged(int CrushChange) { EntityEx A; if (HeightClip()) { return true; } // Crunch bodies to giblets if (bCorpse && Health <= 0) { if (bNoBlood) { Destroy(); } else { if (FindState('Crunch')) { SetState(FindState('Crunch')); } bCorpse = false; bSolid = false; Height = 0.0; Radius = 0.0; if (CrunchSound) { PlaySound(CrunchSound, CHAN_VOICE); } } return true; } // Crunch dropped items if (bDropped) { Destroy(); return true; } if (!bShootable) { // Assume it is bloody gibs or something return true; } if (CrushChange && !(XLevel.TicTime & 3)) { Damage(none, none, CrushChange); // Spray blood in a random direction if (!bNoBlood && !bInvulnerable) { A = Spawn(LineSpecialGameInfo(Level.Game).BloodClass, Origin + vector(0.0, 0.0, Height / 2.0)); A.Velocity.x = (Random() - Random()) * 16.0 * 35.0; A.Velocity.y = (Random() - Random()) * 16.0 * 35.0; } } return false; // Don't fit } //========================================================================== // // ClientTick // //========================================================================== final void ClientTick(float DeltaTime) { dlight_t* dl; TVec fv; int i; particle_t* p; int c; int count; if (bHidden) { return; } if (bDynamicLight) { // Update dynamic lights dl = AllocDlight(self); dl->origin = Origin; dl->radius = DLightRadius; dl->colour = DLightColour; dl->die = XLevel.Time + 0.1; } if (bMuzzleFlash) { dl = AllocDlight(self); dl->origin = Origin; dl->origin.z += 48.0; AngleVector(&Angles, &fv); dl->origin = dl->origin + 18.0 * fv; dl->radius = DLightRadius + Random() * 31.0; dl->colour = DLightColour; dl->minlight = 32.0; dl->die = XLevel.Time + 0.1; } if (bLeaveTrail) { count = ftoi(DeltaTime * 256.0); for (i = 0; i < count; i++) { p = NewParticle(); if (!p) return; p->org = Origin; c = P_Random() >> 1; p->colour = RGB(c, c, c); p->die = XLevel.Time + 0.5 * Random(); p->type = LineSpecialLevelInfo::pt_static; p->vel.x = 16.0 * (Random() - 0.5); p->vel.y = 16.0 * (Random() - 0.5); p->vel.z = 16.0 * (Random() - 0.5); } } } //========================================================================== // // AddInventory // //========================================================================== void AddInventory(Inventory Item) { if (Item.Owner) { if (Item.Owner == self) { // Already in the inventory. return; } // Remove from current owner's inventory. EntityEx(Item.Owner).RemoveInventory(Item); } // Add it at the top of the inventory. Item.Inventory = Inventory; Inventory = Item; // Set itsm's owner Item.Owner = self; } //========================================================================== // // RemoveInventory // //========================================================================== void RemoveInventory(Inventory Item) { // Find previous item or owner itself, if it's the first item. EntityEx Prev = self; while (Prev && Prev.Inventory != Item) { Prev = Prev.Inventory; } // Unlink and remove owner. Prev.Inventory = Item.Inventory; Item.DetachedFromOwner(); Item.Owner = none; } //========================================================================== // // FindInventory // //========================================================================== final Inventory FindInventory(class ItemClass) { Inventory Check; for (Check = Inventory; Check; Check = Check.Inventory) { if (Check.Class == ItemClass) { return Check; } } return none; } //========================================================================== // // GiveInventoryType // //========================================================================== final Inventory GiveInventoryType(class Type) { Inventory Item = Spawn(Type); Item.bDropped = true; if (!Item.TryPickup(self)) { Item.Destroy(); return none; } return Item; } //========================================================================== // // DestroyAllInventory // //========================================================================== final void DestroyAllInventory() { while (Inventory) { Inventory.Destroy(); } } //========================================================================== // // UseInventory // //========================================================================== bool UseInventory(Inventory Item) { // Don't use items if you are dead. if (Health <= 0) { return false; } // Don't use item if don't actually have it. if (Item.Amount <= 0) { return false; } if (!Item.Use(false)) { return false; } // Item was used - remove it from inventory Item.Amount--; if (Item.Amount <= 0 && !Item.bKeepDepleted) { Item.Destroy(); } return true; } //========================================================================== // // ObtainInventory // //========================================================================== void ObtainInventory(EntityEx Other) { // Actor should not have any inventory. if (Inventory) { Error("ObtainInventory called while still having an inventory"); } Inventory = Other.Inventory; Other.Inventory = none; Inventory Item; for (Item = Inventory; Item; Item = Item.Inventory) { Item.Owner = self; } } //=========================================================================== // // GiveInventory // //=========================================================================== void GiveInventory(name ItemName, int Amount) { if (!bIsPlayer) { return; } // Stamina upgrade. class ItemClass = class(FindClass(ItemName)); if (ItemClass) { Inventory Item = Spawn(ItemClass); Item.Amount = Amount; if (Item.TryPickup(self)) { Item.Destroy(); } } } //=========================================================================== // // CheckInventory // //=========================================================================== int CheckInventory(name ItemName) { // Check if it's a player. if (!bIsPlayer) { return 0; } class ItemClass = class(FindClass(ItemName)); if (ItemClass) { Inventory Item = FindInventory(ItemClass); return Item ? Item.Amount : 0; } return 0; } //========================================================================== // // TouchDehackedSpecial // //========================================================================== final void TouchDehackedSpecial(EntityEx Toucher) { if (!Toucher.bPickUp) { // can't remove thing return; } // Dead thing touching. // Can happen with a sliding player corpse. if (Toucher.Health <= 0) { return; } class ItemType = LineSpecialLevelInfo(Level).GetDehackedItemType(self); if (!ItemType) { return; } Inventory Item = Spawn(ItemType); Item.bDropped = bDropped; if (bDropped) { if (Ammo(Item)) { // Half ammo when dropped by bad guys. Item.Amount = Item.Amount / 2; if (Item.Amount < 1) { Item.Amount = 1; } } } if (!Item.TryPickup(Toucher)) { Item.Destroy(); return; } if (Special) { Level.ExecuteActionSpecial(Special, Args[0], Args[1], Args[2], Args[3], Args[4], NULL, 0, Toucher); Special = 0; } if (bCountItem) { Toucher.Player.ItemCount++; Level.CurrentItems++; } Toucher.Player.cprint(Item.GetPickupMessage()); Toucher.PlaySound(Item.PickupSound, CHAN_ITEM); if (bDropped) { Destroy(); } else if (!Item.ShouldStay()) { if (Item.ShouldRespawn()) { bHidden = true; bSpecial = false; SetState(FindState('DehackedDormantPickup')); } else { Destroy(); } } PlayerEx(Toucher.Player).BonusFlash += Inventory::BONUSADD; // Destroy item if it wasn't added to the toucher's inventory. if (!Item.Owner) { Item.Destroy(); } } //========================================================================== // // GiveBody // // Returns false if the body isn't needed at all. // //========================================================================== final bool GiveBody(int num) { int max; if (bIsPlayer) { max = PlayerEx(Player).GetMaxHealth(); if (num < 0) { // For Strife negative body sets you to the percentage of your // full health num = max * -num / 100; if (Player.Health < num) { Player.Health = num; Health = num; return true; } } else if (Player.Health < max) { Player.Health += num; if (Player.Health > max) { Player.Health = max; } Health = Player.Health; return true; } } else { max = default.Health; if (num < 0) { num = max * -num / 100; if (Health < num) { Health = num; return true; } } else if (Health < max) { Health += num; if (Health > max) { Health = max; } return true; } } return false; } //========================================================================== // // SpawnPuff // //========================================================================== final void SpawnPuff(TVec Org, float Range, class PuffType, bool HitThing) { int i; EntityEx puff; if (PuffType.default.bPuffParticles) { for (i = 0; i < MAXPLAYERS; i++) { if (!Level.Game.Players[i]) continue; if (!Level.Game.Players[i].bSpawned) continue; PlayerEx(Level.Game.Players[i]).ClientGunShot(Org); } } Org.z += (Random() - Random()) * 4.0; puff = Spawn(PuffType, Org); if (!HitThing && puff.FindState('Crash')) { puff.SetState(puff.FindState('Crash')); } else if (Range == MELEERANGE && puff.MeleeState) { puff.SetState(puff.MeleeState); } if (HitThing && puff.SightSound) { // Hit thing sound puff.PlaySound(puff.SightSound, CHAN_VOICE); } else if (puff.AttackSound) { puff.PlaySound(puff.AttackSound, CHAN_VOICE); } puff.Velocity.z = puff.PuffVelZ; if (puff.ExplodeEffect) { SendExplosion(ExplodeEffect, Org); } LineSpecialLevelInfo(Level).bPuffSpawned = true; } //========================================================================== // // SpawnBlood // //========================================================================== final void SpawnBlood(TVec Org, int damage) { EntityEx A; int i; for (i = 0; i < MAXPLAYERS; i++) { if (!Level.Game.Players[i]) continue; if (!Level.Game.Players[i].bSpawned) continue; PlayerEx(Level.Game.Players[i]).ClientBlood( Org, damage > 255 ? 255 : damage); } Org.z += (Random() - Random()) * 4.0; A = Spawn(LineSpecialGameInfo(Level.Game).BloodClass, Org); A.Velocity.z = 2.0 * 35.0; if (LineSpecialGameInfo(Level.Game).bBloodRandomiseTime) { A.StateTime -= Random() * 0.1; if (A.StateTime < 0.1) A.StateTime = 0.1; } if (LineSpecialGameInfo(Level.Game).bBloodSpray) { if (damage > 13) { A.SetState(A.FindState('Spray')); } else { damage += 2; } } if (damage <= 12 && damage >= 9) A.SetState(GetStatePlus(A.IdleState, 1)); else if (damage < 9) A.SetState(GetStatePlus(A.IdleState, 2)); } //========================================================================== // // SpawnBloodSplatter // //========================================================================== final void SpawnBloodSplatter(TVec org, int damage) { EntityEx mo; int i; for (i = 0; i < MAXPLAYERS; i++) { if (!Level.Game.Players[i]) continue; if (!Level.Game.Players[i].bSpawned) continue; PlayerEx(Level.Game.Players[i]).ClientBlood(org, damage > 255 ? 255 : damage); } mo = Spawn(LineSpecialGameInfo(Level.Game).BloodSplatterClass, org); mo.Target = self; mo.Velocity.x = (Random() - Random()) * 4.0 * 35.0; mo.Velocity.y = (Random() - Random()) * 4.0 * 35.0; mo.Velocity.z = 3.0 * 35.0; } //=========================================================================== // // SpawnBloodSplatter2 // //=========================================================================== final void SpawnBloodSplatter2(TVec org) { EntityEx mo; org.x += (Random() - 0.5) * 8.0; org.y += (Random() - 0.5) * 8.0; mo = Spawn(LineSpecialGameInfo(Level.Game).AxeBloodClass, org); mo.Target = self; } //========================================================================== // // SpawnRipperBlood // //========================================================================== final void SpawnRipperBlood() { EntityEx th; TVec org; org.x = Origin.x + (Random() - Random()) * 16.0; org.y = Origin.y + (Random() - Random()) * 16.0; org.z = Origin.z + (Random() - Random()) * 16.0; th = Spawn(LineSpecialGameInfo(Level.Game).BloodClass, org); if (LineSpecialGameInfo(Level.Game).bRipperBloodNoGravity) { th.bNoGravity = true; } th.Velocity.x = Velocity.x / 2.0; th.Velocity.y = Velocity.y / 2.0; th.StateTime += Random() * 0.1; } //========================================================================== // // SendExplosion // //========================================================================== final void SendExplosion(int colour, TVec org) { int i; for (i = 0; i < MAXPLAYERS; i++) { if (!Level.Game.Players[i]) continue; if (!Level.Game.Players[i].bSpawned) continue; PlayerEx(Level.Game.Players[i]).ClientExplosion(colour, org); } } //========================================================================== // // AngleIncrements // //========================================================================== int AngleIncrements() { return bMonster ? 45 : LineSpecialGameInfo( Level.Game).NonMonsterAngleIncrements; } //========================================================================== // // SetDormant // //========================================================================== void SetDormant() { bDormant = true; StateTime = -1.0; } //========================================================================== // // PreExplode // //========================================================================== void PreExplode() { } //========================================================================== // // GetExplodeParms // //========================================================================== void GetExplodeParms(out int damage, out float distance, out byte damageSelf) { } //========================================================================== // // CheckBossDeath // //========================================================================== final bool CheckBossDeath() { // make sure there is a player alive for victory int i; for (i = 0; i < MAXPLAYERS; i++) { if (Level.Game.Players[i] && Level.Game.Players[i].bSpawned && Level.Game.Players[i].Health > 0) { break; } } if (i == MAXPLAYERS) { // no one left alive, so do not end game return false; } // scan the remaining thinkers to see if all bosses are dead EntityEx Other; foreach AllThinkers(class(Class), Other) { if (Other != self && Other.Class == Class && Other.Health > 0) { // Found a living boss return false; } } return true; } //========================================================================== // // MonsterMorphed // //========================================================================== void MonsterMorphed() { } //========================================================================== // // GetSpeechIndex // //========================================================================== final int GetSpeechIndex(RogueConSpeech *List, int Count, int ID, int Num) { int Found; int i; Found = 0; for (i = 0; i < Count; i++) { if (List[i].SpeakerID == ID) { Found++; if (Found == Num) { return i + 1; } } } return 0; } //========================================================================== // // GetSpeech // //========================================================================== final int GetSpeech() { int Index; if (!CurrentSpeech) { CurrentSpeech = 1; } Index = GetSpeechIndex(XLevel.LevelSpeeches, XLevel.NumLevelSpeeches, ConversationID, CurrentSpeech); if (Index) { return Index; } return -GetSpeechIndex(XLevel.GenericSpeeches, XLevel.NumGenericSpeeches, ConversationID, CurrentSpeech); } //========================================================================== // // TossUpObject // //========================================================================== final EntityEx TossUpObject(class type) { EntityEx A; float an; float randomspeed; randomspeed = Random() * 8.0 - 6.0; A = Spawn(type, GetCentre()); if (A) { an = AngleMod360(Angles.yaw + Random() * 360.0); A.Angles.yaw = an; A.Tracer = Target; A.Target = self; A.Velocity.x = randomspeed * cos(an) * 35.0; A.Velocity.y = randomspeed * sin(an) * 35.0; A.Velocity.z = (12.0 + Random() / 8.0) * 35.0; } return A; } float GetBaseViewHeight() { return 0.0; } bool PlayerIsMorphed() { return false; } bool IsTeammate(EntityEx Other) { return bIsPlayer && Other.bIsPlayer && Level.Game.netgame && !Level.Game.deathmatch; } bool IsNotAttackingMaster(EntityEx Other) { return false; } bool IsMaster(EntityEx Other) { return false; } bool IsServant() { return false; } void KilledByPlayer(EntityEx source) { } void PlayerKilled(EntityEx source, EntityEx inflictor) { } void TouchSpecial(EntityEx Toucher) { } void AutoUseArmor() { } states { DehackedDormantPickup: TNT1 A 1050 TNT1 A -1 { EntityEx A; bHidden = false; bSpecial = true; SetState(IdleState); // spawn a teleport fog at the new spot A = Spawn(ItemFog, Origin); A.PlaySound('misc/spawn', CHAN_ITEM); } Stop } defaultproperties { Health = 1000; GibsHealth = -1000; ReactionCount = 8; Radius = 20.0; Height = 16.0; Mass = 100.0; MaxStepHeight = 24.0; LightOffset = vector(0.0, 0.0, 0.0); LightColour = 0xffffffff; LightRadius = 128.0; DLightColour = 0xffffffff; DLightRadius = 200.0; MissileChance = 200.0; FloatSpeed = 140.0; BounceFactor = 0.7; RDFactor = 1.0; MeleeRange = 44.0; // MELEERANGE(64.0) - 20.0 }