//=============================================================================
// DawnsNoSeeMover.
// Version: 1.0
// $DateTime: Sun, 02/08/2009 12:02:17 pm$
// Author: Dawn Knight
// UT Version Coded/Tested: 4.36.
//
// A timed Mover that steps through KeyFrames, making instantaneous moves from
// one KeyFrame to the next, but only if no Player can see the change.  The
// KeyFrames are looped so that Key N steps back to Key 1 when reached.
//
// How do you test whether a Player can see a Mover?  There are a number of
// builtin functions within UT that look promising for this task:
//
// Actor.PlayerCanSeeMe()
// Actor.Trace()
// Actor.FastTrace()
// Actor.VisibleActors()
// Actor.VisibleCollidingActors()
// Actor.TraceActors()
// Pawn.LineOfSightTo()
// Pawn.CanSee()
//
// Of these, only Actor.TraceActors() is capable of working correctly for a
// Mover, but it is limited to the Mover's Location and not its extents or
// geometric volume.  Movers do not have a 'hook' in UScript to obtain their
// boundaries or geometric volume, just their Location, so there is no way to
// write a true 'PlayerCanSeeMe' function for them.
// A suitable, though somewhat tedious, solution to the problem of determining
// whether a Player can see any particular part of a Mover is to create a precise
// 'swarm' of sentries within the map that the Mover can call
// Sentry.PlayerCanSeeMe() functions on.  In the absence of any sentrys, the
// Mover can fallback to just using Actor.TraceActors() to which, while only
// testing the Location of the Mover, will provide limited  evidence of a
// Player's abilty to actually see the Mover.
//
// UPDATE:  Actor.PlayerCanSeeMe() doesn't work well even for
// Class 'DawnsNoSeeSentry', which is a subclass of Keypoint with bCollideActors
// set to true.  The problem is PlayerCanSeeMe() has a built in radius of about
// 900 Units which I determined by thorough testing.
//
// Pawn.CanSee() does everything I want it to do, including taking a Player's
// peripheral vision into consideration, but it is FLAKY -- I can stare directly
// at 2 or 3 Sentrys and find spots where I can stand and the function will
// return False on all of them!  I tested it over and over and made a number of
// adjustments to the Map, the Sentrys, and the code and the problem remained.
//
// So, the new strategy is to use the iterator: Actor.TraceActors(), and to loop
// through each Player and each Sentry, forming Trace lines from the Player's
// eyes to Top, Center, and Bottom of the Sentry, using the Sentry's Collision
// Height, testing for Sentry to be returned.  This seems to work flawlessly and
// runs without noticeable slowdown.
//
// The next step is to test if the Player is facing within +-90 degrees to the
// HitNorm so that Players facing away can be excluded.
//
// (vector(aPawn.ViewRotation) dot HitNorm) < 0, where HitNorm is the normalized
// vector from aPawn to the Hit Actor is the test for whether Hit Actor is in
// the front of aPawn, meaning it can potentially be seen.  It corresponds to an
// FOV of just less than 180 degrees.  This seems to work flawlessly!
//
// Another thing to check for is whether any Player is standing on (.Base==) this
// Mover to prevent unintended moves.
//
//
// DawnsNoSeeMovers can be quite large and complex by virtue of their nature as
// 2 or more corridors/pathways through a map.  As such, they may overwhelm the
// engine and not render properly (improper lighting or invisible polys), so a
// solution may be to break the DNSMover up into 2 or more DNSMovers that move
// as a whole.  One Mover becomes the Master and the others become Slaves that
// nevertheless must have their visibility to Players checked and Players must
// be checked for whether they are Based on any of the Slaves.
//
// A significant code and network bandwidth improvement is realized by
// replicating KeyNum to Clients, so use new var KeyNumC throughout all this
// Class's code instead of KeyNum.  Assign KeyNumC = KeyNum once during
// initialization and use KeyNumC thereafter.
//
//
// Since movement of a DawnsNoSeeMover affects Bots navigating along Pathnodes,
// a custom LiftCenter, DawnsNSMCenter, can be used to prevent Bots from getting
// stuck trying to move along a blocked path.  DawnsNoSeeMovers maintain a list
// of DawnsNSMCenters with matching Tags and broadcast which KeyNumC they are
// moving to.  The DawnsNSMCenters check the KeyNumC against var() bool
// bValidPath[KeyNumC] and set ExtraCost = 0 if True, 100000000 otherwise.
// Use DawnsNSMCenters along with LiftExits with matching LiftTags -- they
// define the branching of the possible paths from PathNode fork points and only
// those that represent valid paths when the DawnsNoSeeMover is at KeyNumC
// should have bValidPath[KeyNumC] set True.
//                               ***WARNING ***
// The rules for using LiftExits and DawnsNSMCenters are VERY strict!
// Every path consists of LiftExit-DawnsNSMCenter-LiftExit, which are linked by
// setting all 3 of their LiftTags to the same unique Name.  Additionally, the
// DawnsNSMCenter needs its Tag (don't confuse with LiftTag) set to the Event of
// the DawnsNoSeeMover so that the Mover can control the switching of the
// DawnsNSMCenter on and off per KeyFrame. Set the DawnsNSMCenter's ValidPath[]s
// accordingly.  Be SURE that the only resulting path after building Paths is
// the one intended and that this path is not 'short circuited' by any other
// path, whether blue or red, including a path from LiftExit to LiftExit that
// bypasses the DawnsNSMCenter.  If this happens, the logic created by the
// DawnsNSMCenter will be bypassed and Bots will try to navigate the path when
// it is blocked.  Whereas the DawnsNSMCenter will only connect to LiftExits
// that share the same LiftTag and nothing else, LiftExits will connect to any
// valid NavigationPoint, regardless of Tag, LiftTag, or anything else.  You
// must prevent the LiftTags from connecting in unexpected and undesired ways to
// other NavigationPoints to prevent 'short circuiting' the DawnsNSMCenters.
// Refer to DM-DawnsFermiBELL_TimeLabs.unr for examples of the correct usage of
// DawnsNoSeeMovers, DawnsNoSeeSentrys, and DawnsNSMCenters.
//=============================================================================

