565 lines
13 KiB
Ucode
565 lines
13 KiB
Ucode
/*
|
|
--------------------------------------------------------------
|
|
LD Placeable NPC actor. Can be controlled by AI Scripts
|
|
(for non-combat behaviour). Or with the KF_StoryNPC_AI controller
|
|
for simple combat AI.
|
|
|
|
Author : Alex Quick
|
|
|
|
--------------------------------------------------------------
|
|
*/
|
|
|
|
class KF_StoryNPC extends KFHumanPawn
|
|
placeable
|
|
hidecategories(DeRes,Bob,Blur,UDamage);
|
|
|
|
var () localized array<string> NPCDialogue;
|
|
|
|
var () localized string NPCName;
|
|
|
|
var KF_DialogueSpot DialogueTrigger;
|
|
|
|
var () bool bRunning;
|
|
|
|
var name RunningAnims[8];
|
|
|
|
var () float BaseGroundSpeed;
|
|
|
|
var () float RespawnTime;
|
|
|
|
var () bool bStartActive;
|
|
|
|
var () bool bIndestructible;
|
|
|
|
var (AI) int TeamIndex;
|
|
|
|
var (AI) bool bFireAtWill;
|
|
|
|
var (AI) float BaseAIThreatRating;
|
|
|
|
var (AI) array< class <Pawn> > OnlyThreateningTo,NotThreateningTo;
|
|
|
|
var (AI) bool bNoThreatToZEDs;
|
|
|
|
var bool bShotAnim;
|
|
|
|
var () bool bHasInfiniteAmmo;
|
|
|
|
var bool bInitialFireAtWill;
|
|
|
|
var () bool bDropInventoryOnDeath;
|
|
|
|
/* If true, this pawn is marked Hidden while inactive. */
|
|
var (Display) bool bOnlyVisibleWhenActive;
|
|
|
|
/* Initial location this NPC was placed at by the L.D */
|
|
var vector PlacedLoc;
|
|
|
|
var rotator PlacedRot;
|
|
|
|
/* Modifier to apply to incoming damage from friendly pawns */
|
|
var() float FriendlyFireDamageScale;
|
|
|
|
var() bool bShowHealthBar;
|
|
|
|
var(Movement) bool bUseDefaultPhysics;
|
|
|
|
var() float NPCHealth;
|
|
|
|
var() float StartingHealthPct;
|
|
|
|
var() bool bDamageable;
|
|
|
|
var bool bActive;
|
|
|
|
var bool bInitialActive;
|
|
|
|
/* Event to fire off when this NPC becomes 'Active' */
|
|
var(Events) name ActivationEvent,DeActivationEvent,HealedEvent;
|
|
|
|
var(Pawn) bool bUseHitPoints;
|
|
|
|
enum ENPCTriggerAction
|
|
{
|
|
TA_ToggleHoldFire,
|
|
TA_CommitSuicide,
|
|
TA_ToggleActive,
|
|
};
|
|
|
|
|
|
var () ENPCTriggerAction TriggerAction;
|
|
|
|
replication
|
|
{
|
|
reliable if(Role == Role_Authority)
|
|
bActive;
|
|
}
|
|
|
|
/* Stub. The objective just changed. Called from KFStoryGameinfo.SetActiveObjective() */
|
|
function OnObjectiveChanged(name OldObjectiveName, name NewObjectiveName){}
|
|
|
|
function SetMovemetPhysics()
|
|
{
|
|
if(!bUseDefaultPhysics)
|
|
{
|
|
Super.SetMovementPhysics();
|
|
}
|
|
}
|
|
|
|
function bool HealDamage(int Amount, Controller Healer, class<DamageType> DamageType)
|
|
{
|
|
Health = Min(Health + Amount, HealthMax);
|
|
return true;
|
|
}
|
|
|
|
function ProcessLocationalDamage(int Damage, Pawn instigatedBy, Vector hitlocation, Vector momentum, class<DamageType> damageType, array<int> PointsHit )
|
|
{
|
|
if(!bUseHitPoints)
|
|
{
|
|
TakeDamage(Damage,instigatedBy,HitLocation,Momentum,damageType);
|
|
}
|
|
else
|
|
{
|
|
Super.ProcessLocationalDamage(Damage,InstigatedBy,HitLocation,Momentum,DamageType,PointsHit);
|
|
}
|
|
}
|
|
|
|
function SaveHealthState(){}
|
|
|
|
function Reset()
|
|
{
|
|
bFireAtWill = bInitialFireAtWill ;
|
|
SetActive(bInitialActive);
|
|
ResurrectNPC();
|
|
}
|
|
|
|
simulated function PostBeginPlay()
|
|
{
|
|
local AIScript A;
|
|
|
|
Super.PostbeginPlay();
|
|
|
|
SpawnDialogueTrigger();
|
|
|
|
bInitialFireAtWill = bFireAtWill;
|
|
|
|
if(bStartActive)
|
|
{
|
|
SetActive(true);
|
|
}
|
|
|
|
if(bOnlyVisibleWhenActive)
|
|
{
|
|
UpdateVisibility(bActive);
|
|
}
|
|
|
|
bInitialActive = bActive;
|
|
|
|
PlacedLoc = Location;
|
|
PlacedRot = Rotation;
|
|
GroundSpeed = BaseGroundSpeed;
|
|
MenuName = NPCName;
|
|
Health = NPCHealth * StartingHealthPct;
|
|
HealthMax = NPCHealth;
|
|
|
|
// automatically add controller to pawns which were placed in level
|
|
// NOTE: pawns spawned during gameplay are not automatically possessed by a controller
|
|
if ( (Health > 0) && !bDontPossess )
|
|
{
|
|
// check if I have an AI Script
|
|
if ( AIScriptTag != '' )
|
|
{
|
|
ForEach AllActors(class'AIScript',A,AIScriptTag)
|
|
break;
|
|
// let the AIScript spawn and init my controller
|
|
if ( A != None )
|
|
{
|
|
A.SpawnControllerFor(self);
|
|
if ( Controller != None )
|
|
return;
|
|
}
|
|
}
|
|
if ( (ControllerClass != None) && (Controller == None) )
|
|
Controller = spawn(ControllerClass);
|
|
if ( Controller != None )
|
|
{
|
|
Controller.Possess(self);
|
|
AIController(Controller).Skill += SkillModifier;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function ResurrectNPC()
|
|
{
|
|
if(Health < NPCHealth)
|
|
{
|
|
Health = NPCHealth * StartingHealthPct;
|
|
HealthMax = NPCHealth;
|
|
SetCollision(true,true);
|
|
bHidden = false;
|
|
RepositionNPC();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function RepositionNPC()
|
|
{
|
|
if(bMovable)
|
|
{
|
|
SetRotation(PlacedRot);
|
|
SetLocation(PlacedLoc);
|
|
}
|
|
}
|
|
|
|
function SpawnDialogueTrigger()
|
|
{
|
|
local int i;
|
|
|
|
if(DialogueTrigger == none && NPCDialogue.length > 0)
|
|
{
|
|
DialogueTrigger = Spawn(class 'KF_DialogueSpot');
|
|
DialogueTrigger.bTouchTriggered = true;
|
|
DialogueTrigger.bRandomize = true;
|
|
DialogueTrigger.bHardAttach = true;
|
|
DialogueTrigger.SetBase(self);
|
|
DialogueTrigger.SetCollisionSize(CollisionRadius * 1.5,CollisionHeight) ;
|
|
|
|
for(i = 0 ; i < NPCDialogue.length ; i ++)
|
|
{
|
|
DialogueTrigger.Dialogues.length = i + 1;
|
|
DialogueTrigger.Dialogues[i].Display.Dialogue_text = NPCDialogue[i];
|
|
DialogueTrigger.Dialogues[i].Display.Dialogue_header = NPCName;
|
|
DialogueTrigger.Dialogues[i].BroadcastScope = InstigatorOnly;
|
|
DialogueTrigger.Dialogues[i].VoiceOver.SourceActor = self;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/* Doing this here instead of in Gameinfo.ReduceDamage() because of how hilariously hacked up KFGameType's implementation
|
|
of that function is ... */
|
|
|
|
simulated function TakeDamage(int Damage,pawn instigatedBy, Vector HitLocation, vector Momentum, class<DamageType> damageType, optional int HitIndex)
|
|
{
|
|
local int ReducedDamage;
|
|
|
|
/* log("=============================================================",'Story_Debug');
|
|
log(self@"Took Damage : "@Damage@" from : "@instigatedBy@" Am I damageable ? : "@bDamageable@" Current Health : "@Health,'Story_Debug');
|
|
*/
|
|
|
|
if(!bDamageable || Health <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ReducedDamage = Damage;
|
|
|
|
// friendly fire ... scale down the damage
|
|
if(/*instigatedBy.Controller.GetTeamNum() == Controller.GetTeamNum()*/
|
|
PlayerController(instigatedBy.Controller) != none) // Yeah looks like this guy's never actually getting assigned a correct team.
|
|
{
|
|
ReducedDamage *= FriendlyFireDamageScale;
|
|
}
|
|
|
|
Super.TakeDamage(ReducedDamage,InstigatedBy,HitLocation,Momentum,damageType,HitIndex);
|
|
|
|
}
|
|
|
|
|
|
function bool GiveHealth(int HealAmount, int HealMax)
|
|
{
|
|
local bool bHealed;
|
|
|
|
bHealed = Super.GiveHealth(HealAmount,HealMax);
|
|
|
|
if(HealedEvent != '' && HealAmount > 0 && bHealed)
|
|
{
|
|
TriggerEvent(HealedEvent,self,Instigator);
|
|
}
|
|
|
|
|
|
return bHealed;
|
|
}
|
|
|
|
/*
|
|
function Died(Controller Killer, class<DamageType> damageType, vector HitLocation)
|
|
{
|
|
if(!bPlayedDeath && !bIndestructible)
|
|
{
|
|
SpawnDummyCorpse(Killer,damageType,HitLocation);
|
|
}
|
|
}
|
|
|
|
simulated function PlayDying(class<DamageType> DamageType, vector HitLoc)
|
|
{
|
|
Super(xPawn).PlayDying(DamageType,HitLoc);
|
|
}*/
|
|
|
|
simulated function SpawnDummyCorpse(Controller Killer, class<DamageType> damageType, vector HitLocation)
|
|
{
|
|
local KFPawn DummyCorpse;
|
|
local int i, EquipLength;
|
|
|
|
SetCollision(false,false);
|
|
bHidden = true;
|
|
|
|
DummyCorpse = Spawn(class 'KFHumanPawn',,,Location,Rotation);
|
|
if(DummyCorpse != none)
|
|
{
|
|
DummyCorpse.LinkMesh(Mesh);
|
|
DummyCorpse.tag = tag;
|
|
DummyCorpse.Event = Event;
|
|
|
|
if(bDropInventoryOnDeath)
|
|
{
|
|
EquipLength = arraycount(RequiredEquipment);
|
|
for(i = 0 ; i < EquipLength ; i ++)
|
|
{
|
|
DummyCorpse.RequiredEquipment[i] = RequiredEquipment[i] ;
|
|
}
|
|
|
|
DummyCorpse.AddDefaultInventory();
|
|
}
|
|
|
|
DummyCorpse.Died(Killer,damageType,HitLocation);
|
|
}
|
|
}
|
|
|
|
|
|
function Trigger( Actor Other, Pawn EventInstigator )
|
|
{
|
|
switch(TriggerAction)
|
|
{
|
|
case TA_CommitSuicide : Died(Controller,class 'Suicided', Location) ; break;
|
|
case TA_ToggleHoldFire : bFireAtWill = !bFireAtWill; break;
|
|
case TA_ToggleActive : SetActive(!bActive); break;
|
|
}
|
|
}
|
|
|
|
function UpdateVisibility(bool On)
|
|
{
|
|
if(bOnlyVisibleWhenActive)
|
|
{
|
|
bHidden = !On;
|
|
SetCollision(!bHidden,!bHidden);
|
|
}
|
|
}
|
|
|
|
/* turns this NPC on / off
|
|
ie. Makes it damageable and decides whether AI ignores it
|
|
|
|
Network : Server */
|
|
|
|
function SetActive(bool On)
|
|
{
|
|
if(Role < Role_Authority)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdateVisibility(On);
|
|
|
|
bActive = On;
|
|
bDamageable = bActive;
|
|
log(self@" is now bActive : "@On,'Story_Debug');
|
|
|
|
if(On && ActivationEvent != '')
|
|
{
|
|
TriggerEvent(ActivationEvent,self,self);
|
|
}
|
|
else
|
|
if(!On && DeActivationEvent != '')
|
|
{
|
|
TriggerEvent(DeActivationEvent,self,self);
|
|
}
|
|
}
|
|
|
|
simulated function int GetTeamNum()
|
|
{
|
|
return TeamIndex;
|
|
}
|
|
|
|
/* === AI functions =====================================*/
|
|
|
|
function float AssessThreatTo(KFMonsterController Monster, optional bool CheckDistance)
|
|
{
|
|
if(bNoThreatToZEDs ||
|
|
TeamIndex == 255 ||
|
|
Health <= 0 ||
|
|
!bActive ||
|
|
!bDamageable ||
|
|
(!IsThreateningTo(Monster.Pawn)) )
|
|
{
|
|
return -1.f;
|
|
}
|
|
|
|
return Super.AssessThreatTo(Monster,CheckDistance) + BaseAIThreatRating ;
|
|
}
|
|
|
|
/* Returns true if the supplied Monster considers this NPC a threat (and is prepared to attack it) */
|
|
function bool IsThreateningTo( Pawn Monster)
|
|
{
|
|
local int i;
|
|
local bool Result;
|
|
|
|
if(Monster == none)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Result = true;
|
|
|
|
for(i = 0 ; i < NotThreateningTo.length ; i ++)
|
|
{
|
|
if(ClassisChildOf(Monster.class,NotThreateningTo[i]))
|
|
{
|
|
Result = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
for(i = 0 ; i < OnlyThreateningTo.length ; i ++)
|
|
{
|
|
Result = false;
|
|
if(ClassisChildOf(Monster.class,OnlyThreateningTo[i]))
|
|
{
|
|
Result = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
function bool IsPacifist()
|
|
{
|
|
local Inventory Inv;
|
|
local int Count;
|
|
local Weapon Weap;
|
|
local bool bUsingLethalGear;
|
|
|
|
for( Inv=Inventory; Inv!=None && Count < 1000; Inv=Inv.Inventory )
|
|
{
|
|
Weap = Weapon(Inv);
|
|
|
|
bUsingLethalGear = Weap != none && (Weap.bMeleeWeapon || Weap.HasAmmo()) ;
|
|
if(bUsingLethalGear && bFireAtWill)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Count++;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
simulated event PostRender2D(Canvas C, float ScreenLocX, float ScreenLocY) // called if bScriptPostRender is true, overrides native team beacon drawing code
|
|
{
|
|
local PlayerController PC;
|
|
local float Opacity;
|
|
// local float Dist;
|
|
local HUDKillingFloor KFHUD;
|
|
local vector CameraLocation,CamDir;
|
|
local rotator CameraRotation;
|
|
|
|
if(!bShowHealthBar ||
|
|
!bActive ||
|
|
GetStateName() == 'Dying')
|
|
{
|
|
return;
|
|
}
|
|
|
|
PC = Level.GetLocalPlayerController();
|
|
|
|
if ( PC != None )
|
|
{
|
|
KFHUD = HUDKillingFloor(PC.myHUD);
|
|
if(KFHUD != none)
|
|
{
|
|
C.GetCameraLocation(CameraLocation, CameraRotation);
|
|
CamDir = vector(CameraRotation);
|
|
|
|
/* Rendering behind us... */
|
|
if ( (Normal(Location - CameraLocation) dot CamDir) < 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
Opacity = FClamp(1.f - (VSize(PC.CalcViewLocation - Location) / 3000.f),0.25f,1.f) ;
|
|
KFHUD.DrawKFBar(C,ScreenLocX,ScreenLocY,Health/HealthMax,Byte(Opacity * 255.f),false);
|
|
}
|
|
}
|
|
}
|
|
|
|
function RosterEntry GetPlacedRoster()
|
|
{
|
|
return None;
|
|
}
|
|
|
|
function string GetPlayerName()
|
|
{
|
|
return NPCName;
|
|
}
|
|
|
|
defaultproperties
|
|
{
|
|
/* AccelRate=1000.0
|
|
AirControl=+0.15
|
|
GroundSpeed=200.0
|
|
WaterSpeed=200.000000
|
|
AirSpeed=230.000000
|
|
JumpZ=325.000000
|
|
MaxFallSpeed=600.000000
|
|
BaseEyeHeight=44.0
|
|
EyeHeight=44.0
|
|
CrouchHeight=34.000000
|
|
CollisionHeight=50.000000
|
|
|
|
Mesh=SkeletalMesh'KF_Soldier_Trip.Punk_Harold'
|
|
|
|
MovementAnims(0) ="WalkF_HuskGun"
|
|
MovementAnims(1) ="WalkB_HuskGun"
|
|
MovementAnims(2) ="WalkL_HuskGun"
|
|
MovementAnims(3) ="WalkR_HuskGun"
|
|
|
|
|
|
RunningAnims(0) = "JogF_Bullpup"
|
|
RunningAnims(1) = "JogB_Bullpup"
|
|
RunningAnims(2) = "JogL_Bullpup"
|
|
RunningAnims(3) = "JogR_Bullpup"
|
|
|
|
|
|
IdleWeaponAnim="Idle_Knife"
|
|
IdleRestAnim="Idle_Knife"
|
|
|
|
TurnLeftAnim="TurnL"
|
|
TurnRightAnim="TurnR"
|
|
*/
|
|
|
|
NPCName ="An NPC"
|
|
|
|
|
|
bNoDelete = true
|
|
bFireAtWill = true
|
|
ControllerClass = class 'KF_StoryNPC_AI'
|
|
|
|
TeamIndex = 0
|
|
NPCHealth = 100
|
|
FriendlyFireDamageScale = 1.f
|
|
|
|
bDropInventoryOnDeath = true
|
|
|
|
bStartActive = true
|
|
bDamageable = true
|
|
StartingHealthPct = 1.f
|
|
|
|
bAlwaysRelevant = true
|
|
bUseHitPoints = true
|
|
|
|
TriggerAction = TA_ToggleActive
|
|
}
|