/* Copyright (c) 2002-2012 Croteam Ltd. This program is free software; you can redistribute it and/or modify it under the terms of version 2 of the GNU General Public License as published by the Free Software Foundation This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ 310 %{ #include "StdH.h" #include "EntitiesMP/Common/PathFinding.h" #include "EntitiesMP/NavigationMarker.h" #include "EntitiesMP/TacticsHolder.h" extern void JumpFromBouncer(CEntity *penToBounce, CEntity *penBouncer); extern INDEX ent_bReportBrokenChains; %} uses "EntitiesMP/Watcher"; uses "EntitiesMP/BasicEffects"; uses "EntitiesMP/Projectile"; uses "EntitiesMP/Debris"; uses "EntitiesMP/EnemyMarker"; uses "EntitiesMP/MusicHolder"; uses "EntitiesMP/BloodSpray"; event ERestartAttack { }; // self sent in Active loop to reconsider what enemy should do event EReconsiderBehavior { }; // force wound event EForceWound { }; enum TargetType { 0 TT_NONE "", // no target 1 TT_SOFT "", // soft target - only spoted player but not heavily angry at him 2 TT_HARD "", // hard target - player has provoked enemy and it is very angry }; enum DestinationType { 0 DT_PLAYERCURRENT "", // go to where player is now 1 DT_PLAYERSPOTTED "", // go to where player was last seen 2 DT_PATHTEMPORARY "", // go to navigation marker - temporary, only until you spot player again 3 DT_PATHPERSISTENT "", // go to navigation marker - until you really get there }; %{ #define MF_MOVEZ (1L<<0) #define MF_ROTATEH (1L<<1) #define MF_MOVEXZY (1L<<2) %} class export CEnemyBase : CMovableModelEntity { name "Enemy Base"; thumbnail ""; features "HasName", "IsTargetable", "CanBePredictable"; properties: 1 CEntityPointer m_penWatcher, // watcher 2 FLOAT3D m_vStartPosition = FLOAT3D(0,0,0), // start position 3 CEntityPointer m_penEnemy, // current enemy 4 enum TargetType m_ttTarget = TT_NONE, // type of target 5 CTString m_strDescription = "Enemy base", 6 CTString m_strName "Name" 'N' = "Enemy base", 7 CSoundObject m_soSound, 8 FLOAT3D m_vStartDirection = FLOAT3D(0,0,-1), // for returning to start 9 BOOL m_bOnStartPosition = TRUE, 29 FLOAT m_fFallHeight "Fall height" = 8.0f, 31 FLOAT m_fStepHeight "Step height" = -1.0f, 17 RANGE m_fSenseRange "Sense Range" = 0.0f, // immediately spots any player that gets closer than this 28 FLOAT m_fViewAngle "View angle" 'V' = 360.0f, // view frustum angle for spotting players // moving/attack properties - CAN BE SET // these following must be ordered exactly like this for GetProp() to function 10 FLOAT m_fWalkSpeed = 1.0f, // walk speed 11 ANGLE m_aWalkRotateSpeed = AngleDeg(10.0f), // walk rotate speed 12 FLOAT m_fAttackRunSpeed = 1.0f, // attack run speed 13 ANGLE m_aAttackRotateSpeed = AngleDeg(10.0f), // attack rotate speed 14 FLOAT m_fCloseRunSpeed = 1.0f, // close run speed 15 ANGLE m_aCloseRotateSpeed = AngleDeg(10.0f), // close rotate speed 20 FLOAT m_fAttackDistance = 50.0f, // attack distance mode 21 FLOAT m_fCloseDistance = 10.0f, // close distance mode 22 FLOAT m_fAttackFireTime = 2.0f, // attack distance fire time 23 FLOAT m_fCloseFireTime = 1.0f, // close distance fire time 24 FLOAT m_fStopDistance = 0.0f, // stop moving toward enemy if closer than stop distance 25 FLOAT m_fIgnoreRange = 200.0f, // cease attack if enemy farther 26 FLOAT m_fLockOnEnemyTime = 0.0f, // time needed to fire // damage/explode properties - CAN BE SET 40 FLOAT m_fBlowUpAmount = 0.0f, // damage in minus for blow up 41 INDEX m_fBodyParts = 4, // number of spawned body parts 42 FLOAT m_fDamageWounded = 0.0f, // damage amount to be wounded 43 FLOAT3D m_vDamage = FLOAT3D(0,0,0), // current damage impact 44 FLOAT m_tmLastDamage = -1000.0f, 46 BOOL m_bRobotBlowup = FALSE, // set for robots parts blowup, otherwise blowup flesh 47 FLOAT m_fBlowUpSize = 2.0f, // logic temporary variables -> DO NOT USE 133 FLOAT m_fMoveTime = 0.0f, 52 FLOAT3D m_vDesiredPosition = FLOAT3D(0,0,0), 53 enum DestinationType m_dtDestination = DT_PLAYERCURRENT, // type of current desired position 59 CEntityPointer m_penPathMarker, // current path finding marker 18 FLOAT3D m_vPlayerSpotted = FLOAT3D(0,0,0), // where player was last spotted 54 FLOAT m_fMoveFrequency = 0.0f, 55 FLOAT m_fMoveSpeed = 0.0f, 56 ANGLE m_aRotateSpeed = 0, 57 FLOAT m_fLockStartTime = 0.0f, 58 FLOAT m_fRangeLast = 0.0f, 130 BOOL m_bFadeOut = FALSE, 131 FLOAT m_fFadeStartTime = 0.0f, 132 FLOAT m_fFadeTime = 0.0f, // attack temporary -> DO NOT USE 60 FLOAT m_fShootTime = 0.0f, // time when entity will try to shoot on enemy 61 FLOAT m_fDamageConfused = 0.0f, // damage amount when entity shoot concentration is spoiled 62 INDEX m_iChargeHitAnimation = 0.0f, // charge hit (close attack) properties 63 FLOAT m_fChargeHitDamage = 0.0f, 64 FLOAT m_fChargeHitAngle = 0.0f, 65 FLOAT m_fChargeHitSpeed = 0.0f, // editor variables 83 CEntityPointer m_penSpawnerTarget, // for re-spawning 84 CEntityPointer m_penDeathTarget "Death target" 'D', // death target 85 enum EventEType m_eetDeathType "Death event type" 'F' = EET_TRIGGER, // death event type 86 BOOL m_bTemplate "Template" = FALSE, // template enemy for spawning new enemies 88 RANGE m_fAttackRadius "Radius of attack" 'A' = 10000.0f, // attack sphere range radius 89 COLOR m_colColor "Color" 'L' = 0x00, // color 90 BOOL m_bDeaf "Deaf" = FALSE, // deaf 91 BOOL m_bBlind "Blind" = FALSE, // blind 92 FLOAT m_tmGiveUp "Give up time" = 5.0f, // how fast enemy gives up attack 93 FLOAT m_tmReflexMin "Reflex Min" = 0.0f, // how much to wait before reacting on spotting the player 94 FLOAT m_tmReflexMax "Reflex Max" = 0.0f, 95 FLOAT m_fActivityRange "Activity Range" = 0.0f, // random values variables 106 BOOL m_bApplyRandomStretch "Apply random stretch" = FALSE, // apply random stretch 107 FLOAT m_fRandomStretchFactor "Random stretch factor" = 0.1f, // random stretch 108 FLOAT m_fStretchMultiplier "Stretch multiplier" = 1.0f, // stretch multiplier 109 FLOAT m_fRandomStretchMultiplier = 1.0f, // calculated random stretch // marker variables 120 CEntityPointer m_penMarker "Marker" 'M' COLOR(C_RED|0xFF), // enemy marker pointer // fuss variables 140 CEntityPointer m_penMainMusicHolder, 141 FLOAT m_tmLastFussTime = 0.0f, 142 FLOAT m_iScore = -100000, // how many points this enemy gives when killed 143 FLOAT m_fMaxHealth = -1.0f, // must set this because of crosshair colorizing 144 BOOL m_bBoss = FALSE, // set for bosses (for health display) 145 FLOAT m_fSpiritStartTime = 0.0f, // time when spirit effect has started 146 FLOAT m_tmSpraySpawned = 0.0f, // time when damage has been applied 147 FLOAT m_fSprayDamage = 0.0f, // total ammount of damage 148 CEntityPointer m_penSpray, // the blood spray 149 FLOAT m_fMaxDamageAmmount = 0.0f, // max ammount of damage received in in last few ticks 150 FLOAT3D m_vLastStain = FLOAT3D(0,0,0), // where last stain was left 151 enum SprayParticlesType m_sptType = SPT_BLOOD, // type of particles 160 CEntityPointer m_penTacticsHolder "Tactics Holder", 161 BOOL m_bTacticActive = FALSE, 162 FLOAT m_tmTacticsActivation = 0.0f, // warning! tactic variables are also used for dust spawning 163 FLOAT3D m_vTacticsStartPosition = FLOAT3D(0,0,0), 165 FLOAT m_fTacticVar1 = 0.0f, 166 FLOAT m_fTacticVar2 = 0.0f, 167 FLOAT m_fTacticVar3 = 0.0f, 168 FLOAT m_fTacticVar4 = 0.0f, 169 FLOAT m_fTacticVar5 = 0.0f, 170 BOOL m_bTacticsStartOnSense "Tactics start on sense" = FALSE, 180 COLOR m_colBurning = COLOR(C_WHITE|CT_OPAQUE), // color applied when burning 181 BOOL m_bResizeAttachments "Stretch attachments" = FALSE, // for small enemies with big guns //171 INDEX m_iTacticsRetried = 0, { TIME m_tmPredict; // time to predict the entity to } components: 1 class CLASS_WATCHER "Classes\\Watcher.ecl", 2 class CLASS_PROJECTILE "Classes\\Projectile.ecl", 3 class CLASS_DEBRIS "Classes\\Debris.ecl", 4 class CLASS_BASIC_EFFECT "Classes\\BasicEffect.ecl", 5 class CLASS_BLOOD_SPRAY "Classes\\BloodSpray.ecl", // ************** FLESH PARTS ************** 10 model MODEL_FLESH "Models\\Effects\\Debris\\Flesh\\Flesh.mdl", 11 model MODEL_FLESH_APPLE "Models\\Effects\\Debris\\Fruits\\Apple.mdl", 12 model MODEL_FLESH_BANANA "Models\\Effects\\Debris\\Fruits\\Banana.mdl", 13 model MODEL_FLESH_BURGER "Models\\Effects\\Debris\\Fruits\\CheeseBurger.mdl", 14 model MODEL_FLESH_LOLLY "Models\\Effects\\Debris\\Fruits\\LollyPop.mdl", 15 model MODEL_FLESH_ORANGE "Models\\Effects\\Debris\\Fruits\\Orange.mdl", 20 texture TEXTURE_FLESH_RED "Models\\Effects\\Debris\\Flesh\\FleshRed.tex", 21 texture TEXTURE_FLESH_GREEN "Models\\Effects\\Debris\\Flesh\\FleshGreen.tex", 22 texture TEXTURE_FLESH_APPLE "Models\\Effects\\Debris\\Fruits\\Apple.tex", 23 texture TEXTURE_FLESH_BANANA "Models\\Effects\\Debris\\Fruits\\Banana.tex", 24 texture TEXTURE_FLESH_BURGER "Models\\Effects\\Debris\\Fruits\\CheeseBurger.tex", 25 texture TEXTURE_FLESH_LOLLY "Models\\Effects\\Debris\\Fruits\\LollyPop.tex", 26 texture TEXTURE_FLESH_ORANGE "Models\\Effects\\Debris\\Fruits\\Orange.tex", // ************** MACHINE PARTS ************** 31 model MODEL_MACHINE "Models\\Effects\\Debris\\Stone\\Stone.mdl", 32 texture TEXTURE_MACHINE "Models\\Effects\\Debris\\Stone\\Stone.tex", functions: void CEnemyBase(void) { m_tmPredict = 0; } // called by other entities to set time prediction parameter void SetPredictionTime(TIME tmAdvance) // give time interval in advance to set { ASSERT(!IsPredictor()); m_tmPredict = _pTimer->CurrentTick()+tmAdvance; } // called by engine to get the upper time limit TIME GetPredictionTime(void) // return moment in time up to which to predict this entity { return m_tmPredict; } // describe how this enemy killed player virtual CTString GetPlayerKillDescription(const CTString &strPlayerName, const EDeath &eDeath) { CTString str; str.PrintF(TRANS("%s killed %s"), GetClass()->ec_pdecDLLClass->dec_strName, strPlayerName); return str; } virtual FLOAT GetCrushHealth(void) { return 0.0f; } // if should be counted as kill virtual BOOL CountAsKill(void) { return TRUE; } virtual BOOL ForcesCannonballToExplode(void) { return FALSE; } // overridable function for access to different properties of derived classes (flying/diving) virtual FLOAT &GetProp(FLOAT &m_fBase) { return m_fBase; } // overridable function to get range for switching to another player virtual FLOAT GetThreatDistance(void) { // closer of close or stop range return Max(GetProp(m_fCloseDistance), GetProp(m_fStopDistance)); } // check if we maybe switch to some other player (for large beasts in coop) void MaybeSwitchToAnotherPlayer(void) { // if in single player if (GetSP()->sp_bSinglePlayer) { // no need to check return; } // if current player is inside threat distance if (CalcDist(m_penEnemy)CheckAnotherPlayer(m_penEnemy); if (penNewEnemy!=m_penEnemy && penNewEnemy!=NULL) { m_penEnemy = penNewEnemy; SendEvent(EReconsiderBehavior()); } } class CWatcher *GetWatcher(void) { ASSERT(m_penWatcher!=NULL); return (CWatcher*)&*m_penWatcher; } export void Copy(CEntity &enOther, ULONG ulFlags) { CMovableModelEntity::Copy(enOther, ulFlags); CEnemyBase *penOther = (CEnemyBase *)(&enOther); } void Precache(void) { PrecacheModel(MODEL_FLESH); PrecacheModel(MODEL_FLESH_APPLE); PrecacheModel(MODEL_FLESH_BANANA); PrecacheModel(MODEL_FLESH_BURGER); PrecacheModel(MODEL_MACHINE); PrecacheTexture(TEXTURE_MACHINE); PrecacheTexture(TEXTURE_FLESH_RED); PrecacheTexture(TEXTURE_FLESH_GREEN); PrecacheTexture(TEXTURE_FLESH_APPLE); PrecacheTexture(TEXTURE_FLESH_BANANA); PrecacheTexture(TEXTURE_FLESH_BURGER); PrecacheTexture(TEXTURE_FLESH_LOLLY); PrecacheTexture(TEXTURE_FLESH_ORANGE); PrecacheClass(CLASS_BASIC_EFFECT, BET_BLOODSPILL); PrecacheClass(CLASS_BASIC_EFFECT, BET_BLOODSTAIN); PrecacheClass(CLASS_BASIC_EFFECT, BET_BLOODSTAINGROW); PrecacheClass(CLASS_BASIC_EFFECT, BET_BLOODEXPLODE); PrecacheClass(CLASS_BASIC_EFFECT, BET_BOMB); PrecacheClass(CLASS_BASIC_EFFECT, BET_EXPLOSIONSTAIN); PrecacheClass(CLASS_DEBRIS); } // get position you would like to go to when following player virtual FLOAT3D PlayerDestinationPos(void) { return m_penEnemy->GetPlacement().pl_PositionVector; } // calculate delta to given entity FLOAT3D CalcDelta(CEntity *penEntity) { ASSERT(penEntity!=NULL); // find vector from you to target return penEntity->GetPlacement().pl_PositionVector - GetPlacement().pl_PositionVector; }; // calculate distance to given entity FLOAT CalcDist(CEntity *penEntity) { return CalcDelta(penEntity).Length(); }; BOOL IfTargetCrushed(CEntity *penOther, const FLOAT3D &vDirection) { if( IsOfClass(penOther, "ModelHolder2")) { FLOAT fCrushHealth = GetCrushHealth(); if( fCrushHealth>((CRationalEntity &)*penOther).GetHealth()) { InflictDirectDamage(penOther, this, DMT_EXPLOSION, fCrushHealth, GetPlacement().pl_PositionVector, vDirection); return TRUE; } } return FALSE; } // calculate delta to given entity in current gravity plane FLOAT3D CalcPlaneDelta(CEntity *penEntity) { ASSERT(penEntity!=NULL); FLOAT3D vPlaneDelta; // find vector from you to target in XZ plane GetNormalComponent( penEntity->GetPlacement().pl_PositionVector - GetPlacement().pl_PositionVector, en_vGravityDir, vPlaneDelta); return vPlaneDelta; }; // calculate distance to given entity in current gravity plane FLOAT CalcPlaneDist(CEntity *penEntity) { return CalcPlaneDelta(penEntity).Length(); }; // get cos of angle in direction FLOAT GetFrustumAngle(const FLOAT3D &vDir) { // find front vector FLOAT3D vFront = -GetRotationMatrix().GetColumn(3); // make dot product to determine if you can see target (view angle) return (vDir/vDir.Length())%vFront; } // get cos of angle in direction in current gravity plane FLOAT GetPlaneFrustumAngle(const FLOAT3D &vDir) { FLOAT3D vPlaneDelta; // find vector from you to target in XZ plane GetNormalComponent(vDir, en_vGravityDir, vPlaneDelta); // find front vector FLOAT3D vFront = -GetRotationMatrix().GetColumn(3); FLOAT3D vPlaneFront; GetNormalComponent(vFront, en_vGravityDir, vPlaneFront); // make dot product to determine if you can see target (view angle) vPlaneDelta.SafeNormalize(); vPlaneFront.SafeNormalize(); return vPlaneDelta%vPlaneFront; } // determine if you can see something in given direction BOOL IsInFrustum(CEntity *penEntity, FLOAT fCosHalfFrustum) { // get direction to the entity FLOAT3D vDelta = CalcDelta(penEntity); // find front vector FLOAT3D vFront = -GetRotationMatrix().GetColumn(3); // make dot product to determine if you can see target (view angle) FLOAT fDotProduct = (vDelta/vDelta.Length())%vFront; return fDotProduct >= fCosHalfFrustum; }; // determine if you can see something in given direction in current gravity plane BOOL IsInPlaneFrustum(CEntity *penEntity, FLOAT fCosHalfFrustum) { // get direction to the entity FLOAT3D vPlaneDelta = CalcPlaneDelta(penEntity); // find front vector FLOAT3D vFront = -GetRotationMatrix().GetColumn(3); FLOAT3D vPlaneFront; GetNormalComponent(vFront, en_vGravityDir, vPlaneFront); // make dot product to determine if you can see target (view angle) vPlaneDelta.SafeNormalize(); vPlaneFront.SafeNormalize(); FLOAT fDot = vPlaneDelta%vPlaneFront; return fDot >= fCosHalfFrustum; }; // cast a ray to entity checking only for brushes BOOL IsVisible(CEntity *penEntity) { ASSERT(penEntity!=NULL); // get ray source and target FLOAT3D vSource, vTarget; GetPositionCastRay(this, penEntity, vSource, vTarget); // cast the ray CCastRay crRay(this, vSource, vTarget); crRay.cr_ttHitModels = CCastRay::TT_NONE; // check for brushes only crRay.cr_bHitTranslucentPortals = FALSE; en_pwoWorld->CastRay(crRay); // if hit nothing (no brush) the entity can be seen return (crRay.cr_penHit==NULL); }; // cast a ray to entity checking all BOOL IsVisibleCheckAll(CEntity *penEntity) { ASSERT(penEntity!=NULL); // get ray source and target FLOAT3D vSource, vTarget; GetPositionCastRay(this, penEntity, vSource, vTarget); // cast the ray CCastRay crRay(this, vSource, vTarget); crRay.cr_ttHitModels = CCastRay::TT_COLLISIONBOX; // check for model collision box crRay.cr_bHitTranslucentPortals = FALSE; en_pwoWorld->CastRay(crRay); // if the ray hits wanted entity return crRay.cr_penHit==penEntity; }; /* calculates launch velocity and heading correction for angular launch */ void CalculateAngularLaunchParams( FLOAT3D vShooting, FLOAT fShootHeight, FLOAT3D vTarget, FLOAT3D vSpeedDest, ANGLE aPitch, FLOAT &fLaunchSpeed, FLOAT &fRelativeHdg) { FLOAT3D vNewTarget = vTarget; FLOAT3D &vGravity = en_vGravityDir; FLOAT fYt; FLOAT fXt; FLOAT fA = TanFast(AngleDeg(aPitch)); FLOAT fTime = 0.0f; FLOAT fLastTime = 0.0f; INDEX iIterations = 0; do { iIterations++; FLOAT3D vDistance = vNewTarget-vShooting; FLOAT3D vXt, vYt; GetParallelAndNormalComponents(vDistance, vGravity, vYt, vXt); fYt = vYt.Length(); if (vGravity%vYt>0) { fYt=-fYt; } fXt = vXt.Length(); fLastTime=fTime; fTime = Sqrt(2.0f)*Sqrt((fA*fXt+fShootHeight-fYt)/en_fGravityA); vNewTarget = vTarget+vSpeedDest*fTime; } while( (Abs(fTime-fLastTime) > _pTimer->TickQuantum) && (iIterations<10) ); // calculate launch speed fLaunchSpeed = fXt/(fTime*Cos(aPitch)); // calculate heading correction FLOAT fHdgTargetNow = GetRelativeHeading( (vTarget-vShooting).SafeNormalize()); FLOAT fHdgTargetMoved = GetRelativeHeading( (vNewTarget-vShooting).SafeNormalize()); fRelativeHdg = fHdgTargetMoved-fHdgTargetNow; } /* calculates predicted position for propelled projectile */ FLOAT3D CalculatePredictedPosition( FLOAT3D vShootPos, FLOAT3D vTarget, FLOAT fSpeedSrc, FLOAT3D vSpeedDst, FLOAT fClampY) { FLOAT3D vNewTarget = vTarget; FLOAT3D &vGravity = en_vGravityDir; FLOAT fTime = 0.0f; FLOAT fLastTime = 0.0f; INDEX iIterations = 0; FLOAT3D vDistance = vNewTarget-vShootPos; // iterate to obtain accurate position do { iIterations++; fLastTime=fTime; fTime = vDistance.Length()/fSpeedSrc; vNewTarget = vTarget + vSpeedDst*fTime + vGravity*0.5f*fTime*fTime; vNewTarget(2) = ClampDn( vNewTarget(2), fClampY); vDistance = vNewTarget-vShootPos; } while( (Abs(fTime-fLastTime) > _pTimer->TickQuantum) && (iIterations<10) ); return vNewTarget; } /* Check if entity is moved on a route set up by its targets. */ BOOL MovesByTargetedRoute(CTString &strTargetProperty) const { strTargetProperty = "Marker"; return TRUE; }; /* Check if entity can drop marker for making linked route. */ BOOL DropsMarker(CTFileName &fnmMarkerClass, CTString &strTargetProperty) const { fnmMarkerClass = CTFILENAME("Classes\\EnemyMarker.ecl"); strTargetProperty = "Marker"; return TRUE; } const CTString &GetDescription(void) const { ((CTString&)m_strDescription).PrintF("->"); if (m_penMarker!=NULL) { ((CTString&)m_strDescription).PrintF("->%s", m_penMarker->GetName()); } return m_strDescription; } virtual const CTFileName &GetComputerMessageName(void) const { static CTFileName fnm(CTString("")); return fnm; } // add to prediction any entities that this entity depends on void AddDependentsToPrediction(void) { m_penSpray->AddToPrediction(); if (m_penWatcher!=NULL) { GetWatcher()->AddToPrediction(); } } // create a checksum value for sync-check void ChecksumForSync(ULONG &ulCRC, INDEX iExtensiveSyncCheck) { CMovableModelEntity::ChecksumForSync(ulCRC, iExtensiveSyncCheck); } // dump sync data to text file void DumpSync_t(CTStream &strm, INDEX iExtensiveSyncCheck) // throw char * { CMovableModelEntity ::DumpSync_t(strm, iExtensiveSyncCheck); strm.FPrintF_t("enemy: "); if (m_penEnemy!=NULL) { strm.FPrintF_t("id: %08X\n", m_penEnemy->en_ulID); } else { strm.FPrintF_t("none\n"); } /*INDEX ctStates = en_stslStateStack.Count(); strm.FPrintF_t("state stack @%gs:\n", _pTimer->CurrentTick()); for(INDEX iState=ctStates-1; iState>=0; iState--) { SLONG slState = en_stslStateStack[iState]; strm.FPrintF_t(" 0x%08x %s\n", slState, en_pecClass->ec_pdecDLLClass->HandlerNameForState(slState)); }*/ } /* Read from stream. */ void Read_t( CTStream *istr) { CMovableModelEntity::Read_t(istr); // add to fuss if needed if (m_penMainMusicHolder!=NULL) { ((CMusicHolder&)*m_penMainMusicHolder).m_cenFussMakers.Add(this); } }; /* Fill in entity statistics - for AI purposes only */ BOOL FillEntityStatistics(EntityStats *pes) { pes->es_strName = GetClass()->ec_pdecDLLClass->dec_strName; if (m_bTemplate) { pes->es_ctCount = 0; } else { pes->es_ctCount = 1; } pes->es_ctAmmount = 1; pes->es_fValue = GetHealth(); pes->es_iScore = m_iScore; return TRUE; } /* Receive damage */ void ReceiveDamage(CEntity *penInflictor, enum DamageType dmtType, FLOAT fDamageAmmount, const FLOAT3D &vHitPoint, const FLOAT3D &vDirection) { // if template if (m_bTemplate) { // do nothing return; } FLOAT fNewDamage = fDamageAmmount; // adjust damage fNewDamage *=DamageStrength( ((EntityInfo*)GetEntityInfo())->Eeibt, dmtType); // apply game extra damage per enemy and per player fNewDamage *=GetGameDamageMultiplier(); // if no damage if (fNewDamage==0) { // do nothing return; } FLOAT fKickDamage = fNewDamage; if( (dmtType == DMT_EXPLOSION) || (dmtType == DMT_IMPACT) || (dmtType == DMT_CANNONBALL_EXPLOSION) ) { fKickDamage*=1.5; } if (dmtType==DMT_DROWNING || dmtType==DMT_CLOSERANGE || dmtType==DMT_CHAINSAW) { fKickDamage /= 10; } if (dmtType==DMT_BURNING) { fKickDamage /= 100000; UBYTE ubR, ubG, ubB, ubA; FLOAT fColorFactor=fNewDamage/m_fMaxHealth*255.0f; ColorToRGBA(m_colBurning, ubR, ubG, ubB, ubA); ubR=ClampDn(ubR-fColorFactor, 32.0f); m_colBurning=RGBAToColor(ubR, ubR, ubR, ubA); } // get passed time since last damage TIME tmNow = _pTimer->CurrentTick(); TIME tmDelta = tmNow-m_tmLastDamage; m_tmLastDamage = tmNow; // fade damage out if (tmDelta>=_pTimer->TickQuantum*3) { m_vDamage=FLOAT3D(0,0,0); } // add new damage FLOAT3D vDirectionFixed; if (vDirection.ManhattanNorm()>0.5f) { vDirectionFixed = vDirection; } else { vDirectionFixed = -en_vGravityDir; } FLOAT3D vDamageOld = m_vDamage; /* if( (dmtType == DMT_EXPLOSION) || (dmtType == DMT_CANNONBALL_EXPLOSION) ) { m_vDamage+=(vDirectionFixed/2-en_vGravityDir/2)*fKickDamage; } else*/ { m_vDamage+=(vDirectionFixed-en_vGravityDir/2)*fKickDamage; } FLOAT fOldLen = vDamageOld.Length(); FLOAT fNewLen = m_vDamage.Length(); FLOAT fOldRootLen = Sqrt(fOldLen); FLOAT fNewRootLen = Sqrt(fNewLen); FLOAT fMassFactor = 300.0f/((EntityInfo*)GetEntityInfo())->fMass; if( !(en_ulFlags & ENF_ALIVE)) { fMassFactor /= 3; } if(fOldLen != 0.0f) { // cancel last push GiveImpulseTranslationAbsolute( -vDamageOld/fOldRootLen*fMassFactor); } //-en_vGravityDir*fPushStrength/10 // push it back GiveImpulseTranslationAbsolute( m_vDamage/fNewRootLen*fMassFactor); /*if ((m_tmSpraySpawned<=_pTimer->CurrentTick()-_pTimer->TickQuantum || m_fSprayDamage+fNewDamage>50.0f) && m_fSpiritStartTime==0) {*/ if( m_fMaxDamageAmmountCurrentTick()-_pTimer->TickQuantum*8 || m_fSprayDamage+fNewDamage>50.0f) && m_fSpiritStartTime==0 && dmtType!=DMT_CHAINSAW && !(dmtType==DMT_BURNING && GetHealth()<0) ) { // spawn blood spray CPlacement3D plSpray = CPlacement3D( vHitPoint, ANGLE3D(0, 0, 0)); m_penSpray = CreateEntity( plSpray, CLASS_BLOOD_SPRAY); if(m_sptType != SPT_ELECTRICITY_SPARKS) { m_penSpray->SetParent( this); } ESpawnSpray eSpawnSpray; eSpawnSpray.colBurnColor=C_WHITE|CT_OPAQUE; if( m_fMaxDamageAmmount > 10.0f) { eSpawnSpray.fDamagePower = 3.0f; } else if(m_fSprayDamage+fNewDamage>50.0f) { eSpawnSpray.fDamagePower = 2.0f; } else { eSpawnSpray.fDamagePower = 1.0f; } eSpawnSpray.sptType = m_sptType; eSpawnSpray.fSizeMultiplier = 1.0f; // setup direction of spray FLOAT3D vHitPointRelative = vHitPoint - GetPlacement().pl_PositionVector; FLOAT3D vReflectingNormal; GetNormalComponent( vHitPointRelative, en_vGravityDir, vReflectingNormal); vReflectingNormal.SafeNormalize(); vReflectingNormal(1)/=5.0f; FLOAT3D vProjectedComponent = vReflectingNormal*(vDirection%vReflectingNormal); FLOAT3D vSpilDirection = vDirection-vProjectedComponent*2.0f-en_vGravityDir*0.5f; eSpawnSpray.vDirection = vSpilDirection; eSpawnSpray.penOwner = this; /*if (dmtType==DMT_BURNING && GetHealth()<0) { eSpawnSpray.fDamagePower = 1.0f; }*/ // initialize spray m_penSpray->Initialize( eSpawnSpray); m_tmSpraySpawned = _pTimer->CurrentTick(); m_fSprayDamage = 0.0f; m_fMaxDamageAmmount = 0.0f; } m_fSprayDamage+=fNewDamage; CMovableModelEntity::ReceiveDamage(penInflictor, dmtType, fNewDamage, vHitPoint, vDirection); }; /************************************************************ * FADE OUT * ************************************************************/ BOOL AdjustShadingParameters(FLOAT3D &vLightDirection, COLOR &colLight, COLOR &colAmbient) { colAmbient = AddColors( colAmbient, m_colColor); if( m_bFadeOut) { FLOAT fTimeRemain = m_fFadeStartTime + m_fFadeTime - _pTimer->CurrentTick(); if( fTimeRemain < 0.0f) { fTimeRemain = 0.0f; } COLOR colAlpha; if(en_RenderType == RT_SKAMODEL || en_RenderType == RT_SKAEDITORMODEL) { colAlpha = GetModelInstance()->GetModelColor(); colAlpha = (colAlpha&0xFFFFFF00) + (COLOR(fTimeRemain/m_fFadeTime*0xFF)&0xFF); GetModelInstance()->SetModelColor(colAlpha); } else { colAlpha = GetModelObject()->mo_colBlendColor; colAlpha = (colAlpha&0xFFFFFF00) + (COLOR(fTimeRemain/m_fFadeTime*0xFF)&0xFF); GetModelObject()->mo_colBlendColor = colAlpha; } } else { if (GetSP()->sp_bMental) { if (GetHealth()<=0) { if(en_RenderType == RT_SKAMODEL || en_RenderType == RT_SKAEDITORMODEL) { GetModelInstance()->SetModelColor(C_WHITE&0xFF); } else { GetModelObject()->mo_colBlendColor = C_WHITE&0xFF; } } else { extern FLOAT ent_tmMentalIn ; extern FLOAT ent_tmMentalOut ; extern FLOAT ent_tmMentalFade; FLOAT tmIn = ent_tmMentalIn ; FLOAT tmOut = ent_tmMentalOut ; FLOAT tmFade = ent_tmMentalFade; FLOAT tmExist = tmFade+tmIn+tmFade; FLOAT tmTotal = tmFade+tmIn+tmFade+tmOut; FLOAT tmTime = _pTimer->GetLerpedCurrentTick(); FLOAT fFactor = 1; if (tmTime>0.1f) { tmTime += en_ulID*123.456f; tmTime = fmod(tmTime, tmTotal); fFactor = CalculateRatio(tmTime, 0, tmExist, tmFade/tmExist, tmFade/tmExist); } if(en_RenderType == RT_SKAMODEL || en_RenderType == RT_SKAEDITORMODEL) { GetModelInstance()->SetModelColor(C_WHITE|INDEX(0xFF*fFactor)); } else { GetModelObject()->mo_colBlendColor = C_WHITE|INDEX(0xFF*fFactor); } } } } if(m_colBurning!=COLOR(C_WHITE|CT_OPAQUE)) { colAmbient = MulColors( colAmbient, m_colBurning); colLight = MulColors( colLight, m_colBurning); } return CMovableModelEntity::AdjustShadingParameters(vLightDirection, colLight, colAmbient); }; // fuss functions void AddToFuss(void) { if (IsPredictor()) { // remember last fuss time m_tmLastFussTime = _pTimer->CurrentTick(); return; } // if no music holder remembered - not in fuss if (m_penMainMusicHolder==NULL) { // find main music holder m_penMainMusicHolder = _pNetwork->GetEntityWithName("MusicHolder", 0); // if no music holder found if (m_penMainMusicHolder==NULL) { // just remember last fuss time m_tmLastFussTime = _pTimer->CurrentTick(); // cannot make fuss return; } // add to end of fuss list ((CMusicHolder&)*m_penMainMusicHolder).m_cenFussMakers.Add(this); // if boss set as boss if (m_bBoss) { ((CMusicHolder&)*m_penMainMusicHolder).m_penBoss = this; } // remember last fuss time m_tmLastFussTime = _pTimer->CurrentTick(); // if music holder remembered - still in fuss } else { // must be in list ASSERT(((CMusicHolder&)*m_penMainMusicHolder).m_cenFussMakers.IsMember(this)); // if boss set as boss if (m_bBoss) { ((CMusicHolder&)*m_penMainMusicHolder).m_penBoss = this; } // just remember last fuss time m_tmLastFussTime = _pTimer->CurrentTick(); } } void RemoveFromFuss(void) { if (IsPredictor()) { return; } // if no music holder remembered if (m_penMainMusicHolder==NULL) { // not in fuss return; } // just remove from list ((CMusicHolder&)*m_penMainMusicHolder).m_cenFussMakers.Remove(this); // if boss, clear boss if (m_bBoss) { if (((CMusicHolder&)*m_penMainMusicHolder).m_penBoss != this) { CPrintF(TRANS("More than one boss active!\n")); ((CMusicHolder&)*m_penMainMusicHolder).m_penBoss = NULL; } } m_penMainMusicHolder = NULL; } // check if should give up attacking BOOL ShouldCeaseAttack(void) { // if there is no valid the enemy if (m_penEnemy==NULL || !(m_penEnemy->GetFlags()&ENF_ALIVE) || (m_penEnemy->GetFlags()&ENF_DELETED)) { // cease attack return TRUE; } // if not active in fighting if (_pTimer->CurrentTick()>m_tmLastFussTime+m_tmGiveUp) { // cease attack return TRUE; } // otherwise, continue return FALSE; } // Stretch model - MUST BE DONE BEFORE SETTING MODEL! virtual void SizeModel(void) { FLOAT3D vStretch = GetModelStretch(); // apply defined stretch vStretch *= m_fStretchMultiplier; // if should apply random stretch if (m_bApplyRandomStretch) { // will be done only when user clicks "Apply random" switch m_bApplyRandomStretch = FALSE; // get rnd for random stretch m_fRandomStretchMultiplier = (FRnd()-0.5f)*m_fRandomStretchFactor+1.0f; } // apply random stretch vStretch *= m_fRandomStretchMultiplier; if (m_bResizeAttachments) { StretchModel( vStretch); } else if (TRUE) { StretchSingleModel( vStretch); } ModelChangeNotify(); }; // check if an entity is valid for being your new enemy BOOL IsValidForEnemy(CEntity *penPlayer) { return penPlayer!=NULL && IsDerivedFromClass(penPlayer, "Player") && penPlayer->GetFlags()&ENF_ALIVE; } // unset target void SetTargetNone(void) { m_ttTarget = TT_NONE; m_dtDestination = DT_PLAYERCURRENT; m_penEnemy = NULL; } // set new player as soft target if possible BOOL SetTargetSoft(CEntity *penPlayer) { // if invalid target if (!IsValidForEnemy(penPlayer)) { // do nothing return FALSE; } // if we already have any kind of target if (m_ttTarget!=TT_NONE) { // do nothing return FALSE; } // remember new soft target CEntity *penOld = m_penEnemy; m_ttTarget = TT_SOFT; m_dtDestination = DT_PLAYERCURRENT; m_penEnemy = penPlayer; return penOld!=penPlayer; } // set new player as hard target if possible BOOL SetTargetHard(CEntity *penPlayer) { // if invalid target if (!IsValidForEnemy(penPlayer)) { // do nothing return FALSE; } // if we already have hard target if (m_ttTarget==TT_HARD) { // do nothing return FALSE; } // remember new hard target CEntity *penOld = m_penEnemy; m_ttTarget = TT_HARD; m_dtDestination = DT_PLAYERCURRENT; m_penEnemy = penPlayer; return penOld!=penPlayer; } // force new player to be hard target BOOL SetTargetHardForce(CEntity *penPlayer) { // if invalid target if (!IsValidForEnemy(penPlayer)) { // do nothing return FALSE; } // remember new hard target CEntity *penOld = m_penEnemy; m_ttTarget = TT_HARD; m_dtDestination = DT_PLAYERCURRENT; m_penEnemy = penPlayer; return penOld!=penPlayer; } /************************************************************ * MOVING FUNCTIONS * ************************************************************/ // get movement frequency for attack virtual FLOAT GetAttackMoveFrequency(FLOAT fEnemyDistance) { if (fEnemyDistance>GetProp(m_fCloseDistance)) { return 0.5f; } else { return 0.25f; } } // set speeds for movement towards desired position virtual void SetSpeedsToDesiredPosition(const FLOAT3D &vPosDelta, FLOAT fPosDist, BOOL bGoingToPlayer) { FLOAT fEnemyDistance = CalcDist(m_penEnemy); FLOAT fCloseDistance = GetProp(m_fCloseDistance); FLOAT fStopDistance = GetProp(m_fStopDistance); // find relative direction angle FLOAT fCos = GetPlaneFrustumAngle(vPosDelta); // if may move and if (MayMoveToAttack() && // more or less ahead and fCos>CosFast(45.0f) && // not too close fEnemyDistance>fStopDistance) { // move and rotate towards it if (fEnemyDistanceGetProp(m_fWalkSpeed)) { RunningAnim(); } else { WalkingAnim(); } } else if (ulFlags&MF_ROTATEH) { RotatingAnim(); } else { if (m_penEnemy!=NULL) { StandingAnimFight(); } else { StandingAnim(); } } } // set desired rotation and translation to go/orient towards desired position // and get the resulting movement type virtual ULONG SetDesiredMovement(void) { ULONG ulFlags = 0; // get delta to desired position FLOAT3D vDelta = m_vDesiredPosition - GetPlacement().pl_PositionVector; if (m_dtDestination==DT_PLAYERCURRENT) { ApplyTactics(vDelta); } // if we may rotate if (m_aRotateSpeed>0.0f) { // get desired heading orientation FLOAT3D vDir = vDelta; vDir.SafeNormalize(); ANGLE aWantedHeadingRelative = GetRelativeHeading(vDir); // normalize it to [-180,+180] degrees aWantedHeadingRelative = NormalizeAngle(aWantedHeadingRelative); ANGLE aHeadingRotation; // if desired position is left if (aWantedHeadingRelative < -m_aRotateSpeed*m_fMoveFrequency) { // start turning left aHeadingRotation = -m_aRotateSpeed; // if desired position is right } else if (aWantedHeadingRelative > m_aRotateSpeed*m_fMoveFrequency) { // start turning right aHeadingRotation = +m_aRotateSpeed; // if desired position is more-less ahead } else { // keep just the adjusting fraction of speed aHeadingRotation = aWantedHeadingRelative/m_fMoveFrequency; } // start rotating SetDesiredRotation(ANGLE3D(aHeadingRotation, 0, 0)); if (Abs(aHeadingRotation)>1.0f) { ulFlags |= MF_ROTATEH; } // if we may not rotate } else { // stop rotating SetDesiredRotation(ANGLE3D(0, 0, 0)); } // if we may move if (m_fMoveSpeed>0.0f) { // determine translation speed FLOAT3D vTranslation(0.0f, 0.0f, 0.0f); vTranslation(3) = -m_fMoveSpeed; // start moving SetDesiredTranslation(vTranslation); ulFlags |= MF_MOVEZ; // if we may not move } else { // stop translating SetDesiredTranslation(FLOAT3D(0, 0, 0)); } return ulFlags; }; // stop moving entity void StopMoving() { StopRotating(); StopTranslating(); }; // stop desired rotation void StopRotating() { SetDesiredRotation(ANGLE3D(0, 0, 0)); }; // stop desired translation void StopTranslating() { SetDesiredTranslation(FLOAT3D(0.0f, 0.0f, 0.0f)); }; // calc distance to entity in one plane (relative to owner gravity) FLOAT CalcDistanceInPlaneToDestination(void) { // find vector from you to target in XZ plane FLOAT3D vNormal; GetNormalComponent(m_vDesiredPosition - GetPlacement().pl_PositionVector, en_vGravityDir, vNormal); return vNormal.Length(); }; // initialize path finding virtual void StartPathFinding(void) { ASSERT(m_dtDestination==DT_PATHPERSISTENT || m_dtDestination==DT_PATHTEMPORARY); CEntity *penMarker; FLOAT3D vPath; // find first marker to go to PATH_FindFirstMarker(this, GetPlacement().pl_PositionVector, m_penEnemy->GetPlacement().pl_PositionVector, penMarker, vPath); // if not found, or not visible if (penMarker==NULL || !IsVisible(penMarker)) { // no path finding m_dtDestination=DT_PLAYERSPOTTED; // remember as if spotted position m_vPlayerSpotted = PlayerDestinationPos(); return; } // remember the marker and position m_vDesiredPosition = vPath, m_penPathMarker = penMarker; } // find next navigation marker to go to virtual void FindNextPathMarker(void) { // if invalid situation if (m_penPathMarker==NULL) { // this should not happen ASSERT(FALSE); // no path finding m_dtDestination=DT_PLAYERCURRENT; return; } // find first marker to go to CEntity *penMarker = m_penPathMarker; FLOAT3D vPath; PATH_FindNextMarker(this, GetPlacement().pl_PositionVector, m_penEnemy->GetPlacement().pl_PositionVector, penMarker, vPath); // if not found if (penMarker==NULL || !IsVisible(penMarker)) { // no path finding m_dtDestination=DT_PLAYERSPOTTED; // remember as if spotted position m_vPlayerSpotted = PlayerDestinationPos(); return; } // remember the marker and position m_vDesiredPosition = vPath, m_penPathMarker = penMarker; } // check if a touch event triggers pathfinding BOOL CheckTouchForPathFinding(const ETouch &eTouch) { // if no enemy if (m_penEnemy==NULL) { // do nothing return FALSE; } // if already path finding if (m_dtDestination==DT_PATHPERSISTENT || m_dtDestination==DT_PATHTEMPORARY) { // do nothing return FALSE; } FLOAT3D vDir = en_vDesiredTranslationRelative; vDir.SafeNormalize(); vDir*=GetRotationMatrix(); // if the touched plane is more or less orthogonal to the current velocity if ((eTouch.plCollision%vDir)<-0.5f) { if (m_penEnemy!=NULL && IsVisible(m_penEnemy)) { m_dtDestination = DT_PATHPERSISTENT; } else { m_dtDestination = DT_PATHTEMPORARY; } StartPathFinding(); return m_penPathMarker!=NULL; } else { return FALSE; } } // check if a wouldfall event triggers pathfinding BOOL CheckFallForPathFinding(const EWouldFall &eWouldFall) { // if no enemy if (m_penEnemy==NULL) { // do nothing return FALSE; } // if already path finding if (m_dtDestination==DT_PATHPERSISTENT || m_dtDestination==DT_PATHTEMPORARY) { // do nothing return FALSE; } if (m_penEnemy!=NULL && IsVisible(m_penEnemy)) { m_dtDestination = DT_PATHPERSISTENT; } else { m_dtDestination = DT_PATHTEMPORARY; } StartPathFinding(); return m_penPathMarker!=NULL; } /************************************************************ * TACTICS FUNCTIONS * ************************************************************/ void InitializeTactics( void ) { // return if there is no tactics manager or if it points to wrong type of entity // or if there is no enemy if (m_penTacticsHolder==NULL || !IsOfClass(m_penTacticsHolder, "TacticsHolder") || m_penEnemy==NULL) { return; } CTacticsHolder *penTactics = &(CTacticsHolder &)*m_penTacticsHolder; //m_tmTacticsActivation = penTactics->m_tmLastActivation; m_tmTacticsActivation = _pTimer->CurrentTick(); m_vTacticsStartPosition = GetPlacement().pl_PositionVector; //m_iTacticsRetried = penTactics->m_bRetryCount; FLOAT fSign; // sign for randomized parameters if (Sgn(penTactics->m_fParam2)>0 && Sgn(penTactics->m_fParam1)>0) { fSign = +1.0f; } else if (Sgn(penTactics->m_fParam2)<0 && Sgn(penTactics->m_fParam1)<0) { fSign = -1.0f; } else { fSign = Sgn(FRnd()-0.5f); } switch (penTactics->m_tctType) { case TCT_DAMP_ANGLE_STRIFE: { // 1) random angle (min) m_fTacticVar1=Lerp(Abs(penTactics->m_fParam1), Abs(penTactics->m_fParam2), FRnd())*fSign; // 2) time dump m_fTacticVar2=penTactics->m_fParam4; // 3) dump factor, factor (0-1) of min distance when linear behaviour begins m_fTacticVar3=penTactics->m_fParam3; // 4) initial distance m_fTacticVar4=(m_penEnemy->GetPlacement().pl_PositionVector - m_vTacticsStartPosition).Length(); // 5) tactics stop distance m_fTacticVar5=penTactics->m_fParam5; break; } case TCT_PARALLEL_RANDOM_DISTANCE: // 1) randomized distance m_fTacticVar1=Lerp(penTactics->m_fParam4, penTactics->m_fParam5, FRnd()); // 4) emission angle m_fTacticVar4=Lerp(Abs(penTactics->m_fParam1), Abs(penTactics->m_fParam2), FRnd())*fSign; // 2) tolerance strip width m_fTacticVar2=m_fAttackRunSpeed*2.0f*90.0f/m_aAttackRotateSpeed; //m_fTacticVar2=2.0f*m_fAttackRunSpeed; // 3) fade in/out ratio m_fTacticVar3=penTactics->m_fParam3; // 5) initial distance m_fTacticVar5=(GetPlacement().pl_PositionVector - m_penEnemy->GetPlacement().pl_PositionVector).Length(); // as a precausion, assume minimal strip width of 2m m_fTacticVar2=ClampDn(m_fTacticVar2, 2.0f); break; case TCT_STATIC_RANDOM_V_DISTANCE: // 1) starting angle m_fTacticVar1=Lerp(Abs(penTactics->m_fParam1), Abs(penTactics->m_fParam2), FRnd())*fSign; // 2) time to run in the desired V direction m_fTacticVar2=Lerp(penTactics->m_fParam3, penTactics->m_fParam4, FRnd()); break; } } virtual void ApplyTactics( FLOAT3D &vDesiredPos) { // return if there is no tactics manager or if it points to wrong type of entity // or if there is no enemy if (m_penTacticsHolder==NULL || !IsOfClass(m_penTacticsHolder, "TacticsHolder") || m_penEnemy==NULL) { return; } CTacticsHolder *penTactics = &(CTacticsHolder &)*m_penTacticsHolder; // See if the last activation time of TacticsHolder is greater then the activation // time of this monster. If so, start (or reinitialize) the tactics. if (penTactics->m_tmLastActivation==-1 || penTactics->m_tctType==TCT_NONE) { m_bTacticActive = FALSE; } else if (m_tmTacticsActivation < penTactics->m_tmLastActivation) { InitializeTactics(); m_bTacticActive = TRUE; } if (m_bTacticActive) { // calculate shared parameters FLOAT3D vEnemyDistance=m_vTacticsStartPosition - m_penEnemy->GetPlacement().pl_PositionVector; FLOAT fEnemyDistance=vEnemyDistance.Length(); vEnemyDistance.SafeNormalize(); ANGLE3D angEnemy = ANGLE3D(0.0f, 0.0f, 0.0f); //DirectionVectorToAngles(vEnemyDistance, angEnemy); FLOAT fDistanceRatio = 0.0f; FLOAT fTimeRatio = 0.0f; switch(penTactics->m_tctType) { case TCT_DAMP_ANGLE_STRIFE: { // if very close to player, stop using tactics if (CalcDist(m_penEnemy)0) { // get enemy distance FLOAT fClamped=Clamp(CalcDist(m_penEnemy)-(m_fTacticVar4*m_fTacticVar3), 0.0f, m_fTacticVar4); fDistanceRatio=fClamped/(m_fTacticVar4*(1-m_fTacticVar3)); } fTimeRatio=1.0f; if(m_fTacticVar2>0) { fTimeRatio=1.0f-(ClampUp((_pTimer->CurrentTick() - m_tmTacticsActivation)/m_fTacticVar2, 1.0f)); } angEnemy(1) = m_fTacticVar1*fDistanceRatio*fTimeRatio; angEnemy(2) = 0.0f; angEnemy(3) = 0.0f; FLOATmatrix3D mHeading; MakeRotationMatrixFast(mHeading, angEnemy); vDesiredPos = vDesiredPos*!en_mRotation; vDesiredPos = vDesiredPos*mHeading; vDesiredPos = vDesiredPos*en_mRotation; break; } case TCT_PARALLEL_RANDOM_DISTANCE: { // line from spawner to player FLOAT3D vLinePlayerToSpawn = m_vTacticsStartPosition - m_penEnemy->GetPlacement().pl_PositionVector; // line from *this to player FLOAT3D vLinePlayerToThis = GetPlacement().pl_PositionVector - m_penEnemy->GetPlacement().pl_PositionVector; FLOAT fThisOnLine = (vLinePlayerToThis%vLinePlayerToSpawn)/vLinePlayerToSpawn.Length(); FLOAT3D vThisOnLine = m_penEnemy->GetPlacement().pl_PositionVector + vLinePlayerToSpawn.SafeNormalize()*fThisOnLine; FLOAT fLineDist = (GetPlacement().pl_PositionVector - vThisOnLine).Length(); FLOATmatrix3D mHeading; //CPrintF("line dst = %f at %f\n", fLineDist, _pTimer->CurrentTick()); // if close enough to enemy stop tactics and go linear if (vLinePlayerToThis.Length()m_fTacticVar1+m_fTacticVar2) { if (fLineDist<1.0f) { fLineDist=1.0f; } angEnemy(1) = -m_fTacticVar4/fLineDist; angEnemy(2) = 0.0f; angEnemy(3) = 0.0f; MakeRotationMatrixFast(mHeading, angEnemy); vDesiredPos = vDesiredPos*!en_mRotation; vDesiredPos = vDesiredPos*mHeading; vDesiredPos = vDesiredPos*en_mRotation; // right on the line } else { vDesiredPos = -vLinePlayerToSpawn; } break; } case TCT_STATIC_RANDOM_V_DISTANCE: { if (_pTimer->CurrentTick()GetEntityInfo()); FLOAT3D vShootTarget; GetEntityInfoPosition(m_penEnemy, peiTarget->vTargetCenter, vShootTarget); // launch CPlacement3D pl; PreparePropelledProjectile(pl, vShootTarget, vOffset, aOffset); CEntityPointer penProjectile = CreateEntity(pl, CLASS_PROJECTILE); ELaunchProjectile eLaunch; eLaunch.penLauncher = this; eLaunch.fStretch=1.0f; eLaunch.prtType = pt; penProjectile->Initialize(eLaunch); return penProjectile; }; // shoot projectile at an exact spot CEntity *ShootProjectileAt(FLOAT3D vShootTarget, enum ProjectileType pt, FLOAT3D &vOffset, ANGLE3D &aOffset) { // launch CPlacement3D pl; PreparePropelledProjectile(pl, vShootTarget, vOffset, aOffset); CEntityPointer penProjectile = CreateEntity(pl, CLASS_PROJECTILE); ELaunchProjectile eLaunch; eLaunch.penLauncher = this; eLaunch.prtType = pt; penProjectile->Initialize(eLaunch); return penProjectile; }; // shoot projectile on enemy CEntity *ShootPredictedProjectile(enum ProjectileType pt, FLOAT3D vPredictedPos, FLOAT3D &vOffset, ANGLE3D &aOffset) { ASSERT(m_penEnemy != NULL); // target enemy body (predicted) EntityInfo *peiTarget = (EntityInfo*) (m_penEnemy->GetEntityInfo()); FLOAT3D vShootTarget = vPredictedPos; if (peiTarget != NULL) { // get body center vector FLOAT3D vBody = FLOAT3D(peiTarget->vTargetCenter[0],peiTarget->vTargetCenter[1],peiTarget->vTargetCenter[2]); FLOATmatrix3D mRotation; MakeRotationMatrixFast(mRotation, m_penEnemy->GetPlacement().pl_OrientationAngle); vShootTarget = vPredictedPos + vBody*mRotation; } // launch CPlacement3D pl; PreparePropelledProjectile(pl, vShootTarget, vOffset, aOffset); CEntityPointer penProjectile = CreateEntity(pl, CLASS_PROJECTILE); ELaunchProjectile eLaunch; eLaunch.penLauncher = this; eLaunch.prtType = pt; penProjectile->Initialize(eLaunch); return penProjectile; }; BOOL WouldNotLeaveAttackRadius(void) { if (m_fAttackRadius<=0) { return FALSE; } // test if we are inside radius BOOL bInsideNow = (m_vStartPosition-GetPlacement().pl_PositionVector).Length() < m_fAttackRadius; // test if going towards enemy leads us to center of attack radius circle BOOL bEnemyTowardsCenter = (m_vStartPosition-m_penEnemy->GetPlacement().pl_PositionVector).Length() < (GetPlacement().pl_PositionVector-m_penEnemy->GetPlacement().pl_PositionVector).Length(); return bInsideNow || bEnemyTowardsCenter; } // check whether may move while attacking virtual BOOL MayMoveToAttack(void) { // check if enemy is diving CMovableEntity *pen = (CMovableEntity *)&*m_penEnemy; CContentType &ctUp = pen->en_pwoWorld->wo_actContentTypes[pen->en_iUpContent]; BOOL bEnemyDiving = !(ctUp.ct_ulFlags&CTF_BREATHABLE_LUNGS); // may move if can go to enemy without leaving attack radius, and entity is not diving return WouldNotLeaveAttackRadius() && !bEnemyDiving; }; /************************************************************ * BLOW UP FUNCTIONS * ************************************************************/ // should this enemy blow up (spawn debris) virtual BOOL ShouldBlowUp(void) { // exotech larva boss allways blows up if (IsOfClass(this, "ExotechLarva")) { return TRUE; } // blow up if return // allowed GetSP()->sp_bGibs && // dead and GetHealth()<=0 && // has received large enough damage lately and m_vDamage.Length() > m_fBlowUpAmount && // not already disappearing m_fSpiritStartTime==0; } // base function for blowing up void BlowUpBase(void) { // call derived function BlowUp(); } // spawn body parts virtual void BlowUp(void) { // blow up notify BlowUpNotify(); const BOOL bGibs = GetSP()->sp_bGibs; FLOAT3D vNormalizedDamage = m_vDamage-m_vDamage*(m_fBlowUpAmount/m_vDamage.Length()); vNormalizedDamage /= Sqrt(vNormalizedDamage.Length()); vNormalizedDamage *= 0.75f; FLOAT3D vBodySpeed = en_vCurrentTranslationAbsolute-en_vGravityDir*(en_vGravityDir%en_vCurrentTranslationAbsolute); // if allowed and fleshy if( bGibs && !m_bRobotBlowup) { // readout blood type const INDEX iBloodType = GetSP()->sp_iBlood; // determine debris texture (color) ULONG ulFleshTexture = TEXTURE_FLESH_GREEN; ULONG ulFleshModel = MODEL_FLESH; if( iBloodType==2) { ulFleshTexture = TEXTURE_FLESH_RED; } // spawn debris Debris_Begin(EIBT_FLESH, DPT_BLOODTRAIL, BET_BLOODSTAIN, m_fBlowUpSize, vNormalizedDamage, vBodySpeed, 1.0f, 0.0f); for( INDEX iDebris = 0; iDebrisInitialize(eSpawnEffect); } // hide yourself (must do this after spawning debris) SwitchToEditorModel(); SetPhysicsFlags(EPF_MODEL_IMMATERIAL); SetCollisionFlags(ECF_IMMATERIAL); } /************************************************************ * CLASS SUPPORT FUNCTIONS * ************************************************************/ // leave stain virtual void LeaveStain( BOOL bGrow) { ESpawnEffect ese; FLOAT3D vPoint; FLOATplane3D vPlaneNormal; FLOAT fDistanceToEdge; // get your size FLOATaabbox3D box; GetBoundingBox(box); // on plane if( GetNearestPolygon(vPoint, vPlaneNormal, fDistanceToEdge)) { // if near to polygon and away from last stain point if( (vPoint-GetPlacement().pl_PositionVector).Length()<0.5f && (m_vLastStain-vPoint).Length()>1.0f ) { m_vLastStain = vPoint; FLOAT fStretch = box.Size().Length(); ese.colMuliplier = C_WHITE|CT_OPAQUE; // stain if (bGrow) { ese.betType = BET_BLOODSTAINGROW; ese.vStretch = FLOAT3D( fStretch*1.5f, fStretch*1.5f, 1.0f); } else { ese.betType = BET_BLOODSTAIN; ese.vStretch = FLOAT3D( fStretch*0.75f, fStretch*0.75f, 1.0f); } ese.vNormal = FLOAT3D( vPlaneNormal); ese.vDirection = FLOAT3D( 0, 0, 0); FLOAT3D vPos = vPoint+ese.vNormal/50.0f*(FRnd()+0.5f); CEntityPointer penEffect = CreateEntity( CPlacement3D(vPos, ANGLE3D(0,0,0)), CLASS_BASIC_EFFECT); penEffect->Initialize(ese); } } }; virtual void AdjustDifficulty(void) { FLOAT fMoveSpeed = GetSP()->sp_fEnemyMovementSpeed; FLOAT fAttackSpeed = GetSP()->sp_fEnemyMovementSpeed; // m_fWalkSpeed *= fMoveSpeed; // m_aWalkRotateSpeed *= fMoveSpeed; m_fAttackRunSpeed *= fMoveSpeed; m_aAttackRotateSpeed *= fMoveSpeed; m_fCloseRunSpeed *= fMoveSpeed; m_aCloseRotateSpeed *= fMoveSpeed; m_fAttackFireTime *= 1/fAttackSpeed; m_fCloseFireTime *= 1/fAttackSpeed; /* CSessionProperties::GameDificulty gd = GetSP()->sp_gdGameDificulty; switch(gd) { case CSessionProperties::GD_EASY: { } break; case CSessionProperties::GD_NORMAL: { } break; case CSessionProperties::GD_HARD: { } break; case CSessionProperties::GD_SERIOUS: { } break; } */ } /************************************************************ * SOUND VIRTUAL FUNCTIONS * ************************************************************/ // wounded -> yell void WoundedNotify(const EDamage &eDamage) { // if no enemy if (m_penEnemy==NULL) { // do nothing return; } // if not killed from short distance if (eDamage.dmtType!=DMT_CLOSERANGE && eDamage.dmtType!=DMT_CHAINSAW) { // yell ESound eSound; eSound.EsndtSound = SNDT_YELL; eSound.penTarget = m_penEnemy; SendEventInRange(eSound, FLOATaabbox3D(GetPlacement().pl_PositionVector, 25.0f)); } }; // see enemy -> shout void SeeNotify() { // if no enemy if (m_penEnemy==NULL) { // do nothing return; } // yell ESound eSound; eSound.EsndtSound = SNDT_SHOUT; eSound.penTarget = m_penEnemy; SendEventInRange(eSound, FLOATaabbox3D(GetPlacement().pl_PositionVector, 50.0f)); }; /************************************************************ * VIRTUAL FUNCTIONS THAT NEED OVERRIDE * ************************************************************/ virtual void StandingAnim(void) {}; virtual void StandingAnimFight(void) { StandingAnim(); }; virtual void WalkingAnim(void) {}; virtual void RunningAnim(void) {}; virtual void RotatingAnim(void) {}; virtual void ChargeAnim(void) {}; virtual INDEX AnimForDamage(FLOAT fDamage) { return 0; }; virtual void BlowUpNotify(void) {}; virtual INDEX AnimForDeath(void) { return 0; }; virtual FLOAT WaitForDust(FLOAT3D &vStretch) { return -1; }; virtual void DeathNotify(void) {}; virtual void IdleSound(void) {}; virtual void SightSound(void) {}; virtual void WoundSound(void) {}; virtual void DeathSound(void) {}; virtual FLOAT GetLockRotationSpeed(void) { return 2000.0f;}; // render particles void RenderParticles(void) { // no particles when not existing if (GetRenderType()!=CEntity::RT_MODEL && GetRenderType()!=CEntity::RT_SKAMODEL) { return; } // if is dead if( m_fSpiritStartTime != 0.0f) { // const FLOAT tmNow = _pTimer->CurrentTick(); // Particles_ModelGlow(this, tmNow + 20,PT_STAR08, 0.15f, 2, 0.03f, 0xff00ff00); Particles_Death(this, m_fSpiritStartTime); } } // adjust sound and watcher parameters here if needed virtual void EnemyPostInit(void) {}; /* Handle an event, return false if the event is not handled. */ BOOL HandleEvent(const CEntityEvent &ee) { if (ee.ee_slEvent==EVENTCODE_ETouch) { if( GetCrushHealth() != 0.0f) { ETouch eTouch = ((ETouch &) ee); if (IsOfClass(eTouch.penOther, "ModelHolder2") || IsOfClass(eTouch.penOther, "MovingBrush") || IsOfClass(eTouch.penOther, "DestroyableArchitecture") ) { InflictDirectDamage(eTouch.penOther, this, DMT_EXPLOSION, GetCrushHealth(), eTouch.penOther->GetPlacement().pl_PositionVector, -(FLOAT3D&)eTouch.plCollision); } } } return CMovableModelEntity::HandleEvent(ee); } // returns length of animation FLOAT GetAnimLength(int iAnim) { if(en_RenderType==RT_SKAMODEL) { return GetModelInstance()->GetAnimLength(iAnim); } else { return GetModelObject()->GetAnimLength(iAnim); } } // returns lenght of current anim length FLOAT GetCurrentAnimLength() { if(en_RenderType==RT_SKAMODEL) { return 0.5f; } else { return GetModelObject()->GetCurrentAnimLength(); } } // is animation finished BOOL IsAnimFinished() { if(en_RenderType==RT_SKAMODEL) { return TRUE; } else { return GetModelObject()->IsAnimFinished(); } } // FLOAT GetPassedTime() { if(en_RenderType==RT_SKAMODEL) { return 0.0f; } else { return GetModelObject()->GetPassedTime(); } } FLOAT3D &GetModelStretch() { if(en_RenderType==RT_SKAMODEL) { return GetModelInstance()->mi_vStretch; } else { return GetModelObject()->mo_Stretch; } } // Stretch model void StretchModel(FLOAT3D vStretch) { if(en_RenderType==RT_SKAMODEL) { GetModelInstance()->StretchModel( vStretch); } else { GetModelObject()->StretchModel( vStretch); } } // Stretch single model void StretchSingleModel( FLOAT3D vStretch) { if(en_RenderType==RT_SKAMODEL) { GetModelInstance()->StretchSingleModel( vStretch); } else { GetModelObject()->StretchSingleModel( vStretch); } } // returns bytes of memory used by this object SLONG GetUsedMemory(void) { // initial SLONG slUsedMemory = sizeof(CEnemyBase) - sizeof(CMovableModelEntity) + CMovableModelEntity::GetUsedMemory(); // add some more slUsedMemory += m_strDescription.Length(); slUsedMemory += m_strName.Length(); slUsedMemory += 1* sizeof(CSoundObject); return slUsedMemory; } procedures: //********************************************************** // MOVEMENT PROCEDURES //********************************************************** // move to given destination position MoveToDestination(EVoid) { // setup parameters m_fMoveFrequency = 0.25f; m_fMoveTime = _pTimer->CurrentTick() + 45.0f; // while not there and time not expired while (CalcDistanceInPlaneToDestination()>m_fMoveSpeed*m_fMoveFrequency*2.0f && m_fMoveTime>_pTimer->CurrentTick()) { // every once in a while wait (m_fMoveFrequency) { on (EBegin) : { // adjust direction and speed ULONG ulFlags = SetDesiredMovement(); MovementAnimation(ulFlags); resume; } on (ETimer) : { stop; } } } // return to the caller return EReturn(); }; // go to next randomly choosen position inside patrol range MoveToRandomPatrolPosition(EVoid) { // if the marker is invalid if (!IsOfClass(m_penMarker, "Enemy Marker")) { // this should not happen ASSERT(FALSE); // return to caller return EReturn(); } // get the marker CEnemyMarker *pem = (CEnemyMarker *)&*m_penMarker; // get random destination position FLOAT fMin = pem->m_fPatrolAreaInner; FLOAT fMax = pem->m_fPatrolAreaOuter; if (fMin<0) { fMin = 0; } if (fMaxSendEvent(EStart()); // stand in place StandingAnim(); // repeat forever while(TRUE) { // wait some time autowait(Lerp(5.0f, 20.0f, FRnd())); // play idle sound IdleSound(); } } // return to start position ReturnToStartPosition(EVoid) { jump BeIdle(); /* // start watching GetWatcher()->SendEvent(EStart()); m_vDesiredPosition = m_vStartPosition; m_vStartDirection = (GetPlacement().pl_PositionVector-m_vStartPosition).SafeNormalize(); m_fMoveSpeed = GetProp(m_fAttackRunSpeed); m_aRotateSpeed = GetProp(m_aAttackRotateSpeed); RunningAnim(); autocall MoveToDestination() EReturn; WalkingAnim(); m_vDesiredAngle = m_vStartDirection; StopTranslating(); autocall RotateToStartDirection() EReturn; StopMoving(); StandingAnim(); jump BeIdle(); */ }; /* // rotate to destination RotateToStartDirection(EVoid) { m_fMoveFrequency = 0.1f; m_fMoveTime = _pTimer->CurrentTick() + 45.0f; while (Abs(GetRelativeHeading(GetDesiredPositionDir()))>GetProp(m_aRotateSpeed)*m_fMoveFrequency*1.5f && m_fMoveTime>_pTimer->CurrentTick()) { autowait(m_fMoveFrequency); } return EReturn(); }; */ // move through markers MoveThroughMarkers() { // start watching GetWatcher()->SendEvent(EStart()); // while there is a valid marker, take values from it while (m_penMarker!=NULL && IsOfClass(m_penMarker, "Enemy Marker")) { CEnemyMarker *pem = (CEnemyMarker *)&*m_penMarker; // the marker position is our new start position for attack range m_vStartPosition = m_penMarker->GetPlacement().pl_PositionVector; // make a random position to walk to at the marker FLOAT fR = FRnd()*pem->m_fMarkerRange; FLOAT fA = FRnd()*360.0f; m_vDesiredPosition = m_vStartPosition+FLOAT3D(CosFast(fA)*fR, 0, SinFast(fA)*fR); // if running if (pem->m_betRunToMarker==BET_TRUE) { // use attack speeds m_fMoveSpeed = GetProp(m_fAttackRunSpeed); m_aRotateSpeed = GetProp(m_aAttackRotateSpeed); // start running anim RunningAnim(); // if not running } else { // use walk speeds m_fMoveSpeed = GetProp(m_fWalkSpeed); m_aRotateSpeed = GetProp(m_aWalkRotateSpeed); // start walking anim WalkingAnim(); } // move to the new destination position autocall MoveToDestination() EReturn; // read new blind/deaf values CEnemyMarker *pem = (CEnemyMarker *)&*m_penMarker; SetBoolFromBoolEType(m_bBlind, pem->m_betBlind); SetBoolFromBoolEType(m_bDeaf, pem->m_betDeaf); // if should start tactics if (pem->m_bStartTactics){ // start to see/hear m_bBlind = FALSE; m_bDeaf = FALSE; // unconditional tactics start StartTacticsNow(); } // if should patrol there if (pem->m_fPatrolTime>0.0f) { // spawn a reminder to notify us when the time is up SpawnReminder(this, pem->m_fPatrolTime, 0); // wait wait() { // initially on (EBegin) : { // start patroling call DoPatrolling(); } // if time is up on (EReminder) : { // stop patroling stop; } } } CEnemyMarker *pem = (CEnemyMarker *)&*m_penMarker; // if should wait on the marker if (pem->m_fWaitTime > 0.0f) { // stop there StopMoving(); StandingAnim(); // wait autowait(pem->m_fWaitTime); } // wait a bit always (to prevent eventual busy-looping) autowait(0.05f); // take next marker in loop m_penMarker = ((CEnemyMarker&)*m_penMarker).m_penTarget; } // when no more markers // stop where you are StopMoving(); StandingAnim(); // return to called return EReturn(); }; //********************************************************** // ATTACK PROCEDURES //********************************************************** // sequence that is activated when a new player is spotted visually or heard NewEnemySpotted() { // calculate reflex time FLOAT tmReflex = Lerp(m_tmReflexMin, m_tmReflexMax, FRnd()); tmReflex = ClampDn(tmReflex, 0.0f); // if should wait if (tmReflex>=_pTimer->TickQuantum) { // stop where you are StopMoving(); StandingAnim(); // wait the reflex time wait(tmReflex) { on (ETimer) : { stop; } // pass all damage events on (EDamage) : { pass; } // pass space beam hit on (EHitBySpaceShipBeam) : { pass;} // ignore all other events otherwise () : { resume; } } } // play sight sound SightSound(); // return to caller return EReturn(); } // stop attack StopAttack(EVoid) { // start watching GetWatcher()->SendEvent(EStart()); // no damager SetTargetNone(); m_fDamageConfused = 0.0f; // stop moving StopMoving(); return EReturn(); }; // initial preparation InitializeAttack(EVoid) { // disable blind/deaf mode m_bBlind = FALSE; m_bDeaf = FALSE; SeeNotify(); // notify seeing GetWatcher()->SendEvent(EStop()); // stop looking // make fuss AddToFuss(); // remember spotted position m_vPlayerSpotted = PlayerDestinationPos(); // set timers if (CalcDist(m_penEnemy)CurrentTick() + FRnd(); } m_fDamageConfused = m_fDamageWounded; return EReturn(); }; // attack enemy AttackEnemy(EVoid) { // initial preparation autocall InitializeAttack() EReturn; // while you have some enemy while (m_penEnemy != NULL) { // attack it autocall PerformAttack() EReturn; } // stop attack autocall StopAttack() EReturn; // return to Active() procedure return EBegin(); }; // repeat attacking enemy until enemy is dead or you give up PerformAttack() { // reset last range m_fRangeLast = 1E9f; // set player's position as destination position m_vDesiredPosition = PlayerDestinationPos(); m_dtDestination = DT_PLAYERCURRENT; // repeat while(TRUE) { // if attacking is futile if (ShouldCeaseAttack()) { // cease it SetTargetNone(); return EReturn(); } // get distance from enemy FLOAT fEnemyDistance = CalcDist(m_penEnemy); // if just entered close range if (m_fRangeLast>GetProp(m_fCloseDistance) && fEnemyDistance<=GetProp(m_fCloseDistance)) { // reset shoot time to make it attack immediately m_fShootTime = 0.0f; } m_fRangeLast = fEnemyDistance; // determine movement frequency depending on enemy distance or path finding m_fMoveFrequency = GetAttackMoveFrequency(fEnemyDistance); if (m_dtDestination==DT_PATHPERSISTENT||m_dtDestination==DT_PATHTEMPORARY) { m_fMoveFrequency = 0.1f; } // wait one time interval wait(m_fMoveFrequency) { on (ETimer) : { stop; } // initially on (EBegin) : { // if you haven't fired/hit enemy for some time if (_pTimer->CurrentTick() > m_fShootTime) { // if you have new player visible closer than current and in threat distance CEntity *penNewEnemy = GetWatcher()->CheckCloserPlayer(m_penEnemy, GetThreatDistance()); if (penNewEnemy!=NULL) { // switch to that player SetTargetHardForce(penNewEnemy); // start new behavior SendEvent(EReconsiderBehavior()); stop; } // if you can see player if (IsVisible(m_penEnemy)) { // remember spotted position m_vPlayerSpotted = PlayerDestinationPos(); // if going to player spotted or temporary path position if (m_dtDestination==DT_PLAYERSPOTTED||m_dtDestination==DT_PATHTEMPORARY) { // switch to player current position m_dtDestination=DT_PLAYERCURRENT; } // if you cannot see player } else { // if going to player's current position if (m_dtDestination==DT_PLAYERCURRENT) { // switch to position where player was last seen m_dtDestination=DT_PLAYERSPOTTED; } } // try firing/hitting again now call FireOrHit(); // if between two fire/hit moments } else { // if going to player spotted or temporary path position and you just seen the player if ((m_dtDestination==DT_PLAYERSPOTTED||m_dtDestination==DT_PATHTEMPORARY) && IsVisible(m_penEnemy)) { // switch to player current position m_dtDestination=DT_PLAYERCURRENT; // remember spotted position m_vPlayerSpotted = PlayerDestinationPos(); } } // if you are not following the player and you are near current destination position FLOAT fAllowedError = m_fMoveSpeed*m_fMoveFrequency*2.0f; if (m_dtDestination==DT_PATHPERSISTENT||m_dtDestination==DT_PATHTEMPORARY) { fAllowedError = ((CNavigationMarker&)*m_penPathMarker).m_fMarkerRange; } if (m_dtDestination!=DT_PLAYERCURRENT && (CalcDistanceInPlaneToDestination()GetRenderType()==CEntity::RT_BRUSH) { FLOAT3D vDir = en_vDesiredTranslationRelative; vDir.SafeNormalize(); vDir*=GetRotationMatrix(); // if the touched plane is more or less orthogonal to the current velocity if ((eTouch.plCollision%vDir)<-0.5f) { m_bTacticActive = 0; } resume; } else { resume; } } else { // pass the event pass; } } // if came to an edge on (EWouldFall eWouldFall) : { // if pathfinding must begin if (CheckFallForPathFinding(eWouldFall)) { // stop the loop stop; } else if (m_bTacticActive) { // stop tactics m_bTacticActive = 0; resume; // if edge is ignored } else { // pass the event pass; } } on (ESound) : { resume; } // ignore all sounds on (EWatch) : { resume; } // ignore watch on (EReturn) : { stop; } // returned from subprocedure } } } // fire or hit the enemy if possible FireOrHit() { // if player is in close range and in front if (CalcDist(m_penEnemy)CurrentTick() + GetProp(m_fCloseFireTime)*(1.0f + FRnd()/3.0f); // hit autocall Hit() EReturn; // else if player is in fire range and in front } else if (CalcDist(m_penEnemy)CurrentTick() + GetProp(m_fCloseFireTime)*(1.0f + FRnd()/3.0f); } else { m_fShootTime = _pTimer->CurrentTick() + GetProp(m_fAttackFireTime)*(1.0f + FRnd()/3.0f); } // fire autocall Fire() EReturn; // if cannot fire nor hit } else { // make sure we don't retry again too soon m_fShootTime = _pTimer->CurrentTick() + 0.25f; } // return to caller return EReturn(); }; //********************************************************** // COMBAT IMPLEMENTATION //********************************************************** // this is called to hit the player when near Hit(EVoid) { return EReturn(); } // this is called to shoot at player when far away or otherwise unreachable Fire(EVoid) { return EReturn(); } //********************************************************** // COMBAT HELPERS //********************************************************** // call this to lock on player for some time - set m_fLockOnEnemyTime before calling LockOnEnemy(EVoid) { // stop moving StopMoving(); // play animation for locking ChargeAnim(); // wait charge time m_fLockStartTime = _pTimer->CurrentTick(); while (m_fLockStartTime+GetProp(m_fLockOnEnemyTime) > _pTimer->CurrentTick()) { // each tick m_fMoveFrequency = 0.05f; wait (m_fMoveFrequency) { on (ETimer) : { stop; } on (EBegin) : { m_vDesiredPosition = PlayerDestinationPos(); // if not heading towards enemy if (!IsInPlaneFrustum(m_penEnemy, CosFast(5.0f))) { m_fMoveSpeed = 0.0f; m_aRotateSpeed = GetLockRotationSpeed(); // if heading towards enemy } else { m_fMoveSpeed = 0.0f; m_aRotateSpeed = 0.0f; } // adjust direction and speed ULONG ulFlags = SetDesiredMovement(); //MovementAnimation(ulFlags); don't do this, or they start to jive resume; } } } // stop rotating StopRotating(); // return to caller return EReturn(); }; // call this to jump onto player - set charge properties before calling and spawn a reminder ChargeHitEnemy(EVoid) { // wait for length of hit animation wait(GetAnimLength(m_iChargeHitAnimation)) { on (EBegin) : { resume; } on (ETimer) : { stop; } // ignore damages on (EDamage) : { resume; } // if user-set reminder expired on (EReminder) : { // stop moving StopMoving(); resume; } // if you touch some entity on (ETouch etouch) : { // if it is alive and in front if ((etouch.penOther->GetFlags()&ENF_ALIVE) && IsInPlaneFrustum(etouch.penOther, CosFast(60.0f))) { // get your direction FLOAT3D vSpeed; GetHeadingDirection(m_fChargeHitAngle, vSpeed); // damage entity in that direction InflictDirectDamage(etouch.penOther, this, DMT_CLOSERANGE, m_fChargeHitDamage, FLOAT3D(0, 0, 0), vSpeed); // push it away vSpeed = vSpeed * m_fChargeHitSpeed; KickEntity(etouch.penOther, vSpeed); // stop waiting stop; } pass; } } // if the anim is not yet finished if (!IsAnimFinished()) { // wait the rest of time till the anim end wait(GetCurrentAnimLength() - GetPassedTime()) { on (EBegin) : { resume; } on (ETimer) : { stop; } // if timer expired on (EReminder) : { // stop moving StopMoving(); resume; } } } // return to caller return EReturn(); }; //********************************************************** // WOUNDING AND DYING PROCEDURES //********************************************************** // play wounding animation BeWounded(EDamage eDamage) { StopMoving(); // determine damage anim and play the wounding autowait(GetAnimLength(AnimForDamage(eDamage.fAmount))); return EReturn(); }; // we get here once the enemy is dead Die(EDeath eDeath) { // not alive anymore SetFlags(GetFlags()&~ENF_ALIVE); // find the one who killed, or other best suitable player CEntityPointer penKiller = eDeath.eLastDamage.penInflictor; if (penKiller==NULL || !IsOfClass(penKiller, "Player")) { penKiller = m_penEnemy; } if (penKiller==NULL || !IsOfClass(penKiller, "Player")) { penKiller = FixupCausedToPlayer(this, penKiller, /*bWarning=*/FALSE); } // if killed by someone if (penKiller!=NULL) { // give him score EReceiveScore eScore; eScore.iPoints = m_iScore; penKiller->SendEvent(eScore); if( CountAsKill()) { penKiller->SendEvent(EKilledEnemy()); } // send computer message if in coop if (GetSP()->sp_bCooperative) { EComputerMessage eMsg; eMsg.fnmMessage = GetComputerMessageName(); if (eMsg.fnmMessage!="") { penKiller->SendEvent(eMsg); } } } // destroy watcher class GetWatcher()->SendEvent(EStop()); GetWatcher()->SendEvent(EEnd()); // send event to death target SendToTarget(m_penDeathTarget, m_eetDeathType, penKiller); // send event to spawner if any // NOTE: trigger's penCaused has been changed from penKiller to THIS; if (m_penSpawnerTarget) { SendToTarget(m_penSpawnerTarget, EET_TRIGGER, this); } // wait wait() { // initially on (EBegin) : { // if should already blow up if (ShouldBlowUp()) { // blow up now BlowUpBase(); // stop waiting stop; // if shouldn't blow up yet } else { // invoke death animation sequence call DeathSequence(); } } // if damaged on (EDamage) : { // if should already blow up if (ShouldBlowUp()) { // blow up now BlowUpBase(); // stop waiting stop; } // otherwise, ignore the damage resume; } // if death sequence is over on (EEnd) : { // stop waiting stop; } } // stop making fuss RemoveFromFuss(); // cease to exist Destroy(); // all is over now, entity will be deleted return; }; Death(EVoid) { StopMoving(); // stop moving DeathSound(); // death sound LeaveStain(FALSE); // set physic flags SetPhysicsFlags(EPF_MODEL_CORPSE); SetCollisionFlags(ECF_CORPSE); SetFlags(GetFlags() | ENF_SEETHROUGH); // stop making fuss RemoveFromFuss(); // death notify (usually change collision box and change body density) DeathNotify(); // start death anim INDEX iAnim = AnimForDeath(); // use tactic variables for temporary data m_vTacticsStartPosition=FLOAT3D(1,1,1); m_fTacticVar4=WaitForDust(m_vTacticsStartPosition); // remember start time m_fTacticVar5=_pTimer->CurrentTick(); // mark that we didn't spawned dust yet m_fTacticVar3=-1; // if no dust should be spawned if( m_fTacticVar4<0) { autowait(GetAnimLength(iAnim)); } // should spawn dust else if( TRUE) { while(_pTimer->CurrentTick()TickQuantum); if(en_penReference!=NULL && _pTimer->CurrentTick()>=m_fTacticVar5+m_fTacticVar4 && m_fTacticVar3<0) { // spawn dust effect CPlacement3D plFX=GetPlacement(); ESpawnEffect ese; ese.colMuliplier = C_WHITE|CT_OPAQUE; ese.vStretch = m_vTacticsStartPosition; ese.vNormal = FLOAT3D(0,1,0); ese.betType = BET_DUST_FALL; CPlacement3D plSmoke=plFX; plSmoke.pl_PositionVector+=FLOAT3D(0,0.35f*m_vTacticsStartPosition(2),0); CEntityPointer penFX = CreateEntity(plSmoke, CLASS_BASIC_EFFECT); penFX->Initialize(ese); penFX->SetParent(this); // mark that we spawned dust m_fTacticVar3=1; } } } return EEnd(); }; DeathSequence(EVoid) { // entity death autocall Death() EEnd; // start bloody stain growing out from beneath the corpse LeaveStain(TRUE); // check if you have attached flame CEntityPointer penFlame = GetChildOfClass("Flame"); if (penFlame!=NULL) { // send the event to stop burning EStopFlaming esf; esf.m_bNow=FALSE; penFlame->SendEvent(esf); } autowait(2.0f); // start fading out and turning into stardust effect m_fSpiritStartTime = _pTimer->CurrentTick(); m_fFadeStartTime = _pTimer->CurrentTick(); m_fFadeTime = 1.0f, m_bFadeOut = TRUE; // become passable even if very large corpse SetCollisionFlags(ECF_CORPSE&~((ECBI_PROJECTILE_MAGIC|ECBI_PROJECTILE_SOLID)< stop enemy on (EStop) : { jump Inactive(); } // warn for all obsolete events on (EStartAttack) : { //CPrintF("%s: StartAttack event is obsolete!\n", GetName()); resume; } on (EStopAttack) : { //CPrintF("%s: StopAttack event is obsolete!\n", GetName()); resume; } } }; // not doing anything, waiting until some player comes close enough to start patroling or similar Inactive(EVoid) { // stop moving StopMoving(); StandingAnim(); // start watching GetWatcher()->SendEvent(EStart()); // wait forever wait() { on (EBegin) : { resume; } // if watcher detects that a player is near on (EStart) : { // become active (patroling etc.) jump Active(); } // if returned from wounding on (EReturn) : { // become active (this will attack the enemy) jump Active(); } // if triggered manually on (ETrigger eTrigger) : { CEntity *penCaused = FixupCausedToPlayer(this, eTrigger.penCaused); // if can set the trigerer as soft target if (SetTargetSoft(penCaused)) { // become active (this will attack the player) jump Active(); } } // if you get damaged by someone on (EDamage eDamage) : { // if can set the damager as hard target if (SetTargetHard(eDamage.penInflictor)) { // notify wounding to others WoundedNotify(eDamage); // make pain sound WoundSound(); // play wounding animation call BeWounded(eDamage); } return; } } }; // overridable called before main enemy loop actually begins PreMainLoop(EVoid) { return EReturn(); } // main entry point for enemy behavior MainLoop(EVoid) { // setup some model parameters that are global for all enemies SizeModel(); // check that max health is properly set ASSERT(m_fMaxHealth==GetHealth() || IsOfClass(this, "Devil") || IsOfClass(this, "ExotechLarva") || IsOfClass(this, "AirElemental") || IsOfClass(this, "Summoner")); // normalize parameters if (m_tmReflexMin<0) { m_tmReflexMin = 0.0f; } if (m_tmReflexMin>m_tmReflexMax) { m_tmReflexMax = m_tmReflexMin; } // adjust falldown and step up values if (m_fStepHeight==-1) { m_fStepHeight = 2.0f; } // if this is a template if (m_bTemplate) { // do nothing at all return; } // wait for just one tick // NOTES: // if spawned, we have to wait a bit after spawning for spawner to teleport us into proper place // if placed directly, we have to wait for game to start (not to start behaving while in editor) // IMPORTANT: // this wait amount has to be lower than the one in music holder, so that the enemies are initialized before // they get counted autowait(_pTimer->TickQuantum); // spawn your watcher m_penWatcher = CreateEntity(GetPlacement(), CLASS_WATCHER); EWatcherInit eInitWatcher; eInitWatcher.penOwner = this; GetWatcher()->Initialize(eInitWatcher); // switch to next marker (enemies usually point to the marker they stand on) if (m_penMarker!=NULL && IsOfClass(m_penMarker, "Enemy Marker")) { CEnemyMarker *pem = (CEnemyMarker *)&*m_penMarker; m_penMarker = pem->m_penTarget; } // store starting position m_vStartPosition = GetPlacement().pl_PositionVector; // set sound default parameters m_soSound.Set3DParameters(80.0f, 5.0f, 1.0f, 1.0f); // adjust falldown and step up values en_fStepUpHeight = m_fStepHeight+0.01f; en_fStepDnHeight = m_fFallHeight+0.01f; // let derived class(es) adjust parameters if needed EnemyPostInit(); // adjust your difficulty AdjustDifficulty(); // check enemy params ASSERT(m_fStopDistance>=0); ASSERT(m_fCloseDistance>=0); ASSERT(m_fAttackDistance>m_fCloseDistance); ASSERT(m_fIgnoreRange>m_fAttackDistance); SetPredictable(TRUE); autocall PreMainLoop() EReturn; jump StandardBehavior(); } StandardBehavior(EVoid) { // this is the main enemy loop wait() { // initially on (EBegin) : { // start in active or inactive state if (m_penEnemy!=NULL) { call Active(); } else { call Inactive(); } }; // if dead on (EDeath eDeath) : { // die jump Die(eDeath); } // if an entity exits a teleport nearby on (ETeleport et) : { // proceed message to watcher (so watcher can quickly recheck for players) GetWatcher()->SendEvent(et); resume; } // if should stop being blind on (EStopBlindness) : { // stop being blind m_bBlind = FALSE; resume; } // if should stop being deaf on (EStopDeafness) : { // stop being deaf m_bDeaf = FALSE; resume; } // support for jumping using bouncers on (ETouch eTouch) : { IfTargetCrushed(eTouch.penOther, (FLOAT3D&)eTouch.plCollision); if (IsOfClass(eTouch.penOther, "Bouncer")) { JumpFromBouncer(this, eTouch.penOther); } resume; } } }; // dummy main - never called Main(EVoid) { return; }; };