613 lines
16 KiB
Ucode
613 lines
16 KiB
Ucode
Class HardPat extends ZombieBoss;
|
|
|
|
var transient float GiveUpTime;
|
|
var byte MissilesLeft;
|
|
var bool bEndGameBoss,bMovingChaingunAttack;
|
|
var(Sounds) sound SaveMeSound;
|
|
|
|
replication
|
|
{
|
|
reliable if( ROLE==ROLE_AUTHORITY )
|
|
bMovingChaingunAttack;
|
|
}
|
|
|
|
simulated function PostBeginPlay()
|
|
{
|
|
super.PostBeginPlay();
|
|
SyringeCount = 3; // no healing for mid-game bosses
|
|
Health *= 0.75; // less hp for mid-game bosses
|
|
}
|
|
|
|
function bool MakeGrandEntry()
|
|
{
|
|
Health = HealthMax; // restore original hp
|
|
bEndGameBoss = true;
|
|
SyringeCount = 0; // restore healing for end-game boss
|
|
return Super.MakeGrandEntry();
|
|
}
|
|
function Died(Controller Killer, class<DamageType> damageType, vector HitLocation)
|
|
{
|
|
Super(KFMonster).Died(Killer,damageType,HitLocation);
|
|
if ( bEndGameBoss )
|
|
KFGameType(Level.Game).DoBossDeath();
|
|
}
|
|
|
|
simulated function bool HitCanInterruptAction()
|
|
{
|
|
return (!bWaitForAnim && !bShotAnim);
|
|
}
|
|
|
|
function RangedAttack(Actor A)
|
|
{
|
|
local float D;
|
|
local bool bOnlyE;
|
|
local bool bDesireChainGun;
|
|
|
|
// Randomly make him want to chaingun more
|
|
if( Controller.LineOfSightTo(A) && FRand() < 0.15 && LastChainGunTime<Level.TimeSeconds )
|
|
{
|
|
bDesireChainGun = true;
|
|
}
|
|
|
|
if ( bShotAnim )
|
|
{
|
|
if( !IsAnimating(ExpectingChannel) )
|
|
bShotAnim = false;
|
|
return;
|
|
}
|
|
D = VSize(A.Location-Location);
|
|
bOnlyE = (Pawn(A)!=None && OnlyEnemyAround(Pawn(A)));
|
|
if ( IsCloseEnuf(A) )
|
|
{
|
|
bShotAnim = true;
|
|
if( Health>1500 && Pawn(A)!=None && FRand() < 0.5 )
|
|
{
|
|
SetAnimAction('MeleeImpale');
|
|
}
|
|
else
|
|
{
|
|
SetAnimAction('MeleeClaw');
|
|
//PlaySound(sound'Claw2s', SLOT_None); KFTODO: Replace this
|
|
}
|
|
}
|
|
else if( Level.TimeSeconds>LastSneakedTime )
|
|
{
|
|
if( FRand() < 0.3 )
|
|
{
|
|
// Wait another 20-40 to try this again
|
|
LastSneakedTime = Level.TimeSeconds+20.f+FRand()*20;
|
|
Return;
|
|
}
|
|
SetAnimAction('transition');
|
|
GoToState('SneakAround');
|
|
}
|
|
else if( bChargingPlayer && (bOnlyE || D<200) )
|
|
Return;
|
|
else if( !bDesireChainGun && !bChargingPlayer && (D<300 || (D<700 && bOnlyE)) &&
|
|
(Level.TimeSeconds - LastChargeTime > (5.0 + 5.0 * FRand())) ) // Don't charge again for a few seconds
|
|
{
|
|
SetAnimAction('transition');
|
|
GoToState('Charging');
|
|
}
|
|
else if( LastMissileTime<Level.TimeSeconds && (D>500 || SyringeCount>=2) )
|
|
{
|
|
if( !Controller.LineOfSightTo(A) || FRand() > 0.75 )
|
|
{
|
|
LastMissileTime = Level.TimeSeconds+FRand() * 5;
|
|
Return;
|
|
}
|
|
|
|
LastMissileTime = Level.TimeSeconds + 10 + FRand() * 15;
|
|
|
|
bShotAnim = true;
|
|
Acceleration = vect(0,0,0);
|
|
SetAnimAction('PreFireMissile');
|
|
|
|
HandleWaitForAnim('PreFireMissile');
|
|
|
|
GoToState('FireMissile');
|
|
}
|
|
else if ( !bWaitForAnim && !bShotAnim && LastChainGunTime<Level.TimeSeconds )
|
|
{
|
|
if ( !Controller.LineOfSightTo(A) || FRand()> 0.85 )
|
|
{
|
|
LastChainGunTime = Level.TimeSeconds+FRand()*4;
|
|
Return;
|
|
}
|
|
|
|
LastChainGunTime = Level.TimeSeconds + 5 + FRand() * 10;
|
|
|
|
bShotAnim = true;
|
|
Acceleration = vect(0,0,0);
|
|
SetAnimAction('PreFireMG');
|
|
|
|
HandleWaitForAnim('PreFireMG');
|
|
if ( bEndGameBoss )
|
|
MGFireCounter = Rand(60) + 35*(SyringeCount+1);
|
|
else
|
|
MGFireCounter = Rand(60) + 60;
|
|
|
|
|
|
GoToState('FireChaingun');
|
|
}
|
|
}
|
|
|
|
simulated function bool AnimNeedsWait(name TestAnim)
|
|
{
|
|
if( TestAnim == 'FireMG' )
|
|
return !bMovingChaingunAttack;
|
|
return Super.AnimNeedsWait(TestAnim);
|
|
}
|
|
simulated final function rotator GetBoneTransRot()
|
|
{
|
|
local rotator R;
|
|
|
|
R.Yaw = 24576+Rotation.Yaw;
|
|
R.Pitch = 16384+Rotation.Pitch;
|
|
return R;
|
|
}
|
|
simulated function int DoAnimAction( name AnimName )
|
|
{
|
|
if( AnimName=='FireMG' && bMovingChaingunAttack )
|
|
{
|
|
AnimBlendParams(1, 1.0, 0.0,, FireRootBone, True);
|
|
//SetBoneDirection(FireRootBone,GetBoneTransRot(),,1,1);
|
|
PlayAnim('FireMG',, 0.f, 1);
|
|
return 1;
|
|
}
|
|
else if( AnimName=='FireEndMG' )
|
|
{
|
|
//SetBoneDirection(FireRootBone,rot(0,0,0),,0,0);
|
|
AnimBlendParams(1, 0);
|
|
}
|
|
return Super.DoAnimAction( AnimName );
|
|
}
|
|
simulated function AnimEnd(int Channel)
|
|
{
|
|
local name Sequence;
|
|
local float Frame, Rate;
|
|
|
|
if( Level.NetMode==NM_Client && bMinigunning )
|
|
{
|
|
GetAnimParams( Channel, Sequence, Frame, Rate );
|
|
|
|
if( Sequence != 'PreFireMG' && Sequence != 'FireMG' )
|
|
{
|
|
//SetBoneDirection(FireRootBone,rot(0,0,0),,0,0);
|
|
Super(KFMonster).AnimEnd(Channel);
|
|
return;
|
|
}
|
|
|
|
if( bMovingChaingunAttack )
|
|
DoAnimAction('FireMG');
|
|
else
|
|
{
|
|
PlayAnim('FireMG');
|
|
bWaitForAnim = true;
|
|
bShotAnim = true;
|
|
IdleTime = Level.TimeSeconds;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//SetBoneDirection(FireRootBone,rot(0,0,0),,0,0);
|
|
Super(KFMonster).AnimEnd(Channel);
|
|
}
|
|
}
|
|
|
|
// Fix: Don't spawn needle before last stage.
|
|
simulated function NotifySyringeA()
|
|
{
|
|
if( Level.NetMode!=NM_Client )
|
|
{
|
|
if( SyringeCount<3 )
|
|
SyringeCount++;
|
|
if( Level.NetMode!=NM_DedicatedServer )
|
|
PostNetReceive();
|
|
}
|
|
if( Level.NetMode!=NM_DedicatedServer )
|
|
DropNeedle();
|
|
}
|
|
simulated function NotifySyringeC()
|
|
{
|
|
if( Level.NetMode!=NM_DedicatedServer )
|
|
{
|
|
CurrentNeedle = Spawn(Class'BossHPNeedle');
|
|
CurrentNeedle.Velocity = vect(-45,300,-90) >> Rotation;
|
|
DropNeedle();
|
|
}
|
|
}
|
|
|
|
simulated function ZombieCrispUp() // Don't become crispy.
|
|
{
|
|
bAshen = true;
|
|
bCrispified = true;
|
|
SetBurningBehavior();
|
|
}
|
|
|
|
state KnockDown
|
|
{
|
|
Ignores RangedAttack;
|
|
|
|
Begin:
|
|
if ( Health > 0 )
|
|
{
|
|
Sleep(GetAnimDuration('KnockDown'));
|
|
CloakBoss();
|
|
PlaySound(SaveMeSound, SLOT_Misc, 2.0,,500.0);
|
|
|
|
if ( KFGameType(Level.Game).FinalSquadNum == SyringeCount )
|
|
{
|
|
KFGameType(Level.Game).AddBossBuddySquad();
|
|
}
|
|
|
|
GotoState('Escaping');
|
|
}
|
|
else
|
|
{
|
|
GotoState('');
|
|
}
|
|
}
|
|
state FireChaingun
|
|
{
|
|
function BeginState()
|
|
{
|
|
Super.BeginState();
|
|
bMovingChaingunAttack = (SyringeCount>=2);
|
|
bChargingPlayer = bEndGameBoss && SyringeCount>=3 && FRand()<0.4f;
|
|
bCanStrafe = true;
|
|
}
|
|
function EndState()
|
|
{
|
|
bChargingPlayer = false;
|
|
Super.EndState();
|
|
bMovingChaingunAttack = false;
|
|
bCanStrafe = false;
|
|
}
|
|
function Tick( float Delta )
|
|
{
|
|
Super(KFMonster).Tick(Delta);
|
|
if( bChargingPlayer )
|
|
GroundSpeed = OriginalGroundSpeed * 2.3;
|
|
else GroundSpeed = OriginalGroundSpeed * 1.15;
|
|
}
|
|
function AnimEnd( int Channel )
|
|
{
|
|
if( MGFireCounter <= 0 )
|
|
{
|
|
bShotAnim = true;
|
|
Acceleration = vect(0,0,0);
|
|
SetAnimAction('FireEndMG');
|
|
HandleWaitForAnim('FireEndMG');
|
|
GoToState('');
|
|
}
|
|
else if( bMovingChaingunAttack )
|
|
{
|
|
if( bFireAtWill && Channel!=1 )
|
|
return;
|
|
if( Controller.Target!=None )
|
|
Controller.Focus = Controller.Target;
|
|
bShotAnim = false;
|
|
bFireAtWill = True;
|
|
SetAnimAction('FireMG');
|
|
}
|
|
else
|
|
{
|
|
if ( Controller.Enemy != none )
|
|
{
|
|
if ( Controller.LineOfSightTo(Controller.Enemy) && FastTrace(GetBoneCoords('tip').Origin,Controller.Enemy.Location))
|
|
{
|
|
MGLostSightTimeout = 0.0;
|
|
Controller.Focus = Controller.Enemy;
|
|
Controller.FocalPoint = Controller.Enemy.Location;
|
|
}
|
|
else
|
|
{
|
|
MGLostSightTimeout = Level.TimeSeconds + (0.25 + FRand() * 0.35);
|
|
Controller.Focus = None;
|
|
}
|
|
Controller.Target = Controller.Enemy;
|
|
}
|
|
else
|
|
{
|
|
MGLostSightTimeout = Level.TimeSeconds + (0.25 + FRand() * 0.35);
|
|
Controller.Focus = None;
|
|
}
|
|
|
|
if( !bFireAtWill )
|
|
{
|
|
MGFireDuration = Level.TimeSeconds + (0.75 + FRand() * 0.5);
|
|
}
|
|
else if ( FRand() < 0.03 && Controller.Enemy != none && PlayerController(Controller.Enemy.Controller) != none )
|
|
{
|
|
// Randomly send out a message about Patriarch shooting chain gun(3% chance)
|
|
PlayerController(Controller.Enemy.Controller).Speech('AUTO', 9, "");
|
|
}
|
|
|
|
bFireAtWill = True;
|
|
bShotAnim = true;
|
|
Acceleration = vect(0,0,0);
|
|
|
|
SetAnimAction('FireMG');
|
|
bWaitForAnim = true;
|
|
}
|
|
}
|
|
function TakeDamage( int Damage, Pawn InstigatedBy, Vector Hitlocation, Vector Momentum, class<DamageType> damageType, optional int HitIndex)
|
|
{
|
|
local float EnemyDistSq, DamagerDistSq;
|
|
global.TakeDamage(Damage,instigatedBy,hitlocation,Momentum,damageType);
|
|
if( bMovingChaingunAttack || Health<=0 )
|
|
return;
|
|
|
|
// if someone close up is shooting us, just charge them
|
|
if( InstigatedBy != none )
|
|
{
|
|
DamagerDistSq = VSizeSquared(Location - InstigatedBy.Location);
|
|
|
|
if( (ChargeDamage > 200 && DamagerDistSq < (500 * 500)) || DamagerDistSq < (100 * 100) )
|
|
{
|
|
SetAnimAction('transition');
|
|
GoToState('Charging');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if( Controller.Enemy != none && InstigatedBy != none && InstigatedBy != Controller.Enemy )
|
|
{
|
|
EnemyDistSq = VSizeSquared(Location - Controller.Enemy.Location);
|
|
DamagerDistSq = VSizeSquared(Location - InstigatedBy.Location);
|
|
}
|
|
|
|
if( InstigatedBy != none && (DamagerDistSq < EnemyDistSq || Controller.Enemy == none) )
|
|
{
|
|
MonsterController(Controller).ChangeEnemy(InstigatedBy,Controller.CanSee(InstigatedBy));
|
|
Controller.Target = InstigatedBy;
|
|
Controller.Focus = InstigatedBy;
|
|
|
|
if( DamagerDistSq < (500 * 500) )
|
|
{
|
|
SetAnimAction('transition');
|
|
GoToState('Charging');
|
|
}
|
|
}
|
|
}
|
|
|
|
Begin:
|
|
While( True )
|
|
{
|
|
if( !bMovingChaingunAttack )
|
|
Acceleration = vect(0,0,0);
|
|
|
|
if( MGLostSightTimeout > 0 && Level.TimeSeconds > MGLostSightTimeout )
|
|
{
|
|
Acceleration = vect(0,0,0);
|
|
bShotAnim = true;
|
|
Acceleration = vect(0,0,0);
|
|
SetAnimAction('FireEndMG');
|
|
HandleWaitForAnim('FireEndMG');
|
|
GoToState('');
|
|
}
|
|
|
|
if( MGFireCounter <= 0 )
|
|
{
|
|
bShotAnim = true;
|
|
Acceleration = vect(0,0,0);
|
|
SetAnimAction('FireEndMG');
|
|
HandleWaitForAnim('FireEndMG');
|
|
GoToState('');
|
|
}
|
|
|
|
// Give some randomness to the patriarch's firing (constantly fire after first stage passed)
|
|
if( Level.TimeSeconds > MGFireDuration && SyringeCount==0 )
|
|
{
|
|
if( AmbientSound != MiniGunSpinSound )
|
|
{
|
|
SoundVolume=185;
|
|
SoundRadius=200;
|
|
AmbientSound = MiniGunSpinSound;
|
|
}
|
|
Sleep(0.5 + FRand() * 0.75);
|
|
MGFireDuration = Level.TimeSeconds + (0.75 + FRand() * 0.5);
|
|
}
|
|
else
|
|
{
|
|
if( bFireAtWill )
|
|
FireMGShot();
|
|
Sleep(0.05);
|
|
}
|
|
}
|
|
}
|
|
|
|
state FireMissile
|
|
{
|
|
function RangedAttack(Actor A)
|
|
{
|
|
if( SyringeCount>=2 )
|
|
{
|
|
Controller.Target = A;
|
|
Controller.Focus = A;
|
|
}
|
|
}
|
|
function BeginState()
|
|
{
|
|
if ( bEndGameBoss )
|
|
MissilesLeft = SyringeCount+Rand(SyringeCount);
|
|
else
|
|
MissilesLeft = 1+Rand(3);
|
|
Acceleration = vect(0,0,0);
|
|
}
|
|
|
|
function AnimEnd( int Channel )
|
|
{
|
|
local vector Start;
|
|
local Rotator R;
|
|
|
|
Start = GetBoneCoords('tip').Origin;
|
|
if( Controller.Target==None )
|
|
Controller.Target = Controller.Enemy;
|
|
|
|
if ( !SavedFireProperties.bInitialized )
|
|
{
|
|
SavedFireProperties.AmmoClass = MyAmmo.Class;
|
|
SavedFireProperties.ProjectileClass = Class'BossLAWProj';
|
|
SavedFireProperties.WarnTargetPct = 0.15;
|
|
SavedFireProperties.MaxRange = 10000;
|
|
SavedFireProperties.bTossed = False;
|
|
SavedFireProperties.bLeadTarget = True;
|
|
SavedFireProperties.bInitialized = true;
|
|
}
|
|
SavedFireProperties.bInstantHit = (SyringeCount<1);
|
|
SavedFireProperties.bTrySplash = (SyringeCount>=2);
|
|
|
|
R = AdjustAim(SavedFireProperties,Start,100);
|
|
PlaySound(RocketFireSound,SLOT_Interact,2.0,,TransientSoundRadius,,false);
|
|
Spawn(Class'BossLAWProj',,,Start,R);
|
|
|
|
bShotAnim = true;
|
|
Acceleration = vect(0,0,0);
|
|
SetAnimAction('FireEndMissile');
|
|
HandleWaitForAnim('FireEndMissile');
|
|
|
|
// Randomly send out a message about Patriarch shooting a rocket(5% chance)
|
|
if ( FRand() < 0.05 && Controller.Enemy != none && PlayerController(Controller.Enemy.Controller) != none )
|
|
{
|
|
PlayerController(Controller.Enemy.Controller).Speech('AUTO', 10, "");
|
|
}
|
|
|
|
if( MissilesLeft==0 )
|
|
GoToState('');
|
|
else
|
|
{
|
|
--MissilesLeft;
|
|
GoToState(,'SecondMissile');
|
|
}
|
|
}
|
|
Begin:
|
|
while ( true )
|
|
{
|
|
Acceleration = vect(0,0,0);
|
|
Sleep(0.1);
|
|
}
|
|
SecondMissile:
|
|
Acceleration = vect(0,0,0);
|
|
Sleep(0.5f);
|
|
AnimEnd(0);
|
|
}
|
|
|
|
State Escaping // Added god-mode if bEndGameBoss
|
|
{
|
|
Ignores RangedAttack;
|
|
|
|
function BeginState()
|
|
{
|
|
GiveUpTime = Level.TimeSeconds+20.f+FRand()*20.f;
|
|
Super.BeginState();
|
|
if ( bEndGameBoss ) {
|
|
bBlockActors = false;
|
|
// While escaping Pat doesn't blow up pipebombs -- PooSH
|
|
MotionDetectorThreat=0;
|
|
}
|
|
}
|
|
function EndState()
|
|
{
|
|
Super.EndState();
|
|
if( Health>0 )
|
|
bBlockActors = true;
|
|
MotionDetectorThreat = default.MotionDetectorThreat;
|
|
}
|
|
function Tick( float Delta )
|
|
{
|
|
if( Level.TimeSeconds>GiveUpTime )
|
|
{
|
|
BeginHealing();
|
|
return;
|
|
}
|
|
if( !bChargingPlayer )
|
|
{
|
|
bChargingPlayer = true;
|
|
if( Level.NetMode!=NM_DedicatedServer )
|
|
PostNetReceive();
|
|
}
|
|
GroundSpeed = OriginalGroundSpeed * 2.5;
|
|
Global.Tick(Delta);
|
|
}
|
|
|
|
function TakeDamage( int Damage, Pawn InstigatedBy, Vector Hitlocation, Vector Momentum, class<DamageType> damageType, optional int HitIndex)
|
|
{
|
|
if ( !bEndGameBoss )
|
|
global.TakeDamage(Damage,instigatedBy,hitlocation,Momentum,damageType);
|
|
}
|
|
}
|
|
|
|
State SneakAround
|
|
{
|
|
function BeginState()
|
|
{
|
|
super.BeginState();
|
|
SneakStartTime = Level.TimeSeconds+10.f+FRand()*15.f;
|
|
}
|
|
function EndState()
|
|
{
|
|
super.EndState();
|
|
LastSneakedTime = Level.TimeSeconds+20.f+FRand()*30.f;
|
|
if( Controller!=None && Controller.IsInState('PatFindWay') )
|
|
Controller.GoToState('ZombieHunt');
|
|
}
|
|
function TakeDamage( int Damage, Pawn InstigatedBy, Vector Hitlocation, Vector Momentum, class<DamageType> damageType, optional int HitIndex)
|
|
{
|
|
global.TakeDamage(Damage,instigatedBy,hitlocation,Momentum,damageType);
|
|
if( Health<=0 )
|
|
return;
|
|
|
|
// if someone close up is shooting us, just charge them
|
|
if( InstigatedBy!=none && VSizeSquared(Location - InstigatedBy.Location)<62500 )
|
|
GoToState('Charging');
|
|
}
|
|
|
|
Begin:
|
|
CloakBoss();
|
|
if( SyringeCount>=2 && FRand()<0.6 )
|
|
HardPatController(Controller).FindPathAround();
|
|
While( true )
|
|
{
|
|
Sleep(0.5);
|
|
|
|
if( !bCloaked && !bShotAnim )
|
|
CloakBoss();
|
|
if( !Controller.IsInState('PatFindWay') )
|
|
{
|
|
if( Level.TimeSeconds>SneakStartTime )
|
|
GoToState('');
|
|
if( !Controller.IsInState('WaitForAnim') && !Controller.IsInState('ZombieHunt') )
|
|
Controller.GoToState('ZombieHunt');
|
|
}
|
|
else SneakStartTime = Level.TimeSeconds+30.f;
|
|
}
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
SaveMeSound=SoundGroup'KF_EnemiesFinalSnd.Patriarch.Kev_SaveMe'
|
|
RocketFireSound=SoundGroup'KF_EnemiesFinalSnd.Patriarch.Kev_FireRocket'
|
|
MiniGunFireSound=Sound'KF_BasePatriarch.Attack.Kev_MG_GunfireLoop'
|
|
MiniGunSpinSound=Sound'KF_BasePatriarch.Attack.Kev_MG_TurbineFireLoop'
|
|
MeleeImpaleHitSound=SoundGroup'KF_EnemiesFinalSnd.Patriarch.Kev_HitPlayer_Impale'
|
|
MoanVoice=SoundGroup'KF_EnemiesFinalSnd.Patriarch.Kev_Talk'
|
|
MeleeAttackHitSound=SoundGroup'KF_EnemiesFinalSnd.Patriarch.Kev_HitPlayer_Fist'
|
|
JumpSound=SoundGroup'KF_EnemiesFinalSnd.Patriarch.Kev_Jump'
|
|
DetachedArmClass=Class'KFChar.SeveredArmPatriarch'
|
|
DetachedLegClass=Class'KFChar.SeveredLegPatriarch'
|
|
DetachedHeadClass=Class'KFChar.SeveredHeadPatriarch'
|
|
DetachedSpecialArmClass=Class'KFChar.SeveredRocketArmPatriarch'
|
|
HitSound(0)=SoundGroup'KF_EnemiesFinalSnd.Patriarch.Kev_Pain'
|
|
DeathSound(0)=SoundGroup'KF_EnemiesFinalSnd.Patriarch.Kev_Death'
|
|
ScoringValue=1000
|
|
MenuName="Hard Pat"
|
|
ControllerClass=Class'ScrnZedPack.HardPatController'
|
|
AmbientSound=Sound'KF_BasePatriarch.Idle.Kev_IdleLoop'
|
|
Mesh=SkeletalMesh'KF_Freaks_Trip.Patriarch_Freak'
|
|
LODBias=4.000000
|
|
Skins(0)=Combiner'KF_Specimens_Trip_T.gatling_cmb'
|
|
Skins(1)=Combiner'KF_Specimens_Trip_T.patriarch_cmb'
|
|
}
|