class StinkyController extends ScriptedController; var FtgGame FtgGame; var byte TeamIndex; // is set by FtgGame var StinkyClot StinkyClot; var array MoveTargets; var array AmmoCandidates; var transient KFAmmoPickup CurrentAmmoCandidate; var transient Actor LastAlternatePathTarget; var transient NavigationPoint LastAlternatePathPoint; var localized string BlameStr; function PostBeginPlay() { FtgGame = FtgGame(Level.Game); AmmoCandidates = FtgGame.AmmoPickups; super.PostBeginPlay(); } function Possess(Pawn aPawn) { super.Possess(aPawn); StinkyClot = StinkyClot(Pawn); if ( StinkyClot != none && FtgGame != none) { FtgGame.StinkyControllerReady(self); } } function TakeControlOf(Pawn aPawn) {} function Actor GetMoveTarget() { local Actor result; if ( ActionNum < MoveTargets.length ) { result = MoveTargets[ActionNum]; if ( result == LastAlternatePathTarget && LastAlternatePathPoint != none ) result = LastAlternatePathPoint; // target unreachable -> reroute to closest nav. point } return result; } function NavigationPoint FindClosestPathNode(Actor anActor) { local NavigationPoint N, BestN; local float NDistSquared, BestDistSquared; local bool bNVisible, bBestVisible; if ( anActor == none ) return none; for (N = Level.NavigationPointList; N != none; N = N.nextNavigationPoint) { if ( !N.IsA('PathNode') ) continue; // ignore teleporters, jumpads etc. NDistSquared = VSizeSquared(anActor.Location - N.Location); if ( NDistSquared < 250000 ) { // 10m or closer bNVisible = FastTrace(anActor.Location, N.Location); if ( bBestVisible && !bNVisible ) continue; // ignore invisible points if there are visible alteratives else if ( BestN == none || (bNVisible && !bBestVisible) || NDistSquared < BestDistSquared ) { if ( FtgGame.IsPathTargetValid(N) ) { BestN = N; BestDistSquared = NDistSquared; bBestVisible = bNVisible; } } } } return BestN; } // returns closest reachable point to a given actor function Actor FindAlternatePath(Actor anActor) { local NavigationPoint N; local Actor result; if ( LastAlternatePathTarget == anActor && LastAlternatePathPoint != none ) N = LastAlternatePathPoint; else { N = FindClosestPathNode(anActor); log("Unreachable actor " $ anActor $ " @ (" $ anActor.Location $ ") -> rerouting to " $ N, class.name); if ( TheGuardian(anActor) != none ) { FtgGame.ScrnBalanceMut.BlamePlayer(ScrnPlayerController(TheGuardian(anActor).FirstTouch), BlameStr); } } if ( N != none ) { result = FindPathToward(N, false); if ( result != none ) { // cache found navigation point for next calls LastAlternatePathTarget = anActor; LastAlternatePathPoint = N; } else { FtgGame.InvalidatePathTarget(N, true); // make sure we don't use this navigation point anymore LastAlternatePathPoint = none; } } return result; } function CompleteAction() { FtgGame.StinkyControllerCompeledAction(self, ActionNum++); } function TakeActor(Actor A) { A.SetBase(Pawn); Pawn.AttachToBone(A, StinkyClot.GrabBone); } function int CalcSpeed() { return Pawn.default.GroundSpeed; } function bool CanSpeedAdjust() { return false; } function float PlayCompleteAnimation() { if( Pawn.Physics==PHYS_Falling ) { Pawn.SetPhysics(PHYS_Walking); } Pawn.SetAnimAction('KnockDown'); // dunno why but the next anim doesn't work without this Pawn.SetAnimAction(StinkyClot.CompleteAnim); Pawn.Acceleration = vect(0, 0, 0); Pawn.Velocity.X = 0; Pawn.Velocity.Y = 0; Return 0.8; } function DoAdditionalActions() { } state LatentDeath { Begin: sleep(2.0); Pawn.Suicide(); } state Moving extends Scripting { ignores Timer; function SetMoveTarget() { Focus = ScriptedFocus; Target = GetMoveTarget(); if ( Target == None ) { Pawn.Suicide(); //GotoState('Broken'); return; } if ( Focus == None ) Focus = Target; MoveTarget = Target; if ( !ActorReachable(MoveTarget) ) { MoveTarget = FindPathToward(MoveTarget,false); if ( MoveTarget == None ) { // if we can't reach the target, then move to closest NavigationPoint MoveTarget = FindAlternatePath(Target); } if ( MoveTarget == None ) { AbortScript(); return; } if ( Focus == Target ) Focus = MoveTarget; } // Level.GetLocalPlayerController().ClientMessage("Moving to " $ GetItemName(string(MoveTarget)) $ " / " $ GetItemName(string(Target)), 'log'); } function CompleteAction() { global.CompleteAction(); } Begin: Pawn.SetMovementPhysics(); WaitForLanding(); KeepMoving: SetMoveTarget(); DoAdditionalActions(); Pawn.GroundSpeed = CalcSpeed(); Pawn.WaterSpeed = Pawn.GroundSpeed; Pawn.AirSpeed = Pawn.GroundSpeed; StinkyClot.HiddenGroundSpeed = Pawn.GroundSpeed; // MayShootTarget(); if ( MoveTarget != None && MoveTarget != Pawn ) { if ( StinkyClot.StuckCounter >= 25 && (MoveTarget == StinkyClot.LastMoveTarget[0] || MoveTarget == StinkyClot.LastMoveTarget[1]) ) { // force teleport if ( NextRoutePath != none && NextRoutePath.End != none ) { MoveTarget = NextRoutePath.End; // jump one path node forward // Level.GetLocalPlayerController().ClientMessage("Teleporting to next target " $ MoveTarget, 'log'); } StinkyClot.TeleportLocation = MoveTarget.Location; StinkyClot.TeleportLocation.Z += StinkyClot.CollisionHeight; StinkyClot.StartTeleport(); sleep(1.0); WaitForLanding(); } else { MoveToward(MoveTarget, Focus,,,Pawn.bIsWalking); } if ( !Pawn.ReachedDestination(GetMoveTarget()) ) { Goto('KeepMoving'); } // make sure the Stinky Clot won't teleport at this phase MoveTarget = none; StinkyClot.LastMoveTarget[0] = none; StinkyClot.LastMoveTarget[1] = none; } sleep( PlayCompleteAnimation() ); CompleteAction(); } state MoveToGuardian extends Moving { function int CalcSpeed() { if ( FtgGame.bWaveBossInProgress ) return 150; return min( Pawn.GroundSpeed + 3, 150 ) ; // each call move faster and faster } } state MoveToShop extends Moving { function AbortScript() { if ( ActionNum < MoveTargets.length-1 ) CompleteAction(); else super.AbortScript(); } function Actor GetMoveTarget() { if ( ActionNum < MoveTargets.length-1 && FtgGame.TotalMaxMonsters <= 0 && FtgGame.NumMonsters <= 16 + rand(16) ) { ActionNum = MoveTargets.length-1; // end of the wave -> move directly to the last target } return global.GetMoveTarget(); } function Actor FindAlternatePath(Actor anActor) { if ( KFAmmoPickup(anActor) != none ) { return none; // no alternate paths for ammo boxes -> simply go to next action } return super.FindAlternatePath(anActor); } function DoAdditionalActions() { local KFAmmoPickup ammo; local int i; if ( FtgGame.TSCGRI.MaxMonsters < 16 ) return; // no ammo spawning during end of the game for ( i = AmmoCandidates.length - 1; i >= 0; --i ) { ammo = AmmoCandidates[i]; if ( abs(Pawn.Location.Z - ammo.Location.Z) < 100 && VSizeSquared(Pawn.Location - ammo.Location) < 1000000 // 20m && Pawn.FastTrace(Pawn.Location, ammo.Location) ) { AmmoCandidates.remove(i, 1); if ( ammo.bSleeping ) { CurrentAmmoCandidate = ammo; GotoState( 'MoveToAmmo', 'Begin' ); // go for ammo } // else ammo is already spawned return; } } } function int CalcSpeed() { local TSCBaseGuardian gnome; gnome = FtgGame.TeamBases[TeamIndex]; if ( FtgGame.TotalMaxMonsters <=0 ) { if ( FtgGame.NumMonsters < 10 ) return 150; else if ( gnome.SameTeamCounter + 5 < gnome.default.SameTeamCounter) return 35; // slowdown when nobody at the base to give team a chance to reach the base else return 100; } else if ( gnome.SameTeamCounter + 5 < gnome.default.SameTeamCounter) return 35; // slowdown when nobody at the base to give team a chance to reach the base else if ( FtgGame.TotalMaxMonsters < 50 ) return Lerp( FtgGame.TotalMaxMonsters/50.0 , 100, 35 ); else return 35; } function CompleteAction() { AmmoCandidates = FtgGame.AmmoPickups; // allow respawing ammo boxes global.CompleteAction(); } } state MoveToAmmo extends Moving { function BeginState() { super.BeginState(); SetTimer(30, false); } function EndState() { super.EndState(); SetTimer(0, false); } function Timer() { CurrentAmmoCandidate = none; GotoState('MoveToShop', 'Begin'); // abort ammo get } function Actor GetMoveTarget() { return CurrentAmmoCandidate; } function AbortScript() { // if can't reach ammo box, then just exit the state intead of aborting the entire script CompleteAction(); } function Actor FindAlternatePath(Actor anActor) { return none; } function CompleteAction() { CurrentAmmoCandidate.GotoState('Pickup'); CurrentAmmoCandidate = none; GotoState('MoveToShop', 'Begin'); // get back to the mision } } defaultproperties { TeamIndex=1 BlameStr="%p blamed for placing base in a glitch spot!" }