class DawnsNoSeeMover extends Mover;

var(Mover) bool  bKeyNum0ForLightingOnly;
var(Mover) float PauseInterval;

var   DawnsNoSeeSentry     BaseSentry; // 1st Sentry whos Tag matches Event.
var   DawnsNoSeeMover      NextSlave;  // Link to next Slave Mover.
var   DawnsNoSeeMover      DLeader;    // Link to Leader (non Slave) Mover.
var   DawnsNSMCenter       BaseDNSMCenter; // 1st Center whos Tag matches Event.

var   byte        KeyNumC; // Specifies current KeyFrame index.

replication
{
	// Things the server should send to the client.
	reliable if( Role==ROLE_Authority && !bSlave )
		KeyNumC;
}

function Trigger( Actor other, Pawn EventInstigator ) {}

function UnTrigger( Actor other, Pawn EventInstigator ) {}

function FindTriggerActor() {}

function bool HandleTriggerDoor(pawn Other)
{
	return False;
}

function Actor SpecialHandling(Pawn Other)
{
	return None;
}

function InterpolateEnd( actor Other ) {}

function FinishNotify() {}

function FinishedClosing() {}

function FinishedOpening() {}

function DoOpen() {}

function DoClose() {}

function MakeGroupStop() {}

// Only called on DLeader on Role_Authority
function MakeGroupReturn()
{
	local DawnsNoSeeMover D;

	if( DLeader != self )
		return;

	// Abort move and reverse course.
	KeyNumC = PrevKeyNum;

	SetLocation( BasePos + KeyPos[KeyNumC] );
	SetRotation( BaseRot + KeyRot[KeyNumC] );

	for( D=NextSlave; D!=None; D=D.NextSlave )
	{
		D.SetLocation( D.BasePos + D.KeyPos[KeyNumC] );
		D.SetRotation( D.BaseRot + D.KeyRot[KeyNumC] );
	}
	if ( BaseDNSMCenter != None )
		BaseDNSMCenter.SetPath( KeyNumC );

  PrevKeyNum = KeyNumC;
	SetTimer( PauseInterval, False );
}

// Return true to abort, false to continue.
singular function bool EncroachingOn( actor Other )
{
	if ( Other.IsA('Carcass') || Other.IsA('Decoration') )
	{
		Other.TakeDamage(10000, None, Other.Location, vect(0,0,0), 'Crushed');
		return false;
	}
	if ( Other.IsA('Fragment') || (Other.IsA('Inventory') && (Other.Owner == None)) )
	{
		Other.Destroy();
		return false;
	}
	DLeader.MakeGroupReturn();
	return true;
}

