rott/kf_sources/KFStoryGame/Classes/KF_StoryNPC.uc
2026-07-14 20:27:09 +07:00

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
}