/* * Author: Needles * https://steamcommunity.com/profiles/76561198026257137/ */ printl("*** HANDLER/CORE_BOSS"); ::CoreBoss <- {}; ::CoreBoss.MODEL_CORE_PATH <- "models/props_2fort/chimney006.mdl"; ::CoreBoss.MODEL_EMERGENCY_LIGHT_PATH <- "models/props_gameplay/mvm_gemergency_light.mdl"; ::CoreBoss.MODEL_GUN_PATH <- "models/weapons/c_models/c_bet_rocketlauncher/c_bet_rocketlauncher.mdl"; ::CoreBoss.MODEL_PROJECTILE_PATH <- "models/weapons/w_models/w_rocket_airstrike/w_rocket_airstrike.mdl"; ::CoreBoss.SPRITE_TARGET_PATH <- "needles/sprites/target2.vmt"; ::CoreBoss.SPRITE_TARGET_LOST_PATH <- "needles/sprites/target1.vmt"; ::CoreBoss.SPRITE_TARGET_RADIAL_PATH <- "needles/sprites/target_radial.vmt"; ::CoreBoss.SOUND_SHOOT_PATH <- "weapons/sentry_rocket.wav"; ::CoreBoss.SOUND_SPOT_PATH <- "weapons/sentry_spot.wav"; PrecacheModel(::CoreBoss.MODEL_CORE_PATH); PrecacheModel(::CoreBoss.MODEL_EMERGENCY_LIGHT_PATH); PrecacheModel(::CoreBoss.MODEL_GUN_PATH); PrecacheModel(::CoreBoss.MODEL_PROJECTILE_PATH); PrecacheModel(::CoreBoss.SPRITE_TARGET_PATH); PrecacheModel(::CoreBoss.SPRITE_TARGET_LOST_PATH); PrecacheModel(::CoreBoss.SPRITE_TARGET_RADIAL_PATH); PrecacheSound(::CoreBoss.SOUND_SHOOT_PATH); PrecacheSound(::CoreBoss.SOUND_SPOT_PATH); ::CoreBoss.SOUND_THRESHOLD_PATH_LIST <- [ [ "vo/announcer_dec_kill01.mp3", // Yes, good. "vo/announcer_dec_kill06.mp3", // Yes, that's it! "vo/announcer_dec_kill13.mp3", // Yes! good! ], [ "vo/announcer_dec_kill03.mp3", // Yes, yes... More! "vo/announcer_dec_kill05.mp3", // Yes, do it! Good! ], [ "vo/announcer_dec_kill07.mp3", // Kill them! Slaughter them like a dog! "vo/announcer_dec_kill09.mp3", // Hit them again! And again, and agaaaaaaaain! ], ]; foreach (pathList in ::CoreBoss.SOUND_THRESHOLD_PATH_LIST) { foreach (path in pathList) { PrecacheSound(path); } } ::CoreBoss.COLOR_ACTIVATED <- ::Color.Color(240, 200, 0, 255); ::CoreBoss.COLOR_DEACTIVATED <- ::Color.Color(150, 150, 150, 255); ::CoreBoss.STUN_TIME <- 5.0; ::CoreBoss.THRESHOLD <- 0.25; ::CoreBoss.ANNOTATION_LIFETIME <- 15.0; local CoreBossHandler = class extends ::Handler.BaseHandler { self = null; ent_turnTable = null; ent_gun = null; ent_light = null; ent_annotation = null; gui_sprite = null; handler_sapTarget = null; syncedPlayer = null; isDead = null; reloadDelay = null; reloadTime = null; buildingsDamageBonus = null; rotationSpeed = null; target = null; lastTarget = null; maxRaySprites = null; maxRayDistance = null; raySpriteList = null; weaponResistanceTable = null; doIgnoreDisguisedSpies = null; doIgnoreCloakedSpies = null; isSapped = null; isStunned = null; stunTime = null; isDeactivated = null; thresholdIndex = null; event_onDestroy = null; constructor() { base.constructor(); ent_turnTable = null; ent_gun = null; ent_light = null; ent_annotation = null; gui_sprite = null; handler_sapTarget = null; isDead = false; syncedPlayer = null; reloadDelay = 2.0; reloadTime = Time(); rotationSpeed = 60.0; target = null; lastTarget = null; doIgnoreDisguisedSpies = true; doIgnoreCloakedSpies = true; maxRaySprites = 8; maxRayDistance = 1024.0; raySpriteList = []; buildingsDamageBonus = 1.5; // Pesky sentries! Away! weaponResistanceTable = { "tf_weapon_minigun" : 0.25, // Same as tanks }; isSapped = false; isStunned = false; stunTime = -1.0; isDeactivated = false; thresholdIndex = 0; event_onDestroy = ::Events.Event(); } function OnAdd() { SpawnRaySprites(); local _this = this; handler_sapTarget.eventSapperPlaced.AddListener( function(params) { if (_this.isStunned == false) _this.Deactivate(); _this.isSapped = true; }); handler_sapTarget.eventSapperRemoved.AddListener( function(params) { if (_this.isStunned == false) _this.Reactivate(); _this.isSapped = false; }); } function OnEvent_Tick(params) { if (isDead == true) { return; } // Stun reactivation if (isStunned == true && Time() > stunTime + ::CoreBoss.STUN_TIME) { if (isSapped == false) Reactivate(); isStunned = false; } if (isDeactivated == true) { return; } // Search for potential targets target = null; local potentialTargetList = []; for (local i = 1; i <= ::Players.Max(); i++) { local player = PlayerInstanceFromIndex(i); if (player == null) continue; if (!::Players.IsPlayerValid(player) || !::Players.IsPlayerAlive(player) || player.GetTeam() == self.GetTeam()) { continue; } if (doIgnoreDisguisedSpies == true && player.GetDisguiseTeam() == self.GetTeam()) { continue; } if (doIgnoreCloakedSpies == true && player.InCond(Constants.ETFCond.TF_COND_STEALTHED) && !player.InCond(Constants.ETFCond.TF_COND_STEALTHED_BLINK)) { continue; } potentialTargetList.append(player); } local ent = null; while (ent = Entities.FindByClassname(ent, "obj_sentrygun")) { if (ent.IsValid() && ent.GetTeam() != self.GetTeam()) { potentialTargetList.append(ent); } } local ent = null; while (ent = Entities.FindByClassname(ent, "obj_dispenser")) { if (ent.IsValid() && ent.GetTeam() != self.GetTeam()) { potentialTargetList.append(ent); } } // Focus on the closest target! local closestDistance = -1; foreach(potentialTarget in potentialTargetList) { // Can we actually see this potential target? local trace = { start = ent_gun.GetOrigin(), end = potentialTarget.GetOrigin() + Vector(0.0, 0.0, 32.0), mask = Constants.FContents.CONTENTS_SOLID | Constants.FContents.CONTENTS_GRATE | Constants.FContents.CONTENTS_WINDOW, ignore = null, }; TraceLineEx(trace); if (trace.hit == true) { continue; } // I only care about whoever is closest to me! // @TODO - Create logic that makes the core target whoever has recently dealt the most damage to it. local distance = (potentialTarget.GetOrigin() - self.GetOrigin()).Length(); if (closestDistance == -1 || distance <= closestDistance) { closestDistance = distance; target = potentialTarget; } } // BEEP BEEP BEEP if (target != null && lastTarget != target) { lastTarget = target; ::Utils.TempAmbientGeneric(::CoreBoss.SOUND_SPOT_PATH, ent_gun.GetOrigin(), 8192, 10, 100, 2.0); } // Rotate to face target if (target && target.IsValid()) { local lookatNormal = ((target.GetOrigin() + Vector(0.0, 0.0, 32.0)) - ent_gun.GetOrigin()); lookatNormal.Norm(); local lookatAngles = ::MathN.NormalToAngles(lookatNormal); ent_turnTable.SetAbsAngles(QAngle(0.0, MathN.ApproachAngle(ent_turnTable.GetAbsAngles().y, lookatAngles.y, rotationSpeed * params.time), 0.0)); ent_gun.SetLocalAngles(QAngle(MathN.ApproachAngle(ent_gun.GetAbsAngles().x, lookatAngles.x, rotationSpeed * params.time), 0.0, 0.0)); } // Shoot if (target && Time() >= reloadTime + reloadDelay) { Shoot(); reloadTime = Time(); } // Update the sprite ray local rayVec = ent_gun.GetAbsAngles().Forward() * maxRayDistance; local hit = TraceLine(ent_gun.GetOrigin(), ent_gun.GetOrigin() + rayVec, self); for(local i = 0; i < maxRaySprites; i++) { local guiSprite = raySpriteList[i]; local t = i.tofloat() / maxRaySprites.tofloat(); guiSprite.self.SetAbsOrigin(ent_gun.GetOrigin() + rayVec * t); guiSprite.self.SetAbsAngles(ent_turnTable.GetAbsAngles()); if (t < hit) { guiSprite.Show(); } else { guiSprite.Hide(); } } } function OnEvent_DamagePre(params) { if (params.attacker == self) { // Extra damage to buildings if (params.const_entity.GetClassname() == "obj_sentrygun" || params.const_entity.GetClassname() == "obj_dispenser" || params.const_entity.GetClassname() == "obj_teleporter") params.damage *= buildingsDamageBonus; // Reduced damage to battalion's backup if (::Players.IsPlayerValid(params.const_entity) == true && params.const_entity.InCond(Constants.ETFCond.TF_COND_DEFENSEBUFF)) params.damage *= ::Const.WEAPON_BATTALIONS_BACKUP_SENTRY_FACTOR; } if (params.const_entity != self) return; if (isDead == true) { params.damage = 0; return; } // Minigun resistance if (params.weapon && params.weapon.IsValid() && params.weapon.GetClassname() in weaponResistanceTable) { params.damage *= weaponResistanceTable[params.weapon.GetClassname()]; } if (params.idef == ITEM_DEFINITION_INDEX.COW_MANGLER && NetProps.GetPropBool(params.inflictor, "m_bChargedShot") == true) { if (isSapped == false) Deactivate(); isStunned = true; stunTime = Time(); } // Crit particles appear where the damage was dealt local attachment = ::Attachment.Validate(self); if (attachment != null) { attachment.AddPoint("particle_crit", params.damage_position - self.GetOrigin(), QAngle(0.0, 0.0, 0.0)); } } function OnEvent_DamagePost(params) { if (params.const_entity != self) { return; } // No health left if (self.GetHealth() - params.damage <= 0) { self.SetHealth(self.GetMaxHealth() + params.damage); isDead = true; Destroy(); } // Damage synced player if (::Players.IsPlayerValid(syncedPlayer) && isDead == false) { syncedPlayer.SetHealth(self.GetHealth()); } // Pavlovian conditioning if (self.GetHealth() < self.GetMaxHealth() - self.GetMaxHealth() * ::CoreBoss.THRESHOLD * (thresholdIndex+1)) { ShowThreshold(); thresholdIndex++; } } // @TODO - Move this out of the CoreBoss class and create a general purpose tutorial highlight system (or do it in the mission file) function ShowAnnotation() { if (ent_annotation != null) { return; } ent_annotation = SpawnEntityFromTable("training_annotation", { origin = self.GetOrigin() + Vector(0.0, 0.0, 192.0), display_text = "Destroy the core!", lifetime = ::CoreBoss.ANNOTATION_LIFETIME, }); EntFireByHandle(ent_annotation, "Show", "", 0.1, null, null); EntFireByHandle(ent_annotation, "SetParent", "!activator", -1.0, self, null); local _this = this; ::Timers.AddTimer(::CoreBoss.ANNOTATION_LIFETIME, function(params) { if (_this.self == null || _this.self.IsValid() == null || _this.ent_annotation == null || _this.ent_annotation.IsValid() == false) { return; } _this.ent_annotation.Destroy(); }, {}, 1); } function Destroy() { event_onDestroy.FireListeners({ coreBoss = this }); if (::Players.IsPlayerValid(syncedPlayer)) { // Kill syncedPlayer.SetAbsOrigin(::Map.forceMoneyCollectPoint); syncedPlayer.SetHealth(0); syncedPlayer.TakeDamage(syncedPlayer.GetMaxHealth(), 0, null); // For whatever reason, both sethealth and takedamage need to be used at the same time } // @TODO - Kill this entity properly! // Or, make it a persistent entity and reuse it instead of spawning another one. ::Highlight.RemoveHighlight(self); NetProps.SetPropInt(self, "m_lifeState", LIFE_STATE.DYING); self.SetAbsOrigin(::Const.INVALID_VECTOR); } function Shoot() { EntFireByHandle(ent_gun, "FireOnce", "", -1.0, null, null); ::Utils.TempAmbientGeneric(::CoreBoss.SOUND_SHOOT_PATH, ent_gun.GetOrigin(), 8192, 10, 100, 2.0); // DISGUSTING! local _this = this; ::Utils.DoOnEndOfFrame( @() _this.InitProjectile()); } function InitProjectile() { local projectile = ::Weapons.FindLatestProjectile(ent_gun, "tf_projectile_rocket"); if (projectile != null) { // Stop rockets from exploding on self NetProps.SetPropEntity(projectile, "m_hOwnerEntity", self); } } function ShowThreshold() { if (thresholdIndex > ::CoreBoss.SOUND_THRESHOLD_PATH_LIST.len() - 1) { return; } local pathList = ::CoreBoss.SOUND_THRESHOLD_PATH_LIST[thresholdIndex]; local path = pathList[RandomInt(0, pathList.len()-1)]; EmitSoundEx({ sound_name = path, channel = CHANNEL.CHAN_VOICE2, volume = 1.0, }); } function SetSyncedPlayer(player) { syncedPlayer = player; player.SetMaxHealth(self.GetMaxHealth()); player.SetHealth(self.GetHealth()); } function SpawnRaySprites() { // Destroy any previous ray sprites foreach(guiSprite in raySpriteList) { guiSprite.self.Destroy(); } raySpriteList = []; for (local i = 0; i < maxRaySprites; i++) { local ent_sprite = SpawnEntityFromTable("env_glow", { scale = 0.2, origin = self.GetOrigin(), angles = QAngle(0.0, 0.0, 0.0), model = ::CoreBoss.SPRITE_TARGET_PATH, framerate = 0, frame = 0, rendermode = 1, spawnflags = 1, }); local guiSprite = ::Gui.GuiSprite(ent_sprite); guiSprite.SetColor(::CoreBoss.COLOR_ACTIVATED); raySpriteList.append(guiSprite); } } function DestroyRaySprites() { foreach(guiSprite in raySpriteList) { guiSprite.self.Destroy(); } } function SetRaySprite(path, color) { gui_sprite.SetColor(color) foreach(guiSprite in raySpriteList) { guiSprite.SetSprite(path); guiSprite.SetColor(color); } } function Deactivate() { if (isDeactivated == true) return; isDeactivated = true; SetRaySprite(::CoreBoss.SPRITE_TARGET_LOST_PATH, ::CoreBoss.COLOR_DEACTIVATED); ent_light.SetSkin(1); ent_light.ResetSequence(ent_light.LookupSequence("idle")); } function Reactivate() { if (isDeactivated == false) return; isDeactivated = false; SetRaySprite(::CoreBoss.SPRITE_TARGET_PATH, ::CoreBoss.COLOR_ACTIVATED); foreach(guiSprite in raySpriteList) { guiSprite.Show(); } ent_light.SetSkin(2); ent_light.ResetSequence(ent_light.LookupSequence("spin")); } } ::Handler.RegisterHandler("core_boss", @() CoreBossHandler()); function CoreBoss::BuildCoreBoss(origin, angles, team, health) { local ent_boss = SpawnEntityFromTable("base_boss", { targetname = UniqueString(), // @TODO - Temporary fix for highlighting behaviour. Remove in future when a better system exists. origin = origin, angles = angles, health = health, teamnum = team, model = ::CoreBoss.MODEL_CORE_PATH, }); ent_boss.SetModel(::CoreBoss.MODEL_CORE_PATH); ent_boss.SetSolid(Constants.ESolidType.SOLID_BBOX); local ent_buffer = SpawnEntityFromTable("prop_dynamic", { origin = origin + Vector(0.0, 0.0, 0.0), angles = angles, model = "models/empty.mdl", }); local ent_light = SpawnEntityFromTable("prop_dynamic", { origin = origin + Vector(0.0, 0.0, ent_boss.GetBoundingMaxs().z), angles = angles, model = MODEL_EMERGENCY_LIGHT_PATH, skin = 2, modelscale = 1.25, disableshadows = true, }); ent_light.ResetSequence(ent_light.LookupSequence("spin")); local ent_turnTable = SpawnEntityFromTable("prop_dynamic", { origin = origin, angles = angles, model = "models/empty.mdl", }); local ent_propGun = SpawnEntityFromTable("prop_dynamic", { origin = origin + Vector(28.0, 0.0, 80.0), angles = angles, model = ::CoreBoss.MODEL_GUN_PATH, modelscale = 1.25, disableshadows = true, }); local ent_gun = SpawnEntityFromTable("tf_point_weapon_mimic", { origin = origin + Vector(80.0, 0.0, 88.0), angles = angles, WeaponType = 0, ModelOverride = ::CoreBoss.MODEL_PROJECTILE_PATH, SpeedMin = ::Const.ROCKET_PROJECTILE_SPEED, SpeedMax = ::Const.ROCKET_PROJECTILE_SPEED, Damage = ::Const.ROCKET_BASE_DAMAGE, SpreadAngle = 5.0, }); ent_gun.SetOwner(ent_boss); local ent_sprite = SpawnEntityFromTable("env_glow", { scale = 0.5, origin = origin + Vector(80.0, 0.0, 88.0), angles = angles, model = ::CoreBoss.SPRITE_TARGET_RADIAL_PATH, framerate = 0, frame = 0, rendermode = 1, spawnflags = 1, }); ::Gui.GuiSprite(ent_sprite).SetColor(::CoreBoss.COLOR_ACTIVATED); local ent_sapTarget = ::Utils.BuildEmptyDispenser(origin + Vector(-32.0, 0.0, 64.0), angles + QAngle(-90.0, 0.0, 0.0), team); ent_sapTarget.DisableDraw(); local handler_sapTarget = ::Handler.AddHandler(ent_sapTarget, "sap_target", null, -1.0, {removeDelay = ::CoreBoss.STUN_TIME}); EntFireByHandle(ent_buffer, "SetParent", "!activator", -1.0, ent_boss, null); EntFireByHandle(ent_turnTable, "SetParent", "!activator", -1.0, ent_boss, null); EntFireByHandle(ent_propGun, "SetParent", "!activator", -1.0, ent_turnTable, null); EntFireByHandle(ent_gun, "SetParent", "!activator", -1.0, ent_turnTable, null); EntFireByHandle(ent_sprite, "SetParent", "!activator", -1.0, ent_turnTable, null); EntFireByHandle(ent_sapTarget, "SetParent", "!activator", -1.0, ent_turnTable, null); EntFireByHandle(ent_light, "SetParent", "!activator", -1.0, ent_buffer, null); ::Particle.Validate(ent_boss); local attachment = ::Attachment.Validate(ent_boss); attachment.AddPoint("particle_top", Vector(0.0, 0.0, 192.0), QAngle(0.0, 0.0, 0.0)); attachment.AddPoint("particle_crit", Vector(0.0, 0.0, 192.0), QAngle(0.0, 0.0, 0.0)); ::Handler.AddHandler(ent_boss, "damage_start", null, -1.0); ::Handler.AddHandler(ent_boss, "damage_end", null, -1.0); ::Handler.AddHandler(ent_boss, "weapon_interaction", null, -1.0); ::Handler.AddHandler(ent_boss, "fake_headshot", null, -1.0, {transformOverride = ent_turnTable, headshotVolume = {origin = Vector(52.0, 0.0, 88.0), radius = 18.0}, blockingVolume = {origin = Vector(0.0, 0.0, 0.0), radius = 48.0, height = 128.0}}); local handler_coreBoss = ::Handler.AddHandler(ent_boss, "core_boss", null, -1.0, {ent_turnTable = ent_turnTable, ent_gun = ent_gun, ent_light = ent_light, gui_sprite = ::Gui.GuiSprite(ent_sprite), handler_sapTarget = handler_sapTarget}); return handler_coreBoss; }