function Bump( actor Other ) {}

function TakeDamage( int Damage, Pawn instigatedBy, Vector hitlocation,
						Vector momentum, name damageType) {}


// Initialization (Engine notifications)
// When mover enters gameplay.
simulated function BeginPlay()
{
	local DawnsNoSeeSentry aSentry;
	local DawnsNoSeeMover aDNSMover;
	local DawnsNSMCenter  aDNSMCenter;

	super(Actor).BeginPlay();

	if( event != '' && !bSlave)
	{
		// Create a linked list of associated Slave DNSMovers on Authority AND Clients
		foreach AllActors( class 'DawnsNoSeeMover', aDNSMover, Event )
		{
			if( aDNSMover.bSlave )
			{
				aDNSMover.NextSlave = NextSlave;
				NextSlave = aDNSMover;
			}
		}
	}

	// Init key info.
	if( bKeyNum0ForLightingOnly )
		KeyNumC = 1;
	else
		KeyNumC = 0;

	// Set initial location.
	SetLocation( BasePos + KeyPos[KeyNumC] );

	// Set initial rotation.
	SetRotation( BaseRot + KeyRot[KeyNumC] );

	if( bSlave || Role < ROLE_Authority )
		return;

	if( event != '' )
	{
		// Create a linked list of associated DawnsNoSeeSentrys.
		foreach AllActors( class 'DawnsNoSeeSentry', aSentry, Event )
		{
			if( BaseSentry == None )
				BaseSentry = aSentry;
			else
			{
				aSentry.NextSentry = BaseSentry.NextSentry;
				BaseSentry.NextSentry = aSentry;
			}
		}
		if( BaseSentry == None )
			Log( "No Sentries found for "$self$"!" );

		// Create a linked list of associated DawnsNoSeeNodes.
		foreach AllActors( class 'DawnsNSMCenter', aDNSMCenter, Event )
		{
			if( BaseDNSMCenter == None )
				BaseDNSMCenter = aDNSMCenter;
			else
			{
				aDNSMCenter.NextDNSMCenter = BaseDNSMCenter.NextDNSMCenter;
				BaseDNSMCenter.NextDNSMCenter = aDNSMCenter;
			}
		}
		if( BaseDNSMCenter == None )
			Log( "No DawnsNSMCenter found for "$self$"!" );
		else
			BaseDNSMCenter.SetPath( KeyNumC );
	}
	else
		Log( "No event defined for "$self$"!" );
}

// Called immediately after gameplay begins.
simulated function PostBeginPlay()
{
	local DawnsNoSeeMover M, aSlave;

	super(Actor).PostBeginPlay();

	if ( bSlave )
		return;

	// Set up all the Slave DNSMovers of this DNSMover
	DLeader = self;
	for( aSlave=NextSlave; aSlave!=None; aSlave=aSlave.NextSlave )
	{
		aSlave.DLeader = self;
		aSlave.GotoState('');
		aSlave.SetBase( self );
		aSlave.SetLocation( aSlave.BasePos + aSlave.KeyPos[KeyNumC] );
		aSlave.SetRotation( aSlave.BaseRot + aSlave.KeyRot[KeyNumC] );
	}
}

