Библиотека VJ Base
July 11
VJ-Base - npc_vj_creature_base - init файл
Путь к файлу init.lua. «VJ-Base/lua/entities/npc_vj_creature_base/init.lua»
«VJ-Base/lua/entities/npc_vj_creature_base/init.lua»
AddCSLuaFile("shared.lua") include("vj_base/ai/core.lua") include("vj_base/ai/schedules.lua") include("vj_base/ai/base_aa.lua") include("shared.lua") /*-------------------------------------------------- *** Copyright (c) 2012-2025 by DrVrej, All rights reserved. *** No parts of this code or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials. --------------------------------------------------*/ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Main & Misc ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ENT.Model = false -- Model(s) to spawn with | Picks a random one if it's a table ENT.CanChatMessage = true -- Is it allowed to post in a player's chat? | Example: "Blank no longer likes you." -- ====== Health ====== -- ENT.StartHealth = 50 ENT.HealthRegenParams = { Enabled = false, -- Can it regenerate its health? Amount = 4, -- How much should the health increase after every delay? Delay = VJ.SET(2, 4), -- How much time until the health increases ResetOnDmg = true, -- Should the delay reset when it receives damage? } -- ====== Collision ====== -- ENT.HullType = HULL_HUMAN -- List of Hull types: https://wiki.facepunch.com/gmod/Enums/HULL ENT.EntitiesToNoCollide = false -- Set to a table of entity class names for it to not collide with otherwise leave it to false -- ====== NPC Controller ====== -- ENT.ControllerParams = { CameraMode = 1, -- Sets the default camera mode | 1 = Third Person, 2 = First Person ThirdP_Offset = Vector(0, 0, 0), -- The offset for the controller when the camera is in third person FirstP_Bone = "ValveBiped.Bip01_Head1", -- If left empty, the base will attempt to calculate a position for first person FirstP_Offset = Vector(0, 0, 5), -- The offset for the controller when the camera is in first person FirstP_ShrinkBone = true, -- Should the bone shrink? Useful if the bone is obscuring the player's view FirstP_CameraBoneAng = 0, -- Should the camera's angle be affected by the bone's angle? | 0 = No, 1 = Pitch, 2 = Yaw, 3 = Roll FirstP_CameraBoneAng_Offset = 0, -- How much should the camera's angle be rotated by? | Useful for weird bone angles } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Movement & Sight ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ENT.SightDistance = 6500 -- Initial sight distance | To retrieve: "self:GetMaxLookDistance()" | To change: "self:SetMaxLookDistance(distance)" ENT.SightAngle = 156 -- Initial field of view | To retrieve: "self:GetFOV()" | To change: "self:SetFOV(degree)" | 360 = See all around ENT.TurningSpeed = 20 -- Initial turning speed | To retrieve: "self:GetMaxYawSpeed()" | To change: "self:SetMaxYawSpeed(speed)" ENT.TurningUseAllAxis = false -- If set to true, angles will not be restricted to y-axis, it will change all axes (plural axis) ENT.CanTurnWhileMoving = true -- Can it turn while moving? | EX: GoldSrc NPCs, Facing enemy while running to cover, Facing the player while moving out of the way ENT.MovementType = VJ_MOVETYPE_GROUND -- Types: VJ_MOVETYPE_GROUND | VJ_MOVETYPE_AERIAL | VJ_MOVETYPE_AQUATIC | VJ_MOVETYPE_STATIONARY | VJ_MOVETYPE_PHYSICS ENT.UsePoseParameterMovement = false -- Sets the model's "move_x" and "move_y" pose parameters while moving | Required for player models to move properly! -- ====== JUMPING ====== -- -- Requires "CAP_MOVE_JUMP" capability -- Applied automatically by the base if "ACT_JUMP" is valid on the NPC's model -- Example scenario: -- [A] <- Apex -- / \ -- / [S] <- Start -- [E] <- End ENT.JumpParams = { Enabled = true, -- Can it do movement jumps? MaxRise = 220, -- How high it can jump up ((S -> A) AND (S -> E)) MaxDrop = 384, -- How low it can jump down (E -> S) MaxDistance = 512, -- Maximum distance between Start and End } -- ====== STATIONARY ====== -- ENT.CanTurnWhileStationary = true -- Can it turn while using stationary move type? -- ====== AERIAL & AQUATIC ====== -- ENT.AA_GroundLimit = 100 -- If the NPC's distance from itself to the ground is less than this, it will attempt to move up ENT.AA_MinWanderDist = 150 -- Minimum distance that it should move when wandering ENT.AA_MoveAccelerate = 5 -- It will gradually speed up to the max movement speed as it moves towards its destination | Calculation = FrameTime * x -- 0 = Constant max speed | 1 = Slight acceleration | 50 = Rapid acceleration ENT.AA_MoveDecelerate = 5 -- It will slow down as it approaches its destination | Calculation = MaxSpeed / x -- 1 = Constant max speed | 2 = Slight deceleration | 50 = Rapid deceleration -- AERIAL -- ENT.Aerial_FlyingSpeed_Calm = 80 -- Speed it should fly with, when it's wandering, moving slowly, etc. | Basically walking compared to ground NPCs ENT.Aerial_FlyingSpeed_Alerted = 200 -- Speed it should fly with, when it's chasing an enemy, moving away quickly, etc. | Basically running compared to ground NPCs ENT.Aerial_AnimTbl_Calm = ACT_FLY -- Flying animations to play while idle | Equivalent to "walking" | Unlike other movements, sequences are allowed! ENT.Aerial_AnimTbl_Alerted = ACT_FLY -- Flying animations to play while alert | Equivalent to "Running" | Unlike other movements, sequences are allowed! -- AQUATIC -- ENT.Aquatic_SwimmingSpeed_Calm = 80 -- The speed it should swim with, when it's wandering, moving slowly, etc. | Basically walking compared to ground NPCs ENT.Aquatic_SwimmingSpeed_Alerted = 200 -- The speed it should swim with, when it's chasing an enemy, moving away quickly, etc. | Basically running compared to ground NPCs ENT.Aquatic_AnimTbl_Calm = ACT_SWIM -- Swimming animations to play while idle | Equivalent to "walking" | Unlike other movements, sequences are allowed! ENT.Aquatic_AnimTbl_Alerted = ACT_SWIM -- Swimming animations to play while alert | Equivalent to "Running" | Unlike other movements, sequences are allowed! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ AI & Relationship ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ENT.Behavior = VJ_BEHAVIOR_AGGRESSIVE -- What type of AI behavior is it? ENT.IsGuard = false -- Should it guard its position? | Will attempt to stay around its guarding position ENT.NextProcessTime = 1 -- Time until it runs the essential performance-heavy AI components ENT.EnemyDetection = true -- Can it search and detect for enemies? ENT.EnemyTouchDetection = true -- Can it turn and detect enemies that collide with it? ENT.EnemyXRayDetection = false -- Can it detect enemies through walls & objects? ENT.EnemyTimeout = 15 -- Time until the enemy target is reset if it's not visible ENT.AlertTimeout = VJ.SET(14, 16) -- Time until it transitions from alerted state to idle state assuming it has no enemy ENT.IdleAlwaysWander = false -- Should it constantly wander while idle? ENT.DisableWandering = false ENT.DisableChasingEnemy = false ENT.CanOpenDoors = true -- Can it open doors? ENT.CanEat = false -- Can it search and eat organic stuff? ENT.EatCooldown = 30 -- How much time until it can eat again after devouring something? -- ====== Alliances ====== -- ENT.CanAlly = true -- Can it ally with other entities? ENT.VJ_NPC_Class = {} -- Relationship classes, any entity with the same class will be seen as an ally -- Common Classes: -- Players / Resistance / Black Mesa = "CLASS_PLAYER_ALLY" || HECU = "CLASS_UNITED_STATES" || Portal = "CLASS_APERTURE" -- Combine = "CLASS_COMBINE" || Zombie = "CLASS_ZOMBIE" || Antlions = "CLASS_ANTLION" || Xen = "CLASS_XEN" || Black-Ops = "CLASS_BLACKOPS" ENT.AlliedWithPlayerAllies = false -- Should it be allied with other player allies? | Both entities must have "CLASS_PLAYER_ALLY" ENT.YieldToAlliedPlayers = true -- Should it give space to allied players? ENT.BecomeEnemyToPlayer = false -- Should it become enemy towards an allied player if it's damaged by them or it witnesses another ally killed by them? -- false = Don't turn hostile to allied players | number = Threshold, where each negative event increases it by 1, if it passes this number it will become hostile ENT.CanReceiveOrders = true -- Can it receive orders from allies? | Ex: Allies calling for help, allies requesting backup on damage, etc. -- false = Will not receive the following: "CallForHelp", "DamageAllyResponse", "DeathAllyResponse", "Passive_AlliesRunOnDamage" -- ====== Passive Behaviors ====== -- ENT.Passive_RunOnTouch = true -- Should it run and make a alert sound when something collides with it? ENT.Passive_AlliesRunOnDamage = true -- Should its allies (other passive NPCs) also run when it's damaged? -- ====== On Player Sight ====== -- ENT.HasOnPlayerSight = false -- Should do something when it a player? ENT.OnPlayerSightDistance = 200 -- How close should the player be until it runs the code? ENT.OnPlayerSightDispositionLevel = 1 -- 0 = Run it every time | 1 = Run it only when friendly to player | 2 = Run it only when enemy to player ENT.OnPlayerSightOnlyOnce = true -- If true, it will only run it once | Sets "self.HasOnPlayerSight" to false after it runs! ENT.OnPlayerSightNextTime = VJ.SET(15, 20) -- How much time should it pass until it runs the code again? -- ====== Call For Help ====== -- ENT.CallForHelp = true -- Can it request allies for help while in combat? ENT.CallForHelpDistance = 2000 -- Max distance its request for help travels ENT.CallForHelpCooldown = 4 -- Time until it calls for help again ENT.AnimTbl_CallForHelp = false -- Call for help animations | false = Don't play an animation ENT.CallForHelpAnimFaceEnemy = true -- Should it face the enemy while playing the animation? ENT.CallForHelpAnimCooldown = 30 -- How much time until it can play an animation again? -- ====== Medic ====== -- -- Medics only heal allied entities that are tagged with "self.VJ_ID_Healable", by default it includes VJ NPCs and players ENT.IsMedic = false -- Should it heal allied entities? ENT.Medic_CheckDistance = 600 -- Max distance to check for injured allies ENT.Medic_HealDistance = 30 -- How close does it have to be until it stops moving and heals its ally? ENT.Medic_TimeUntilHeal = false -- Time until the ally receives health | false = Base auto calculates the duration ENT.AnimTbl_Medic_GiveHealth = ACT_SPECIAL_ATTACK1 -- Animations to play when it heals an ally | false = Don't play an animation ENT.Medic_HealAmount = 25 -- How health does it give? ENT.Medic_NextHealTime = VJ.SET(10, 15) -- How much time until it can give health to an ally again ENT.Medic_SpawnPropOnHeal = true -- Should it spawn a prop, such as small health vial at a attachment when healing an ally? ENT.Medic_SpawnPropOnHealModel = "models/healthvial.mdl" -- The model that it spawns ENT.Medic_SpawnPropOnHealAttachment = "anim_attachment_LH" -- The attachment it spawns on -- ====== Follow System ====== -- -- Associated variables: self.FollowData, self.IsFollowing -- NOTE: Stationary NPCs can't use follow system! ENT.FollowPlayer = true -- Should it follow allied players when the player presses the USE key? ENT.FollowMinDistance = 100 -- Minimum distance it should come when following something | The base automatically adds the NPC's size to this variable to account for different sizes! -- ====== Constantly Face Enemy ====== -- ENT.ConstantlyFaceEnemy = false -- Should it face the enemy constantly? ENT.ConstantlyFaceEnemy_IfVisible = true -- Should it only face the enemy if it's visible? ENT.ConstantlyFaceEnemy_IfAttacking = false -- Should it face the enemy when attacking? ENT.ConstantlyFaceEnemy_Postures = "Both" -- "Both" = Moving or standing | "Moving" = Only when moving | "Standing" = Only when standing ENT.ConstantlyFaceEnemy_MinDistance = 2500 -- How close does it have to be until it starts to face the enemy? -- ====== Pose Parameter Tracking ====== -- ENT.HasPoseParameterLooking = true -- Does it look at its enemy using pose parameters? ENT.PoseParameterLooking_Names = {pitch = {}, yaw = {}, roll = {}} -- Custom pose parameters to use, can put as many as needed ENT.PoseParameterLooking_InvertPitch = false -- Inverts the pitch pose parameters (X) ENT.PoseParameterLooking_InvertYaw = false -- Inverts the yaw pose parameters (Y) ENT.PoseParameterLooking_InvertRoll = false -- Inverts the roll pose parameters (Z) ENT.PoseParameterLooking_TurningSpeed = 10 -- How fast does the parameter turn? ENT.PoseParameterLooking_CanReset = true -- Should it reset its pose parameters if there is no enemies? -- ====== Investigation ====== -- -- Showcase: https://www.youtube.com/watch?v=cCqoqSDFyC4 ENT.CanInvestigate = true -- Can it detect and investigate disturbances? | EX: Sounds, movement, flashlight, bullet hits ENT.InvestigateSoundMultiplier = 9 -- Max sound hearing distance multiplier | This multiplies the calculated volume of the sound -- ====== Limit Chase Distance Behavior ====== -- ENT.LimitChaseDistance = false -- Should it limit chasing when between certain distances? | true = Always limit | "OnlyRange" = Only limit if it's able to range attack ENT.LimitChaseDistance_Min = 300 -- Min distance from the enemy to limit its chasing | "UseRangeDistance" = Use range attack's min distance ENT.LimitChaseDistance_Max = 2000 -- Max distance from the enemy to limit its chasing | "UseRangeDistance" = Use range attack's max distance -- ====== Prop Damaging & Pushing Behavior ====== -- -- By default this requires the NPC to have a melee attack, unless coded otherwise ENT.PropInteraction = true -- Controls how it should interact with props -- false = Disable both damaging and pushing | true = Damage and push | "OnlyDamage" = Damage but don't push | "OnlyPush" = Push but don't damage ENT.PropInteraction_MaxScale = 1 -- Max prop size multiplier | x < 1 = Smaller props | x > 1 = Larger props ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Damaged / Injured ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ -- ====== Blood ====== -- -- Leave blood tables empty to let the base decide depending on the blood type ENT.Bleeds = true -- Can it bleed? Controls all bleeding related components such blood decal, particle, pool, etc. ENT.BloodColor = VJ.BLOOD_COLOR_NONE -- Its blood type, this will determine the blood decal, particle, etc. ENT.HasBloodDecal = true -- Should it spawn a decal when damaged? ENT.BloodDecal = {} -- Decals to spawn when it's damaged ENT.BloodDecalUseGMod = false -- Should it use the current default decals defined by Garry's Mod? | Only applies for certain blood types! ENT.BloodDecalDistance = 150 -- Max distance blood decals can splatter ENT.HasBloodParticle = true -- Should it spawn a particle when damaged? ENT.BloodParticle = {} -- Particles to spawn when it's damaged ENT.HasBloodPool = true -- Should a blood pool spawn by its corpse? ENT.BloodPool = {} -- Blood pools to be spawned by the corpse -- ====== Immunity ====== -- ENT.GodMode = false -- Immune to everything ENT.ForceDamageFromBosses = false -- Should it receive damage by bosses regardless of its immunities? | Bosses are attackers tagged with "VJ_ID_Boss" ENT.AllowIgnition = true -- Can it be set on fire? ENT.Immune_Bullet = false -- Immune to bullet damages ENT.Immune_Melee = false -- Immune to melee damages (Ex: Slashes, stabs, punches, claws, crowbar, blunt attacks) ENT.Immune_Explosive = false -- Immune to explosive damages (Ex: Grenades, rockets, bombs, missiles) ENT.Immune_Dissolve = false -- Immune to dissolving damage (Ex: Combine ball) ENT.Immune_Toxic = false -- Immune to toxic effect damages (Ex: Acid, poison, radiation, gas) ENT.Immune_Fire = false -- Immune to fire / flame damages ENT.Immune_Electricity = false -- Immune to electrical damages (Ex: Shocks, lasers, gravity gun) ENT.Immune_Sonic = false -- Immune to sonic damages (Ex: Sound blasts) -- ====== Flinching ====== -- ENT.CanFlinch = false -- Can it flinch? | false = Don't flinch | true = Always flinch | "DamageTypes" = Flinch only from certain damages types ENT.FlinchDamageTypes = {DMG_BLAST} -- Which types of damage types should it flinch from when "DamageTypes" is used? ENT.FlinchChance = 14 -- Chance of flinching from 1 to x | 1 = Always flinch ENT.FlinchCooldown = 5 -- How much time until it can flinch again? | false = Base auto calculates the duration ENT.AnimTbl_Flinch = ACT_FLINCH_PHYSICS ENT.FlinchHitGroupMap = false -- EXAMPLE: {{HitGroup = HITGROUP_HEAD, Animation = ACT_FLINCH_HEAD}, {HitGroup = HITGROUP_LEFTARM, Animation = ACT_FLINCH_LEFTARM}, {HitGroup = HITGROUP_RIGHTARM, Animation = ACT_FLINCH_RIGHTARM}, {HitGroup = HITGROUP_LEFTLEG, Animation = ACT_FLINCH_LEFTLEG}, {HitGroup = HITGROUP_RIGHTLEG, Animation = ACT_FLINCH_RIGHTLEG}} ENT.FlinchHitGroupPlayDefault = true -- Should it play "self.AnimTbl_Flinch" when none of the mapped hit groups hit? -- ====== Non-Combat Damage Response Behaviors ====== -- -- For passive behavior NPC, these responses will run regardless if it has an active enemy or not ENT.DamageResponse = true -- Should it respond to damages while it has no enemy? -- true = Search for enemies or run to a covered position | "OnlyMove" = Will only run to a covered position | "OnlySearch" = Will only search for enemies ENT.DamageAllyResponse = true -- Should allies respond when it's damaged while it has no enemy? ENT.AnimTbl_DamageAllyResponse = false -- Animations to play when it calls allies to respond | false = Don't play an animation ENT.DamageAllyResponse_Cooldown = VJ.SET(9, 12) -- How long until it can call allies again? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Death & Corpse ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ENT.DeathDelayTime = 0 -- Time until it spawns the corpse, removes itself, etc. -- ====== Ally Responses ====== -- -- An ally must have "self.CanReceiveOrders" enabled to respond! ENT.DeathAllyResponse = true -- How should allies response when it dies? -- false = No reactions | true = Allies respond by becoming alert and moving to its location | "OnlyAlert" = Allies respond by becoming alert ENT.DeathAllyResponse_MoveLimit = 4 -- Max number of allies that can move to its location when responding to its death -- ====== Death Animation ====== -- -- NOTE: This is added on top of "self.DeathDelayTime" ENT.HasDeathAnimation = false -- Should it play death animations? ENT.AnimTbl_Death = {} ENT.DeathAnimationTime = false -- How long should the death animation play? | false = Base auto calculates the duration ENT.DeathAnimationChance = 1 -- Put 1 if you want it to play the animation all the time ENT.DeathAnimationDecreaseLengthAmount = 0 -- This will decrease the time until it turns into a corpse -- ====== Corpse ====== -- ENT.HasDeathCorpse = true -- Should a corpse spawn when it's killed? ENT.DeathCorpseEntityClass = false -- Corpse's class | false = Let the base automatically detect the class ENT.DeathCorpseModel = false -- Model(s) to use as the corpse | false = Use its current model | Can be a string or a table of strings ENT.DeathCorpseCollisionType = COLLISION_GROUP_DEBRIS -- Collision type for the corpse | NPC Options Menu can only override this value if it's set to COLLISION_GROUP_DEBRIS! ENT.DeathCorpseFade = false -- Should the corpse fade after the given amount of seconds? | false = Don't fade | number = Fade out time ENT.DeathCorpseSetBoneAngles = true -- This can be used to stop the corpse glitching or flying on death ENT.DeathCorpseApplyForce = true -- Should the force of the damage be applied to the corpse? ENT.DeathCorpseSubMaterials = nil -- Apply a table of indexes that correspond to a sub material index, this will cause the base to copy the NPC's sub material to the corpse. -- ====== Dismemberment / Gib ====== -- ENT.CanGib = true -- Can it dismember? | Makes "CreateGibEntity" fail and overrides "CanGibOnDeath" to false ENT.CanGibOnDeath = true -- Can it dismember on death? ENT.GibOnDeathFilter = true -- Should it only gib and call "self:HandleGibOnDeath" when it's killed by a specific damage types? | false = Call "self:HandleGibOnDeath" from any damage type ENT.HasGibOnDeathSounds = true -- Does it have gib sounds? | Mostly used for the settings menu ENT.HasGibOnDeathEffects = true -- Does it spawn particles on death or when it gibs? | Mostly used for the settings menu -- ====== Drops On Death ====== -- ENT.DropDeathLoot = true -- Should it drop loot on death? ENT.DeathLoot = {} -- List of entities it will randomly pick to drop | Leave it empty to drop nothing ENT.DeathLootChance = 14 -- If set to 1, it will always drop loot ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Melee Attack ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ENT.HasMeleeAttack = true ENT.MeleeAttackDamage = 10 ENT.MeleeAttackDamageType = DMG_SLASH ENT.HasMeleeAttackKnockBack = false -- Should knockback be applied on melee hit? | Use "MeleeAttackKnockbackVelocity" function to edit the velocity ENT.DisableDefaultMeleeAttackDamageCode = false -- Disables the default melee attack damage code -- ====== Animation ====== -- ENT.AnimTbl_MeleeAttack = ACT_MELEE_ATTACK1 -- Animations to play when it melee attacks | false = Don't play an animation ENT.MeleeAttackAnimationFaceEnemy = true -- Should it face the enemy while playing the melee attack animation? ENT.MeleeAttackAnimationDecreaseLengthAmount = 0 -- Decreases animation time | Use it to fix animations that have extra frames at the end -- ====== Distance ====== -- ENT.MeleeAttackDistance = false -- How close an enemy has to be to trigger a melee attack | false = Auto calculate on initialize based on its collision bounds ENT.MeleeAttackAngleRadius = 100 -- What is the attack angle radius? | 100 = In front of it | 180 = All around it ENT.MeleeAttackDamageDistance = false -- How far does the damage go? | false = Auto calculate on initialize based on its collision bounds ENT.MeleeAttackDamageAngleRadius = 100 -- What is the damage angle radius? | 100 = In front of it | 180 = All around it -- ====== Timer ====== -- ENT.TimeUntilMeleeAttackDamage = 0.6 -- How much time until it executes the damage? | false = Make the attack event-based ENT.NextMeleeAttackTime = 0 -- How much time until it can use a melee attack? | number = Specific time | VJ.SET = Randomized between the 2 numbers ENT.NextAnyAttackTime_Melee = false -- How much time until it can do any attack again? | false = Base auto calculates the duration | number = Specific time | VJ.SET = Randomized between the 2 numbers ENT.MeleeAttackReps = 1 -- How many times does it run the melee attack code? ENT.MeleeAttackExtraTimers = false -- Extra melee attack timers | EX: {1, 1.4} ENT.MeleeAttackStopOnHit = false -- Should it stop executing the melee attack after it hits an enemy? -- ====== Bleeding System ====== -- -- Causes the enemy to continue taking damage after it's hit based on the given parameters: ENT.MeleeAttackBleedEnemy = false -- Should it bleed enemies it hits? ENT.MeleeAttackBleedEnemyChance = 3 -- Chance that the enemy bleeds | 1 = always ENT.MeleeAttackBleedEnemyDamage = 1 -- How much damage per repetition ENT.MeleeAttackBleedEnemyTime = 1 -- How much time until the next repetition? ENT.MeleeAttackBleedEnemyReps = 4 -- How many repetitions? -- ====== Player Speed Modifier ====== -- ENT.MeleeAttackPlayerSpeed = false -- Should it modify the movement speed of players that got damaged? ENT.MeleeAttackPlayerSpeedWalk = 100 ENT.MeleeAttackPlayerSpeedRun = 100 ENT.MeleeAttackPlayerSpeedTime = 5 -- How much time until player's speed resets back to normal -- ====== Digital Signal Processor (DSP) Effect ====== -- -- DSP ID Presents: https://wiki.facepunch.com/gmod/DSP_Presets AND https://developer.valvesoftware.com/wiki/Dsp_presets ENT.MeleeAttackDSP = 32 -- Should it apply a DSP effect to players? | false = Disable applying DSP effect | number = DSP effect ID ENT.MeleeAttackDSPLimit = 60 -- Should it only apply if the damage surpasses the given number? | false = Always apply | number = Only apply when damage is greater than or equal to this number ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Range Attack ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ENT.HasRangeAttack = false ENT.RangeAttackProjectiles = "obj_vj_rocket" -- Entities that it can spawn when range attacking | table = Picks randomly -- ====== Animation ====== -- ENT.AnimTbl_RangeAttack = ACT_RANGE_ATTACK1 -- Animations to play when it range attacks | false = Don't play an animation ENT.RangeAttackAnimationDelay = 0 -- It will wait certain amount of time before playing the animation ENT.RangeAttackAnimationFaceEnemy = true -- Should it face the enemy while playing the range attack animation? ENT.RangeAttackAnimationDecreaseLengthAmount = 0 -- Decreases animation time | Use it to fix animations that have extra frames at the end -- ====== Distance ====== -- ENT.RangeAttackMinDistance = 800 -- Min range attack distance ENT.RangeAttackMaxDistance = 2000 -- Max range attack distance ENT.RangeAttackAngleRadius = 100 -- What is the attack angle radius? | 100 = In front of it | 180 = All around it -- ====== Timer ====== -- ENT.TimeUntilRangeAttackProjectileRelease = 1.5 -- How much time until the projectile is thrown? | false = Make the attack event-based ENT.NextRangeAttackTime = 3 -- How much time until it can use a range attack? | number = Specific time | VJ.SET = Randomized between the 2 numbers ENT.NextAnyAttackTime_Range = false -- How much time until it can do any attack again? | false = Base auto calculates the duration | number = Specific time | VJ.SET = Randomized between the 2 numbers ENT.RangeAttackReps = 1 -- How many times does it throw a projectile? ENT.RangeAttackExtraTimers = false -- Extra range attack timers | EX: {1, 1.4} ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Leap Attack ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ENT.HasLeapAttack = false ENT.LeapAttackDamage = 15 ENT.LeapAttackDamageType = DMG_SLASH ENT.DisableDefaultLeapAttackDamageCode = false -- Disables the default leap attack damage code -- ====== Animation ====== -- ENT.AnimTbl_LeapAttack = ACT_SPECIAL_ATTACK1 -- Animations to play when it leap attacks | false = Don't play an animation ENT.LeapAttackAnimationFaceEnemy = 2 -- 2 = Face the enemy UNTIL it jumps! | true = Face the enemy the entire time! | false = Don't face the enemy AT ALL! ENT.LeapAttackAnimationDecreaseLengthAmount = 0 -- Decreases animation time | Use it to fix animations that have extra frames at the end -- ====== Distance ====== -- ENT.LeapAttackMinDistance = 200 -- Min distance that it can leap from ENT.LeapAttackMaxDistance = 500 -- Max distance that it can leap from ENT.LeapAttackDamageDistance = 100 -- How far does the damage go? ENT.LeapAttackAngleRadius = 60 -- What is the attack angle radius? | 100 = In front of it | 180 = All around it -- ====== Timer ====== -- ENT.TimeUntilLeapAttackDamage = 0.2 -- How much time until it executes the damage? | false = Make the attack event-based ENT.TimeUntilLeapAttackVelocity = 0.1 -- How much time until it jumps and applies the velocity? ENT.NextLeapAttackTime = 3 -- How much time until it can use a leap attack? | number = Specific time | VJ.SET = Randomized between the 2 numbers ENT.NextAnyAttackTime_Leap = false -- How much time until it can do any attack again? | false = Base auto calculates the duration | number = Specific time | VJ.SET = Randomized between the 2 numbers ENT.LeapAttackReps = 1 -- How many times does it run the leap attack code? ENT.LeapAttackExtraTimers = false -- Extra leap attack timers | EX: {1, 1.4} ENT.LeapAttackStopOnHit = true -- Should it stop executing the leap attack after it hits an enemy? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Sound ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ENT.HasSounds = true -- Can it play sounds? | false = Disable ALL sounds ENT.DamageByPlayerDispositionLevel = 1 -- When should it play "DamageByPlayer" sounds? | 0 = Always | 1 = ONLY when friendly to player | 2 = ONLY when enemy to player -- ====== Footstep Sound ====== -- ENT.HasFootstepSounds = true -- Can it play footstep sounds? ENT.DisableFootStepSoundTimer = false -- Disables the timer system, allowing to utilize model events ENT.FootstepSoundTimerWalk = 1 -- Delay between footstep sounds while it is walking | false = Disable while walking ENT.FootstepSoundTimerRun = 0.5 -- Delay between footstep sounds while it is running | false = Disable while running -- ====== Idle Sound ====== -- ENT.HasIdleSounds = true -- Can it play idle sounds? | Controls "self.SoundTbl_Idle", "self.SoundTbl_IdleDialogue", "self.SoundTbl_CombatIdle" ENT.IdleSoundsWhileAttacking = false -- Can it play idle sounds while performing an attack? ENT.IdleSoundsRegWhileAlert = false -- Should it disable playing regular idle sounds when combat idle sound is empty? -- ====== Idle Dialogue Sound ====== -- -- When an allied NPC or player is within range, it will play these sounds rather than regular idle sounds -- If the ally is a VJ NPC and has dialogue answer sounds, it will respond back ENT.HasIdleDialogueSounds = true -- Can it play idle dialogue sounds? ENT.HasIdleDialogueAnswerSounds = true -- Can it play idle dialogue answer sounds? ENT.IdleDialogueDistance = 400 -- How close should an ally be for it to initiate a dialogue ENT.IdleDialogueCanTurn = true -- Should it turn to to face its dialogue target? -- ====== On Killed Enemy ====== -- ENT.HasKilledEnemySounds = true -- Can it play sounds when it kills an enemy? ENT.KilledEnemySoundLast = true -- Should it only play "self.SoundTbl_KilledEnemy" if there is no enemies left? -- ====== Sound Track ====== -- ENT.HasSoundTrack = false -- Can it play sound tracks? ENT.SoundTrackVolume = 1 -- Volume of the sound track | 1 = Normal | 2 = 200% | 0.5 = 50% ENT.SoundTrackPlaybackRate = 1 -- Playback speed of sound tracks | 1 = Normal | 2 = Twice the speed | 0.5 = Half the speed -- ====== Other Sound Controls ====== -- ENT.HasBreathSound = true -- Can it play breathing sounds? ENT.HasReceiveOrderSounds = true -- Can it play sounds when it receives an order? ENT.HasFollowPlayerSounds = true -- Can it play follow and unfollow player sounds? | Controls "self.SoundTbl_FollowPlayer", "self.SoundTbl_UnFollowPlayer" ENT.HasYieldToPlayerSounds = true -- Can it play sounds when it yields to an allied player? ENT.HasMedicSounds = true -- Can it play medic sounds? | Controls "self.SoundTbl_MedicBeforeHeal", "self.SoundTbl_MedicOnHeal", "self.SoundTbl_MedicReceiveHeal" ENT.HasOnPlayerSightSounds = true -- Can it play sounds when it sees a player? ENT.HasInvestigateSounds = true -- Can it play sounds when it investigates something? ENT.HasLostEnemySounds = true -- Can it play sounds when it looses its enemy? ENT.HasAlertSounds = true -- Can it play alert sounds? ENT.HasCallForHelpSounds = true -- Can it play sounds when it call allies for help? ENT.HasBecomeEnemyToPlayerSounds = true -- Can it play sounds when it becomes hostile to an allied player? ENT.HasMeleeAttackSounds = true -- Can it play melee attack sounds? | Controls "self.SoundTbl_BeforeMeleeAttack", "self.SoundTbl_MeleeAttack", "self.SoundTbl_MeleeAttackExtra" ENT.HasExtraMeleeAttackSounds = false -- Can it play extra melee attack sound effects? ENT.HasMeleeAttackMissSounds = true -- Can it play melee attack miss sounds? ENT.HasMeleeAttackPlayerSpeedSounds = true -- Does it have a sound when it slows down the player? ENT.HasRangeAttackSounds = true -- Can it play range attack sounds? | Controls "self.SoundTbl_BeforeRangeAttack", "self.SoundTbl_RangeAttack" ENT.HasBeforeLeapAttackSounds = true -- Can it play leap attack before jump sounds? ENT.HasLeapAttackJumpSounds = true -- Can it play leap attack jump sounds? ENT.HasLeapAttackDamageSounds = true -- Can it play leap attack damage sounds? ENT.HasLeapAttackDamageMissSounds = true -- Can it play leap attack miss sounds? ENT.HasAllyDeathSounds = true -- Can it play sounds when an ally dies? ENT.HasPainSounds = true -- Can it play pain sounds? ENT.HasImpactSounds = true -- Can it play impact sound effects? ENT.HasDamageByPlayerSounds = true -- Can it play sounds when it's damaged by a player? ENT.HasDeathSounds = true -- Can it play death sounds? -- ====== Sound Paths ====== -- -- There are 2 types of sounds: "SPEECH" and "EFFECT" | Most sound tables are "SPEECH" unless stated -- SPEECH : Mostly play speech sounds | Will stop when another speech sound is played -- EFFECT : Mostly play sound effects | EX: Movement sound, impact sound, attack swipe sound, etc. ENT.SoundTbl_SoundTrack = false ENT.SoundTbl_FootStep = false -- EFFECT ENT.SoundTbl_Breath = false -- EFFECT ENT.SoundTbl_Idle = false ENT.SoundTbl_IdleDialogue = false ENT.SoundTbl_IdleDialogueAnswer = false ENT.SoundTbl_CombatIdle = false ENT.SoundTbl_ReceiveOrder = false ENT.SoundTbl_FollowPlayer = false ENT.SoundTbl_UnFollowPlayer = false ENT.SoundTbl_YieldToPlayer = false ENT.SoundTbl_MedicBeforeHeal = false ENT.SoundTbl_MedicOnHeal = "items/smallmedkit1.wav" -- EFFECT ENT.SoundTbl_MedicReceiveHeal = false ENT.SoundTbl_OnPlayerSight = false ENT.SoundTbl_Investigate = false ENT.SoundTbl_LostEnemy = false ENT.SoundTbl_Alert = false ENT.SoundTbl_CallForHelp = false ENT.SoundTbl_BecomeEnemyToPlayer = false ENT.SoundTbl_BeforeMeleeAttack = false ENT.SoundTbl_MeleeAttack = false ENT.SoundTbl_MeleeAttackExtra = "Zombie.AttackHit" -- EFFECT ENT.SoundTbl_MeleeAttackMiss = false -- EFFECT ENT.SoundTbl_MeleeAttackPlayerSpeed = "vj_base/player/heartbeat_loop.wav" ENT.SoundTbl_BeforeRangeAttack = false ENT.SoundTbl_RangeAttack = false ENT.SoundTbl_BeforeLeapAttack = false ENT.SoundTbl_LeapAttackJump = false ENT.SoundTbl_LeapAttackDamage = false -- EFFECT ENT.SoundTbl_LeapAttackDamageMiss = false -- EFFECT ENT.SoundTbl_KilledEnemy = false ENT.SoundTbl_AllyDeath = false ENT.SoundTbl_Pain = false ENT.SoundTbl_Impact = "VJ.Impact.Flesh_Alien" -- EFFECT ENT.SoundTbl_DamageByPlayer = false ENT.SoundTbl_Death = false -- ====== Sound Chance ====== -- -- Higher number = less chance of playing | 1 = Always play ENT.IdleSoundChance = 2 ENT.IdleDialogueAnswerSoundChance = 1 ENT.CombatIdleSoundChance = 1 ENT.ReceiveOrderSoundChance = 1 ENT.FollowPlayerSoundChance = 1 -- Controls "self.SoundTbl_FollowPlayer", "self.SoundTbl_UnFollowPlayer" ENT.YieldToPlayerSoundChance = 2 ENT.MedicBeforeHealSoundChance = 1 ENT.MedicOnHealSoundChance = 1 ENT.MedicReceiveHealSoundChance = 1 ENT.OnPlayerSightSoundChance = 1 ENT.InvestigateSoundChance = 1 ENT.LostEnemySoundChance = 1 ENT.AlertSoundChance = 1 ENT.CallForHelpSoundChance = 1 ENT.BecomeEnemyToPlayerChance = 1 ENT.BeforeMeleeAttackSoundChance = 1 ENT.MeleeAttackSoundChance = 1 ENT.ExtraMeleeSoundChance = 1 ENT.MeleeAttackMissSoundChance = 1 ENT.BeforeRangeAttackSoundChance = 1 ENT.RangeAttackSoundChance = 1 ENT.BeforeLeapAttackSoundChance = 1 ENT.LeapAttackJumpSoundChance = 1 ENT.LeapAttackDamageSoundChance = 1 ENT.LeapAttackDamageMissSoundChance = 1 ENT.KilledEnemySoundChance = 1 ENT.AllyDeathSoundChance = 4 ENT.PainSoundChance = 1 ENT.ImpactSoundChance = 1 ENT.DamageByPlayerSoundChance = 1 ENT.DeathSoundChance = 1 ENT.SoundTrackChance = 1 -- ====== Timer ====== -- -- Randomized time between the two variables, x amount of time has to pass for the sound to play again | Counted in seconds -- false = Base will decide the time ENT.NextSoundTime_Breath = false ENT.NextSoundTime_Idle = VJ.SET(4, 11) ENT.NextSoundTime_Investigate = VJ.SET(5, 5) ENT.NextSoundTime_LostEnemy = VJ.SET(5, 6) ENT.NextSoundTime_Alert = VJ.SET(2, 3) ENT.NextSoundTime_KilledEnemy = VJ.SET(3, 5) ENT.NextSoundTime_AllyDeath = VJ.SET(3, 5) -- ====== Sound Level ====== -- -- The proper number are usually range from 0 to 180, though it can go as high as 511 -- More Information: https://developer.valvesoftware.com/wiki/Soundscripts#SoundLevel_Flags ENT.FootstepSoundLevel = 70 ENT.BreathSoundLevel = 60 ENT.IdleSoundLevel = 75 ENT.IdleDialogueSoundLevel = 75 -- Controls "self.SoundTbl_IdleDialogue", "self.SoundTbl_IdleDialogueAnswer" ENT.CombatIdleSoundLevel = 80 ENT.ReceiveOrderSoundLevel = 80 ENT.FollowPlayerSoundLevel = 75 -- Controls "self.SoundTbl_FollowPlayer", "self.SoundTbl_UnFollowPlayer" ENT.YieldToPlayerSoundLevel = 75 ENT.MedicBeforeHealSoundLevel = 75 ENT.MedicOnHealSoundLevel = 75 ENT.MedicReceiveHealSoundLevel = 75 ENT.OnPlayerSightSoundLevel = 75 ENT.InvestigateSoundLevel = 80 ENT.LostEnemySoundLevel = 75 ENT.AlertSoundLevel = 80 ENT.CallForHelpSoundLevel = 80 ENT.BecomeEnemyToPlayerSoundLevel = 75 ENT.BeforeMeleeAttackSoundLevel = 75 ENT.MeleeAttackSoundLevel = 75 ENT.ExtraMeleeAttackSoundLevel = 75 ENT.MeleeAttackMissSoundLevel = 75 ENT.MeleeAttackPlayerSpeedSoundLevel = 100 ENT.BeforeRangeAttackSoundLevel = 75 ENT.RangeAttackSoundLevel = 75 ENT.BeforeLeapAttackSoundLevel = 75 ENT.LeapAttackJumpSoundLevel = 75 ENT.LeapAttackDamageSoundLevel = 75 ENT.LeapAttackDamageMissSoundLevel = 75 ENT.KilledEnemySoundLevel = 80 ENT.AllyDeathSoundLevel = 80 ENT.PainSoundLevel = 80 ENT.ImpactSoundLevel = 60 ENT.DamageByPlayerSoundLevel = 75 ENT.DeathSoundLevel = 80 -- ====== Sound Pitch ====== -- -- Range: 0 - 255 | Lower pitch < x > Higher pitch ENT.MainSoundPitch = VJ.SET(90, 100) -- Can be a number or VJ.SET ENT.MainSoundPitchStatic = true -- Should it decide a number on spawn and use it as the main pitch? -- false = Use main pitch | number = Use a specific pitch | VJ.SET = Pick randomly between numbers every time it plays ENT.FootstepSoundPitch = VJ.SET(80, 100) ENT.BreathSoundPitch = 100 ENT.IdleSoundPitch = false ENT.IdleDialogueSoundPitch = false -- Controls "self.SoundTbl_IdleDialogue", "self.SoundTbl_IdleDialogueAnswer" ENT.CombatIdleSoundPitch = false ENT.ReceiveOrderSoundPitch = false ENT.FollowPlayerPitch = false -- Controls "self.SoundTbl_FollowPlayer", "self.SoundTbl_UnFollowPlayer" ENT.YieldToPlayerSoundPitch = false ENT.MedicBeforeHealSoundPitch = false ENT.MedicOnHealSoundPitch = 100 ENT.MedicReceiveHealSoundPitch = false ENT.OnPlayerSightSoundPitch = false ENT.InvestigateSoundPitch = false ENT.LostEnemySoundPitch = false ENT.AlertSoundPitch = false ENT.CallForHelpSoundPitch = false ENT.BecomeEnemyToPlayerPitch = false ENT.BeforeMeleeAttackSoundPitch = false ENT.MeleeAttackSoundPitch = false ENT.ExtraMeleeSoundPitch = VJ.SET(80, 100) ENT.MeleeAttackMissSoundPitch = VJ.SET(90, 100) ENT.BeforeRangeAttackPitch = false ENT.RangeAttackPitch = false ENT.BeforeLeapAttackSoundPitch = false ENT.LeapAttackJumpSoundPitch = false ENT.LeapAttackDamageSoundPitch = false ENT.LeapAttackDamageMissSoundPitch = false ENT.KilledEnemySoundPitch = false ENT.AllyDeathSoundPitch = false ENT.PainSoundPitch = false ENT.ImpactSoundPitch = VJ.SET(80, 100) ENT.DamageByPlayerPitch = false ENT.DeathSoundPitch = false ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ Customization Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ -- Use the functions below to customize parts of the NPC or add new systems without overriding parts of the base -- Some base functions don't have a hook because you can simply override them | Call "self.BaseClass.FuncName(self)" or "baseclass.Get(baseName)" to run the base code as well -- --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:PreInit() end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:Init() -- Collision bounds of the NPC | NOTE: Both Xs and Ys should be the same! | To view: "cl_ent_bbox" -- self:SetCollisionBounds(Vector(50, 50, 100), Vector(-50, -50, 0)) -- Damage bounds of the NPC | NOTE: Both Xs and Ys should be the same! | To view: "cl_ent_absbox" -- self:SetSurroundingBounds(Vector(150, 150, 200), Vector(-150, -150, 0)) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnThink() end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnThinkActive() end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE | Called at the end of every entity it checks every process time -- NOTE: "calculatedDisp" can in some cases be nil -- function ENT:OnMaintainRelationships(ent, calculatedDisp, entDist) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE -- function ENT:OnUpdatePoseParamTracking(pitch, yaw, roll) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE | Called from the engine -- function ENT:ExpressionFinished(strExp) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE | Called whenever VJ.CreateSound or VJ.EmitSound is called | return a new file path to replace the one that is about to play -- function ENT:OnPlaySound(sdFile) return "example/sound.wav" end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE | Called whenever a sound starts playing through VJ.CreateSound -- function ENT:OnCreateSound(sdData, sdFile) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE | Called whenever a sound starts playing through VJ.EmitSound -- function ENT:OnEmitSound(sdFile) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE | Called every time "self:FireBullets" is called -- function ENT:OnFireBullet(data) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE | Called whenever something collides with the NPC -- function ENT:OnTouch(ent) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE | Called from the engine -- function ENT:OnCondition(cond) VJ.DEBUG_Print(self, "OnCondition", cond, " = ", self:ConditionName(cond)) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE -- function ENT:OnInput(key, activator, caller, data) VJ.DEBUG_Print(self, "OnInput", key, activator, caller, data) end --------------------------------------------------------------------------------------------------------------------------------------------- -- UNCOMMENT TO USE -- local getEventName = util.GetAnimEventNameByID -- -- -- function ENT:OnAnimEvent(ev, evTime, evCycle, evType, evOptions) -- local eventName = getEventName(ev) -- VJ.DEBUG_Print(self, "OnAnimEvent", eventName, ev, evTime, evCycle, evType, evOptions) -- end --------------------------------------------------------------------------------------------------------------------------------------------- --[[--------------------------------------------------------- Called whenever the NPC begins following or stops following an entity - status = Type of call: - "Start" = NPC is now following the given entity - "Stop" = NPC is now unfollowing the given entity - ent = The entity that the NPC is now following or unfollowing -----------------------------------------------------------]] function ENT:OnFollow(status, ent) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[--------------------------------------------------------- Called every time a change occurs in the eating system - ent = The entity that it is checking OR speaking with - status = Type of update that is occurring, holds one of the following states: - "CheckEnt" = Possible friendly entity found, should we speak to it? | return anything other than true to skip and not speak to this entity! - "Speak" = Everything passed, start speaking - "Answer" = Another entity has spoken to me, answer back! | return anything other than true to not play an answer back dialogue! - statusData = Some status may have extra info, possible infos: - For "CheckEnt" = Boolean value, whether or not the entity can answer back - For "Speak" = Duration of our sentence Returns - ONLY used for "CheckEnt" & "Answer" | Check above for what each status return does -----------------------------------------------------------]] function ENT:OnIdleDialogue(ent, status, statusData) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called whenever the medic behavior updates =-=-=| PARAMETERS |=-=-= 1. status [string] : Type of update that is occurring, holds one of the following states: -> "BeforeHeal" : Right before it's about to heal an entity USAGE EXAMPLES -> Play chain of animations | Additional sound effect PARAMETERS 2. statusData [nil] RETURNS -> [nil] -> "OnHeal" : When the timer expires and is about to give health USAGE EXAMPLES -> Override healing code | Play an after heal animation PARAMETERS 2. statusData [entity] : The entity that it's about to heal RETURNS -> [bool] : Returning false will NOT update entity's health and will NOT clear its decals (Useful for custom code) -> "OnReset" : When the behavior ends OR has to move because entity moved USAGE EXAMPLES -> Cleanup bodygroups | Play a sound PARAMETERS 2. statusData [string] : Holds one of the following states: --> "Retry" : When it attempts to retry healing the entity, such as when the entity moved away so it has to chase again --> "End" : When the medic behavior exits completely RETURNS -> [nil] 2. statusData [nil | entity | string] : Depends on `status` value, refer to it for more details =-=-=| RETURNS |=-=-= -> [nil | bool] : Depends on `status` value, refer to it for more details --]] function ENT:OnMedicBehavior(status, statusData) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnPlayerSight(ent) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[--------------------------------------------------------- UNCOMMENT TO USE | Called every time footstep sound plays - moveType = Type of movement | Types: "Walk", "Run", "Event" - sdFile = Sound that it just played -----------------------------------------------------------]] -- function ENT:OnFootstepSound(moveType, sdFile) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnInvestigate(ent) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnResetEnemy() end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnAlert(ent) end --------------------------------------------------------------------------------------------------------------------------------------------- -- "ally" = Ally that we called for help -- "isFirst" = Is this the first ally that received this call? Use this to avoid running certain multiple times when many allies are around! function ENT:OnCallForHelp(ally, isFirst) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[--------------------------------------------------------- UNCOMMENT TO USE | Called constantly on think as long as it can attack and has an enemy This can be used to create a completely new attack system OR switch between multiple attacks (such as multiple melee attacks with varying distances) 1. isAttacking [boolean] : Whether or not the base has detected that performing an attacking 2. enemy [entity] : Current active enemy -----------------------------------------------------------]] -- function ENT:OnThinkAttack(isAttacking, enemy) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called when melee attack is triggered =-=-=| PARAMETERS |=-=-= 1. status [string] : Type of update that is occurring, holds one of the following states: -> "PreInit" : Before the attack is initialized | Before anything is set, useful to prevent the attack completely RETURNS -> [nil | boolean] : Return true to prevent the attack from being triggered -> "Init" : When the attack initially starts | Before sound, timers, and animations are set! RETURNS -> [nil] -> "PostInit" : After the sound, timers, and animations are set! RETURNS -> [nil] 2. enemy [entity] : Enemy that caused the attack to trigger =-=-=| RETURNS |=-=-= -> [nil | boolean] : Depends on `status` value, refer to it for more details --]] function ENT:OnMeleeAttack(status, enemy) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:MeleeAttackTraceOrigin() return (IsValid(self:GetEnemy()) and VJ.GetNearestPositions(self, self:GetEnemy(), true)) or self:GetPos() + self:GetForward() end --------------------------------------------------------------------------------------------------------------------------------------------- -- "self.MeleeAttackDamageAngleRadius" uses this to determine the direction of the attack and if something is within the angle radius function ENT:MeleeAttackTraceDirection() return self:GetHeadDirection() end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:MeleeAttackKnockbackVelocity(ent) return self:GetForward() * math.random(100, 140) + self:GetUp() * 10 end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called when melee attack is executed =-=-=| PARAMETERS |=-=-= 1. status [string] : Type of update that is occurring, holds one of the following states: -> "Init" : When the attack initially executed | Before entities are checked and damaged RETURNS -> [nil | boolean] : Return true to skip running the default execution (Useful for custom code) -> "PreDamage" : Right before the damage is applied to an entity PARAMETERS 2. ent [entity] : The entity that is about to be damaged 3. isProp [entity] : Is the entity detected as a prop? RETURNS -> [nil | boolean] : Return true to skip hitting this entity -> "Miss" : When the attack misses and doesn't hit anything RETURNS -> [nil] 2. ent [nil | entity] : Depends on `status` value, refer to it for more details 3. isProp [nil | entity] : Depends on `status` value, refer to it for more details =-=-=| RETURNS |=-=-= -> [nil | boolean] : Depends on `status` value, refer to it for more details --]] function ENT:OnMeleeAttackExecute(status, ent, isProp) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called when range attack is triggered =-=-=| PARAMETERS |=-=-= 1. status [string] : Type of update that is occurring, holds one of the following states: -> "PreInit" : Before the attack is initialized | Before anything is set, useful to prevent the attack completely RETURNS -> [nil | boolean] : Return true to prevent the attack from being triggered -> "Init" : When the attack initially starts | Before sound, timers, and animations are set! RETURNS -> [nil] -> "PostInit" : After the sound, timers, and animations are set! RETURNS -> [nil] 2. enemy [entity] : Enemy that caused the attack to trigger =-=-=| RETURNS |=-=-= -> [nil | boolean] : Depends on `status` value, refer to it for more details --]] function ENT:OnRangeAttack(status, enemy) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called when range attack is executed =-=-=| PARAMETERS |=-=-= 1. status [string] : Type of update that is occurring, holds one of the following states: -> "Init" : When the attack initially executed | Before entities are checked and damaged RETURNS -> [nil | boolean] : Return true to skip spawning the projectile -> "PreSpawn" : Right before "Spawn()" is called on the projectile PARAMETERS 3. projectile [entity] : The projectile entity that is about to spawn RETURNS -> [nil] -> "PostSpawn" : After "Spawn()" is called and velocity is set on the projectile PARAMETERS 3. projectile [entity] : The projectile entity that just spawned RETURNS -> [nil] 2. enemy [entity] : Enemy that it's about to fire at 3. projectile [nil | entity] : Depends on `status` value, refer to it for more details =-=-=| RETURNS |=-=-= -> [nil | boolean] : Depends on `status` value, refer to it for more details --]] function ENT:OnRangeAttackExecute(status, enemy, projectile) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:RangeAttackProjPos(projectile) // return self:GetAttachment(self:LookupAttachment("muzzle")).Pos -- Attachment example return self:GetPos() + self:GetUp() * 20 end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:RangeAttackProjVel(projectile) -- Use curve if the projectile has physics, otherwise use a simple line local phys = projectile:GetPhysicsObject() if IsValid(phys) && phys:IsGravityEnabled() then return VJ.CalculateTrajectory(self, self:GetEnemy(), "Curve", projectile:GetPos(), 1, 10) end return VJ.CalculateTrajectory(self, self:GetEnemy(), "Line", projectile:GetPos(), 1, 1500) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called when leap attack is triggered =-=-=| PARAMETERS |=-=-= 1. status [string] : Type of update that is occurring, holds one of the following states: -> "PreInit" : Before the attack is initialized | Before anything is set, useful to prevent the attack completely RETURNS -> [nil | boolean] : Return true to prevent the attack from being triggered -> "Init" : When the attack initially starts | Before sound, timers, and animations are set! RETURNS -> [nil] -> "PostInit" : After the sound, timers, and animations are set! RETURNS -> [nil] -> "Jump" : When the leap velocity is about to apply RETURNS -> [nil | vector] : Return a vector to override the velocity 2. enemy [entity] : Enemy that caused the attack to trigger =-=-=| RETURNS |=-=-= -> [nil | vector | boolean] : Depends on `status` value, refer to it for more details --]] function ENT:OnLeapAttack(status, enemy) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called when leap attack is executed =-=-=| PARAMETERS |=-=-= 1. status [string] : Type of update that is occurring, holds one of the following states: -> "Init" : When the attack initially executed | Before entities are checked and damaged RETURNS -> [nil | boolean] : Return true to skip running the default execution (Useful for custom code) -> "PreDamage" : Right before the damage is applied to an entity PARAMETERS 2. ent [entity] : The entity that is about to be damaged RETURNS -> [nil | boolean] : Return true to skip hitting this entity -> "Miss" : When the attack misses and doesn't hit anything RETURNS -> [nil] 2. ent [nil | entity] : Depends on `status` value, refer to it for more details =-=-=| RETURNS |=-=-= -> [nil | boolean] : Depends on `status` value, refer to it for more details --]] function ENT:OnLeapAttackExecute(status, ent) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnKilledEnemy(ent, inflictor, wasLast) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnAllyKilled(ent) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called whenever the NPC takes damage =-=-=| PARAMETERS |=-=-= 1. dmginfo [object] = CTakeDamageInfo object 2. hitgroup [number] = The hitgroup that it hit 3. status [string] : Type of update that is occurring, holds one of the following states: -> "Init" : First call on take damage, even before immune checks -> "PreDamage" : Right before the damage is applied to the NPC -> "PostDamage" : Right after the damage is applied to the NPC --]] function ENT:OnDamaged(dmginfo, hitgroup, status) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnBleed(dmginfo, hitgroup) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called whenever the NPC attempts to play flinch =-=-=| PARAMETERS |=-=-= 1. dmginfo [object] = CTakeDamageInfo object 2. hitgroup [number] = The hitgroup that it hit 3. status [string] : Type of update that is occurring, holds one of the following states: -> "Init" : Before the animation is played or any values are set USAGE EXAMPLES -> Disallow flinch | Override the animation | Add a extra check RETURNS -> [nil | bool] : Return true to disallow the flinch from playing -> "Execute" : Right after the flinch animation starts playing and all the values are set RETURNS -> [nil] =-=-=| RETURNS |=-=-= -> [nil | bool] : Depends on `status` value, refer to it for more details --]] function ENT:OnFlinch(dmginfo, hitgroup, status) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnBecomeEnemyToPlayer(dmginfo, hitgroup) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnSetEnemyFromDamage(dmginfo, hitgroup) end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called on death when the NPC is supposed to gib =-=-=| PARAMETERS |=-=-= 1. dmginfo [object] = CTakeDamageInfo object 2. hitgroup [number] = The hitgroup that it hit =-=-=| RETURNS |=-=-= -> [bool] : Notifies the base if the NPC gibbed or not - false : Spawns death corpse | Plays death animations | Does NOT play gib sounds - true : Disallows death corpse | Disallows death animations | Plays gib sounds -> [nil | table] : Overrides default actions, first return must be "true" for this to apply! - AllowCorpse : Allows death corpse to spawn | DEFAULT: false - AllowAnim : Allows death animations to play | DEFAULT: false - AllowSound : Allows default gib sounds to play | DEFAULT: true EXAMPLE: - {AllowCorpse = true} : Will spawn death corpse --]] function ENT:HandleGibOnDeath(dmginfo, hitgroup) return false end --------------------------------------------------------------------------------------------------------------------------------------------- --[[ Called when the NPC dies =-=-=| PARAMETERS |=-=-= 1. dmginfo [object] = CTakeDamageInfo object 2. hitgroup [number] = The hitgroup that it hit 3. status [string] : Type of update that is occurring, holds one of the following states: -> "Init" : First call when it dies before anything is changed or reset -> "DeathAnim" : Right before the death animation plays -> "Finish" : Right before the corpse is spawned, the active weapon is dropped and the NPC is removed --]] function ENT:OnDeath(dmginfo, hitgroup, status) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnCreateDeathCorpse(dmginfo, hitgroup, corpse) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:CustomOnRemove() end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:Controller_Initialize(ply, controlEnt) //ply:ChatPrint("CTRL + MOUSE2: Rocket Attack") -- Example key binding message end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:SetAnimationTranslations(wepHoldType) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------ ///// WARNING: Don't touch anything below this line! \\\\\ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ local defPos = Vector(0, 0, 0) local StopSD = VJ.STOPSOUND local CurTime = CurTime local IsValid = IsValid local GetConVar = GetConVar local math_min = math.min local math_max = math.max local math_rad = math.rad local math_cos = math.cos local math_angApproach = math.ApproachAngle local PICK = VJ.PICK local VJ_STATE_NONE = VJ_STATE_NONE local VJ_STATE_FREEZE = VJ_STATE_FREEZE local VJ_STATE_ONLY_ANIMATION = VJ_STATE_ONLY_ANIMATION local VJ_STATE_ONLY_ANIMATION_CONSTANT = VJ_STATE_ONLY_ANIMATION_CONSTANT local VJ_STATE_ONLY_ANIMATION_NOATTACK = VJ_STATE_ONLY_ANIMATION_NOATTACK local VJ_BEHAVIOR_PASSIVE = VJ_BEHAVIOR_PASSIVE local VJ_BEHAVIOR_PASSIVE_NATURE = VJ_BEHAVIOR_PASSIVE_NATURE local VJ_MOVETYPE_GROUND = VJ_MOVETYPE_GROUND local VJ_MOVETYPE_AERIAL = VJ_MOVETYPE_AERIAL local VJ_MOVETYPE_AQUATIC = VJ_MOVETYPE_AQUATIC local VJ_MOVETYPE_STATIONARY = VJ_MOVETYPE_STATIONARY local VJ_MOVETYPE_PHYSICS = VJ_MOVETYPE_PHYSICS local ANIM_TYPE_GESTURE = VJ.ANIM_TYPE_GESTURE local metaEntity = FindMetaTable("Entity") local funcGetPoseParameter = metaEntity.GetPoseParameter local funcSetPoseParameter = metaEntity.SetPoseParameter -- local metaNPC = FindMetaTable("NPC") local funcHasCondition = metaNPC.HasCondition ENT.PropInteraction_Found = false ENT.PropInteraction_NextCheckT = 0 ENT.IsAbleToRangeAttack = true ENT.IsAbleToLeapAttack = true ENT.LeapAttackHasJumped = false //ENT.EatingData = {} -- Set later local vj_npc_debug = GetConVar("vj_npc_debug") local vj_npc_processtime = GetConVar("vj_npc_processtime") local vj_npc_poseparams = GetConVar("vj_npc_poseparams") local vj_npc_shadows = GetConVar("vj_npc_shadows") local vj_npc_snd = GetConVar("vj_npc_snd") local vj_npc_fri_base = GetConVar("vj_npc_fri_base") local vj_npc_fri_player = GetConVar("vj_npc_fri_player") local vj_npc_fri_antlion = GetConVar("vj_npc_fri_antlion") local vj_npc_fri_combine = GetConVar("vj_npc_fri_combine") local vj_npc_fri_zombie = GetConVar("vj_npc_fri_zombie") local vj_npc_allies = GetConVar("vj_npc_allies") local vj_npc_anim_death = GetConVar("vj_npc_anim_death") local vj_npc_corpse = GetConVar("vj_npc_corpse") local vj_npc_loot = GetConVar("vj_npc_loot") local vj_npc_melee_bleed = GetConVar("vj_npc_melee_bleed") local vj_npc_melee_ply_speed = GetConVar("vj_npc_melee_ply_speed") local vj_npc_wander = GetConVar("vj_npc_wander") local vj_npc_chase = GetConVar("vj_npc_chase") local vj_npc_flinch = GetConVar("vj_npc_flinch") local vj_npc_melee = GetConVar("vj_npc_melee") local vj_npc_range = GetConVar("vj_npc_range") local vj_npc_leap = GetConVar("vj_npc_leap") local vj_npc_blood = GetConVar("vj_npc_blood") local vj_npc_god = GetConVar("vj_npc_god") local vj_npc_ply_betray = GetConVar("vj_npc_ply_betray") local vj_npc_callhelp = GetConVar("vj_npc_callhelp") local vj_npc_investigate = GetConVar("vj_npc_investigate") local vj_npc_eat = GetConVar("vj_npc_eat") local vj_npc_ply_follow = GetConVar("vj_npc_ply_follow") local vj_npc_ply_chat = GetConVar("vj_npc_ply_chat") local vj_npc_medic = GetConVar("vj_npc_medic") local vj_npc_gib_vfx = GetConVar("vj_npc_gib_vfx") local vj_npc_gib = GetConVar("vj_npc_gib") local vj_npc_blood_gmod = GetConVar("vj_npc_blood_gmod") local vj_npc_sight_xray = GetConVar("vj_npc_sight_xray") local vj_npc_snd_gib = GetConVar("vj_npc_snd_gib") local vj_npc_snd_track = GetConVar("vj_npc_snd_track") local vj_npc_snd_footstep = GetConVar("vj_npc_snd_footstep") local vj_npc_snd_idle = GetConVar("vj_npc_snd_idle") local vj_npc_snd_breath = GetConVar("vj_npc_snd_breath") local vj_npc_snd_alert = GetConVar("vj_npc_snd_alert") local vj_npc_snd_melee = GetConVar("vj_npc_snd_melee") local vj_npc_snd_plyspeed = GetConVar("vj_npc_snd_plyspeed") local vj_npc_snd_range = GetConVar("vj_npc_snd_range") local vj_npc_snd_leap = GetConVar("vj_npc_snd_leap") local vj_npc_snd_pain = GetConVar("vj_npc_snd_pain") local vj_npc_snd_death = GetConVar("vj_npc_snd_death") local vj_npc_snd_plyfollow = GetConVar("vj_npc_snd_plyfollow") local vj_npc_snd_plybetrayal = GetConVar("vj_npc_snd_plybetrayal") local vj_npc_snd_plydamage = GetConVar("vj_npc_snd_plydamage") local vj_npc_snd_plysight = GetConVar("vj_npc_snd_plysight") local vj_npc_snd_medic = GetConVar("vj_npc_snd_medic") local vj_npc_snd_callhelp = GetConVar("vj_npc_snd_callhelp") local vj_npc_snd_receiveorder = GetConVar("vj_npc_snd_receiveorder") local vj_npc_creature_opendoor = GetConVar("vj_npc_creature_opendoor") local vj_npc_melee_propint = GetConVar("vj_npc_melee_propint") local vj_npc_corpse_collision = GetConVar("vj_npc_corpse_collision") local vj_npc_debug_engine = GetConVar("vj_npc_debug_engine") local vj_npc_difficulty = GetConVar("vj_npc_difficulty") local vj_npc_sight_distance = GetConVar("vj_npc_sight_distance") local vj_npc_health = GetConVar("vj_npc_health") local vj_npc_melee_ply_dsp = GetConVar("vj_npc_melee_ply_dsp") local vj_npc_ply_frag = GetConVar("vj_npc_ply_frag") local vj_npc_blood_pool = GetConVar("vj_npc_blood_pool") local vj_npc_corpse_undo = GetConVar("vj_npc_corpse_undo") local vj_npc_corpse_fade = GetConVar("vj_npc_corpse_fade") local vj_npc_corpse_fadetime = GetConVar("vj_npc_corpse_fadetime") local ai_serverragdolls = GetConVar("ai_serverragdolls") --------------------------------------------------------------------------------------------------------------------------------------------- local function InitConvars(self) if vj_npc_debug:GetInt() == 1 then self.VJ_DEBUG = true end if vj_npc_poseparams:GetInt() == 0 && !self.OnUpdatePoseParamTracking then self.HasPoseParameterLooking = false end if vj_npc_shadows:GetInt() == 0 then self:DrawShadow(false) end if vj_npc_snd:GetInt() == 0 then self.HasSounds = false end if vj_npc_fri_base:GetInt() == 1 then self.VJ_NPC_Class[#self.VJ_NPC_Class + 1] = "CLASS_VJ_BASE" end if vj_npc_fri_player:GetInt() == 1 then self.VJ_NPC_Class[#self.VJ_NPC_Class + 1] = "CLASS_PLAYER_ALLY" end if vj_npc_fri_antlion:GetInt() == 1 then self.VJ_NPC_Class[#self.VJ_NPC_Class + 1] = "CLASS_ANTLION" end if vj_npc_fri_combine:GetInt() == 1 then self.VJ_NPC_Class[#self.VJ_NPC_Class + 1] = "CLASS_COMBINE" end if vj_npc_fri_zombie:GetInt() == 1 then self.VJ_NPC_Class[#self.VJ_NPC_Class + 1] = "CLASS_ZOMBIE" end if vj_npc_allies:GetInt() == 0 then self.CanAlly = false end if vj_npc_anim_death:GetInt() == 0 then self.HasDeathAnimation = false end if vj_npc_corpse:GetInt() == 0 then self.HasDeathCorpse = false end if vj_npc_loot:GetInt() == 0 then self.DropDeathLoot = false end if vj_npc_melee_bleed:GetInt() == 0 then self.MeleeAttackBleedEnemy = false end if vj_npc_melee_ply_dsp:GetInt() == 0 then self.MeleeAttackDSP = false end if vj_npc_melee_ply_speed:GetInt() == 0 then self.MeleeAttackPlayerSpeed = false end if vj_npc_wander:GetInt() == 0 then self.DisableWandering = true end if vj_npc_chase:GetInt() == 0 then self.DisableChasingEnemy = true end if vj_npc_flinch:GetInt() == 0 then self.CanFlinch = false end if vj_npc_melee:GetInt() == 0 then self.HasMeleeAttack = false end if vj_npc_range:GetInt() == 0 then self.HasRangeAttack = false end if vj_npc_leap:GetInt() == 0 then self.HasLeapAttack = false end if vj_npc_blood:GetInt() == 0 then self.Bleeds = false end if vj_npc_god:GetInt() == 1 then self.GodMode = true end if vj_npc_ply_betray:GetInt() == 0 then self.BecomeEnemyToPlayer = false end if vj_npc_callhelp:GetInt() == 0 then self.CallForHelp = false end if vj_npc_investigate:GetInt() == 0 then self.CanInvestigate = false end if vj_npc_eat:GetInt() == 0 then self.CanEat = false end if vj_npc_ply_follow:GetInt() == 0 then self.FollowPlayer = false end if vj_npc_ply_chat:GetInt() == 0 then self.CanChatMessage = false end if vj_npc_medic:GetInt() == 0 then self.IsMedic = false end if vj_npc_gib_vfx:GetInt() == 0 then self.HasGibOnDeathEffects = false end if vj_npc_gib:GetInt() == 0 then self.CanGib = false self.CanGibOnDeath = false end if vj_npc_blood_gmod:GetInt() == 1 then self.BloodDecalUseGMod = true end if vj_npc_sight_xray:GetInt() == 1 then self.SightAngle = 360 self.EnemyXRayDetection = true end if vj_npc_snd_gib:GetInt() == 0 then self.HasGibOnDeathSounds = false end if vj_npc_snd_track:GetInt() == 0 then self.HasSoundTrack = false end if vj_npc_snd_footstep:GetInt() == 0 then self.HasFootstepSounds = false end if vj_npc_snd_idle:GetInt() == 0 then self.HasIdleSounds = false end if vj_npc_snd_breath:GetInt() == 0 then self.HasBreathSound = false end if vj_npc_snd_alert:GetInt() == 0 then self.HasAlertSounds = false end if vj_npc_snd_melee:GetInt() == 0 then self.HasMeleeAttackSounds = false self.HasExtraMeleeAttackSounds = false self.HasMeleeAttackMissSounds = false end if vj_npc_snd_plyspeed:GetInt() == 0 then self.HasMeleeAttackPlayerSpeedSounds = false end if vj_npc_snd_range:GetInt() == 0 then self.HasRangeAttackSounds = false end if vj_npc_snd_leap:GetInt() == 0 then self.HasBeforeLeapAttackSounds = false self.HasLeapAttackJumpSounds = false self.HasLeapAttackDamageSounds = false self.HasLeapAttackDamageMissSounds = false end if vj_npc_snd_pain:GetInt() == 0 then self.HasPainSounds = false end if vj_npc_snd_death:GetInt() == 0 then self.HasDeathSounds = false end if vj_npc_snd_plyfollow:GetInt() == 0 then self.HasFollowPlayerSounds = false end if vj_npc_snd_plybetrayal:GetInt() == 0 then self.HasBecomeEnemyToPlayerSounds = false end if vj_npc_snd_plydamage:GetInt() == 0 then self.HasDamageByPlayerSounds = false end if vj_npc_snd_plysight:GetInt() == 0 then self.HasOnPlayerSightSounds = false end if vj_npc_snd_medic:GetInt() == 0 then self.HasMedicSounds = false end if vj_npc_snd_callhelp:GetInt() == 0 then self.HasCallForHelpSounds = false end if vj_npc_snd_receiveorder:GetInt() == 0 then self.HasReceiveOrderSounds = false end if vj_npc_creature_opendoor:GetInt() == 0 then self.CanOpenDoors = false end local propAPType = vj_npc_melee_propint:GetInt() if propAPType != 1 then if propAPType == 0 then -- Disable self.PropInteraction = false elseif propAPType == 2 && self.PropInteraction != "OnlyPush" then -- Only damage if self.PropInteraction == "OnlyDamage" then self.PropInteraction = false else self.PropInteraction = "OnlyDamage" end elseif propAPType == 3 && self.PropInteraction != "OnlyDamage" then -- Only push if self.PropInteraction == "OnlyPush" then self.PropInteraction = false else self.PropInteraction = "OnlyPush" end end end local corpseCollision = vj_npc_corpse_collision:GetInt() if corpseCollision != 0 && self.DeathCorpseCollisionType == COLLISION_GROUP_DEBRIS then if corpseCollision == 1 then self.DeathCorpseCollisionType = COLLISION_GROUP_NONE elseif corpseCollision == 2 then self.DeathCorpseCollisionType = COLLISION_GROUP_WORLD elseif corpseCollision == 3 then self.DeathCorpseCollisionType = COLLISION_GROUP_INTERACTIVE elseif corpseCollision == 4 then self.DeathCorpseCollisionType = COLLISION_GROUP_WEAPON elseif corpseCollision == 5 then self.DeathCorpseCollisionType = COLLISION_GROUP_PASSABLE_DOOR elseif corpseCollision == 6 then self.DeathCorpseCollisionType = COLLISION_GROUP_NONE end end -- Enables source engine debug overlays (some commands like 'npc_conditions' need it) if self.VJ_DEBUG && vj_npc_debug_engine:GetInt() == 1 then self:SetSaveValue("m_debugOverlays", bit.bor(0x00000001, 0x00000002, 0x00000004, 0x00000008, 0x00000010, 0x00000020, 0x00000040, 0x00000080, 0x00000100, 0x00000200, 0x00001000, 0x00002000, 0x00004000, 0x00008000, 0x00020000, 0x00040000, 0x00080000, 0x00100000, 0x00200000, 0x00400000, 0x04000000, 0x08000000, 0x10000000, 0x20000000, 0x40000000)) end end --------------------------------------------------------------------------------------------------------------------------------------------- local function ApplyBackwardsCompatibility(self) -- !!!!!!!!!!!!!! DO NOT USE ANY OF THESE !!!!!!!!!!!!!! [Backwards Compatibility!] -- Most of these are pre-revamp variables & functions if self.CustomOnInitialize then self:CustomOnInitialize() end if self.CustomInitialize then self:CustomInitialize() end if self.CustomOn_PoseParameterLookingCode then self.OnUpdatePoseParamTracking = function(_, pitch, yaw, roll) self:CustomOn_PoseParameterLookingCode(pitch, yaw, roll) end end if self.CustomOnAlert then self.OnAlert = function(_, ent) self:CustomOnAlert(ent) end end if self.CustomOnInvestigate then self.OnInvestigate = function(_, ent) self:CustomOnInvestigate(ent) end end if self.CustomOnFootStepSound then self.OnFootstepSound = function(_, moveType, sdFile) self:CustomOnFootStepSound(moveType, sdFile) end end if self.CustomOnCallForHelp then self.OnCallForHelp = function(_, ally, isFirst) self:CustomOnCallForHelp(ally, isFirst) end end if self.CustomOnPlayerSight then self.OnPlayerSight = function(_, ent) self:CustomOnPlayerSight(ent) end end if self.CustomOnThink then self.OnThink = function() self:CustomOnThink() end end if self.CustomOnThink_AIEnabled then self.OnThinkActive = function() self:CustomOnThink_AIEnabled() end end if self.CustomOnTakeDamage_OnBleed then self.OnBleed = function(_, dmginfo, hitgroup) self:CustomOnTakeDamage_OnBleed(dmginfo, hitgroup) end end if self.CustomOnAcceptInput then self.OnInput = function(_, key, activator, caller, data) self:CustomOnAcceptInput(key, activator, caller, data) end end if self.CustomOnHandleAnimEvent then self.OnAnimEvent = function(_, ev, evTime, evCycle, evType, evOptions) self:CustomOnHandleAnimEvent(ev, evTime, evCycle, evType, evOptions) end end if self.CustomOnDeath_AfterCorpseSpawned then self.OnCreateDeathCorpse = function(_, dmginfo, hitgroup, corpse) self:CustomOnDeath_AfterCorpseSpawned(dmginfo, hitgroup, corpse) end end if self.PlayerFriendly == true then self.VJ_NPC_Class[#self.VJ_NPC_Class + 1] = "CLASS_PLAYER_ALLY" end if self.HasHealthRegeneration then self.HealthRegenParams.Enabled = true end if self.HealthRegenerationAmount then self.HealthRegenParams.Amount = self.HealthRegenerationAmount end if self.HealthRegenerationDelay then self.HealthRegenParams.Delay = self.HealthRegenerationDelay end if self.HealthRegenerationResetOnDmg then self.HealthRegenParams.ResetOnDmg = self.HealthRegenerationResetOnDmg end if self.FriendsWithAllPlayerAllies != nil then self.AlliedWithPlayerAllies = self.FriendsWithAllPlayerAllies end if self.Medic_CanBeHealed == false then self.VJ_ID_Healable = false end if self.Immune_AcidPoisonRadiation != nil then self.Immune_Toxic = self.Immune_AcidPoisonRadiation end if self.Immune_Blast != nil then self.Immune_Explosive = self.Immune_Blast end if self.FindEnemy_CanSeeThroughWalls == true then self.EnemyXRayDetection = true end if self.DisableFindEnemy == true then self.EnemyDetection = false end if self.DisableTouchFindEnemy == true then self.EnemyTouchDetection = false end if self.HasFootStepSound then self.HasFootstepSounds = self.HasFootStepSound end if self.FootStepPitch then self.FootstepSoundPitch = self.FootStepPitch end if self.FootStepSoundLevel then self.FootstepSoundLevel = self.FootStepSoundLevel end if self.FootStepTimeWalk then self.FootstepSoundTimerWalk = self.FootStepTimeWalk end if self.FootStepTimeRun then self.FootstepSoundTimerRun = self.FootStepTimeRun end if self.HitGroupFlinching_Values then self.FlinchHitGroupMap = self.HitGroupFlinching_Values end if self.HitGroupFlinching_DefaultWhenNotHit != nil then self.FlinchHitGroupPlayDefault = self.HitGroupFlinching_DefaultWhenNotHit end if self.NextFlinchTime != nil then self.FlinchCooldown = self.NextFlinchTime end if self.NextCallForHelpTime then self.CallForHelpCooldown = self.NextCallForHelpTime end if self.CallForHelpAnimationFaceEnemy != nil then self.CallForHelpAnimFaceEnemy = self.CallForHelpAnimationFaceEnemy end if self.NextCallForHelpAnimationTime != nil then self.CallForHelpAnimCooldown = self.NextCallForHelpAnimationTime end if self.InvestigateSoundDistance != nil then self.InvestigateSoundMultiplier = self.InvestigateSoundDistance end if self.SoundTbl_OnKilledEnemy != nil then self.SoundTbl_KilledEnemy = self.SoundTbl_OnKilledEnemy end if self.HasOnKilledEnemySounds != nil then self.HasKilledEnemySounds = self.HasOnKilledEnemySounds end if self.OnKilledEnemySoundChance then self.OnKilledEnemySoundChance = self.OnKilledEnemySoundChance end if self.NextSoundTime_OnKilledEnemy then self.NextSoundTime_KilledEnemy = self.NextSoundTime_OnKilledEnemy end if self.OnKilledEnemySoundLevel then self.KilledEnemySoundLevel = self.OnKilledEnemySoundLevel end if self.OnKilledEnemySoundPitch != nil then self.KilledEnemySoundPitch = self.OnKilledEnemySoundPitch end if self.IdleSounds_PlayOnAttacks != nil then self.IdleSoundsWhileAttacking = self.IdleSounds_PlayOnAttacks end if self.IdleSounds_NoRegularIdleOnAlerted != nil then self.IdleSoundsRegWhileAlert = self.IdleSounds_NoRegularIdleOnAlerted end if self.HasOnReceiveOrderSounds != nil then self.HasReceiveOrderSounds = self.HasOnReceiveOrderSounds end if self.SoundTbl_OnReceiveOrder != nil then self.SoundTbl_ReceiveOrder = self.SoundTbl_OnReceiveOrder end if self.OnReceiveOrderSoundChance != nil then self.ReceiveOrderSoundChance = self.OnReceiveOrderSoundChance end if self.OnReceiveOrderSoundLevel != nil then self.ReceiveOrderSoundLevel = self.OnReceiveOrderSoundLevel end if self.OnReceiveOrderSoundPitch != nil then self.ReceiveOrderSoundPitch = self.OnReceiveOrderSoundPitch end if self.SoundTbl_MedicAfterHeal != nil then self.SoundTbl_MedicOnHeal = self.SoundTbl_MedicAfterHeal end if self.MedicAfterHealSoundChance != nil then self.MedicOnHealSoundChance = self.MedicAfterHealSoundChance end if self.BeforeHealSoundLevel != nil then self.MedicBeforeHealSoundLevel = self.BeforeHealSoundLevel end if self.AfterHealSoundLevel != nil then self.MedicOnHealSoundLevel = self.AfterHealSoundLevel end if self.BeforeHealSoundPitch != nil then self.MedicBeforeHealSoundPitch = self.BeforeHealSoundPitch end if self.AfterHealSoundPitch != nil then self.MedicOnHealSoundPitch = self.AfterHealSoundPitch end if self.Immune_Physics then self:SetPhysicsDamageScale(0) end if self.SlowPlayerOnMeleeAttack then self.MeleeAttackPlayerSpeed = true end if self.SlowPlayerOnMeleeAttack_WalkSpeed then self.MeleeAttackPlayerSpeedWalk = self.SlowPlayerOnMeleeAttack_WalkSpeed end if self.SlowPlayerOnMeleeAttack_RunSpeed then self.MeleeAttackPlayerSpeedRun = self.SlowPlayerOnMeleeAttack_RunSpeed end if self.SlowPlayerOnMeleeAttackTime then self.MeleeAttackPlayerSpeedTime = self.SlowPlayerOnMeleeAttackTime end if self.HasMeleeAttackSlowPlayerSound != nil then self.HasMeleeAttackPlayerSpeedSounds = self.HasMeleeAttackSlowPlayerSound end if self.SoundTbl_MeleeAttackSlowPlayer != nil then self.SoundTbl_MeleeAttackPlayerSpeed = self.SoundTbl_MeleeAttackSlowPlayer end if self.MeleeAttackSlowPlayerSoundLevel != nil then self.MeleeAttackPlayerSpeedSoundLevel = self.MeleeAttackSlowPlayerSoundLevel end if self.StopMeleeAttackAfterFirstHit != nil then self.MeleeAttackStopOnHit = self.StopMeleeAttackAfterFirstHit end if self.StopLeapAttackAfterFirstHit != nil then self.LeapAttackStopOnHit = self.StopLeapAttackAfterFirstHit end if self.NextLeapAttackTime_DoRand then self.NextLeapAttackTime = VJ.SET(self.NextLeapAttackTime, self.NextLeapAttackTime_DoRand) end if self.NextAnyAttackTime_Leap_DoRand then self.NextAnyAttackTime_Leap = VJ.SET(self.NextAnyAttackTime_Leap, self.NextAnyAttackTime_Leap_DoRand) end if self.NextRangeAttackTime_DoRand then self.NextRangeAttackTime = VJ.SET(self.NextRangeAttackTime, self.NextRangeAttackTime_DoRand) end if self.NextAnyAttackTime_Range_DoRand then self.NextAnyAttackTime_Range = VJ.SET(self.NextAnyAttackTime_Range, self.NextAnyAttackTime_Range_DoRand) end if self.MeleeAttackDSPSoundType != nil then self.MeleeAttackDSP = self.MeleeAttackDSPSoundType end if self.MeleeAttackDSPSoundUseDamage == false then self.MeleeAttackDSPLimit = false end if self.MeleeAttackDSPSoundUseDamageAmount then self.MeleeAttackDSPLimit = self.MeleeAttackDSPSoundUseDamageAmount end if self.DisableMeleeAttackAnimation == true then self.AnimTbl_MeleeAttack = false end if self.DisableRangeAttackAnimation == true then self.AnimTbl_RangeAttack = false end if self.DisableLeapAttackAnimation == true then self.AnimTbl_LeapAttack = false end if self.RangeAttackEntityToSpawn then self.RangeAttackProjectiles = self.RangeAttackEntityToSpawn end if self.RangeDistance then self.RangeAttackMaxDistance = self.RangeDistance end if self.RangeToMeleeDistance then self.RangeAttackMinDistance = self.RangeToMeleeDistance end if self.LeapDistance then self.LeapAttackMaxDistance = self.LeapDistance end if self.LeapToMeleeDistance then self.LeapAttackMinDistance = self.LeapToMeleeDistance end if self.Passive_RunOnDamage == false then self.DamageResponse = false end if self.HideOnUnknownDamage == false then self.DamageResponse = "OnlySearch" end if self.DisableTakeDamageFindEnemy == true then if self.HideOnUnknownDamage == false then self.DamageResponse = false else self.DamageResponse = "OnlyMove" end end if self.CanFlinch == 0 then self.CanFlinch = false end if self.CanFlinch == 1 then self.CanFlinch = true end if self.CanFlinch == 2 then self.CanFlinch = "DamageTypes" end if self.BringFriendsOnDeath != nil or self.AlertFriendsOnDeath != nil then if self.AlertFriendsOnDeath == true && (self.BringFriendsOnDeath == false or self.BringFriendsOnDeath == nil) then self.DeathAllyResponse = "OnlyAlert" elseif self.BringFriendsOnDeath == false && self.AlertFriendsOnDeath == false then self.DeathAllyResponse = false end end if self.BringFriendsOnDeathLimit then self.DeathAllyResponse_MoveLimit = self.BringFriendsOnDeathLimit end if self.VJC_Data then self.ControllerParams = self.VJC_Data end if self.HasCallForHelpAnimation == false then self.AnimTbl_CallForHelp = false end if self.Medic_DisableAnimation == true then self.AnimTbl_Medic_GiveHealth = false end if self.ConstantlyFaceEnemyDistance then self.ConstantlyFaceEnemy_MinDistance = self.ConstantlyFaceEnemyDistance end if self.CallForBackUpOnDamage != nil then self.DamageAllyResponse = self.CallForBackUpOnDamage end if self.NextCallForBackUpOnDamageTime then self.DamageAllyResponse_Cooldown = self.NextCallForBackUpOnDamageTime end if self.CallForBackUpOnDamageAnimation then self.AnimTbl_DamageAllyResponse = self.CallForBackUpOnDamageAnimation end if self.UseTheSameGeneralSoundPitch != nil then self.MainSoundPitchStatic = self.UseTheSameGeneralSoundPitch end if self.GeneralSoundPitch1 or self.GeneralSoundPitch2 then self.MainSoundPitch = VJ.SET(self.GeneralSoundPitch1 or 90, self.GeneralSoundPitch2 or 100) end if self.PropAP_MaxSize then self.PropInteraction_MaxScale = self.PropAP_MaxSize end if self.AttackProps == false or self.PushProps == false then if self.AttackProps == false && self.PushProps == false then self.PropInteraction = false elseif self.AttackProps == false then self.PropInteraction = "OnlyPush" elseif self.PushProps == false then self.PropInteraction = "OnlyDamage" end end if self.NoChaseAfterCertainRange then self.LimitChaseDistance = self.NoChaseAfterCertainRange end if self.NoChaseAfterCertainRange_CloseDistance then self.LimitChaseDistance_Min = self.NoChaseAfterCertainRange_CloseDistance end if self.NoChaseAfterCertainRange_FarDistance then self.LimitChaseDistance_Max = self.NoChaseAfterCertainRange_FarDistance end if self.NoChaseAfterCertainRange_Type then if self.NoChaseAfterCertainRange_Type == "Regular" then self.LimitChaseDistance = true elseif self.NoChaseAfterCertainRange_Type == "OnlyRange" then self.LimitChaseDistance = "OnlyRange" end end if self.AlertedToIdleTime then self.AlertTimeout = self.AlertedToIdleTime end if self.SoundTbl_MoveOutOfPlayersWay then self.SoundTbl_YieldToPlayer = self.SoundTbl_MoveOutOfPlayersWay end if self.MaxJumpLegalDistance then self.JumpParams.MaxRise = self.MaxJumpLegalDistance.a; self.JumpParams.MaxDrop = self.MaxJumpLegalDistance.b end if self.VJ_IsHugeMonster then self.VJ_ID_Boss = self.VJ_IsHugeMonster end if self.Medic_HealthAmount then self.Medic_HealAmount = self.Medic_HealthAmount end if self.UsePlayerModelMovement then self.UsePoseParameterMovement = true end if self.MoveOutOfFriendlyPlayersWay != nil then self.YieldToAlliedPlayers = self.MoveOutOfFriendlyPlayersWay end if self.WaitBeforeDeathTime then self.DeathDelayTime = self.WaitBeforeDeathTime end if self.HasDeathRagdoll != nil then self.HasDeathCorpse = self.HasDeathRagdoll end if self.AllowedToGib != nil then self.CanGib = self.AllowedToGib end if self.HasGibOnDeath != nil then self.CanGibOnDeath = self.HasGibOnDeath end if self.HasGibDeathParticles != nil then self.HasGibOnDeathEffects = self.HasGibDeathParticles else self.HasGibDeathParticles = self.HasGibOnDeathEffects end if self.HasItemDropsOnDeath != nil then self.DropDeathLoot = self.HasItemDropsOnDeath end if self.ItemDropsOnDeathChance != nil then self.DeathLootChance = self.ItemDropsOnDeathChance end if self.ItemDropsOnDeath_EntityList != nil then self.DeathLoot = self.ItemDropsOnDeath_EntityList end if self.AllowMovementJumping != nil then self.JumpParams.Enabled = self.AllowMovementJumping end if self.OnlyDoKillEnemyWhenClear != nil then self.KilledEnemySoundLast = self.OnlyDoKillEnemyWhenClear end if self.DisableFootStepOnWalk then self.FootstepSoundTimerWalk = false end if self.DisableFootStepOnRun then self.FootstepSoundTimerRun = false end if self.FindEnemy_UseSphere then self.SightAngle = 360 end if self.IsMedicSNPC then self.IsMedic = self.IsMedicSNPC end if self.BecomeEnemyToPlayer == true then self.BecomeEnemyToPlayer = self.BecomeEnemyToPlayerLevel or 2 end if self.CustomBlood_Particle then self.BloodParticle = self.CustomBlood_Particle end if self.CustomBlood_Pool then self.BloodPool = self.CustomBlood_Pool end if self.CustomBlood_Decal then self.BloodDecal = self.CustomBlood_Decal end if self.GibOnDeathDamagesTable then for _, v in ipairs(self.GibOnDeathDamagesTable) do if v == "All" then self.GibOnDeathFilter = false end end end if self.SetUpGibesOnDeath then self.HandleGibOnDeath = function(_, dmginfo, hitgroup) local gibbed, overrides = self:SetUpGibesOnDeath(dmginfo, hitgroup) local tbl = {} if overrides then if overrides.AllowCorpse then tbl.AllowCorpse = true end if overrides.DeathAnim then tbl.AllowAnim = true end end if self.CustomGibOnDeathSounds && !self:CustomGibOnDeathSounds(dmginfo, hitgroup) then tbl.AllowSound = false end return gibbed, tbl end end if self.CustomOnDoKilledEnemy then self.OnKilledEnemy = function(_, ent, inflictor, wasLast) if (self.KilledEnemySoundLast == false) or (self.KilledEnemySoundLast == true && wasLast) then self:CustomOnDoKilledEnemy(ent, self, inflictor) end end end if self.CustomOnMedic_BeforeHeal or self.CustomOnMedic_OnHeal or self.CustomOnMedic_OnReset then self.OnMedicBehavior = function(_, status, statusData) if status == "BeforeHeal" && self.CustomOnMedic_BeforeHeal then self:CustomOnMedic_BeforeHeal() elseif status == "OnHeal" && self.CustomOnMedic_OnHeal then return self:CustomOnMedic_OnHeal(statusData) elseif status == "OnReset" && self.CustomOnMedic_OnReset then self:CustomOnMedic_OnReset() end end end if self.CustomOnTakeDamage_BeforeImmuneChecks or self.CustomOnTakeDamage_BeforeDamage or self.CustomOnTakeDamage_AfterDamage then self.OnDamaged = function(_, dmginfo, hitgroup, status) if status == "Init" && self.CustomOnTakeDamage_BeforeImmuneChecks then self:CustomOnTakeDamage_BeforeImmuneChecks(dmginfo, hitgroup) elseif status == "PreDamage" && self.CustomOnTakeDamage_BeforeDamage then self:CustomOnTakeDamage_BeforeDamage(dmginfo, hitgroup) elseif status == "PostDamage" && self.CustomOnTakeDamage_AfterDamage then self:CustomOnTakeDamage_AfterDamage(dmginfo, hitgroup) end end end if self.CustomOnFlinch_BeforeFlinch or self.CustomOnFlinch_AfterFlinch then self.OnFlinch = function(_, dmginfo, hitgroup, status) if status == "Init" then if self.CustomOnFlinch_BeforeFlinch then return !self:CustomOnFlinch_BeforeFlinch(dmginfo, hitgroup) end elseif status == "Execute" then if self.CustomOnFlinch_AfterFlinch then self:CustomOnFlinch_AfterFlinch(dmginfo, hitgroup) end end end end if self.CustomOnInitialKilled or self.CustomOnPriorToKilled or self.CustomDeathAnimationCode or self.CustomOnKilled or self.CustomOnDeath_BeforeCorpseSpawned then self.OnDeath = function(_, dmginfo, hitgroup, status) if status == "Init" then if self.CustomOnInitialKilled then self:CustomOnInitialKilled(dmginfo, hitgroup) end if self.CustomOnPriorToKilled then self:CustomOnPriorToKilled(dmginfo, hitgroup) end elseif status == "DeathAnim" && self.CustomDeathAnimationCode then self:CustomDeathAnimationCode(dmginfo, hitgroup) elseif status == "Finish" then if self.CustomOnKilled then self:CustomOnKilled(dmginfo, hitgroup) end if self.CustomOnDeath_BeforeCorpseSpawned then self:CustomOnDeath_BeforeCorpseSpawned(dmginfo, hitgroup) end end end end if self.HasWorldShakeOnMove && !self.OnFootstepSound then -- Only do this if "self.OnFootstepSound" isn't already being used self.OnFootstepSound = function() util.ScreenShake(self:GetPos(), self.WorldShakeOnMoveAmplitude or 10, self.WorldShakeOnMoveFrequency or 100, self.WorldShakeOnMoveDuration or 0.4, self.WorldShakeOnMoveRadius or 1000) end end if self.MeleeAttackKnockBack_Forward1 or self.MeleeAttackKnockBack_Forward2 or self.MeleeAttackKnockBack_Up1 or self.MeleeAttackKnockBack_Up2 then self.MeleeAttackKnockbackVelocity = function() return self:GetForward()*math.random(self.MeleeAttackKnockBack_Forward1 or 100, self.MeleeAttackKnockBack_Forward2 or 100) + self:GetUp()*math.random(self.MeleeAttackKnockBack_Up1 or 10, self.MeleeAttackKnockBack_Up2 or 10) + self:GetRight()*math.random(self.MeleeAttackKnockBack_Right1 or 0, self.MeleeAttackKnockBack_Right2 or 0) end end if self.DeathCorpseSkin && self.DeathCorpseSkin != -1 then local orgFunc = self.OnCreateDeathCorpse self.OnCreateDeathCorpse = function(_, dmginfo, hitgroup, corpse) orgFunc(self, dmginfo, hitgroup, corpse) corpse:SetSkin(self.DeathCorpseSkin) end end if self.CustomOnTouch then self.OnTouch = function(_, ent) self:CustomOnTouch(ent) end end if self.RangeUseAttachmentForPos then self.RangeAttackProjPos = function(_, projectile) return self:GetAttachment(self:LookupAttachment(self.RangeUseAttachmentForPosID)).Pos end elseif self.RangeAttackPos_Up or self.RangeAttackPos_Forward or self.RangeAttackPos_Right then self.RangeAttackProjPos = function(_, projectile) return self:GetPos() + self:GetUp()*(self.RangeAttackPos_Up or 20) + self:GetForward()*(self.RangeAttackPos_Forward or 0) + self:GetRight()*(self.RangeAttackPos_Right or 0) end end if self.RangeAttackCode_GetShootPos then self.RangeAttackProjVel = function(_, projectile) return self.RangeAttackCode_GetShootPos(self, projectile) end end if self.LeapAttackVelocityForward or self.LeapAttackVelocityUp or self.CustomAttackCheck_LeapAttack or self.CustomOnLeapAttack_BeforeStartTimer or self.CustomOnLeapAttack_AfterStartTimer then self.OnLeapAttack = function(_, status, enemy) if status == "PreInit" && self.CustomAttackCheck_LeapAttack then return !self:CustomAttackCheck_LeapAttack(enemy) elseif status == "Init" && self.CustomOnLeapAttack_BeforeStartTimer then self:CustomOnLeapAttack_BeforeStartTimer(self.AttackSeed) elseif status == "PostInit" && self.CustomOnLeapAttack_AfterStartTimer then self:CustomOnLeapAttack_AfterStartTimer(self.AttackSeed) elseif status == "Jump" && (self.LeapAttackVelocityForward or self.LeapAttackVelocityUp) then local ene = self:GetEnemy() return ((ene:GetPos() + ene:OBBCenter()) - (self:GetPos() + self:OBBCenter())):GetNormal()*400 + self:GetForward()*(self.LeapAttackVelocityForward or 2000) + self:GetUp()*(self.LeapAttackVelocityUp or 200) end end end if self.CustomOnLeapAttack_BeforeChecks or self.CustomOnLeapAttack_AfterChecks or self.CustomOnLeapAttack_Miss then self.OnLeapAttackExecute = function(_, status, ent) if status == "Init" && self.CustomOnLeapAttack_BeforeChecks then self:CustomOnLeapAttack_BeforeChecks() elseif status == "PreDamage" && self.CustomOnLeapAttack_AfterChecks then self:CustomOnLeapAttack_AfterChecks(ent) elseif status == "Miss" && self.CustomOnLeapAttack_Miss then self:CustomOnLeapAttack_Miss() end end end if self.CustomAttack or self.MultipleMeleeAttacks or self.MultipleRangeAttacks or self.MultipleLeapAttacks then self.OnThinkAttack = function(_, isAttacking, enemy) if self.CustomAttack then self:CustomAttack(enemy, self.EnemyData.Visible) end if isAttacking then return end if self.MultipleMeleeAttacks then self:MultipleMeleeAttacks() end if self.MultipleRangeAttacks then self:MultipleRangeAttacks() end if self.MultipleLeapAttacks then self:MultipleLeapAttacks() end end end if self.CustomAttackCheck_RangeAttack or self.CustomOnRangeAttack_BeforeStartTimer or self.CustomOnRangeAttack_AfterStartTimer then self.OnRangeAttack = function(_, status, enemy) if status == "PreInit" && self.CustomAttackCheck_RangeAttack then return !self:CustomAttackCheck_RangeAttack(enemy) elseif status == "Init" && self.CustomOnRangeAttack_BeforeStartTimer then self:CustomOnRangeAttack_BeforeStartTimer(self.AttackSeed) elseif status == "PostInit" && self.CustomOnRangeAttack_AfterStartTimer then self:CustomOnRangeAttack_AfterStartTimer(self.AttackSeed) end end end if self.DisableDefaultRangeAttackCode or self.CustomRangeAttackCode or self.CustomRangeAttackCode_BeforeProjectileSpawn or self.CustomRangeAttackCode_AfterProjectileSpawn then self.OnRangeAttackExecute = function(_, status, enemy, projectile) if status == "Init" && (self.CustomRangeAttackCode or self.DisableDefaultRangeAttackCode) then if self.CustomRangeAttackCode then self:CustomRangeAttackCode() end if self.DisableDefaultRangeAttackCode then return true end elseif status == "PreSpawn" && self.CustomRangeAttackCode_BeforeProjectileSpawn then self:CustomRangeAttackCode_BeforeProjectileSpawn(projectile) elseif status == "PostSpawn" && self.CustomRangeAttackCode_AfterProjectileSpawn then self:CustomRangeAttackCode_AfterProjectileSpawn(projectile) end end end if self.CustomAttackCheck_MeleeAttack or self.CustomOnMeleeAttack_BeforeStartTimer or self.CustomOnMeleeAttack_AfterStartTimer then self.OnMeleeAttack = function(_, status, enemy) if status == "PreInit" && self.CustomAttackCheck_MeleeAttack then return !self:CustomAttackCheck_MeleeAttack(enemy) elseif status == "Init" && self.CustomOnMeleeAttack_BeforeStartTimer then self:CustomOnMeleeAttack_BeforeStartTimer(self.AttackSeed) elseif status == "PostInit" && self.CustomOnMeleeAttack_AfterStartTimer then self:CustomOnMeleeAttack_AfterStartTimer(self.AttackSeed) end end end if self.DisableDefaultMeleeAttackCode or self.MeleeAttackWorldShakeOnMiss or self.CustomOnMeleeAttack_BeforeChecks or self.CustomOnMeleeAttack_AfterChecks or self.CustomOnMeleeAttack_Miss then self.OnMeleeAttackExecute = function(_, status, ent, isProp) if status == "Init" && (self.CustomOnMeleeAttack_BeforeChecks or self.DisableDefaultMeleeAttackCode) then if self.CustomOnMeleeAttack_BeforeChecks then self:CustomOnMeleeAttack_BeforeChecks() end if self.DisableDefaultMeleeAttackCode then return true end elseif status == "PreDamage" && self.CustomOnMeleeAttack_AfterChecks then return self:CustomOnMeleeAttack_AfterChecks(ent, isProp) elseif status == "Miss" && (self.CustomOnMeleeAttack_Miss or self.MeleeAttackWorldShakeOnMiss) then if self.CustomOnMeleeAttack_Miss then self:CustomOnMeleeAttack_Miss() end if self.MeleeAttackWorldShakeOnMiss then util.ScreenShake(self:GetPos(), self.MeleeAttackWorldShakeOnMissAmplitude or 16, 100, self.MeleeAttackWorldShakeOnMissDuration or 1, self.MeleeAttackWorldShakeOnMissRadius or 2000) end end end end if self.GetMeleeAttackDamageOrigin then self.MeleeAttackTraceOrigin = function() return self:GetMeleeAttackDamageOrigin() end end -- !!!!!!!!!!!!!! DO NOT USE ANY OF THESE !!!!!!!!!!!!!! [Backwards Compatibility!] end --------------------------------------------------------------------------------------------------------------------------------------------- local defShootVec = Vector(0, 0, 55) local capBitsDefault = bit.bor(CAP_SKIP_NAV_GROUND_CHECK, CAP_TURN_HEAD) local capBitsDoors = bit.bor(CAP_OPEN_DOORS, CAP_AUTO_DOORS, CAP_USE) -- function ENT:Initialize() self:PreInit() if self.CustomOnPreInitialize then self:CustomOnPreInitialize() end -- !!!!!!!!!!!!!! DO NOT USE !!!!!!!!!!!!!! [Backwards Compatibility!] self:SetSpawnEffect(false) self:SetRenderMode(RENDERMODE_NORMAL) self:AddEFlags(EFL_NO_DISSOLVE) self:SetUseType(SIMPLE_USE) if !self:GetModel() then local models = PICK(self.Model) if models then self:SetModel(models) end end self:SetHullType(self.HullType) self:SetHullSizeNormal() self:SetSolid(SOLID_BBOX) self:SetCollisionGroup(COLLISION_GROUP_NPC) self:SetMaxYawSpeed(self.TurningSpeed) self:SetSaveValue("m_HackedGunPos", defShootVec) -- Overrides the location of self:GetShootPos() -- Set a name if it doesn't have one if self:GetName() == "" then local findListing = list.Get("NPC")[self:GetClass()] if findListing then self:SetName((self.PrintName == "" and findListing.Name) or self.PrintName) end end -- Initialize variables InitConvars(self) self.NextProcessTime = vj_npc_processtime:GetInt() self.SelectedDifficulty = vj_npc_difficulty:GetInt() if !self.RelationshipEnts then self.RelationshipEnts = {} end if !self.RelationshipMemory then self.RelationshipMemory = {} end self.AnimationTranslations = {} self.NextIdleSoundT_Reg = CurTime() + math.random(0.3, 6) self.MainSoundPitchValue = (self.MainSoundPitchStatic and (istable(self.MainSoundPitch) and math.random(self.MainSoundPitch.a, self.MainSoundPitch.b) or self.MainSoundPitch)) or 0 local sightConvar = vj_npc_sight_distance:GetInt(); if sightConvar > 0 then self.SightDistance = sightConvar end -- Capabilities & Movement self:DoChangeMovementType(self.MovementType) self:CapabilitiesAdd(capBitsDefault) if self.CanOpenDoors then self:CapabilitiesAdd(capBitsDoors) end -- Both of these attachments have to be valid for "ai_baseactor" to work properly! if self:LookupAttachment("eyes") > 0 && self:LookupAttachment("forward") > 0 then self:CapabilitiesAdd(CAP_ANIMATEDFACE) end -- Health local hpConvar = vj_npc_health:GetInt() local hp = hpConvar > 0 && hpConvar or self:ScaleByDifficulty(self.StartHealth) self:SetHealth(hp) self.StartHealth = hp self:Init() ApplyBackwardsCompatibility(self) -- Collision-based computations //self:SetSurroundingBoundsType(BOUNDS_HITBOXES) -- AVOID! Has to constantly recompute the bounds! | Issues: Entities get stuck inside the NPC, movements failing, unable to grab the NPC with physgun local collisionMin, collisionMax = self:GetCollisionBounds() -- Auto compute damage bounds if the damage bounds == collision bounds then the developer has NOT changed it | Call after "Init" if self:GetSurroundingBounds() == self:WorldSpaceAABB() then self:SetSurroundingBounds(Vector(collisionMin.x * 2, collisionMin.y * 2, collisionMin.z * 1.2), Vector(collisionMax.x * 2, collisionMax.y * 2, collisionMax.z * 1.2)) end if !self.MeleeAttackDistance then self.MeleeAttackDistance = math.abs(collisionMax.x) + 30 end if !self.MeleeAttackDamageDistance then self.MeleeAttackDamageDistance = math.abs(collisionMax.x) + 60 end self:SetupBloodColor(self.BloodColor) -- Collision bounds dependent self.NextWanderTime = ((self.NextWanderTime != 0) and self.NextWanderTime) or (CurTime() + (self.IdleAlwaysWander and 0 or 1)) -- If self.NextWanderTime isn't given a value THEN if self.IdleAlwaysWander isn't true, wait at least 1 sec before wandering duplicator.RegisterEntityClass(self:GetClass(), VJ.CreateDupe_NPC, "Model", "Class", "Equipment", "SpawnFlags", "Data") -- Delayed init timer.Simple(0.1, function() if IsValid(self) then self:SetMaxLookDistance(self.SightDistance) self:SetFOV(self.SightAngle) if self:GetNPCState() <= NPC_STATE_NONE then self:SetNPCState(NPC_STATE_IDLE) end if IsValid(self:GetCreator()) && self:GetCreator():GetInfoNum("vj_npc_spawn_guard", 0) == 1 then self.IsGuard = true end self:StartSoundTrack() -- Setup common default pose parameters if self:LookupPoseParameter("aim_pitch") != -1 then self.PoseParameterLooking_Names.pitch[#self.PoseParameterLooking_Names.pitch + 1] = "aim_pitch" end if self:LookupPoseParameter("head_pitch") != -1 then self.PoseParameterLooking_Names.pitch[#self.PoseParameterLooking_Names.pitch + 1] = "head_pitch" end if self:LookupPoseParameter("aim_yaw") != -1 then self.PoseParameterLooking_Names.yaw[#self.PoseParameterLooking_Names.yaw + 1] = "aim_yaw" end if self:LookupPoseParameter("head_yaw") != -1 then self.PoseParameterLooking_Names.yaw[#self.PoseParameterLooking_Names.yaw + 1] = "head_yaw" end if self:LookupPoseParameter("aim_roll") != -1 then self.PoseParameterLooking_Names.roll[#self.PoseParameterLooking_Names.roll + 1] = "aim_roll" end if self:LookupPoseParameter("head_roll") != -1 then self.PoseParameterLooking_Names.roll[#self.PoseParameterLooking_Names.roll + 1] = "head_roll" end self:UpdateAnimationTranslations() if self:GetIdealActivity() == ACT_IDLE then -- Reset the idle animation in case animation translations changed it! self:MaintainIdleAnimation(true) end -- This is needed as setting "NextThink" to CurTime will cause performance drops, so we set the idle maintain in a separate hook that runs every tick local thinkHook = hook.GetTable()["Think"] if (thinkHook && !thinkHook[self]) or (!thinkHook) then local idleFunc = self.MaintainIdleAnimation if #self:GetBoneFollowers() > 0 then hook.Add("Think", self, function() if VJ_CVAR_AI_ENABLED then idleFunc(self) end self:UpdateBoneFollowers() end) else hook.Add("Think", self, function() if VJ_CVAR_AI_ENABLED then idleFunc(self) end end) end else VJ.DEBUG_Print(self, false, "warn", "has an existing embedded \"Think\" hook already, which is disallowing the default base hook from assigning. Make sure to handle \"MaintainIdleAnimation\" in the overridden hook!") end end end) end --------------------------------------------------------------------------------------------------------------------------------------------- local capBitsGround = bit.bor(CAP_MOVE_GROUND, CAP_MOVE_JUMP, CAP_MOVE_CLIMB, CAP_MOVE_SHOOT) local capBitsShared = bit.bor(CAP_MOVE_GROUND, CAP_MOVE_JUMP, CAP_MOVE_CLIMB, CAP_MOVE_SHOOT, CAP_MOVE_FLY) -- function ENT:DoChangeMovementType(movType) if movType then self.MovementType = movType if movType == VJ_MOVETYPE_GROUND then self:RemoveFlags(FL_FLY) self:CapabilitiesRemove(CAP_MOVE_FLY) self:SetNavType(NAV_GROUND) self:SetMoveType(MOVETYPE_STEP) self:CapabilitiesAdd(CAP_MOVE_GROUND) if VJ.AnimExists(self, ACT_JUMP) or self.UsePoseParameterMovement then self:CapabilitiesAdd(CAP_MOVE_JUMP) end if VJ.AnimExists(self, ACT_CLIMB_UP) then self:CapabilitiesAdd(CAP_MOVE_CLIMB) end elseif movType == VJ_MOVETYPE_AERIAL or movType == VJ_MOVETYPE_AQUATIC then self:CapabilitiesRemove(capBitsGround) self:SetGroundEntity(NULL) self:AddFlags(FL_FLY) self:SetNavType(NAV_FLY) self:SetMoveType(MOVETYPE_STEP) // MOVETYPE_FLY = causes issues like Lerp functions not being smooth self:CapabilitiesAdd(CAP_MOVE_FLY) elseif movType == VJ_MOVETYPE_STATIONARY then self:RemoveFlags(FL_FLY) self:CapabilitiesRemove(capBitsShared) self:SetNavType(NAV_NONE) if !IsValid(self:GetParent()) then -- Only set move type if it does NOT have a parent! self:SetMoveType(MOVETYPE_FLY) end elseif movType == VJ_MOVETYPE_PHYSICS then self:RemoveFlags(FL_FLY) self:CapabilitiesRemove(capBitsShared) self:SetNavType(NAV_NONE) self:SetMoveType(MOVETYPE_VPHYSICS) end end end --------------------------------------------------------------------------------------------------------------------------------------------- local schedule_alert_chaseLOS = vj_ai_schedule.New("SCHEDULE_ALERT_CHASE_LOS") schedule_alert_chaseLOS:EngTask("TASK_GET_PATH_TO_ENEMY_LOS", 0) //schedule_alert_chaseLOS:EngTask("TASK_RUN_PATH", 0) schedule_alert_chaseLOS:EngTask("TASK_WAIT_FOR_MOVEMENT", 0) //schedule_alert_chaseLOS:EngTask("TASK_FACE_ENEMY", 0) //schedule_alert_chaseLOS.ResetOnFail = true schedule_alert_chaseLOS.CanShootWhenMoving = true schedule_alert_chaseLOS.CanBeInterrupted = true -- local schedule_alert_chase = vj_ai_schedule.New("SCHEDULE_ALERT_CHASE") schedule_alert_chase:EngTask("TASK_GET_PATH_TO_ENEMY", 0) schedule_alert_chase:EngTask("TASK_RUN_PATH", 0) schedule_alert_chase:EngTask("TASK_WAIT_FOR_MOVEMENT", 0) //schedule_alert_chase:EngTask("TASK_FACE_ENEMY", 0) //schedule_alert_chase.ResetOnFail = true schedule_alert_chase.CanShootWhenMoving = true schedule_alert_chase.CanBeInterrupted = true -- function ENT:SCHEDULE_ALERT_CHASE(doLOSChase) self:ClearCondition(COND_ENEMY_UNREACHABLE) local moveType = self.MovementType; if moveType == VJ_MOVETYPE_AERIAL or moveType == VJ_MOVETYPE_AQUATIC then self:AA_ChaseEnemy() return end if self.CurrentScheduleName == "SCHEDULE_ALERT_CHASE" then return end // && (self:GetEnemyLastKnownPos():Distance(self:GetEnemy():GetPos()) <= 12) local navType = self:GetNavType(); if navType == NAV_JUMP or navType == NAV_CLIMB then return end if doLOSChase then schedule_alert_chaseLOS.RunCode_OnFinish = function() local ene = self:GetEnemy() if IsValid(ene) then //self:RememberUnreachable(ene, 0) self:SCHEDULE_ALERT_CHASE(false) end end self:StartSchedule(schedule_alert_chaseLOS) else schedule_alert_chase.RunCode_OnFail = function() if self.SCHEDULE_IDLE_STAND then self:SCHEDULE_IDLE_STAND() end end self:StartSchedule(schedule_alert_chase) end end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:MaintainAlertBehavior(alwaysChase) -- alwaysChase = Override to always make the NPC chase local curTime = CurTime() local selfData = self:GetTable() if selfData.NextChaseTime > curTime or selfData.Dead or selfData.VJ_IsBeingControlled or selfData.Flinching or self:GetState() == VJ_STATE_ONLY_ANIMATION_CONSTANT then return end local eneData = selfData.EnemyData local ene = eneData.Target local moveType = selfData.MovementType if !IsValid(ene) or selfData.TakingCoverT > curTime or (selfData.AttackAnimTime > curTime && moveType != VJ_MOVETYPE_AERIAL && moveType != VJ_MOVETYPE_AQUATIC) then return end -- Not melee attacking yet but it is in range, so don't chase the enemy! if selfData.HasMeleeAttack && eneData.DistanceNearest < selfData.MeleeAttackDistance && eneData.Visible && (self:GetHeadDirection():Dot((ene:GetPos() - self:GetPos()):GetNormalized()) > math_cos(math_rad(selfData.MeleeAttackAngleRadius))) then if moveType == VJ_MOVETYPE_AERIAL or moveType == VJ_MOVETYPE_AQUATIC then self:AA_StopMoving() end self:SCHEDULE_IDLE_STAND() return end -- Things that override can't bypass, Forces the NPC to ONLY idle stand! if moveType == VJ_MOVETYPE_STATIONARY or selfData.IsFollowing or selfData.MedicData.Status or self:GetState() == VJ_STATE_ONLY_ANIMATION then self:SCHEDULE_IDLE_STAND() return end -- Non-aggressive NPCs local behaviorType = selfData.Behavior if behaviorType == VJ_BEHAVIOR_PASSIVE or behaviorType == VJ_BEHAVIOR_PASSIVE_NATURE then self:SCHEDULE_COVER_ENEMY("TASK_RUN_PATH") selfData.NextChaseTime = curTime + 3 return end if !alwaysChase && (selfData.DisableChasingEnemy or selfData.IsGuard) then self:SCHEDULE_IDLE_STAND() return end -- If the enemy is not reachable then wander around if self:IsUnreachable(ene) then if selfData.HasRangeAttack then -- Ranged NPCs self:SCHEDULE_ALERT_CHASE(true) elseif math.random(1, 30) == 1 && !self:IsMoving() then selfData.NextWanderTime = 0 self:MaintainIdleBehavior(1) self:RememberUnreachable(ene, 4) else self:SCHEDULE_IDLE_STAND() end else -- Is reachable, so chase the enemy! self:SCHEDULE_ALERT_CHASE() end -- Set the next chase time if selfData.NextChaseTime > curTime then return end -- Don't set it if it's already set! selfData.NextChaseTime = curTime + (((eneData.Distance > 2000) and 1) or 0.1) -- If the enemy is far, increase the delay! end --------------------------------------------------------------------------------------------------------------------------------------------- --[[--------------------------------------------------------- Overrides any activity by returning another activity - act = Activity that is being called to be translated Returns - Activity, the translated activity, otherwise it will return the given activity back RULES 1. Always return an activity, never return nothing or a table! - Suggested to call `return self.BaseClass.TranslateActivity(self, act)` at the end of the function 2. If you are replacing ACT_IDLE from a randomized table, then you must call `self:ResolveAnimation` - This is to ensure the idle animation system properly detects if it should be setting a new idle animation -----------------------------------------------------------]] function ENT:TranslateActivity(act) //VJ.DEBUG_Print(self, "TranslateActivity", act) -- Handle translations table local translation = self.AnimationTranslations[act] if translation then if istable(translation) then if act == ACT_IDLE then return self:ResolveAnimation(translation) end return translation[math.random(1, #translation)] or act -- "or act" = To make sure it doesn't return nil when the table is empty! end return translation end return act end --------------------------------------------------------------------------------------------------------------------------------------------- local attackTimers = { [VJ.ATTACK_TYPE_MELEE] = function(self, skipStopAttacks) if !skipStopAttacks then timer.Create("attack_melee_reset" .. self:EntIndex(), self:GetAttackTimer(self.NextAnyAttackTime_Melee, self.TimeUntilMeleeAttackDamage, self.AttackAnimDuration), 1, function() self:StopAttacks() self:MaintainAlertBehavior() end) end timer.Create("attack_melee_reset_able" .. self:EntIndex(), self:GetAttackTimer(self.NextMeleeAttackTime), 1, function() self.IsAbleToMeleeAttack = true end) end, [VJ.ATTACK_TYPE_RANGE] = function(self, skipStopAttacks) if !skipStopAttacks then timer.Create("attack_range_reset" .. self:EntIndex(), self:GetAttackTimer(self.NextAnyAttackTime_Range, self.TimeUntilRangeAttackProjectileRelease, self.AttackAnimDuration), 1, function() self:StopAttacks() self:MaintainAlertBehavior() end) end timer.Create("attack_range_reset_able" .. self:EntIndex(), self:GetAttackTimer(self.NextRangeAttackTime), 1, function() self.IsAbleToRangeAttack = true end) end, [VJ.ATTACK_TYPE_LEAP] = function(self, skipStopAttacks) if !skipStopAttacks then timer.Create("attack_leap_reset" .. self:EntIndex(), self:GetAttackTimer(self.NextAnyAttackTime_Leap, self.TimeUntilLeapAttackDamage, self.AttackAnimDuration), 1, function() self:StopAttacks() self:MaintainAlertBehavior() end) end timer.Create("attack_leap_reset_able" .. self:EntIndex(), self:GetAttackTimer(self.NextLeapAttackTime), 1, function() self.IsAbleToLeapAttack = true end) end } --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:Think() /* local newEyeOffset = self:WorldToLocal(self:GetAttachment(self:LookupAttachment("mouth")).Pos) self:SetViewOffset(newEyeOffset) self:SetSaveValue("m_vDefaultEyeOffset", newEyeOffset) */ //if self.NextActualThink <= CurTime() then //self.NextActualThink = CurTime() + 0.065 -- Schedule debug //if self.CurrentSchedule then PrintTable(self.CurrentSchedule) end //if self.CurrentTask then PrintTable(self.CurrentTask) end //self:SetCondition(1) -- Probably not needed as "sv_pvsskipanimation" handles it | Fix attachments, bones, positions, angles etc. being broken in NPCs! This condition is used as a backup in case "sv_pvsskipanimation" isn't disabled! //if self.MovementType == VJ_MOVETYPE_GROUND && self:GetVelocity():Length() <= 0 && !self:IsEFlagSet(EFL_IS_BEING_LIFTED_BY_BARNACLE) /*&& curSchedule.HasMovement*/ then self:DropToFloor() end -- No need, already handled by the engine local curTime = CurTime() local selfData = self:GetTable() -- This is here to make sure the initialized process time stays in place... -- otherwise if AI is disabled then reenabled, all the NPCs will now start processing at the same exact CurTime! local doHeavyProcesses = curTime > selfData.NextProcessT if doHeavyProcesses then selfData.NextProcessT = curTime + selfData.NextProcessTime end -- Breath sound system if !selfData.Dead && selfData.HasBreathSound && selfData.HasSounds && curTime > selfData.NextBreathSoundT then local pickedSD = PICK(selfData.SoundTbl_Breath) local dur = 10 -- Make the default value large so we don't check it too much! if pickedSD then StopSD(selfData.CurrentBreathSound) dur = (selfData.NextSoundTime_Breath == false and SoundDuration(pickedSD)) or math.Rand(selfData.NextSoundTime_Breath.a, selfData.NextSoundTime_Breath.b) selfData.CurrentBreathSound = VJ.CreateSound(self, pickedSD, selfData.BreathSoundLevel, self:GetSoundPitch(selfData.BreathSoundPitch)) end selfData.NextBreathSoundT = curTime + dur end self:OnThink() --=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-- local moveType = selfData.MovementType local moveTypeAA = moveType == VJ_MOVETYPE_AERIAL or moveType == VJ_MOVETYPE_AQUATIC if VJ_CVAR_AI_ENABLED && self:GetState() != VJ_STATE_FREEZE && !self:IsEFlagSet(EFL_IS_BEING_LIFTED_BY_BARNACLE) then if selfData.VJ_DEBUG then if GetConVar("vj_npc_debug_enemy"):GetInt() == 1 then VJ.DEBUG_Print(self, false, "Enemy -> " .. tostring(self:GetEnemy() or "NULL") .. " | Alerted? " .. tostring(selfData.Alerted)) end if GetConVar("vj_npc_debug_takingcover"):GetInt() == 1 then if curTime > selfData.TakingCoverT then VJ.DEBUG_Print(self, false, "NOT taking cover") else VJ.DEBUG_Print(self, false, "Taking cover (" .. selfData.TakingCoverT - curTime .. ")") end end if GetConVar("vj_npc_debug_lastseenenemytime"):GetInt() == 1 then PrintMessage(HUD_PRINTTALK, (curTime - selfData.EnemyData.VisibleTime) .. " (" .. self:GetName() .. ")") end end //self:SetPlaybackRate(self.AnimationPlaybackRate) self:OnThinkActive() -- For AA move types if moveTypeAA then local myVelLen = self:GetVelocity():Length() if myVelLen > 0 then if selfData.AA_CurrentMovePos then local dist = selfData.AA_CurrentMovePos:Distance(self:GetPos()) -- Make sure we are making progress so we don't get stuck in a infinite movement! if selfData.AA_CurrentMoveDist == -1 or selfData.AA_CurrentMoveDist >= dist then selfData.AA_CurrentMoveDist = dist local moveSpeed = selfData.AA_CurrentMoveMaxSpeed -- Only decelerate if the distance is smaller than the max speed! if selfData.AA_MoveDecelerate > 1 && dist < moveSpeed then moveSpeed = math_min(math_max(dist, moveSpeed / selfData.AA_MoveDecelerate), moveSpeed) elseif selfData.AA_MoveAccelerate > 0 then moveSpeed = Lerp(FrameTime() * selfData.AA_MoveAccelerate, myVelLen, moveSpeed) end local velPos = selfData.AA_CurrentMovePosDir:GetNormal() * moveSpeed local velTimeCur = curTime + (dist / velPos:Length()) if velTimeCur == velTimeCur then -- Check for NaN selfData.AA_CurrentMoveTime = velTimeCur end self:SetLocalVelocity(velPos) -- We are NOT making any progress, stop the movement else self:AA_StopMoving() end end -- Is aquatic and is NOT completely in water then attempt to go down! if moveType == VJ_MOVETYPE_AQUATIC && self:WaterLevel() <= 2 then self:AA_IdleWander() end if selfData.AA_CurrentMoveAnim != -1 then self:AA_MoveAnimation() end -- Not moving, reset its move time! else selfData.AA_CurrentMoveTime = 0 end end -- Update follow system's data //print("------------------") //PrintTable(selfData.FollowData) if selfData.IsFollowing && self:GetNavType() != NAV_JUMP && self:GetNavType() != NAV_CLIMB then local followData = selfData.FollowData local followEnt = followData.Target local followIsLiving = followEnt.VJ_ID_Living //print(self:GetTarget()) if IsValid(followEnt) && (!followIsLiving or (followIsLiving && (self:Disposition(followEnt) == D_LI or self:GetClass() == followEnt:GetClass()) && followEnt:Alive())) then if curTime > followData.NextUpdateT && !selfData.VJ_ST_Healing then local distToPly = self:GetPos():Distance(followEnt:GetPos()) local busy = self:IsBusy("Activities") self:SetTarget(followEnt) followData.StopAct = false if distToPly > followData.MinDist then -- Entity is far away, move towards it! local isFar = distToPly > (followData.MinDist * 4) -- IF (we are busy but far) OR (not busy) THEN move if (busy && isFar) or (!busy) then followData.Moving = true -- If we are far then stop all activities (ex: attacks) and just go there already! if isFar then followData.StopAct = true end if moveTypeAA then self:AA_MoveTo(self:GetTarget(), true, (distToPly < (followData.MinDist * 1.5) and "Calm") or "Alert", {FaceDestTarget = true}) elseif !self:IsMoving() or self:GetCurGoalType() != 1 then //self:NavSetGoalTarget(followEnt) // local goalTarget = -- No longer works, a recent GMod commit broke it -- Do NOT check for validity! Let it be sent to "OnTaskFailed" so an NPC can capture it! (Ex: HL1 scientist complaining to the player) //if goalTarget then local schedule = vj_ai_schedule.New("SCHEDULE_FOLLOW") schedule:EngTask("TASK_GET_PATH_TO_TARGET", 0) -- Required to generate the path! schedule:EngTask("TASK_MOVE_TO_TARGET_RANGE", followData.MinDist * 0.8) schedule:EngTask("TASK_WAIT_FOR_MOVEMENT", 0) schedule:EngTask("TASK_FACE_TARGET", 1) schedule.CanShootWhenMoving = true if IsValid(self:GetActiveWeapon()) then schedule.TurnData = {Type = VJ.FACE_ENEMY_VISIBLE} end self:StartSchedule(schedule) //else // self:ClearGoal() //end /*self:SCHEDULE_GOTO_TARGET((distToPly < (followData.MinDist * 1.5) and "TASK_WALK_PATH") or "TASK_RUN_PATH", function(schedule) schedule.CanShootWhenMoving = true if IsValid(self:GetActiveWeapon()) then schedule.TurnData = {Type = VJ.FACE_ENEMY_VISIBLE} end end)*/ end end elseif followData.Moving == true then -- Entity is very close, stop moving! if !busy then -- If not busy then make it stop moving and do something self:TaskComplete() self:StopMoving(false) self:SelectSchedule() end followData.Moving = false end followData.NextUpdateT = curTime + 0.5 end else self:ResetFollowBehavior() end end if !selfData.Dead then -- Health Regeneration System local healthRegen = selfData.HealthRegenParams if healthRegen.Enabled && curTime > selfData.HealthRegenDelayT then local myHP = self:Health() self:SetHealth(math_min(math_max(myHP + healthRegen.Amount, myHP), self:GetMaxHealth())) selfData.HealthRegenDelayT = curTime + math.Rand(healthRegen.Delay.a, healthRegen.Delay.b) end -- Run the heavy processes if doHeavyProcesses then self:MaintainRelationships() if selfData.IsMedic then self:MaintainMedicBehavior() end //selfData.NextProcessT = curTime + selfData.NextProcessTime end local plyControlled = selfData.VJ_IsBeingControlled local myPos = self:GetPos() local ene = self:GetEnemy() local eneValid = IsValid(ene) local eneData = selfData.EnemyData if !eneData.Reset then -- Reset enemy if it doesn't exist or it's dead if !eneValid then self:ResetEnemy(true, true) ene = self:GetEnemy() eneValid = IsValid(ene) -- Reset enemy if it has been unseen for a while elseif (curTime - eneData.VisibleTime) > selfData.EnemyTimeout && !selfData.IsVJBaseSNPC_Tank then self:PlaySoundSystem("LostEnemy") self:ResetEnemy(true, true) ene = self:GetEnemy() eneValid = IsValid(ene) end end -- Eating system if selfData.CanEat then local eatingData = selfData.EatingData if !eatingData then -- Eating data has NOT been initialized, so initialize it! self.EatingData = {Target = NULL, NextCheck = 0, AnimStatus = "None", OrgIdle = nil} -- AnimStatus: "None" = Not prepared (Probably moving to food location) | "Prepared" = Prepared (Ex: Played crouch down anim) | "Eating" = Prepared and is actively eating eatingData = self.EatingData end if eneValid or selfData.Alerted then if selfData.VJ_ST_Eating then eatingData.NextCheck = curTime + 15 self:ResetEatingBehavior("Enemy") end elseif curTime > eatingData.NextCheck then if selfData.VJ_ST_Eating then local food = eatingData.Target if !IsValid(food) then -- Food no longer exists, reset! eatingData.NextCheck = curTime + 10 self:ResetEatingBehavior("Unspecified") elseif !self:IsMoving() then local foodDist = VJ.GetNearestDistance(self, food) // myPos:Distance(food:GetPos()) if foodDist > 400 then -- Food too far away, reset! eatingData.NextCheck = curTime + 10 self:ResetEatingBehavior("Unspecified") elseif foodDist > 30 then -- Food moved a bit, go to new location if self:IsBusy() then -- Something else has come up, stop eating completely! eatingData.NextCheck = curTime + 15 self:ResetEatingBehavior("Unspecified") else if eatingData.AnimStatus != "None" then -- We need to play get up anim first! eatingData.AnimStatus = "None" selfData.AnimationTranslations[ACT_IDLE] = eatingData.OrgIdle -- Reset the idle animation table in case it changed! eatingData.NextCheck = curTime + (self:OnEat("StopEating", "HaltOnly") or 1) else selfData.NextWanderTime = CurTime() + math.Rand(3, 5) self:SetState(VJ_STATE_NONE) self:SetLastPosition(select(2, VJ.GetNearestPositions(self, food))) self:SCHEDULE_GOTO_POSITION("TASK_WALK_PATH") //self:SetTarget(food) //self:SCHEDULE_GOTO_TARGET("TASK_WALK_PATH") eatingData.NextCheck = curTime + 1 end end else -- No changes, continue eating self:SetTurnTarget(food, 1) self:SetState(VJ_STATE_ONLY_ANIMATION_NOATTACK) if eatingData.AnimStatus != "None" then -- We are already prepared, so eat! eatingData.AnimStatus = "Eating" eatingData.NextCheck = curTime + self:OnEat("Eat") if food:Health() <= 0 then -- Finished eating! eatingData.NextCheck = curTime + selfData.EatCooldown self:ResetEatingBehavior("Devoured") food:TakeDamage(100, self, self) -- For entities that react to dmg, Ex: HLR corpses food:Remove() end else -- We need to first prepare before eating! (Ex: Crouch-down animation) eatingData.AnimStatus = "Prepared" eatingData.NextCheck = curTime + (self:OnEat("BeginEating") or 1) end end end elseif funcHasCondition(self, COND_SMELL) && !self:IsMoving() && !self:IsBusy() then local hint = sound.GetLoudestSoundHint(SOUND_CARCASS, myPos) if hint then local food = hint.owner if IsValid(food) /*&& !food.VJ_ST_BeingEaten*/ then if !food.FoodData then local size = food:OBBMaxs():Distance(food:OBBMins()) * 2 food.FoodData = { NumConsumers = 0, Size = size, SizeRemaining = size, } end //print("food", food, self) if food.FoodData.SizeRemaining > 0 && self:OnEat("CheckFood", hint) then local foodData = food.FoodData foodData.NumConsumers = foodData.NumConsumers + 1 foodData.SizeRemaining = foodData.SizeRemaining - self:OBBMaxs():Distance(self:OBBMins()) //PrintTable(hint) selfData.VJ_ST_Eating = true food.VJ_ST_BeingEaten = true selfData.EatingData.OrgIdle = selfData.AnimationTranslations[ACT_IDLE] -- Save the current idle anim table in case we gonna change it while eating! eatingData.Target = food self:OnEat("StartBehavior") self:SetState(VJ_STATE_ONLY_ANIMATION_NOATTACK) selfData.NextWanderTime = curTime + math.Rand(3, 5) end end end //else -- No food was found OR it's not eating //eatingData.NextCheck = curTime + 3 end end end if eneValid then local enePos = ene:GetPos() local eneDist = myPos:Distance(enePos) local eneDistNear = VJ.GetNearestDistance(self, ene, true) local eneIsVisible = plyControlled and true or self:Visible(ene) -- Set latest enemy information self:UpdateEnemyMemory(ene, enePos) eneData.Target = ene eneData.Reset = false eneData.Visible = eneIsVisible eneData.Distance = eneDist eneData.DistanceNearest = eneDistNear if eneIsVisible && self:IsInViewCone(enePos) && (eneDist < self:GetMaxLookDistance()) then eneData.VisibleTime = curTime -- Why 2 vars? Because the last "Visible" tick is usually not updated in time, causing the engine to give false positive, thinking the enemy IS visible eneData.VisiblePos = eneData.VisiblePosReal eneData.VisiblePosReal = ene:EyePos() -- Use EyePos because "Visible" uses it to run the trace in the engine! | For origin, use "self:GetEnemyLastSeenPos()" end -- Call for help if selfData.CallForHelp && curTime > selfData.NextCallForHelpT && !selfData.AttackType then self:Allies_CallHelp(selfData.CallForHelpDistance) selfData.NextCallForHelpT = curTime + selfData.CallForHelpCooldown end -- Stop chasing at certain distance local limitChase = selfData.LimitChaseDistance if limitChase && eneIsVisible && ((limitChase == true) or (limitChase == "OnlyRange" && selfData.HasRangeAttack)) then local minDist = selfData.LimitChaseDistance_Min local maxDist = selfData.LimitChaseDistance_Max if minDist == "UseRangeDistance" then minDist = selfData.RangeAttackMinDistance end if maxDist == "UseRangeDistance" then maxDist = selfData.RangeAttackMaxDistance end if (eneDist < maxDist) && (eneDist > minDist) then -- If the selfData.NextChaseTime is about to expire, then give it 0.5 delay so it does NOT chase! if (selfData.NextChaseTime - curTime) < 0.1 then selfData.NextChaseTime = curTime + 0.5 end self:MaintainIdleBehavior(2) -- Otherwise it won't play the idle animation and will loop the last PlayAct animation if range attack doesn't use animations! if selfData.CurrentScheduleName == "SCHEDULE_ALERT_CHASE" then self:StopMoving() end -- Interrupt enemy chasing because we are in range! if moveType == VJ_MOVETYPE_GROUND then if !self:IsMoving() && self:OnGround() then self:SetTurnTarget("Enemy", 0.5) end elseif moveTypeAA then if selfData.AA_CurrentMoveType == 3 then self:AA_StopMoving() end -- Interrupt enemy chasing because we are in range! if curTime > selfData.AA_CurrentMoveTime then self:AA_IdleWander(true, "Calm", {FaceDest = !selfData.ConstantlyFaceEnemy}) /*self:AA_StopMoving()*/ end -- Only face the position if ConstantlyFaceEnemy is false! end else if selfData.CurrentScheduleName != "SCHEDULE_ALERT_CHASE" then self:MaintainAlertBehavior() end end end self:UpdatePoseParamTracking() -- Attacks if !selfData.PauseAttacks && !selfData.Flinching && !selfData.FollowData.StopAct && curTime > selfData.NextDoAnyAttackT && self:GetState() != VJ_STATE_ONLY_ANIMATION_NOATTACK && selfData.Behavior != VJ_BEHAVIOR_PASSIVE && selfData.Behavior != VJ_BEHAVIOR_PASSIVE_NATURE then -- Attack priority in order: Custom --> Melee --> Range --> Leap local funcThinkAtk = self.OnThinkAttack; if funcThinkAtk then funcThinkAtk(self, !!selfData.AttackType, ene) end -- Melee Attack if selfData.HasMeleeAttack && selfData.IsAbleToMeleeAttack && !selfData.AttackType then local atkType = false -- false = No attack | 1 = Normal attack | 2 = Prop attack if plyControlled then if selfData.VJ_TheController:KeyDown(IN_ATTACK) then atkType = 1 end else -- Regular non-prop attack if eneIsVisible && eneDistNear < selfData.MeleeAttackDistance && self:GetHeadDirection():Dot((enePos - myPos):GetNormalized()) > math_cos(math_rad(selfData.MeleeAttackAngleRadius)) then atkType = 1 -- Check for possible props that we can attack/push elseif curTime > selfData.PropInteraction_NextCheckT then local propCheck = self:MaintainPropInteraction() if propCheck then atkType = 2 end selfData.PropInteraction_Found = propCheck selfData.PropInteraction_NextCheckT = curTime + 0.5 end end if atkType && self:OnMeleeAttack("PreInit", ene) != true then local seed = curTime; selfData.AttackSeed = seed selfData.IsAbleToMeleeAttack = false selfData.AttackType = VJ.ATTACK_TYPE_MELEE selfData.AttackState = VJ.ATTACK_STATE_STARTED selfData.AttackAnim = ACT_INVALID selfData.AttackAnimDuration = 0 selfData.AttackAnimTime = 0 selfData.NextAlertSoundT = curTime + 0.4 if atkType == 2 then selfData.MeleeAttack_IsPropAttack = true else self:SetTurnTarget("Enemy") -- Always turn towards the enemy at the start selfData.MeleeAttack_IsPropAttack = false end self:OnMeleeAttack("Init", ene) self:PlaySoundSystem("BeforeMeleeAttack") if selfData.AnimTbl_MeleeAttack then local anim, animDur, animType = self:PlayAnim(selfData.AnimTbl_MeleeAttack, false, 0, false) if anim != ACT_INVALID then selfData.AttackAnim = anim selfData.AttackAnimDuration = animDur - (selfData.MeleeAttackAnimationDecreaseLengthAmount / selfData.AnimPlaybackRate) if animType != ANIM_TYPE_GESTURE then -- Allow things like chasing to continue for gestures selfData.AttackAnimTime = curTime + selfData.AttackAnimDuration end end end if !selfData.TimeUntilMeleeAttackDamage then attackTimers[VJ.ATTACK_TYPE_MELEE](self) else -- NOT event based... timer.Create("attack_melee_start" .. self:EntIndex(), selfData.TimeUntilMeleeAttackDamage / selfData.AnimPlaybackRate, selfData.MeleeAttackReps, function() if selfData.AttackSeed == seed then self:ExecuteMeleeAttack(atkType == 2) end end) if selfData.MeleeAttackExtraTimers then for k, t in ipairs(selfData.MeleeAttackExtraTimers) do self:AddExtraAttackTimer("timer_melee_start_" .. curTime + k, t, function() if selfData.AttackSeed == seed then self:ExecuteMeleeAttack(atkType == 2) end end) end end end self:OnMeleeAttack("PostInit", ene) end end -- Range Attack if eneIsVisible && selfData.HasRangeAttack && selfData.IsAbleToRangeAttack && !selfData.AttackType && ((plyControlled && selfData.VJ_TheController:KeyDown(IN_ATTACK2)) or (!plyControlled && (eneDist < selfData.RangeAttackMaxDistance) && (eneDist > selfData.RangeAttackMinDistance) && (self:GetHeadDirection():Dot((enePos - myPos):GetNormalized()) > math_cos(math_rad(selfData.RangeAttackAngleRadius))))) && self:OnRangeAttack("PreInit", ene) != true then local seed = curTime; selfData.AttackSeed = seed selfData.IsAbleToRangeAttack = false selfData.AttackType = VJ.ATTACK_TYPE_RANGE selfData.AttackState = VJ.ATTACK_STATE_STARTED selfData.AttackAnim = ACT_INVALID selfData.AttackAnimDuration = 0 selfData.AttackAnimTime = 0 self:OnRangeAttack("Init", ene) self:PlaySoundSystem("BeforeRangeAttack") if selfData.AnimTbl_RangeAttack then local anim, animDur, animType = self:PlayAnim(selfData.AnimTbl_RangeAttack, false, 0, false, selfData.RangeAttackAnimationDelay) if anim != ACT_INVALID then selfData.AttackAnim = anim selfData.AttackAnimDuration = animDur - (selfData.RangeAttackAnimationDecreaseLengthAmount / selfData.AnimPlaybackRate) if animType != ANIM_TYPE_GESTURE then -- Allow things like chasing to continue for gestures selfData.AttackAnimTime = curTime + selfData.AttackAnimDuration end end end if !selfData.TimeUntilRangeAttackProjectileRelease then attackTimers[VJ.ATTACK_TYPE_RANGE](self) else -- NOT event based... timer.Create("attack_range_start" .. self:EntIndex(), selfData.TimeUntilRangeAttackProjectileRelease / selfData.AnimPlaybackRate, selfData.RangeAttackReps, function() if selfData.AttackSeed == seed then self:ExecuteRangeAttack() end end) if selfData.RangeAttackExtraTimers then for k, t in ipairs(selfData.RangeAttackExtraTimers) do self:AddExtraAttackTimer("timer_range_start_" .. curTime + k, t, function() if selfData.AttackSeed == seed then self:ExecuteRangeAttack() end end) end end end self:OnRangeAttack("PostInit", ene) end -- Leap Attack if eneIsVisible && selfData.HasLeapAttack && selfData.IsAbleToLeapAttack && !selfData.AttackType && ((plyControlled && selfData.VJ_TheController:KeyDown(IN_JUMP)) or (!plyControlled && (self:IsOnGround() && eneDist < selfData.LeapAttackMaxDistance) && (eneDist > selfData.LeapAttackMinDistance) && (self:GetHeadDirection():Dot((enePos - myPos):GetNormalized()) > math_cos(math_rad(selfData.LeapAttackAngleRadius))))) && self:OnLeapAttack("PreInit", ene) != true then local seed = curTime; selfData.AttackSeed = seed selfData.IsAbleToLeapAttack = false selfData.LeapAttackHasJumped = false selfData.AttackType = VJ.ATTACK_TYPE_LEAP selfData.AttackState = VJ.ATTACK_STATE_STARTED selfData.AttackAnim = ACT_INVALID selfData.AttackAnimDuration = 0 selfData.AttackAnimTime = 0 self:OnLeapAttack("Init", ene) self:PlaySoundSystem("BeforeLeapAttack") timer.Create("attack_leap_jump" .. self:EntIndex(), selfData.TimeUntilLeapAttackVelocity / selfData.AnimPlaybackRate, 1, function() self:LeapAttackJump() end) if selfData.AnimTbl_LeapAttack then local anim, animDur, animType = self:PlayAnim(selfData.AnimTbl_LeapAttack, false, 0, false) if anim != ACT_INVALID then selfData.AttackAnim = anim selfData.AttackAnimDuration = animDur - (selfData.LeapAttackAnimationDecreaseLengthAmount / selfData.AnimPlaybackRate) if animType != ANIM_TYPE_GESTURE then -- Allow things like chasing to continue for gestures selfData.AttackAnimTime = curTime + selfData.AttackAnimDuration end end end if !selfData.TimeUntilLeapAttackDamage then attackTimers[VJ.ATTACK_TYPE_LEAP](self) else -- NOT event based... timer.Create("attack_leap_start" .. self:EntIndex(), selfData.TimeUntilLeapAttackDamage / selfData.AnimPlaybackRate, selfData.LeapAttackReps, function() if selfData.AttackSeed == seed then self:ExecuteLeapAttack() end end) if selfData.LeapAttackExtraTimers then for k, t in ipairs(selfData.LeapAttackExtraTimers) do self:AddExtraAttackTimer("timer_leap_start_" .. curTime + k, t, function() if selfData.AttackSeed == seed then self:ExecuteLeapAttack() end end) end end end self:OnLeapAttack("PostInit", ene) end end else -- No enemy if !plyControlled then self:UpdatePoseParamTracking(true) //self:ClearPoseParameters() end eneData.TimeAcquired = 0 end if moveTypeAA then if eneValid && selfData.AttackAnimTime > curTime && eneData.DistanceNearest < selfData.MeleeAttackDistance then self:AA_StopMoving() else self:SelectSchedule() end end -- Guarding Behavior if selfData.IsGuard && !selfData.IsFollowing && !selfData.IsVJBaseSNPC_Tank then local guardData = selfData.GuardData if !guardData.Position then -- If we don't have a position, then set it! guardData.Position = myPos guardData.Direction = myPos + self:GetForward() * 51 end -- If it's far from the guarding position, then go there! if !self:IsMoving() && !self:IsBusy("Activities") then local dist = myPos:Distance(guardData.Position) -- Distance to the guard position if dist > 50 then self:SetLastPosition(guardData.Position) self:SCHEDULE_GOTO_POSITION(dist <= 800 and "TASK_WALK_PATH" or "TASK_RUN_PATH", function(x) x.CanShootWhenMoving = true x.TurnData = {Type = VJ.FACE_ENEMY} x.RunCode_OnFinish = function() timer.Simple(0.01, function() if IsValid(self) && !self:IsMoving() && !self:IsBusy("Activities") && selfData.IsGuard && guardData.Position then self:SetLastPosition(guardData.Direction) self:SCHEDULE_FACE("TASK_FACE_LASTPOSITION") end end) end end) end end end end -- Handle the unique movement system for player models if selfData.UsePoseParameterMovement && moveType == VJ_MOVETYPE_GROUND then local moveDir = VJ.GetMoveDirection(self, true) if moveDir then funcSetPoseParameter(self, "move_x", moveDir.x) funcSetPoseParameter(self, "move_y", moveDir.y) else -- I am not moving, reset the pose parameters, otherwise I will run in place! funcSetPoseParameter(self, "move_x", 0) funcSetPoseParameter(self, "move_y", 0) end end else -- AI Not enabled if moveTypeAA then self:AA_StopMoving() end end //if aiEnabled then //self:MaintainIdleAnimation() //end -- Maintain turning when needed otherwise Engine will take over during movements! -- No longer needed as "OverrideMoveFacing" now handles it! /*if !didTurn then local curTurnData = self.TurnData if curTurnData.Type && curTurnData.LastYaw != 0 then self:SetIdealYawAndUpdate(curTurnData.LastYaw) didTurn = true end end*/ self:NextThink(curTime + 0.065) return true end -------------------------------------------------------------------------------------------------------------------------------------------- local propColBlacklist = {[COLLISION_GROUP_DEBRIS] = true, [COLLISION_GROUP_DEBRIS_TRIGGER] = true, [COLLISION_GROUP_DISSOLVING] = true, [COLLISION_GROUP_IN_VEHICLE] = true, [COLLISION_GROUP_WORLD] = true} -- function ENT:MaintainPropInteraction(customEnts) local behavior = self.PropInteraction if !behavior then return false end local myPos = self:GetPos() local myCenter = myPos + self:OBBCenter() for _, ent in ipairs(customEnts or ents.FindInSphere(myCenter, self.MeleeAttackDistance * 1.2)) do if ent.VJ_ID_Attackable then local vPhys = ent:GetPhysicsObject() if IsValid(vPhys) && !propColBlacklist[ent:GetCollisionGroup()] && (customEnts or (self:GetHeadDirection():Dot((ent:GetPos() - myPos):GetNormalized()) > math_cos(math_rad(self.MeleeAttackAngleRadius / 1.3)))) then local tr = util.TraceLine({ start = myCenter, endpos = ent:NearestPoint(myCenter), filter = self }) if !tr.HitWorld && !tr.HitSky then -- Attacking: Make sure it has health if (behavior == true or behavior == "OnlyDamage") && ent:Health() > 0 then return true end -- Pushing: Make sure it's not a small object and the NPC is appropriately sized to push the object local surfaceArea = vPhys:GetSurfaceArea() or 900 if (behavior == true or behavior == "OnlyPush") && ent:GetMoveType() != MOVETYPE_PUSH && surfaceArea > 800 then // && vPhys:GetMass() > 4 local myPhys = self:GetPhysicsObject() if IsValid(myPhys) && (myPhys:GetSurfaceArea() * self.PropInteraction_MaxScale) >= surfaceArea then return true end end end end end end return false end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:ExecuteMeleeAttack(isPropAttack) local selfData = self:GetTable() if selfData.Dead or selfData.PauseAttacks or selfData.Flinching or (selfData.MeleeAttackStopOnHit && selfData.AttackState == VJ.ATTACK_STATE_EXECUTED_HIT) then return end isPropAttack = isPropAttack or selfData.MeleeAttack_IsPropAttack -- Is this a prop attack? local skip = self:OnMeleeAttackExecute("Init") local hitRegistered = false if !skip then local myPos = self:GetPos() local myClass = self:GetClass() //debugoverlay.Cross(self:MeleeAttackTraceOrigin(), 5, 3, Color(255, 255, 0)) //debugoverlay.EntityTextAtPosition(self:MeleeAttackTraceOrigin(), 0, "Melee damage origin", 3, Color(255, 255, 0)) //debugoverlay.Cross(self:MeleeAttackTraceOrigin() + self:GetForward()*selfData.MeleeAttackDamageDistance, 5, 3, Color(238, 119, 222)) //debugoverlay.EntityTextAtPosition(self:MeleeAttackTraceOrigin() + self:GetForward()*selfData.MeleeAttackDamageDistance, 0, "Melee damage distance", 3, Color(238, 119, 222)) for _, ent in ipairs(ents.FindInSphere(self:MeleeAttackTraceOrigin(), selfData.MeleeAttackDamageDistance)) do if ent == self or ent:GetClass() == myClass or (ent.IsVJBaseBullseye && ent.VJ_IsBeingControlled) then continue end if ent:IsPlayer() && (ent.VJ_IsControllingNPC or !ent:Alive() or VJ_CVAR_IGNOREPLAYERS) then continue end if ((ent.VJ_ID_Living && self:Disposition(ent) != D_LI) or ent.VJ_ID_Attackable or ent.VJ_ID_Destructible) && self:MeleeAttackTraceDirection():Dot((Vector(ent:GetPos().x, ent:GetPos().y, 0) - Vector(myPos.x, myPos.y, 0)):GetNormalized()) > math_cos(math_rad(selfData.MeleeAttackDamageAngleRadius)) then if isPropAttack && ent.VJ_ID_Living && VJ.GetNearestDistance(self, ent, true) > selfData.MeleeAttackDistance then continue end -- Since this attack initiated as prop attack, its melee distance may be off! local applyDmg = true local isProp = ent.VJ_ID_Attackable if self:OnMeleeAttackExecute("PreDamage", ent, isProp) == true then continue end local dmgAmount = self:ScaleByDifficulty(selfData.MeleeAttackDamage) -- Handle prop interaction local propBehavior = selfData.PropInteraction if isProp then if propBehavior then if (propBehavior == true or propBehavior == "OnlyDamage") && (ent:Health() > 0 or ent:GetInternalVariable("m_takedamage") == 2) then hitRegistered = true applyDmg = true elseif propBehavior == "OnlyPush" then applyDmg = false end local phys = ent:GetPhysicsObject() if IsValid(phys) && self:MaintainPropInteraction({ent}) then phys:EnableMotion(true) phys:Wake() constraint.RemoveConstraints(ent, "Weld") //constraint.RemoveAll(ent) if propBehavior == true or propBehavior == "OnlyPush" then hitRegistered = true local curEnemy = self:GetEnemy() local physMass = phys:GetMass() phys:ApplyForceCenter((IsValid(curEnemy) and curEnemy:GetPos() or myPos) + self:GetForward() * (physMass * 700) + self:GetUp() * (physMass * 200)) end end else -- We can't damage or push props applyDmg = false end end if applyDmg then -- Knockback | Ignore doors, trains, elevators as it will make them fly when activated if selfData.HasMeleeAttackKnockBack && ent:GetMoveType() != MOVETYPE_PUSH && ent.MovementType != VJ_MOVETYPE_STATIONARY && (!ent.VJ_ID_Boss or ent.IsVJBaseSNPC_Tank) then local isNextBot = ent:IsNextBot() if !isNextBot then ent:SetGroundEntity(NULL) end local vel = self:MeleeAttackKnockbackVelocity(ent) ent:SetVelocity(vel) if isNextBot then ent.loco:Approach(vel, 1) ent.loco:Jump() ent.loco:SetVelocity(vel) end end -- Apply damage if !selfData.DisableDefaultMeleeAttackDamageCode then local dmgInfo = DamageInfo() dmgInfo:SetDamage(self:ScaleByDifficulty(dmgAmount)) dmgInfo:SetDamageType(selfData.MeleeAttackDamageType) if ent.VJ_ID_Living then dmgInfo:SetDamageForce(self:GetForward() * ((dmgInfo:GetDamage() + 100) * 70)) end dmgInfo:SetInflictor(self) dmgInfo:SetAttacker(self) VJ.DamageSpecialEnts(self, ent, dmgInfo) ent:TakeDamageInfo(dmgInfo, self) end -- Apply bleeding damage if selfData.MeleeAttackBleedEnemy && ent.VJ_ID_Living && (!ent.VJ_ID_Boss or selfData.VJ_ID_Boss) && math.random(1, selfData.MeleeAttackBleedEnemyChance) == 1 then local bleedName = "timer_melee_bleed" .. ent:EntIndex() -- Timer's name local bleedDmg = self:ScaleByDifficulty(selfData.MeleeAttackBleedEnemyDamage) -- How much damage each rep does timer.Create(bleedName, selfData.MeleeAttackBleedEnemyTime, selfData.MeleeAttackBleedEnemyReps, function() if IsValid(ent) && ent:Health() > 0 then local dmgInfo = DamageInfo() dmgInfo:SetDamage(bleedDmg) dmgInfo:SetDamageType(DMG_GENERIC) dmgInfo:SetDamageCustom(VJ.DMG_BLEED) if self:IsValid() then dmgInfo:SetInflictor(self) dmgInfo:SetAttacker(self) end ent:TakeDamageInfo(dmgInfo) else -- Remove the timer if the entity is dead in attempt to remove it before the entity respawns (Essential for players) timer.Remove(bleedName) end end) end end if ent:IsPlayer() then ent:ViewPunch(Angle(math.random(-1, 1) * dmgAmount, math.random(-1, 1) * dmgAmount, math.random(-1, 1) * dmgAmount)) -- Apply DSP if selfData.MeleeAttackDSP && ((!selfData.MeleeAttackDSPLimit) or (dmgAmount >= selfData.MeleeAttackDSPLimit)) then ent:SetDSP(selfData.MeleeAttackDSP, false) end -- Speed modifier if selfData.MeleeAttackPlayerSpeed then self:DoMeleeAttackPlayerSpeed(ent, selfData.MeleeAttackPlayerSpeedWalk, selfData.MeleeAttackPlayerSpeedRun, selfData.MeleeAttackPlayerSpeedTime, {PlaySound = selfData.HasMeleeAttackPlayerSpeedSounds, SoundTable = selfData.SoundTbl_MeleeAttackPlayerSpeed, SoundLevel = selfData.MeleeAttackPlayerSpeedSoundLevel, FadeOutTime = 1}) end end if !isProp then -- Only for non-props... hitRegistered = true if selfData.MeleeAttackStopOnHit then break end end end end end if selfData.AttackState < VJ.ATTACK_STATE_EXECUTED then selfData.AttackState = VJ.ATTACK_STATE_EXECUTED if selfData.TimeUntilMeleeAttackDamage then attackTimers[VJ.ATTACK_TYPE_MELEE](self) end end if !skip then if hitRegistered then self:PlaySoundSystem("MeleeAttack") selfData.AttackState = VJ.ATTACK_STATE_EXECUTED_HIT else self:OnMeleeAttackExecute("Miss") self:PlaySoundSystem("MeleeAttackMiss") end end end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:DoMeleeAttackPlayerSpeed(ent, walkSpeed, runSpeed, speedTime, sdData, extraOptions, customFunc) speedTime = speedTime or 5 sdData = sdData or {} local vSD_PlaySound = sdData.PlaySound or false -- Should it play a sound? local vSD_SoundTable = sdData.SoundTable or {} -- Sounds it should play (Picks randomly) local vSD_SoundLevel = sdData.SoundLevel or 100 -- How loud should the sound play? local vSD_FadeOutTime = sdData.FadeOutTime or 1 -- How long until it the sound fully fades out? extraOptions = extraOptions or {} local vEF_NoInterrupt = extraOptions.NoInterrupt or false -- If set to true, the player's speed won't change by another instance of this code local walkspeed_before = ent:GetWalkSpeed() local runspeed_before = ent:GetRunSpeed() if ent.VJ_SpeedModified && ent.VJ_SpeedModified_NoInterrupt then return end if (!ent.VJ_SpeedModified) then ent.VJ_SpeedModified = true if vEF_NoInterrupt then ent.VJ_SpeedModified_NoInterrupt = true end ent.VJ_SlowDownPlayerWalkSpeed = walkspeed_before ent.VJ_SlowDownPlayerRunSpeed = runspeed_before end ent:SetWalkSpeed(walkSpeed or 50) ent:SetRunSpeed(runSpeed or 50) if (customFunc) then customFunc() end if self.HasSounds && vSD_PlaySound then self.CurrentMeleeAttackPlayerSpeedSound = CreateSound(ent, PICK(vSD_SoundTable)) self.CurrentMeleeAttackPlayerSpeedSound:Play() self.CurrentMeleeAttackPlayerSpeedSound:SetSoundLevel(vSD_SoundLevel) if !ent:Alive() && self.CurrentMeleeAttackPlayerSpeedSound then self.CurrentMeleeAttackPlayerSpeedSound:FadeOut(vSD_FadeOutTime) end end local pickedSD = self.CurrentMeleeAttackPlayerSpeedSound local sdFadeTime = vSD_FadeOutTime local timerName = "timer_melee_slowply" .. ent:EntIndex() if timer.Exists(timerName) && timer.TimeLeft(timerName) > speedTime then return end timer.Create(timerName, speedTime, 1, function() ent:SetWalkSpeed(ent.VJ_SlowDownPlayerWalkSpeed) ent:SetRunSpeed(ent.VJ_SlowDownPlayerRunSpeed) ent.VJ_SpeedModified = false ent.VJ_SpeedModified_NoInterrupt = false if pickedSD then pickedSD:FadeOut(sdFadeTime) end if !IsValid(ent) then timer.Remove(timerName) end end) end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:ExecuteRangeAttack() local selfData = self:GetTable() if selfData.Dead or selfData.PauseAttacks or selfData.Flinching or selfData.AttackType == VJ.ATTACK_TYPE_MELEE then return end local ene = self:GetEnemy() local eneValid = IsValid(ene) if eneValid then selfData.AttackType = VJ.ATTACK_TYPE_RANGE //self:PointAtEntity(ene) -- Create projectile if !self:OnRangeAttackExecute("Init", ene) then local projectileClass = PICK(selfData.RangeAttackProjectiles) or PICK(selfData.RangeAttackEntityToSpawn) if projectileClass then local projectile = ents.Create(projectileClass) local spawnPos = self:RangeAttackProjPos(projectile) projectile:SetPos(spawnPos) projectile:SetAngles((ene:GetPos() - spawnPos):Angle()) self:OnRangeAttackExecute("PreSpawn", ene, projectile) projectile:SetOwner(self) projectile:SetPhysicsAttacker(self) projectile:Spawn() projectile:Activate() //constraint.NoCollide(self, projectile, 0, 0) local phys = projectile:GetPhysicsObject() if IsValid(phys) then phys:Wake() local vel = self:RangeAttackProjVel(projectile) phys:SetVelocity(vel) projectile:SetAngles(vel:GetNormal():Angle()) else local vel = self:RangeAttackProjVel(projectile) projectile:SetVelocity(vel) projectile:SetAngles(vel:GetNormal():Angle()) end self:OnRangeAttackExecute("PostSpawn", ene, projectile) end end end if selfData.AttackState < VJ.ATTACK_STATE_EXECUTED then if eneValid then -- Play range attack only once, otherwise it will spam it for every projectile! self:PlaySoundSystem("RangeAttack") end selfData.AttackState = VJ.ATTACK_STATE_EXECUTED if selfData.TimeUntilRangeAttackProjectileRelease then attackTimers[VJ.ATTACK_TYPE_RANGE](self) end end end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:ExecuteLeapAttack() local selfData = self:GetTable() if selfData.Dead or selfData.PauseAttacks or selfData.Flinching or (selfData.LeapAttackStopOnHit && selfData.AttackState == VJ.ATTACK_STATE_EXECUTED_HIT) then return end local skip = self:OnLeapAttackExecute("Init") local hitRegistered = false if !skip then local myClass = self:GetClass() for _, ent in ipairs(ents.FindInSphere(self:GetPos(), selfData.LeapAttackDamageDistance)) do if ent == self or ent:GetClass() == myClass or (ent.IsVJBaseBullseye && ent.VJ_IsBeingControlled) then continue end if ent:IsPlayer() && (ent.VJ_IsControllingNPC or !ent:Alive() or VJ_CVAR_IGNOREPLAYERS) then continue end if (ent.VJ_ID_Living && self:Disposition(ent) != D_LI) or ent.VJ_ID_Attackable or ent.VJ_ID_Destructible then if self:OnLeapAttackExecute("PreDamage", ent) == true then continue end local dmgAmount = self:ScaleByDifficulty(selfData.LeapAttackDamage) -- Damage if !selfData.DisableDefaultLeapAttackDamageCode then local dmgInfo = DamageInfo() dmgInfo:SetDamage(dmgAmount) dmgInfo:SetInflictor(self) dmgInfo:SetDamageType(selfData.LeapAttackDamageType) dmgInfo:SetAttacker(self) if ent.VJ_ID_Living then dmgInfo:SetDamageForce(self:GetForward() * ((dmgInfo:GetDamage() + 100) * 70)) end ent:TakeDamageInfo(dmgInfo, self) end if ent:IsPlayer() then ent:ViewPunch(Angle(math.random(-1, 1) * dmgAmount, math.random(-1, 1) * dmgAmount, math.random(-1, 1) * dmgAmount)) end hitRegistered = true if selfData.LeapAttackStopOnHit then break end end end end if selfData.AttackState < VJ.ATTACK_STATE_EXECUTED then selfData.AttackState = VJ.ATTACK_STATE_EXECUTED if selfData.TimeUntilLeapAttackDamage then attackTimers[VJ.ATTACK_TYPE_LEAP](self) end end if !skip then if hitRegistered then self:PlaySoundSystem("LeapAttackDamage") selfData.AttackState = VJ.ATTACK_STATE_EXECUTED_HIT else self:OnLeapAttackExecute("Miss") self:PlaySoundSystem("LeapAttackDamageMiss") end end end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:LeapAttackJump() local ene = self:GetEnemy() if !IsValid(ene) then return end self:SetGroundEntity(NULL) self.LeapAttackHasJumped = true -- Classic velocity, useful for more straight line jumps //return ((ene:GetPos() + ene:OBBCenter()) - (self:GetPos() + self:OBBCenter())):GetNormal() * 400 + self:GetForward() * 200 + self:GetUp() * 100 self:SetLocalVelocity(self:OnLeapAttack("Jump", ene) or VJ.CalculateTrajectory(self, ene, "Curve", self:GetPos() + self:OBBCenter(), ene:GetPos() + ene:OBBCenter(), 1)) self:PlaySoundSystem("LeapAttackJump") end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:StopAttacks(checkTimers) if !self:Alive() then return end local selfData = self:GetTable() if selfData.VJ_DEBUG && GetConVar("vj_npc_debug_attack"):GetInt() == 1 then VJ.DEBUG_Print(self, "StopAttacks", "Attack type = " .. selfData.AttackType) end if checkTimers && attackTimers[selfData.AttackType] && selfData.AttackState < VJ.ATTACK_STATE_EXECUTED then attackTimers[selfData.AttackType](self, true) end selfData.AttackType = VJ.ATTACK_TYPE_NONE selfData.AttackState = VJ.ATTACK_STATE_DONE selfData.AttackSeed = 0 selfData.LeapAttackHasJumped = false self:MaintainAlertBehavior() end --------------------------------------------------------------------------------------------------------------------------------------------- local function math_angDif(diff) diff = diff % 360 return diff > 180 and (diff - 360) or diff end -- function ENT:UpdatePoseParamTracking(resetPoses) local selfData = self:GetTable() if !selfData.HasPoseParameterLooking then return end //VJ.GetPoseParameters(self) local ene = self:GetEnemy() local newPitch = 0 local newYaw = 0 local newRoll = 0 if !resetPoses && IsValid(ene) then local myEyePos = self:EyePos() local myAng = self:GetAngles() local eneAng = (self:GetAimPosition(ene, myEyePos) - myEyePos):Angle() newPitch = math_angDif(eneAng.p - myAng.p) if selfData.PoseParameterLooking_InvertPitch then newPitch = -newPitch end newYaw = math_angDif(eneAng.y - myAng.y) if selfData.PoseParameterLooking_InvertYaw then newYaw = -newYaw end newRoll = math_angDif(eneAng.z - myAng.z) if selfData.PoseParameterLooking_InvertRoll then newRoll = -newRoll end elseif !selfData.PoseParameterLooking_CanReset then return -- Should it reset its pose parameters if there is no enemies? end local funcCustom = self.OnUpdatePoseParamTracking; if funcCustom then funcCustom(self, newPitch, newYaw, newRoll) end local speed = selfData.PoseParameterLooking_TurningSpeed local names = selfData.PoseParameterLooking_Names local namesPitch = names.pitch local namesYaw = names.yaw local namesRoll = names.roll for x = 1, #namesPitch do local pose = namesPitch[x] funcSetPoseParameter(self, pose, math_angApproach(funcGetPoseParameter(self, pose), newPitch, speed)) end for x = 1, #namesYaw do local pose = namesYaw[x] funcSetPoseParameter(self, pose, math_angApproach(funcGetPoseParameter(self, pose), newYaw, speed)) end for x = 1, #namesRoll do local pose = namesRoll[x] funcSetPoseParameter(self, pose, math_angApproach(funcGetPoseParameter(self, pose), newRoll, speed)) end end --------------------------------------------------------------------------------------------------------------------------------------------- local schedule_yield_player = vj_ai_schedule.New("SCHEDULE_YIELD_PLAYER") schedule_yield_player:EngTask("TASK_MOVE_AWAY_PATH", 120) schedule_yield_player:EngTask("TASK_RUN_PATH", 0) schedule_yield_player:EngTask("TASK_WAIT_FOR_MOVEMENT", 0) schedule_yield_player.CanShootWhenMoving = true schedule_yield_player.TurnData = {} -- This is constantly edited! local bitsDanger = bit.bor(SOUND_BULLET_IMPACT, SOUND_COMBAT, SOUND_WORLD, SOUND_DANGER) // SOUND_PLAYER, SOUND_PLAYER_VEHICLE -- function ENT:SelectSchedule() local selfData = self:GetTable() if selfData.VJ_IsBeingControlled or selfData.Dead then return end local curTime = CurTime() local eneValid = IsValid(self:GetEnemy()) self:PlayIdleSound(nil, nil, eneValid) -- Handle move away behavior if funcHasCondition(self, COND_PLAYER_PUSHING) && curTime > selfData.TakingCoverT && !self:IsBusy("Activities") then self:PlaySoundSystem("YieldToPlayer") if eneValid then -- Face current enemy schedule_yield_player.TurnData.Type = VJ.FACE_ENEMY_VISIBLE schedule_yield_player.TurnData.Target = nil elseif IsValid(self:GetTarget()) then -- Face current target schedule_yield_player.TurnData.Type = VJ.FACE_ENTITY_VISIBLE schedule_yield_player.TurnData.Target = self:GetTarget() else -- Reset if both others fail! (Remember this is a localized table shared between all NPCs!) schedule_yield_player.TurnData.Type = nil schedule_yield_player.TurnData.Target = nil end self:StartSchedule(schedule_yield_player) selfData.TakingCoverT = curTime + 2 end if eneValid then -- Chase the enemy self:MaintainAlertBehavior() /*elseif selfData.Alerted then -- No enemy, but alerted selfData.TakingCoverT = 0 self:MaintainIdleBehavior()*/ else -- Idle if !selfData.Alerted then selfData.TakingCoverT = 0 end -- Investigation: Conditions // funcHasCondition(self, COND_HEAR_PLAYER) if selfData.CanInvestigate && (funcHasCondition(self, COND_HEAR_BULLET_IMPACT) or funcHasCondition(self, COND_HEAR_COMBAT) or funcHasCondition(self, COND_HEAR_WORLD) or funcHasCondition(self, COND_HEAR_DANGER)) && selfData.NextInvestigationMove < curTime && selfData.TakingCoverT < curTime && !self:IsBusy() then local sdSrc = self:GetBestSoundHint(bitsDanger) if sdSrc then //PrintTable(sdSrc) local allowed = true local sdOwner = sdSrc.owner if IsValid(sdOwner) then -- Ignore dangers produced by vehicles driven by an allies if sdSrc.type == SOUND_DANGER && sdOwner:IsVehicle() && IsValid(sdOwner:GetDriver()) && self:Disposition(sdOwner:GetDriver()) == D_LI then allowed = false -- Ignore dangers by allies and combat sounds (such as death sounds) from dead NPCs elseif self:Disposition(sdOwner) == D_LI or (sdSrc.type == SOUND_COMBAT && sdOwner:IsNPC() && !sdOwner:Alive()) then allowed = false end end -- For now ignore player sounds because friendly NPCs also see it since the sound owner is NULL //if sdSrc.type == SOUND_PLAYER then // if VJ_CVAR_IGNOREPLAYERS or self:IsMoving() or self.IsGuard then // skip = true // end //end if allowed then self:DoReadyAlert() self:StopMoving() self:SetLastPosition(sdSrc.origin) self:SCHEDULE_FACE("TASK_FACE_LASTPOSITION") -- Works but just faces the enemy that fired at //local sched = vj_ai_schedule.New("SCHEDULE_HEAR_SOUND") //sched:EngTask("TASK_STORE_BESTSOUND_REACTORIGIN_IN_SAVEPOSITION", 0) //sched:EngTask("TASK_STOP_MOVING", 0) //sched:EngTask("TASK_FACE_SAVEPOSITION", 0) //self:StartSchedule(sched) self:OnInvestigate(sdOwner) self:PlaySoundSystem("Investigate") selfData.TakingCoverT = curTime + 1 end end end self:MaintainIdleBehavior() end end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:ResetEnemy(checkAllies, checkVis) local selfData = self:GetTable() if selfData.Dead or (selfData.VJ_IsBeingControlled && selfData.VJ_TheControllerBullseye == self:GetEnemy()) then selfData.EnemyData.Reset = false return false end local ene = self:GetEnemy() local eneValid = IsValid(ene) local eneData = selfData.EnemyData local curTime = CurTime() if checkAllies then local getAllies = self:Allies_Check(1000) if getAllies then for _, ally in ipairs(getAllies) do local allyEne = ally:GetEnemy() if IsValid(allyEne) && (curTime - ally.EnemyData.VisibleTime) < selfData.EnemyTimeout && allyEne:Alive() && self:GetPos():Distance(allyEne:GetPos()) <= self:GetMaxLookDistance() && self:CheckRelationship(allyEne) == D_HT then self:ForceSetEnemy(allyEne, false) eneData.VisibleTime = curTime -- Reset the time otherwise it will run "ResetEnemy" none-stop! eneData.Reset = false return false end end end end if checkVis then -- If the current number of reachable enemies is higher then 1, then don't reset local curEnemies = eneData.VisibleCount //selfData.CurrentReachableEnemies if (eneValid && (curEnemies - 1) >= 1) or (!eneValid && curEnemies >= 1) then self:MaintainRelationships() -- Select a new enemy -- Check that the reset enemy wasn't the only visible enemy -- If we don't this, it will call "ResetEnemy" again! if eneData.VisibleCount > 0 then eneData.Reset = false return false end end end if selfData.VJ_DEBUG && GetConVar("vj_npc_debug_resetenemy"):GetInt() == 1 then VJ.DEBUG_Print(self, "ResetEnemy", tostring(ene)) end eneData.Reset = true self:SetNPCState(NPC_STATE_ALERT) timer.Create("alert_reset" .. self:EntIndex(), math.Rand(selfData.AlertTimeout.a, selfData.AlertTimeout.b), 1, function() if !IsValid(self:GetEnemy()) then selfData.Alerted = false self:SetNPCState(NPC_STATE_IDLE) end end) self:OnResetEnemy() local moveToEnemy = false if eneValid then if !selfData.IsFollowing && !selfData.IsGuard && !selfData.IsVJBaseSNPC_Tank && !selfData.VJ_IsBeingControlled && selfData.LastHiddenZone_CanWander == true && !selfData.Weapon_UnarmedBehavior_Active && selfData.Behavior != VJ_BEHAVIOR_PASSIVE && selfData.Behavior != VJ_BEHAVIOR_PASSIVE_NATURE && !self:IsBusy() && !self:Visible(ene) && self:GetEnemyLastKnownPos() != defPos then moveToEnemy = self:GetEnemyLastKnownPos() end self:MarkEnemyAsEluded(ene) //self:ClearEnemyMemory(ene) // Completely resets the enemy memory self:AddEntityRelationship(ene, D_NU, 10) end -- Clear memory of the enemy if it's not a player AND it's dead if eneValid && !ene:IsPlayer() && !ene:Alive() then //print("Clear memory", ene) self:ClearEnemyMemory(ene) end selfData.NextWanderTime = curTime + math.Rand(3, 5) self:SetEnemy(NULL) if moveToEnemy then self:SetLastPosition(moveToEnemy) self:SCHEDULE_GOTO_POSITION("TASK_WALK_PATH", function(schedule) //if eneValid then schedule:EngTask("TASK_FORGET", ene) end //schedule:EngTask("TASK_IGNORE_OLD_ENEMIES", 0) schedule.ResetOnFail = true schedule.CanShootWhenMoving = true schedule.CanBeInterrupted = true schedule.TurnData = {Type = VJ.FACE_ENEMY} end) end end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:OnTakeDamage(dmginfo) local dmgAttacker = dmginfo:GetAttacker() if !IsValid(dmgAttacker) then dmgAttacker = false end -- Don't take bullet damage from friendly NPCs if dmgAttacker && dmginfo:IsBulletDamage() && dmgAttacker:IsNPC() && dmgAttacker:Disposition(self) != D_HT && (dmgAttacker:GetClass() == self:GetClass() or self:Disposition(dmgAttacker) == D_LI) then return 0 end local dmgInflictor = dmginfo:GetInflictor() if !IsValid(dmgInflictor) then dmgInflictor = false end -- Attempt to avoid taking damage when walking on ragdolls if dmgInflictor && dmgInflictor:GetClass() == "prop_ragdoll" && dmgInflictor:GetVelocity():Length() <= 100 then return 0 end local selfData = self:GetTable() local hitgroup = self:GetLastDamageHitGroup() self:OnDamaged(dmginfo, hitgroup, "Init") if selfData.GodMode or dmginfo:GetDamage() <= 0 then return 0 end local dmgType = dmginfo:GetDamageType() local curTime = CurTime() local isFireEnt = false if self:IsOnFire() then isFireEnt = dmgInflictor && dmgAttacker && dmgInflictor:GetClass() == "entityflame" && dmgAttacker:GetClass() == "entityflame" if self:WaterLevel() > 1 then self:Extinguish() end -- If we are in water, then extinguish the fire end -- If it should always take damage from huge monsters, then skip immunity checks! if dmgAttacker && selfData.ForceDamageFromBosses && dmgAttacker.VJ_ID_Boss then goto skip_immunity end -- Immunity checks if isFireEnt && !selfData.AllowIgnition then self:Extinguish() return 0 end if (selfData.Immune_Fire && (dmgType == DMG_BURN or dmgType == DMG_SLOWBURN or isFireEnt)) or (selfData.Immune_Toxic && (dmgType == DMG_ACID or dmgType == DMG_RADIATION or dmgType == DMG_POISON or dmgType == DMG_NERVEGAS or dmgType == DMG_PARALYZE)) or (selfData.Immune_Bullet && (dmginfo:IsBulletDamage() or dmgType == DMG_BULLET or dmgType == DMG_AIRBOAT or dmgType == DMG_BUCKSHOT or dmgType == DMG_SNIPER)) or (selfData.Immune_Explosive && (dmgType == DMG_BLAST or dmgType == DMG_BLAST_SURFACE or dmgType == DMG_MISSILEDEFENSE)) or (selfData.Immune_Dissolve && dmginfo:IsDamageType(DMG_DISSOLVE)) or (selfData.Immune_Electricity && (dmgType == DMG_SHOCK or dmgType == DMG_ENERGYBEAM or dmgType == DMG_PHYSGUN)) or (selfData.Immune_Melee && (dmgType == DMG_CLUB or dmgType == DMG_SLASH)) or (selfData.Immune_Sonic && dmgType == DMG_SONIC) then return 0 end -- Make sure combine ball does reasonable damage and doesn't spam! if (dmgInflictor && dmgInflictor:GetClass() == "prop_combine_ball") or (dmgAttacker && dmgAttacker:GetClass() == "prop_combine_ball") then if selfData.Immune_Dissolve then return 0 end if curTime > selfData.NextCombineBallDmgT then dmginfo:SetDamage(math.random(400, 500)) dmginfo:SetDamageType(DMG_DISSOLVE) selfData.NextCombineBallDmgT = curTime + 0.2 else return 0 end end ::skip_immunity:: local function DoBleed() if selfData.Bleeds then self:OnBleed(dmginfo, hitgroup) -- Spawn the blood particle only if it's not caused by the default fire entity [Causes the damage position to be at Vector(0, 0, 0)] if selfData.HasBloodParticle && !isFireEnt then self:SpawnBloodParticles(dmginfo, hitgroup) end if selfData.HasBloodDecal then self:SpawnBloodDecals(dmginfo, hitgroup) end self:PlaySoundSystem("Impact") end end if selfData.Dead then DoBleed() return 0 end -- If dead then just bleed but take no damage self:OnDamaged(dmginfo, hitgroup, "PreDamage") if dmginfo:GetDamage() <= 0 then return 0 end -- Only take damage if it's above 0! -- Why? Because GMod resets/randomizes dmginfo after a tick... selfData.SavedDmgInfo = { dmginfo = dmginfo, -- The actual CTakeDamageInfo object | WARNING: Can be corrupted after a tick, recommended not to use this! attacker = dmginfo:GetAttacker(), inflictor = dmginfo:GetInflictor(), amount = dmginfo:GetDamage(), pos = dmginfo:GetDamagePosition(), type = dmginfo:GetDamageType(), force = dmginfo:GetDamageForce(), ammoType = dmginfo:GetAmmoType(), hitgroup = hitgroup, } self:SetHealth(self:Health() - dmginfo:GetDamage()) if selfData.VJ_DEBUG && GetConVar("vj_npc_debug_damage"):GetInt() == 1 then VJ.DEBUG_Print(self, "OnTakeDamage", "Amount = ", dmginfo:GetDamage(), " | Attacker = ", dmgAttacker, " | Inflictor = ", dmgInflictor) end local healthRegen = selfData.HealthRegenParams if healthRegen.Enabled && healthRegen.ResetOnDmg then selfData.HealthRegenDelayT = curTime + (math.Rand(healthRegen.Delay.a, healthRegen.Delay.b) * 1.5) end self:SetSaveValue("m_iDamageCount", self:GetTotalDamageCount() + 1) self:SetSaveValue("m_flLastDamageTime", curTime) self:OnDamaged(dmginfo, hitgroup, "PostDamage") DoBleed() -- I/O events, from: https://github.com/ValveSoftware/source-sdk-2013/blob/0d8dceea4310fde5706b3ce1c70609d72a38efdf/sp/src/game/server/ai_basenpc.cpp#L764 if dmgAttacker then self:TriggerOutput("OnDamaged", dmgAttacker) self:MarkTookDamageFromEnemy(dmgAttacker) else self:TriggerOutput("OnDamaged", self) end local stillAlive = self:Health() > 0 if stillAlive then self:PlaySoundSystem("Pain") end if VJ_CVAR_AI_ENABLED && self:GetState() != VJ_STATE_FREEZE then local isPassive = selfData.Behavior == VJ_BEHAVIOR_PASSIVE or selfData.Behavior == VJ_BEHAVIOR_PASSIVE_NATURE if stillAlive then if !isFireEnt then self:Flinch(dmginfo, hitgroup) end -- Player attackers if dmgAttacker && dmgAttacker:IsPlayer() then -- Become enemy to a friendly player | RESULT: May become hostile to an allied player if selfData.BecomeEnemyToPlayer && self:CheckRelationship(dmgAttacker) == D_LI then local relationMemory = selfData.RelationshipMemory[dmgAttacker] self:SetRelationshipMemory(dmgAttacker, VJ.MEM_HOSTILITY_LEVEL, relationMemory[VJ.MEM_HOSTILITY_LEVEL] and relationMemory[VJ.MEM_HOSTILITY_LEVEL] + 1 or 1) if relationMemory[VJ.MEM_HOSTILITY_LEVEL] > selfData.BecomeEnemyToPlayer && self:Disposition(dmgAttacker) != D_HT then self:OnBecomeEnemyToPlayer(dmginfo, hitgroup) if selfData.IsFollowing && selfData.FollowData.Target == dmgAttacker then self:ResetFollowBehavior() end self:SetRelationshipMemory(dmgAttacker, VJ.MEM_OVERRIDE_DISPOSITION, D_HT) self:AddEntityRelationship(dmgAttacker, D_HT, 2) selfData.TakingCoverT = curTime + 2 self:PlaySoundSystem("BecomeEnemyToPlayer") if !IsValid(self:GetEnemy()) then self:StopMoving() self:SetTarget(dmgAttacker) self:SCHEDULE_FACE("TASK_FACE_TARGET") end if selfData.CanChatMessage then dmgAttacker:PrintMessage(HUD_PRINTTALK, self:GetName() .. " no longer likes you.") end end end -- React to damage by a player -- 0 = Run it every time | 1 = Run it only when friendly to player | 2 = Run it only when enemy to player if selfData.HasDamageByPlayerSounds && curTime > selfData.NextDamageByPlayerSoundT && self:Visible(dmgAttacker) then local dispLvl = selfData.DamageByPlayerDispositionLevel if (dispLvl == 0 or (dispLvl == 1 && self:Disposition(dmgAttacker) == D_LI) or (dispLvl == 2 && self:Disposition(dmgAttacker) != D_HT)) then self:PlaySoundSystem("DamageByPlayer") end end end self:PlaySoundSystem("Pain") if !isPassive && !IsValid(self:GetEnemy()) then local canMove = true -- How allies respond when it's damaged if selfData.DamageAllyResponse && curTime > selfData.NextDamageAllyResponseT && !selfData.IsFollowing then local responseDist = math_max(800, self:OBBMaxs():Distance(self:OBBMins()) * 12) local allies = self:Allies_Check(responseDist) if allies != false then if !isFireEnt then self:Allies_Bring("Diamond", responseDist, allies, 4) end for _, ally in ipairs(allies) do ally:DoReadyAlert() end if !isFireEnt && !self:IsBusy("Activities") then self:DoReadyAlert() local anim = self:PlayAnim(selfData.AnimTbl_DamageAllyResponse, true, false, true) if anim != ACT_INVALID then canMove = false selfData.NextFlinchT = curTime + 1 end end selfData.NextDamageAllyResponseT = curTime + math.Rand(selfData.DamageAllyResponse_Cooldown.a, selfData.DamageAllyResponse_Cooldown.b) end end local dmgResponse = selfData.DamageResponse if dmgResponse && curTime > selfData.TakingCoverT && !self:IsBusy("Activities") then -- Attempt to find who damaged me | RESULT: May become alerted if attacker is visible OR it may hide if it didn't find the attacker if dmgAttacker && (dmgResponse == true or dmgResponse == "OnlySearch") then local sightDist = self:GetMaxLookDistance() sightDist = math_min(math_max(sightDist / 2, sightDist <= 1000 and sightDist or 1000), sightDist) -- IF normal sight dist is less than 1000 then change nothing, OR ELSE use half the distance with 1000 as minimum if self:GetPos():Distance(dmgAttacker:GetPos()) <= sightDist && self:Visible(dmgAttacker) then local dispLvl = self:CheckRelationship(dmgAttacker) if dispLvl == D_HT or dispLvl == D_NU then //self:AddEntityRelationship(dmgAttacker, D_HT, 10) self:OnSetEnemyFromDamage(dmginfo, hitgroup) selfData.NextCallForHelpT = curTime + 1 self:ForceSetEnemy(dmgAttacker, true) self:MaintainAlertBehavior() canMove = false end end end -- If all else failed then take cover! if canMove && (dmgResponse == true or dmgResponse == "OnlyMove") && !selfData.IsFollowing && selfData.MovementType != VJ_MOVETYPE_STATIONARY && dmginfo:GetDamageCustom() != VJ.DMG_BLEED then self:SCHEDULE_COVER_ORIGIN("TASK_RUN_PATH", function(x) x.CanShootWhenMoving = true x.TurnData = {Type = VJ.FACE_ENEMY} end) selfData.TakingCoverT = curTime + 5 end end -- Passive NPCs elseif isPassive && curTime > selfData.TakingCoverT then if selfData.DamageResponse && !self:IsBusy() then self:SCHEDULE_COVER_ORIGIN("TASK_RUN_PATH") end end end -- Make passive NPCs move away | RESULT: May move away AND may cause other passive NPCs to move as well if isPassive && curTime > selfData.TakingCoverT then if selfData.Passive_AlliesRunOnDamage then -- Make passive allies run too! local allies = self:Allies_Check(math_max(800, self:OBBMaxs():Distance(self:OBBMins()) * 20)) if allies != false then for _, ally in ipairs(allies) do ally.TakingCoverT = curTime + math.Rand(6, 7) ally:SCHEDULE_COVER_ORIGIN("TASK_RUN_PATH") ally:PlaySoundSystem("Alert") end end end selfData.TakingCoverT = curTime + math.Rand(6, 7) end end -- If eating, stop! if selfData.CanEat && selfData.VJ_ST_Eating then selfData.EatingData.NextCheck = curTime + 15 self:ResetEatingBehavior("Injured") end if self:Health() <= 0 && !selfData.Dead then self:RemoveEFlags(EFL_NO_DISSOLVE) if (dmginfo:IsDamageType(DMG_DISSOLVE)) or (dmgInflictor && dmgInflictor:GetClass() == "prop_combine_ball") then local dissolve = DamageInfo() dissolve:SetDamage(self:Health()) dissolve:SetAttacker(dmginfo:GetAttacker()) dissolve:SetDamageType(DMG_DISSOLVE) self:TakeDamageInfo(dissolve) end self:BeginDeath(dmginfo, hitgroup) end return 1 end --------------------------------------------------------------------------------------------------------------------------------------------- local vecZ500 = Vector(0, 0, 500) local vecZ4 = Vector(0, 0, 4) -- function ENT:BeginDeath(dmginfo, hitgroup) self.Dead = true self:SetSaveValue("m_lifeState", 1) -- LIFE_DYING self:OnDeath(dmginfo, hitgroup, "Init") if self.MedicData.Status then self:ResetMedicBehavior() end if self.IsFollowing then self:ResetFollowBehavior() end local dmgInflictor = dmginfo:GetInflictor() local dmgAttacker = dmginfo:GetAttacker() local myPos = self:GetPos() if VJ_CVAR_AI_ENABLED then local responseDist = math_max(800, self:OBBMaxs():Distance(self:OBBMins()) * 12) local allies = self:Allies_Check(responseDist) if allies then local doBecomeEnemyToPlayer = (self.BecomeEnemyToPlayer && dmgAttacker:IsPlayer() && !VJ_CVAR_IGNOREPLAYERS) or false local responseType = self.DeathAllyResponse local movedAllyNum = 0 -- Number of allies that have moved for _, ally in ipairs(allies) do ally:OnAllyKilled(self) ally:PlaySoundSystem("AllyDeath") if responseType && myPos:Distance(ally:GetPos()) < responseDist then local moved = false -- Bring ally if responseType == true && movedAllyNum < self.DeathAllyResponse_MoveLimit then moved = self:Allies_Bring("Random", responseDist, {ally}, 0, true) if moved then movedAllyNum = movedAllyNum + 1 end end -- Alert ally if (responseType == true or responseType == "OnlyAlert") && !IsValid(ally:GetEnemy()) then ally:DoReadyAlert() if !moved then local faceTime = math.Rand(5, 8) ally:SetTurnTarget(myPos, faceTime, true) ally.NextIdleTime = CurTime() + faceTime end end end -- BecomeEnemyToPlayer if doBecomeEnemyToPlayer && ally.BecomeEnemyToPlayer && ally:Disposition(dmgAttacker) == D_LI then local relationMemory = ally.RelationshipMemory[dmgAttacker] ally:SetRelationshipMemory(dmgAttacker, VJ.MEM_HOSTILITY_LEVEL, relationMemory[VJ.MEM_HOSTILITY_LEVEL] and relationMemory[VJ.MEM_HOSTILITY_LEVEL] + 1 or 1) if relationMemory[VJ.MEM_HOSTILITY_LEVEL] > ally.BecomeEnemyToPlayer then if ally:Disposition(dmgAttacker) != D_HT then ally:OnBecomeEnemyToPlayer(dmginfo, hitgroup) if ally.IsFollowing && ally.FollowData.Target == dmgAttacker then ally:ResetFollowBehavior() end ally:SetRelationshipMemory(dmgAttacker, VJ.MEM_OVERRIDE_DISPOSITION, D_HT) ally:AddEntityRelationship(dmgAttacker, D_HT, 2) if ally.CanChatMessage then dmgAttacker:PrintMessage(HUD_PRINTTALK, ally:GetName() .. " no longer likes you.") end ally:PlaySoundSystem("BecomeEnemyToPlayer") end ally.Alerted = true end end end end end -- Blood decal on the ground if self.Bleeds && self.HasBloodDecal then local bloodDecal = PICK(self.BloodDecal) if bloodDecal then local decalPos = myPos + vecZ4 self:SetLocalPos(decalPos) -- NPC is too close to the ground, we need to move it up a bit local tr = util.TraceLine({start = decalPos, endpos = decalPos - vecZ500, filter = self}) util.Decal(bloodDecal, tr.HitPos + tr.HitNormal, tr.HitPos - tr.HitNormal) end end self:RemoveTimers() self:StopAllSounds() self.AttackType = VJ.ATTACK_TYPE_NONE self.HasMeleeAttack = false self.HasRangeAttack = false self.HasLeapAttack = false if IsValid(dmgAttacker) then if dmgAttacker:GetClass() == "npc_barnacle" then self.HasDeathCorpse = false end -- Don't make a corpse if it's killed by a barnacle! if vj_npc_ply_frag:GetInt() == 1 && dmgAttacker:IsPlayer() then dmgAttacker:AddFrags(1) end if IsValid(dmgInflictor) then gamemode.Call("OnNPCKilled", self, dmgAttacker, dmgInflictor, dmginfo) end end self:SetCollisionGroup(COLLISION_GROUP_DEBRIS) self:GibOnDeath(dmginfo, hitgroup) self:PlaySoundSystem("Death") //if (self.MovementType == VJ_MOVETYPE_AERIAL or self.MovementType == VJ_MOVETYPE_AQUATIC) then self:AA_StopMoving() end -- I/O events, from: https://github.com/ValveSoftware/source-sdk-2013/blob/0d8dceea4310fde5706b3ce1c70609d72a38efdf/mp/src/game/server/basecombatcharacter.cpp#L1582 if IsValid(dmgAttacker) then -- Someone else killed me self:TriggerOutput("OnDeath", dmgAttacker) dmgAttacker:Fire("KilledNPC", "", 0, self, self) -- Allows player companions (npc_citizen) to respond to kill else self:TriggerOutput("OnDeath", self) end -- Handle death animation, death delay, and the final death phase local deathTime = self.DeathDelayTime if IsValid(dmgInflictor) && dmgInflictor:GetClass() == "prop_combine_ball" then self.HasDeathAnimation = false end if self.HasDeathAnimation && VJ_CVAR_AI_ENABLED && !dmginfo:IsDamageType(DMG_REMOVENORAGDOLL) && !dmginfo:IsDamageType(DMG_DISSOLVE) && self:GetNavType() != NAV_CLIMB && math.random(1, self.DeathAnimationChance) == 1 then self:RemoveAllGestures() self:OnDeath(dmginfo, hitgroup, "DeathAnim") local chosenAnim = PICK(self.AnimTbl_Death) local animTime = VJ.AnimDurationEx(self, chosenAnim, self.DeathAnimationTime) - self.DeathAnimationDecreaseLengthAmount self:PlayAnim(chosenAnim, true, animTime, false, 0, {PlayBackRateCalculated = true}) deathTime = deathTime + animTime self.DeathAnimationCodeRan = true else -- If no death anim then just set the NPC to dead even if it has a delayed remove self:SetSaveValue("m_lifeState", 2) -- LIFE_DEAD end if deathTime > 0 then timer.Simple(deathTime, function() if IsValid(self) then self:FinishDeath(dmginfo, hitgroup) end end) else self:FinishDeath(dmginfo, hitgroup) end end --------------------------------------------------------------------------------------------------------------------------------------------- function ENT:FinishDeath(dmginfo, hitgroup) if self.VJ_DEBUG && GetConVar("vj_npc_debug_damage"):GetInt() == 1 then VJ.DEBUG_Print(self, "FinishDeath", "Attacker = ", self.SavedDmgInfo.attacker, " | Inflictor = ", self.SavedDmgInfo.inflictor) end self:SetSaveValue("m_lifeState", 2) -- LIFE_DEAD //self:SetNPCState(NPC_STATE_DEAD) self:OnDeath(dmginfo, hitgroup, "Finish") if self.DropDeathLoot then self:CreateDeathLoot(dmginfo, hitgroup) end if bit.band(self.SavedDmgInfo.type, DMG_REMOVENORAGDOLL) == 0 then self:CreateDeathCorpse(dmginfo, hitgroup) end self:Remove() end --------------------------------------------------------------------------------------------------------------------------------------------- local colorGrey = Color(90, 90, 90) -- function ENT:CreateDeathCorpse(dmginfo, hitgroup) -- In case it was not set -- NOTE: dmginfo at this point can be incorrect/corrupted, but its better than leaving the self.SavedDmgInfo empty! if !self.SavedDmgInfo then self.SavedDmgInfo = { dmginfo = dmginfo, -- The actual CTakeDamageInfo object | WARNING: Can be corrupted after a tick, recommended not to use this! attacker = dmginfo:GetAttacker(), inflictor = dmginfo:GetInflictor(), amount = dmginfo:GetDamage(), pos = dmginfo:GetDamagePosition(), type = dmginfo:GetDamageType(), force = dmginfo:GetDamageForce(), ammoType = dmginfo:GetAmmoType(), hitgroup = hitgroup, } end if self.HasDeathCorpse && self.HasDeathRagdoll != false then local corpseMdl = self:GetModel() local corpseMdlCustom = PICK(self.DeathCorpseModel) if corpseMdlCustom then corpseMdl = corpseMdlCustom end local corpseClass = "prop_physics" if self.DeathCorpseEntityClass then corpseClass = self.DeathCorpseEntityClass else if util.IsValidRagdoll(corpseMdl) then corpseClass = "prop_ragdoll" elseif !util.IsValidProp(corpseMdl) or !util.IsValidModel(corpseMdl) then return false end end self.Corpse = ents.Create(corpseClass) local corpse = self.Corpse corpse:SetModel(corpseMdl) corpse:SetPos(self:GetPos()) corpse:SetAngles(self:GetAngles()) corpse:Spawn() corpse:Activate() corpse:SetSkin(self:GetSkin()) for i = 0, self:GetNumBodyGroups() do corpse:SetBodygroup(i, self:GetBodygroup(i)) end corpse:SetColor(self:GetColor()) corpse:SetMaterial(self:GetMaterial()) if corpseMdlCustom == false && self.DeathCorpseSubMaterials != nil then -- Take care of sub materials for _, x in ipairs(self.DeathCorpseSubMaterials) do if self:GetSubMaterial(x) != "" then corpse:SetSubMaterial(x, self:GetSubMaterial(x)) end end -- This causes lag, not a very good way to do it. /*for x = 0, #self:GetMaterials() do if self:GetSubMaterial(x) != "" then corpse:SetSubMaterial(x, self:GetSubMaterial(x)) end end*/ end //corpse:SetName("corpse" .. self:EntIndex()) //corpse:SetModelScale(self:GetModelScale()) corpse.FadeCorpseType = (corpse:GetClass() == "prop_ragdoll" and "FadeAndRemove") or "kill" corpse.IsVJBaseCorpse = true corpse.DamageInfo = dmginfo corpse.ChildEnts = self.DeathCorpse_ChildEnts or {} corpse.BloodData = {Color = self.BloodColor, Particle = self.BloodParticle, Decal = self.BloodDecal} if self.Bleeds && self.HasBloodPool && vj_npc_blood_pool:GetInt() == 1 then self:SpawnBloodPool(dmginfo, hitgroup, corpse) end -- Collision corpse:SetCollisionGroup(self.DeathCorpseCollisionType) if ai_serverragdolls:GetInt() == 1 then undo.ReplaceEntity(self, corpse) else -- Keep corpses is not enabled... VJ.Corpse_Add(corpse) if vj_npc_corpse_undo:GetInt() == 1 then undo.ReplaceEntity(self, corpse) end -- Undoable end cleanup.ReplaceEntity(self, corpse) -- Delete on cleanup -- On fire if self:IsOnFire() then corpse:Ignite(math.Rand(8, 10), 0) if !self.Immune_Fire then -- Don't darken the corpse if we are immune to fire! corpse:SetColor(colorGrey) //corpse:SetMaterial("models/props_foliage/tree_deciduous_01a_trunk") end end -- Dissolve if (bit.band(self.SavedDmgInfo.type, DMG_DISSOLVE) != 0) or (IsValid(self.SavedDmgInfo.inflictor) && self.SavedDmgInfo.inflictor:GetClass() == "prop_combine_ball") then corpse:Dissolve(0, 1) end -- Bone and Angle -- If it's a bullet, it will use localized velocity on each bone depending on how far away the bone is from the dmg position local useLocalVel = (bit.band(self.SavedDmgInfo.type, DMG_BULLET) != 0 and self.SavedDmgInfo.pos != defPos) or false local dmgForce = (self.SavedDmgInfo.force / 40) + self:GetMoveVelocity() + self:GetVelocity() if self.DeathAnimationCodeRan then useLocalVel = false dmgForce = self:GetGroundSpeedVelocity() end local totalSurface = 0 local physCount = corpse:GetPhysicsObjectCount() for childNum = 0, physCount - 1 do -- 128 = Bone Limit local childPhysObj = corpse:GetPhysicsObjectNum(childNum) if IsValid(childPhysObj) then totalSurface = totalSurface + childPhysObj:GetSurfaceArea() local childPhysObj_BonePos, childPhysObj_BoneAng = self:GetBonePosition(corpse:TranslatePhysBoneToBone(childNum)) if childPhysObj_BonePos then if self.DeathCorpseSetBoneAngles then childPhysObj:SetAngles(childPhysObj_BoneAng) end childPhysObj:SetPos(childPhysObj_BonePos) if self.DeathCorpseApplyForce then childPhysObj:SetVelocity(dmgForce / math_max(1, (useLocalVel and childPhysObj_BonePos:Distance(self.SavedDmgInfo.pos) / 12) or 1)) end -- If it's 1, then it's likely a regular physics model with no bones elseif physCount == 1 then if self.DeathCorpseApplyForce then childPhysObj:SetVelocity(dmgForce / math_max(1, (useLocalVel and corpse:GetPos():Distance(self.SavedDmgInfo.pos) / 12) or 1)) end end end end -- Health & stink system if corpse:Health() <= 0 then local hpCalc = totalSurface / 60 corpse:SetMaxHealth(hpCalc) corpse:SetHealth(hpCalc) end VJ.Corpse_AddStinky(corpse, true) if self.DeathCorpseFade then corpse:Fire(corpse.FadeCorpseType, "", self.DeathCorpseFade) end if vj_npc_corpse_fade:GetInt() == 1 then corpse:Fire(corpse.FadeCorpseType, "", vj_npc_corpse_fadetime:GetInt()) end self:OnCreateDeathCorpse(dmginfo, hitgroup, corpse) if corpse:IsFlagSet(FL_DISSOLVING) && corpse.ChildEnts then for _, child in ipairs(corpse.ChildEnts) do child:Dissolve(0, 1) end end corpse:CallOnRemove("vj_" .. corpse:EntIndex(), function(ent, childPieces) for _, child in ipairs(childPieces) do if IsValid(child) then if child:GetClass() == "prop_ragdoll" then -- Make ragdolls fade child:Fire("FadeAndRemove", "", 0) else child:Fire("kill", "", 0) end end end end, corpse.ChildEnts) hook.Call("CreateEntityRagdoll", nil, self, corpse) return corpse else -- Remove child entities | No fade effects as it will look weird, remove it instantly! if self.DeathCorpse_ChildEnts then for _, child in ipairs(self.DeathCorpse_ChildEnts) do child:Remove() end end end end