#include "../tempents" #include "monsters" #include "../weapons/projectile" #include "../items" const string Q1_BOSS_MODEL = "models/quake1/m_boss.mdl"; const string Q1_BOSS_RISE = "quake1/monsters/boss/rise.wav"; const string Q1_BOSS_SIGHT = "quake1/monsters/boss/sight.wav"; const string Q1_BOSS_PAIN = "quake1/monsters/boss/pain.wav"; const string Q1_BOSS_DEATH = "quake1/monsters/boss/death.wav"; const string Q1_BOSS_SHOOT = "quake1/monsters/boss/shoot.wav"; enum Q1_BOSS_EVENTS { BOSS_OUT_SOUND = 1, BOSS_SIGHT_SOUND, BOSS_LAUNCH_LEFT_BALL, BOSS_LAUNCH_RIGHT_BALL, BOSS_DEATH_SOUND, BOSS_DEATH_SPLASH } class monster_qboss : ScriptBaseMonsterEntity, monster_qgeneric { void Spawn() { Precache(); SetUse(UseFunction(MonsterAwake)); SetThink(ThinkFunction(MonsterThink)); } void Precache() { g_Game.PrecacheModel(Q1_BOSS_MODEL); g_Game.PrecacheModel("models/quake1/lavaball.mdl"); g_SoundSystem.PrecacheSound(Q1_BOSS_RISE); g_SoundSystem.PrecacheSound(Q1_BOSS_SIGHT); g_SoundSystem.PrecacheSound(Q1_BOSS_DEATH); g_SoundSystem.PrecacheSound(Q1_BOSS_SHOOT); g_SoundSystem.PrecacheSound(Q1_BOSS_PAIN); } bool MonsterHasMeleeAttack() { return true; } void MonsterAwake(CBaseEntity@ pActivator, CBaseEntity@ pCaller, USE_TYPE useType, float flValue) { g_EntityFuncs.SetModel(self, Q1_BOSS_MODEL); self.m_bloodColor = BLOOD_COLOR_RED; self.pev.health = 3; self.pev.yaw_speed = 20; self.pev.solid = SOLID_BBOX; self.pev.movetype = MOVETYPE_STEP; g_EntityFuncs.SetSize(self.pev, Vector(-128, -128, -24), Vector(128, 128, 256)); SetUse(null); self.StartMonster(); self.m_MonsterState = MONSTERSTATE_NONE; self.m_FormattedName = "Chthon"; m_hEnemy = pActivator; m_iAIState = STATE_IDLE; SetActivity(ACT_USE); MonsterThink(); q1_TE_LavaSplash(self.pev.origin); q1_TE_LavaSplash(self.pev.origin); } int TakeDamage(entvars_t@ pevInflictor, entvars_t@ pevAttacker, float flDamage, int bitsDamageType) { if (bitsDamageType != DMG_ENERGYBEAM) return 0; // ignore all damage except env_laser g_SoundSystem.EmitSound(self.edict(), CHAN_VOICE, Q1_BOSS_PAIN, 1.0, ATTN_NORM); self.pev.health -= 1; if (self.pev.health > 0) { SetActivity(ACT_SMALL_FLINCH); m_iAIState = STATE_WALK; m_iAttackState = ATTACK_NONE; } else { SetActivity(ACT_BIG_FLINCH); m_iAIState = STATE_WALK; m_iAttackState = ATTACK_NONE; } return 1; } void AI_Idle() { if (self.m_fSequenceFinished) AI_Run_Missile(); } void AI_Face() { CBaseEntity@ pEnemy = m_hEnemy; if (m_hEnemy && pEnemy.pev.health <= 0.0 || Math.RandomFloat(0, 1) < 0.02) { @pEnemy = @g_EntityFuncs.FindEntityByClassname(pEnemy, "player"); if (pEnemy is null) @pEnemy = @g_EntityFuncs.FindEntityByClassname(pEnemy, "player"); if (pEnemy !is null) m_hEnemy = pEnemy; } if (m_hEnemy) self.pev.ideal_yaw = Math.VecToYaw(pEnemy.pev.origin - self.pev.origin); g_EngineFuncs.ChangeYaw(self.edict()); } void AI_Walk(float flDist) { if (m_iAttackState == ATTACK_MISSILE) { AI_Run_Missile(); return; } CBaseEntity@ pEnemy = m_hEnemy; if (self.m_fSequenceFinished) { // just a switch between walk and attack if (!m_hEnemy || pEnemy.pev.health <= 0) { m_iAttackState = ATTACK_NONE; m_iAIState = STATE_WALK; SetActivity(ACT_WALK); // play walk animation } else if (m_Activity == ACT_MELEE_ATTACK1 || m_Activity == ACT_SMALL_FLINCH) { m_iAIState = STATE_WALK; m_iAttackState = ATTACK_MISSILE; } else if (m_Activity == ACT_BIG_FLINCH) { m_iAIState = STATE_WALK; m_iAttackState = ATTACK_NONE; SetActivity(ACT_DIEVIOLENT); } else if (m_Activity == ACT_DIEVIOLENT) { m_iAIState = STATE_DEAD; if (m_hEnemy) self.SUB_UseTargets(pEnemy, USE_TOGGLE, 0); g_EntityFuncs.Remove(self); return; } else { // this prevents Chthon from stalling after his target dies AI_Run_Missile(); } } AI_Face(); } void AI_Run_Missile() { m_iAIState = STATE_WALK; m_iAttackState = ATTACK_NONE; // wait for sequence end SetActivity(ACT_MELEE_ATTACK1); } void LaunchMissile(Vector p, int iAttachment) { if (!m_hEnemy) return; CBaseEntity@ pEnemy = m_hEnemy; Vector vecAngles = Math.VecToAngles(pEnemy.pev.origin - self.pev.origin); g_EngineFuncs.MakeVectors(vecAngles); Vector vecSrc = self.pev.origin + p.x * g_Engine.v_forward + p.y * g_Engine.v_right + p.z * Vector(0, 0, 1); Vector vecEnd, vecDir; // lead the player float t = (pEnemy.pev.origin - vecSrc).Length() / 300.0; Vector vec = pEnemy.pev.velocity; vec.z = 0; vecEnd = pEnemy.pev.origin + t * vec; vecDir = (vecEnd - vecSrc).Normalize(); g_SoundSystem.EmitSound(self.edict(), CHAN_WEAPON, Q1_BOSS_SHOOT, 1.0, ATTN_NORM); auto @pGrenade = q1_ShootCustomProjectile("projectile_qrocket", "models/quake1/lavaball.mdl", vecSrc, vecDir * 300, vecAngles, self); pGrenade.pev.dmg = 100; pGrenade.pev.avelocity = Vector(200, 100, 300); g_EntityFuncs.SetModel(pGrenade, "models/quake1/lavaball.mdl"); // TODO: remove the default SetModel on qrocket } void HandleAnimEvent(MonsterEvent@ pEvent) { switch (pEvent.event) { case BOSS_OUT_SOUND: g_SoundSystem.EmitSound(self.edict(), CHAN_BODY, Q1_BOSS_RISE, 1.0, ATTN_NORM); break; case BOSS_SIGHT_SOUND: g_SoundSystem.EmitSound(self.edict(), CHAN_VOICE, Q1_BOSS_SIGHT, 1.0, ATTN_NORM); break; case BOSS_LAUNCH_RIGHT_BALL: LaunchMissile(Vector(100, -100, 200), 1); break; case BOSS_LAUNCH_LEFT_BALL: LaunchMissile(Vector(100, 100, 200), 2); break; case BOSS_DEATH_SOUND: g_SoundSystem.EmitSound(self.edict(), CHAN_VOICE, Q1_BOSS_DEATH, 1.0, ATTN_NORM); break; case BOSS_DEATH_SPLASH: g_SoundSystem.EmitSound(self.edict(), CHAN_BODY, Q1_BOSS_RISE, 1.0, ATTN_NORM); q1_TE_LavaSplash(self.pev.origin); q1_TE_LavaSplash(self.pev.origin); break; default: BaseClass.HandleAnimEvent(pEvent); } } } void q1_RegisterMonster_BOSS() { g_CustomEntityFuncs.RegisterCustomEntity("monster_qboss", "monster_qboss"); }