final function bool PlayerMightSee()
{
	local Pawn aPawn;
	local DawnsNoSeeMover  aM;
	local DawnsNoSeeSentry aS;
	local Actor A;
	local vector HitLoc, HitNorm, PEyeHght;

	// Check PlayerPawns for visibility and .Base of me or any of my Slaves
	for( aPawn=Level.PawnList; aPawn!=None; aPawn=aPawn.nextPawn )
	{
		if( !aPawn.IsA('PlayerPawn') && !aPawn.IsA('Bot') )
			continue;

		// Check if PlayerPawn is Based on me or any of my Slaves
		for( aM=self; aM!=None; aM=aM.NextSlave )
		{
			if( aPawn.Base == aM )
				return True;
		}
		// Get Location of PlayerPawn's EyeHeight
		PEyeHght = aPawn.Location+aPawn.EyeHeight*vect(0,0,1);

		// Check if PlayerPawn can see me or any of my Slaves
		for( aM=self; aM!=None; aM=aM.NextSlave )
		{
			foreach TraceActors( class'Actor', A, HitLoc, HitNorm,
					aM.Location, PEyeHght )
			{
				if( (A == aM ) && ((vector(aPawn.ViewRotation) dot HitNorm) < 0) )
				{
					return True;
				}
			}
		}

		// Check if PlayerPawn is touching or can see any of my Sentrys
		for( aS=BaseSentry; aS!=None; aS=aS.NextSentry )
		{
			// Touching a Sentry?
			if( aS.bCheckTouchingOnly )
			{
				foreach aS.TouchingActors( class'Actor', A )
				{
					if( A == aPawn )
						return True;
				}
				continue;
			}
			// Seeing a Sentry? ( Middle, Bottom, or Top )
			foreach TraceActors( class'Actor', A, HitLoc, HitNorm,
					aS.Location, PEyeHght )
			{
				if( A == aS && ((vector(aPawn.ViewRotation) dot HitNorm) < 0) )
				{
					return True;
				}
			}
			foreach TraceActors( class'Actor', A, HitLoc, HitNorm,
					aS.Location-aS.CollisionHeight*vect(0,0,1), PEyeHght )
			{
				if( A == aS && ((vector(aPawn.ViewRotation) dot HitNorm) < 0) )
				{
					return True;
				}
			}
			foreach TraceActors( class'Actor', A, HitLoc, HitNorm,
					aS.Location+aS.CollisionHeight*vect(0,0,1), PEyeHght )
			{
				if( A == aS && ((vector(aPawn.ViewRotation) dot HitNorm) < 0) )
				{
					return True;
				}
			}
		}
	}
	return False;
}

function Timer() // Runs only on non-Slave ROLE_Authority
{
  local DawnsNoSeeMover aSlave;

	if( PlayerMightSee() )
	{
		// Oops, try again a short time from now
		SetTimer( 0.5, False );
		return;
	}

	// Good, Players can't see me
	PrevKeyNum = KeyNumC;
	KeyNumC++;
	if( KeyNumC >= NumKeys )
	{
		if( bKeyNum0ForLightingOnly )
			KeyNumC = 1;
		else
			KeyNumC = 0;
	}
	SetLocation( BasePos + KeyPos[KeyNumC] );
	SetRotation( BaseRot + KeyRot[KeyNumC] );

	for( aSlave=NextSlave; aSlave!=None; aSlave=aSlave.NextSlave )
	{
		aSlave.SetLocation(aSlave.BasePos + aSlave.KeyPos[KeyNumC] );
		aSlave.SetRotation( aSlave.BaseRot + aSlave.KeyRot[KeyNumC] );
	}
	if ( BaseDNSMCenter != None )
  	BaseDNSMCenter.SetPath( KeyNumC );

	SetTimer( PauseInterval, False );
}

simulated function Tick( float DeltaTime ) // Runs only on non-Slave Client
{
	local DawnsNoSeeMover aSlave;

	if( KeyNumC != PrevKeyNum )
	{
		SetLocation( BasePos + KeyPos[KeyNumC] );
		for( aSlave=NextSlave; aSlave!=None; aSlave=aSlave.NextSlave )
			aSlave.SetLocation( aSlave.BasePos + aSlave.KeyPos[KeyNumC] );

		SetRotation( BaseRot + KeyRot[KeyNumC] );
		for( aSlave=NextSlave; aSlave!=None; aSlave=aSlave.NextSlave )
			aSlave.SetRotation( aSlave.BaseRot + aSlave.KeyRot[KeyNumC] );

		PrevKeyNum = KeyNumC;
	}
}

auto state() StartUp
{
	// Runs everywhere, even though the StartUp state isn't explicitly "simulated"!
	simulated function BeginState()
	{
		if( Role < ROLE_Authority || bSlave )
		{
			Disable('Timer'); // No Timer on anything but non-Slave Authority
		}
		else
			SetTimer( PauseInterval, False );

		if( Role == ROLE_Authority || bSlave )
		{
			Disable('Tick'); // No Tick on anything but non-Slave Clients
		}
	}
}

// Trigger states.

state TriggerOpenTimed {}

state TriggerToggle {}

state TriggerControl {}

state TriggerPound {}

// Bump states.

state BumpOpenTimed {}

state BumpButton {}

// Stand states.

state StandOpenTimed {}

defaultproperties
{
     MoverEncroachType=ME_ReturnWhenEncroach
     PauseInterval=10.0
     Physics=PHYS_None
     RemoteRole=ROLE_SimulatedProxy
     CollisionRadius=160.000000
     CollisionHeight=160.000000
     InitialState=StartUp
     bDynamicLightMover=True
}
