diff --git a/lua/variables.lua b/lua/variables.lua index ab650329..39ffdde3 100644 --- a/lua/variables.lua +++ b/lua/variables.lua @@ -689,6 +689,10 @@ M_CHICK_INITIAL_HEALTH = 50 -- 200 M_CHICK_ADDON_HEALTH = 15 M_CHICK_INITIAL_ARMOR = 25 -- 175 M_CHICK_ADDON_ARMOR = 15 +M_CHICK_HEAT_INITIAL_HEALTH = 75 -- heat praetor +M_CHICK_HEAT_ADDON_HEALTH = 25 +M_CHICK_HEAT_INITIAL_ARMOR = 35 +M_CHICK_HEAT_ADDON_ARMOR = 20 M_BRAIN_INITIAL_HEALTH = 100 -- 500 M_BRAIN_ADDON_HEALTH = 40 M_BRAIN_INITIAL_ARMOR = 0 -- 1500 @@ -705,22 +709,46 @@ M_GLADIATOR_INITIAL_HEALTH = 100 -- 200 M_GLADIATOR_ADDON_HEALTH = 10 M_GLADIATOR_INITIAL_ARMOR = 100 -- 300 M_GLADIATOR_ADDON_ARMOR = 20 +M_GLADB_INITIAL_HEALTH = 120 -- gladiator disruptor +M_GLADB_ADDON_HEALTH = 35 +M_GLADB_INITIAL_ARMOR = 100 +M_GLADB_ADDON_ARMOR = 55 +M_GLADC_INITIAL_HEALTH = 110 -- gladiator plasma +M_GLADC_ADDON_HEALTH = 25 +M_GLADC_INITIAL_ARMOR = 100 +M_GLADC_ADDON_ARMOR = 40 M_GUNNER_INITIAL_HEALTH = 50 -- 300 M_GUNNER_ADDON_HEALTH = 25 M_GUNNER_INITIAL_ARMOR = 50 -- 200 M_GUNNER_ADDON_ARMOR = 15 +M_GUNCMDR_INITIAL_HEALTH = 100 -- Gunner Commander +M_GUNCMDR_ADDON_HEALTH = 65 +M_GUNCMDR_INITIAL_ARMOR = 200 +M_GUNCMDR_ADDON_ARMOR = 105 M_HOVER_INITIAL_HEALTH = 50 -- 150 M_HOVER_ADDON_HEALTH = 10 M_HOVER_INITIAL_ARMOR = 50 -- 350 M_HOVER_ADDON_ARMOR = 30 +M_DAEDALUS_INITIAL_HEALTH = 50 -- hover/daedalus +M_DAEDALUS_ADDON_HEALTH = 20 +M_DAEDALUS_INITIAL_ARMOR = 150 +M_DAEDALUS_ADDON_ARMOR = 60 M_MEDIC_INITIAL_HEALTH = 100 -- 200 M_MEDIC_ADDON_HEALTH = 10 M_MEDIC_INITIAL_ARMOR = 100 -- 300 M_MEDIC_ADDON_ARMOR = 20 +M_MEDIC_COMMANDER_INITIAL_HEALTH = 240 +M_MEDIC_COMMANDER_ADDON_HEALTH = 25 +M_MEDIC_COMMANDER_INITIAL_ARMOR = 200 +M_MEDIC_COMMANDER_ADDON_ARMOR = 40 M_MUTANT_INITIAL_HEALTH = 150 -- 1000 M_MUTANT_ADDON_HEALTH = 85 M_MUTANT_INITIAL_ARMOR = 0 M_MUTANT_ADDON_ARMOR = 0 +M_REDMUTANT_INITIAL_HEALTH = 180 +M_REDMUTANT_ADDON_HEALTH = 100 +M_REDMUTANT_INITIAL_ARMOR = 0 +M_REDMUTANT_ADDON_ARMOR = 0 M_PARASITE_INITIAL_HEALTH = 150 -- 1000 M_PARASITE_ADDON_HEALTH = 85 M_PARASITE_INITIAL_ARMOR = 0 @@ -729,12 +757,84 @@ M_TANK_INITIAL_HEALTH = 100 -- 750 M_TANK_ADDON_HEALTH = 65 M_TANK_INITIAL_ARMOR = 200 -- 1250 M_TANK_ADDON_ARMOR = 105 +M_RUNNERTANK_INITIAL_HEALTH = 100 +M_RUNNERTANK_ADDON_HEALTH = 65 +M_RUNNERTANK_INITIAL_ARMOR = 200 +M_RUNNERTANK_ADDON_ARMOR = 90 M_BARON_FIRE_INITIAL_HEALTH = 0 M_BARON_FIRE_ADDON_HEALTH = 2500 M_BARON_FIRE_INITIAL_ARMOR = 0 M_BARON_FIRE_ADDON_ARMOR = 0 M_SHAMBLER_INITIAL_HEALTH = 250 -- 1500 M_SHAMBLER_ADDON_HEALTH = 125 +M_GEKK_INITIAL_HEALTH = 150 +M_GEKK_ADDON_HEALTH = 85 +M_GEKK_INITIAL_ARMOR = 0 +M_GEKK_ADDON_ARMOR = 0 +M_STALKER_INITIAL_HEALTH = 150 +M_STALKER_ADDON_HEALTH = 85 +M_STALKER_INITIAL_ARMOR = 0 +M_STALKER_ADDON_ARMOR = 0 +M_ARACHNID_INITIAL_HEALTH = 100 +M_ARACHNID_ADDON_HEALTH = 55 +M_ARACHNID_INITIAL_ARMOR = 200 +M_ARACHNID_ADDON_ARMOR = 85 +M_CARRIER_INITIAL_HEALTH = 2200 +M_CARRIER_ADDON_HEALTH = 650 +M_CARRIER_INITIAL_ARMOR = 500 +M_CARRIER_ADDON_ARMOR = 350 +M_WIDOW_INITIAL_HEALTH = 2400 +M_WIDOW_ADDON_HEALTH = 700 +M_WIDOW_INITIAL_ARMOR = 600 +M_WIDOW_ADDON_ARMOR = 350 +M_WIDOW2_INITIAL_HEALTH = 3000 +M_WIDOW2_ADDON_HEALTH = 900 +M_WIDOW2_INITIAL_ARMOR = 750 +M_WIDOW2_ADDON_ARMOR = 450 +M_FIXBOT_INITIAL_HEALTH = 250 +M_FIXBOT_ADDON_HEALTH = 90 +M_FIXBOT_INITIAL_ARMOR = 120 +M_FIXBOT_ADDON_ARMOR = 45 +M_FIXBOT_BOSS_INITIAL_HEALTH = 3200 +M_FIXBOT_BOSS_ADDON_HEALTH = 850 +M_FIXBOT_BOSS_INITIAL_ARMOR = 700 +M_FIXBOT_BOSS_ADDON_ARMOR = 350 +M_ROGUE_TURRET_INITIAL_HEALTH = 50 +M_ROGUE_TURRET_ADDON_HEALTH = 25 +M_ROGUE_TURRET_INITIAL_ARMOR = 50 +M_ROGUE_TURRET_ADDON_ARMOR = 15 +M_BOSS2_INITIAL_HEALTH = 2400 +M_BOSS2_ADDON_HEALTH = 700 +M_BOSS2_INITIAL_ARMOR = 600 +M_BOSS2_ADDON_ARMOR = 350 +M_BOSS2_SMALL_INITIAL_HEALTH = 250 +M_BOSS2_SMALL_ADDON_HEALTH = 100 +M_BOSS2_SMALL_INITIAL_ARMOR = 160 +M_BOSS2_SMALL_ADDON_ARMOR = 70 +M_GUARDIAN_INITIAL_HEALTH = 6500 +M_GUARDIAN_ADDON_HEALTH = 1200 +M_GUARDIAN_INITIAL_ARMOR = 550 +M_GUARDIAN_ADDON_ARMOR = 250 +M_JANITOR_INITIAL_HEALTH = 125 +M_JANITOR_ADDON_HEALTH = 50 +M_JANITOR_INITIAL_ARMOR = 300 +M_JANITOR_ADDON_ARMOR = 100 +M_MINIGUARDIAN_INITIAL_HEALTH = 105 +M_MINIGUARDIAN_ADDON_HEALTH = 65 +M_MINIGUARDIAN_INITIAL_ARMOR = 120 +M_MINIGUARDIAN_ADDON_ARMOR = 100 +M_SOLDIER_RIPPER_INITIAL_HEALTH = 100 +M_SOLDIER_RIPPER_ADDON_HEALTH = 40 +M_SOLDIER_RIPPER_INITIAL_ARMOR = 50 +M_SOLDIER_RIPPER_ADDON_ARMOR = 20 +M_SOLDIER_BLUEBLASTER_INITIAL_HEALTH = 100 +M_SOLDIER_BLUEBLASTER_ADDON_HEALTH = 40 +M_SOLDIER_BLUEBLASTER_INITIAL_ARMOR = 50 +M_SOLDIER_BLUEBLASTER_ADDON_ARMOR = 20 +M_SOLDIER_LASER_INITIAL_HEALTH = 120 +M_SOLDIER_LASER_ADDON_HEALTH = 45 +M_SOLDIER_LASER_INITIAL_ARMOR = 70 +M_SOLDIER_LASER_ADDON_ARMOR = 25 -- Monster Weapons -- M_ENABLE_WORLDSPAWN_SOFTCAP = 1 @@ -762,6 +862,9 @@ M_SHOTGUN_DMG_MAX = 0 M_RAILGUN_DMG_BASE = 50 M_RAILGUN_DMG_ADDON = 15 M_RAILGUN_DMG_MAX = 0 +M_DABEAM_DMG_BASE = 2 +M_DABEAM_DMG_ADDON = 1 +M_DABEAM_DMG_MAX = 0 M_MELEE_DMG_BASE = 50 M_MELEE_DMG_ADDON = 25 M_MELEE_DMG_MAX = 0 @@ -778,12 +881,43 @@ M_BLASTER_DMG_MAX = 0 M_BLASTER_SPEED_BASE = 1000 M_BLASTER_SPEED_ADDON = 50 M_BLASTER_SPEED_MAX = 1500 +M_BLASTER2_DMG_BASE = 50 +M_BLASTER2_DMG_ADDON = 2 +M_BLASTER2_DMG_MAX = 0 +M_BLASTER2_SPEED_BASE = 500 +M_BLASTER2_SPEED_ADDON = 0 +M_BLASTER2_SPEED_MAX = 0 +M_PLASMA_DMG_BASE = 50 +M_PLASMA_DMG_ADDON = 10 +M_PLASMA_DMG_MAX = 0 +M_PLASMA_SPEED_BASE = 750 +M_PLASMA_SPEED_ADDON = 30 +M_PLASMA_SPEED_MAX = 1500 +M_PLASMA_DAMAGE_RADIUS = 40 M_20MM_RANGE_BASE = 750 M_20MM_RANGE_ADDON = 0 M_20MM_RANGE_MAX = 750 M_20MM_DMG_BASE = 40 M_20MM_DMG_ADDON = 2 M_20MM_DMG_MAX = 0 +M_ETFRIFLE_DMG_BASE = 15 +M_ETFRIFLE_DMG_ADDON = 2 +M_ETFRIFLE_DMG_MAX = 0 +M_ETFRIFLE_SPEED_BASE = 750 +M_ETFRIFLE_SPEED_ADDON = 40 +M_ETFRIFLE_SPEED_MAX = 1200 +M_IONRIPPER_DMG_BASE = 12 +M_IONRIPPER_DMG_ADDON = 8 +M_IONRIPPER_DMG_MAX = 0 +M_IONRIPPER_SPEED_BASE = 680 +M_IONRIPPER_SPEED_ADDON = 30 +M_IONRIPPER_SPEED_MAX = 1000 +M_DISRUPTOR_DMG_BASE = 35 +M_DISRUPTOR_DMG_ADDON = 15 +M_DISRUPTOR_DMG_MAX = 0 +M_DISRUPTOR_SPEED_BASE = 700 +M_DISRUPTOR_SPEED_ADDON = 40 +M_DISRUPTOR_SPEED_MAX = 1000 -- 20mm cannon diff --git a/src/characters/settings.h b/src/characters/settings.h index d281fb21..4af8b224 100644 --- a/src/characters/settings.h +++ b/src/characters/settings.h @@ -754,6 +754,10 @@ VRX_V_LUASETTINGS_IMPL double M_MUTANT_INITIAL_HEALTH; VRX_V_LUASETTINGS_IMPL double M_MUTANT_ADDON_HEALTH; VRX_V_LUASETTINGS_IMPL double M_MUTANT_INITIAL_ARMOR; VRX_V_LUASETTINGS_IMPL double M_MUTANT_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_REDMUTANT_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_REDMUTANT_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_REDMUTANT_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_REDMUTANT_ADDON_ARMOR; VRX_V_LUASETTINGS_IMPL double M_PARASITE_INITIAL_HEALTH; VRX_V_LUASETTINGS_IMPL double M_PARASITE_ADDON_HEALTH; VRX_V_LUASETTINGS_IMPL double M_PARASITE_INITIAL_ARMOR; @@ -768,6 +772,102 @@ VRX_V_LUASETTINGS_IMPL double M_BARON_FIRE_INITIAL_ARMOR; VRX_V_LUASETTINGS_IMPL double M_BARON_FIRE_ADDON_ARMOR; VRX_V_LUASETTINGS_IMPL double M_SHAMBLER_INITIAL_HEALTH; VRX_V_LUASETTINGS_IMPL double M_SHAMBLER_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_CHICK_HEAT_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_CHICK_HEAT_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_CHICK_HEAT_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_CHICK_HEAT_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_DAEDALUS_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_DAEDALUS_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_DAEDALUS_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_DAEDALUS_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_RUNNERTANK_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_RUNNERTANK_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_RUNNERTANK_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_RUNNERTANK_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GEKK_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GEKK_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GEKK_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GEKK_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_STALKER_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_STALKER_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_STALKER_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_STALKER_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_MEDIC_COMMANDER_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_MEDIC_COMMANDER_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_MEDIC_COMMANDER_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_MEDIC_COMMANDER_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GUNCMDR_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GUNCMDR_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GUNCMDR_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GUNCMDR_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GLADB_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GLADB_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GLADB_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GLADB_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GLADC_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GLADC_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GLADC_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GLADC_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_ARACHNID_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_ARACHNID_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_ARACHNID_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_ARACHNID_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_CARRIER_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_CARRIER_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_CARRIER_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_CARRIER_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_WIDOW_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_WIDOW_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_WIDOW_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_WIDOW_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_WIDOW2_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_WIDOW2_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_WIDOW2_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_WIDOW2_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_FIXBOT_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_FIXBOT_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_FIXBOT_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_FIXBOT_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_FIXBOT_BOSS_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_FIXBOT_BOSS_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_FIXBOT_BOSS_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_FIXBOT_BOSS_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_ROGUE_TURRET_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_ROGUE_TURRET_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_ROGUE_TURRET_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_ROGUE_TURRET_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_BOSS2_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_BOSS2_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_BOSS2_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_BOSS2_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_BOSS2_SMALL_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_BOSS2_SMALL_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_BOSS2_SMALL_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_BOSS2_SMALL_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GUARDIAN_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GUARDIAN_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_GUARDIAN_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_GUARDIAN_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_JANITOR_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_JANITOR_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_JANITOR_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_JANITOR_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_MINIGUARDIAN_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_MINIGUARDIAN_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_MINIGUARDIAN_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_MINIGUARDIAN_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_RIPPER_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_RIPPER_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_RIPPER_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_RIPPER_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_BLUEBLASTER_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_BLUEBLASTER_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_BLUEBLASTER_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_BLUEBLASTER_ADDON_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_LASER_INITIAL_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_LASER_ADDON_HEALTH; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_LASER_INITIAL_ARMOR; +VRX_V_LUASETTINGS_IMPL double M_SOLDIER_LASER_ADDON_ARMOR; VRX_V_LUASETTINGS_IMPL double M_BRAIN_INITIAL_PULL; VRX_V_LUASETTINGS_IMPL double M_BRAIN_ADDON_PULL; VRX_V_LUASETTINGS_IMPL double M_BRAIN_MAX_PULL; @@ -793,6 +893,9 @@ VRX_V_LUASETTINGS_IMPL double M_RAILGUN_DMG_BASE; VRX_V_LUASETTINGS_IMPL double M_SHOTGUN_DMG_MAX; VRX_V_LUASETTINGS_IMPL double M_RAILGUN_DMG_ADDON; VRX_V_LUASETTINGS_IMPL double M_RAILGUN_DMG_MAX; +VRX_V_LUASETTINGS_IMPL double M_DABEAM_DMG_BASE; +VRX_V_LUASETTINGS_IMPL double M_DABEAM_DMG_ADDON; +VRX_V_LUASETTINGS_IMPL double M_DABEAM_DMG_MAX; VRX_V_LUASETTINGS_IMPL double M_MELEE_DMG_BASE; VRX_V_LUASETTINGS_IMPL double M_MELEE_DMG_ADDON; VRX_V_LUASETTINGS_IMPL double M_MELEE_DMG_MAX; @@ -809,12 +912,47 @@ VRX_V_LUASETTINGS_IMPL double M_BLASTER_DMG_MAX; VRX_V_LUASETTINGS_IMPL double M_BLASTER_SPEED_BASE; VRX_V_LUASETTINGS_IMPL double M_BLASTER_SPEED_ADDON; VRX_V_LUASETTINGS_IMPL double M_BLASTER_SPEED_MAX; +VRX_V_LUASETTINGS_IMPL double M_BLASTER2_DMG_BASE; +VRX_V_LUASETTINGS_IMPL double M_BLASTER2_DMG_ADDON; +VRX_V_LUASETTINGS_IMPL double M_BLASTER2_DMG_MAX; +VRX_V_LUASETTINGS_IMPL double M_BLASTER2_SPEED_BASE; +VRX_V_LUASETTINGS_IMPL double M_BLASTER2_SPEED_ADDON; +VRX_V_LUASETTINGS_IMPL double M_BLASTER2_SPEED_MAX; +VRX_V_LUASETTINGS_IMPL double M_PLASMA_DMG_BASE; +VRX_V_LUASETTINGS_IMPL double M_PLASMA_DMG_ADDON; +VRX_V_LUASETTINGS_IMPL double M_PLASMA_DMG_MAX; +VRX_V_LUASETTINGS_IMPL double M_PLASMA_SPEED_BASE; +VRX_V_LUASETTINGS_IMPL double M_PLASMA_SPEED_ADDON; +VRX_V_LUASETTINGS_IMPL double M_PLASMA_SPEED_MAX; +VRX_V_LUASETTINGS_IMPL double M_PLASMA_DAMAGE_RADIUS; VRX_V_LUASETTINGS_IMPL double M_20MM_RANGE_BASE; VRX_V_LUASETTINGS_IMPL double M_20MM_RANGE_ADDON; VRX_V_LUASETTINGS_IMPL double M_20MM_RANGE_MAX; VRX_V_LUASETTINGS_IMPL double M_20MM_DMG_BASE; VRX_V_LUASETTINGS_IMPL double M_20MM_DMG_ADDON; VRX_V_LUASETTINGS_IMPL double M_20MM_DMG_MAX; + +VRX_V_LUASETTINGS_IMPL double M_IONRIPPER_DMG_BASE; +VRX_V_LUASETTINGS_IMPL double M_IONRIPPER_DMG_ADDON; +VRX_V_LUASETTINGS_IMPL double M_IONRIPPER_DMG_MAX; +VRX_V_LUASETTINGS_IMPL double M_IONRIPPER_SPEED_BASE; +VRX_V_LUASETTINGS_IMPL double M_IONRIPPER_SPEED_ADDON; +VRX_V_LUASETTINGS_IMPL double M_IONRIPPER_SPEED_MAX; + +VRX_V_LUASETTINGS_IMPL double M_ETFRIFLE_DMG_BASE; +VRX_V_LUASETTINGS_IMPL double M_ETFRIFLE_DMG_ADDON; +VRX_V_LUASETTINGS_IMPL double M_ETFRIFLE_DMG_MAX; +VRX_V_LUASETTINGS_IMPL double M_ETFRIFLE_SPEED_BASE; +VRX_V_LUASETTINGS_IMPL double M_ETFRIFLE_SPEED_ADDON; +VRX_V_LUASETTINGS_IMPL double M_ETFRIFLE_SPEED_MAX; + +VRX_V_LUASETTINGS_IMPL double M_DISRUPTOR_DMG_BASE; +VRX_V_LUASETTINGS_IMPL double M_DISRUPTOR_DMG_ADDON; +VRX_V_LUASETTINGS_IMPL double M_DISRUPTOR_DMG_MAX; +VRX_V_LUASETTINGS_IMPL double M_DISRUPTOR_SPEED_BASE; +VRX_V_LUASETTINGS_IMPL double M_DISRUPTOR_SPEED_ADDON; +VRX_V_LUASETTINGS_IMPL double M_DISRUPTOR_SPEED_MAX; + VRX_V_LUASETTINGS_IMPL double BERSERK_SLASH_INITIAL_DAMAGE; VRX_V_LUASETTINGS_IMPL double BERSERK_SLASH_ADDON_DAMAGE; VRX_V_LUASETTINGS_IMPL double BERSERK_SLASH_RANGE; diff --git a/src/characters/v_utils.c b/src/characters/v_utils.c index f736ce7a..3855dd57 100644 --- a/src/characters/v_utils.c +++ b/src/characters/v_utils.c @@ -2043,6 +2043,12 @@ char *V_GetMonsterKind(int mtype) { case M_SOLDIERLT: case M_SOLDIERSS: return "soldier"; + case M_SOLDIER_RIPPER: + return "ripper Guard"; + case M_SOLDIER_BLUEBLASTER: + return "hyper Guard"; + case M_SOLDIER_LASER: + return "laser Guard"; case M_FLIPPER: return "flipper"; case M_FLYER: @@ -2056,6 +2062,8 @@ char *V_GetMonsterKind(int mtype) { return "gunner"; case M_CHICK: return "iron praetor"; + case M_CHICK_HEAT: + return "heat praetor"; case M_PARASITE: return "parasite"; case M_FLOATER: @@ -2066,6 +2074,8 @@ char *V_GetMonsterKind(int mtype) { return "berserker"; case M_MEDIC: return "medic"; + case M_MEDIC_COMMANDER: + return "medic commander"; case M_MUTANT: return "mutant"; case M_BRAIN: @@ -2077,8 +2087,50 @@ char *V_GetMonsterKind(int mtype) { return "tank"; case M_SUPERTANK: return "supertank"; + case M_BOSS5: + return "supertank heat"; case M_SHAMBLER: return "shambler"; + case M_REDMUTANT: + return "red mutant"; + case M_RUNNERTANK: + return "runner tank"; + case M_GUNCMDR: + return "gunner commander"; + case M_DAEDALUS: + return "daedalus"; + case M_GLADB: + return "disruptor gladiator"; + case M_GLADC: + return "plasma gladiator"; + case M_STALKER: + return "stalker"; + case M_GEKK: + return "gekk"; + case M_ARACHNID: + return "arachnid"; + case M_BOSS2: + return "hornet"; + case M_BOSS2_SMALL: + return "mini hornet"; + case M_CARRIER: + return "carrier"; + case M_WIDOW: + return "widow"; + case M_WIDOW2: + return "black widow"; + case M_FIXBOT: + return "fixbot"; + case M_FIXBOT_BOSS: + return "fixer"; + case M_ROGUE_TURRET: + return "rocket turret"; + case M_GUARDIAN: + return "guardian"; + case M_JANITOR: + return "janitor"; + case M_MINIGUARDIAN: + return "mini guardian"; case M_SKELETON: return "skeleton"; case M_GOLEM: @@ -2752,7 +2804,9 @@ void V_NonShellEffects(edict_t *ent) { } void V_SetEffects(edict_t *ent) { - int effects, r_effects; + int effects, r_effects, preserved_renderfx; + + preserved_renderfx = ent->s.renderfx & RF_CUSTOMSKIN; // clear all effects ent->s.effects = ent->s.renderfx = 0; @@ -2761,8 +2815,10 @@ void V_SetEffects(edict_t *ent) { if (que_typeexists(ent->curses, CURSE_PLAGUE)) ent->s.effects |= EF_FLIES; - if (ent->mtype != M_MAGMINE && ent->health < 1) + if (ent->mtype != M_MAGMINE && ent->health < 1) { + ent->s.renderfx |= preserved_renderfx; return; + } // apply non-ability shell effects V_ShellNonAbilityEffects(ent); @@ -2791,6 +2847,8 @@ void V_SetEffects(edict_t *ent) { // apply non-shell effects V_NonShellEffects(ent); + + ent->s.renderfx |= preserved_renderfx; } /* @@ -2824,7 +2882,32 @@ qboolean vrx_is_morphing_polt(edict_t *ent) { // returns true if ent has a pain/damaged skin qboolean vrx_has_pain_skin(edict_t* ent) { - return (ent->mtype != M_DECOY && ent->mtype != M_SKELETON && ent->mtype != M_GOLEM); + return (ent->mtype != M_DECOY && ent->mtype != M_SKELETON && ent->mtype != M_GOLEM + && ent->mtype != M_RUNNERTANK && ent->mtype != M_REDMUTANT + && ent->mtype != M_ROGUE_TURRET); +} + +void vrx_update_drone_death_skin(edict_t* ent) +{ + if (!ent || !ent->inuse || !(ent->svflags & SVF_MONSTER)) + return; + if (ent->s.modelindex == 255 || ent->max_health <= 0 || ent->health <= ent->gib_health) + return; + if (ent->health >= 0.5f * ent->max_health) + return; + if (!vrx_has_pain_skin(ent)) + return; + + if (ent->mtype == M_GEKK) + { + ent->s.skinnum = (ent->health < 0.25f * ent->max_health) ? 2 : 1; + return; + } + + if (ent->mtype == M_BARON_FIRE && ent->health < 0.2f * ent->max_health) + ent->s.skinnum = 2; + else + ent->s.skinnum |= 1; } // returns a value >= 1 based on any synergy bonuses that apply for ability_index diff --git a/src/characters/v_utils.h b/src/characters/v_utils.h index 5785ce34..b0e43b2a 100644 --- a/src/characters/v_utils.h +++ b/src/characters/v_utils.h @@ -27,4 +27,5 @@ qboolean V_MatchPlayerPrefs (edict_t *player, int monsters, int players);//4.5 qboolean isMonster (edict_t *ent); qboolean vrx_is_morphing_polt(edict_t *ent); qboolean vrx_has_pain_skin(edict_t* ent); +void vrx_update_drone_death_skin(edict_t* ent); float vrx_get_synergy_mult(const edict_t* ent, int ability_index); \ No newline at end of file diff --git a/src/combat/abilities/emp.c b/src/combat/abilities/emp.c index cdb04a4f..7676fd9f 100644 --- a/src/combat/abilities/emp.c +++ b/src/combat/abilities/emp.c @@ -299,6 +299,8 @@ void fire_emp_grenade (edict_t *self, vec3_t start, vec3_t aimdir, int slevel, f grenade->monsterinfo.level = slevel; grenade->dmg_radius = radius; grenade->classname = "emp grenade"; + if (self->client) + grenade->svflags |= SVF_PROJECTILE; gi.linkentity (grenade); grenade->nextthink = level.time + timer; diff --git a/src/combat/abilities/fire.c b/src/combat/abilities/fire.c index d2cbb8a1..0ca8d6b1 100644 --- a/src/combat/abilities/fire.c +++ b/src/combat/abilities/fire.c @@ -241,6 +241,8 @@ void fire_fireball(edict_t* self, vec3_t start, vec3_t aimdir, int damage, float fireball->radius_dmg = flame_damage; fireball->count = flames; fireball->classname = "fireball"; + if (self->client) + fireball->svflags |= SVF_PROJECTILE; fireball->delay = level.time + 10.0; gi.linkentity(fireball); fireball->nextthink = level.time + FRAMETIME; diff --git a/src/combat/abilities/gloom.c b/src/combat/abilities/gloom.c index ac55d664..7c6a9b7d 100644 --- a/src/combat/abilities/gloom.c +++ b/src/combat/abilities/gloom.c @@ -3660,18 +3660,76 @@ void acid_touch (edict_t *ent, edict_t *other, cplane_t *plane, csurface_t *surf acid_explode(ent); } +#define ACID_VERTICAL_BOOST 200.0f + +static float acid_throwing_pitch(vec3_t start, vec3_t end, float speed, float vertical_boost) +{ + const float gravity = sv_gravity->value; + const float dist = Get2dDistance(start, end); + const float height = end[2] - start[2]; + const float effective_speed = sqrtf(speed * speed + vertical_boost * vertical_boost); + const double speed_sq = (double)effective_speed * effective_speed; + const double discriminant = speed_sq * speed_sq - (double)gravity * + ((double)gravity * dist * dist + 2.0 * height * speed_sq); + float pitch, boost_angle; + + if (speed <= 0 || gravity <= 0 || dist < 1.0f || discriminant < 0) + return -999; + + pitch = (float)(-atan((speed_sq - sqrt(discriminant)) / ((double)gravity * dist)) * (180.0 / M_PI)); + boost_angle = (float)(atan2(vertical_boost, speed) * (180.0 / M_PI)); + return pitch + boost_angle; +} + +static qboolean monster_adjust_acid_aim(edict_t *self, vec3_t start, int speed, vec3_t aimdir) +{ + float dist, flat_len, pitch; + vec3_t angles, flat, target; + + if (!(self->svflags & SVF_MONSTER) || !G_EntExists(self->enemy) || speed <= 100) + return false; + + if (!M_MonsterFindClearShot(self, start, target)) + G_EntMidPoint(self->enemy, target); + + dist = Get2dDistance(start, target); + VectorCopy(aimdir, flat); + flat[2] = 0; + flat_len = VectorNormalize(flat); + if (dist < 1.0f || flat_len < 0.01f) + return false; + + target[0] = start[0] + flat[0] * dist; + target[1] = start[1] + flat[1] * dist; + target[2] = start[2] + aimdir[2] / flat_len * dist; + + pitch = acid_throwing_pitch(start, target, speed, ACID_VERTICAL_BOOST); + if (pitch < -90) + return false; + + vectoangles(aimdir, angles); + angles[PITCH] = pitch; + AngleVectors(angles, aimdir, NULL, NULL); + VectorNormalize(aimdir); + return true; +} + void fire_acid (edict_t *self, vec3_t start, vec3_t aimdir, int projectile_damage, float radius, int speed, int acid_damage, float acid_duration, int gas_damage, float gas_radius, float gas_duration) { edict_t *grenade; vec3_t dir; vec3_t forward, right, up; + vec3_t adjusted_aim; // calling entity made a sound, used to alert monsters self->lastsound = level.framenum; + VectorCopy(aimdir, adjusted_aim); + monster_adjust_acid_aim(self, start, speed, adjusted_aim); + // get aiming angles - vectoangles(aimdir, dir); + vectoangles(adjusted_aim, dir); // get directional vectors AngleVectors(dir, forward, right, up); @@ -3691,6 +3749,8 @@ void fire_acid (edict_t *self, vec3_t start, vec3_t aimdir, int projectile_damag grenade->dmg_radius = radius; grenade->delay = acid_duration; grenade->classname = "acid"; + if (self->client) + grenade->svflags |= SVF_PROJECTILE; // Talent: Spitting Gasser if (gas_damage > 0 && gas_radius > 0 && gas_duration > 0) { @@ -3702,7 +3762,7 @@ void fire_acid (edict_t *self, vec3_t start, vec3_t aimdir, int projectile_damag grenade->nextthink = level.time + 10.0; // adjust velocity - VectorScale (aimdir, speed, grenade->velocity); + VectorScale (adjusted_aim, speed, grenade->velocity); VectorMA (grenade->velocity, 200 + crandom() * 10.0, up, grenade->velocity); VectorMA (grenade->velocity, crandom() * 10.0, right, grenade->velocity); VectorSet (grenade->avelocity, 300, 300, 300); diff --git a/src/combat/abilities/hammer.c b/src/combat/abilities/hammer.c index 7fe362a7..8824f4b3 100644 --- a/src/combat/abilities/hammer.c +++ b/src/combat/abilities/hammer.c @@ -248,6 +248,8 @@ void SpawnBlessedHammer (edict_t *ent, int boomerang_level) hammer->nextthink = level.time + FRAMETIME; hammer->dmg = HAMMER_INITIAL_DAMAGE+HAMMER_ADDON_DAMAGE*ent->myskills.abilities[HAMMER].current_level; hammer->classname = "hammer"; + if (ent->client) + hammer->svflags |= SVF_PROJECTILE; gi.linkentity (hammer); // write a nice effect so everyone knows we've cast a spell diff --git a/src/combat/abilities/ice.c b/src/combat/abilities/ice.c index 25781746..b8947827 100644 --- a/src/combat/abilities/ice.c +++ b/src/combat/abilities/ice.c @@ -251,6 +251,8 @@ void fire_icebolt(edict_t* self, vec3_t start, vec3_t aimdir, int damage, float icebolt->chill_level = chillLevel; icebolt->chill_time = chillDuration; icebolt->classname = "icebolt"; + if (self->client) + icebolt->svflags |= SVF_PROJECTILE; icebolt->delay = level.time + 10.0; // timeout gi.linkentity(icebolt); icebolt->nextthink = level.time + FRAMETIME; @@ -359,6 +361,8 @@ void fire_iceshard(edict_t* self, vec3_t start, vec3_t dir, float speed, int dam bolt->chill_level = chillLevel; bolt->chill_time = chillDuration; bolt->classname = "ice shard"; + if (G_GetClient(self)) + bolt->svflags |= SVF_PROJECTILE; gi.linkentity(bolt); // cloak a player-owned ice shard in PvM if there are too many entities nearby @@ -489,6 +493,8 @@ void fire_frozenorb(edict_t* self, vec3_t start, vec3_t aimdir, int damage, int orb->chill_level = chillLevel; orb->chill_time = chillDuration; orb->classname = "frozen orb"; + if (self->client) + orb->svflags |= SVF_PROJECTILE; orb->delay = level.time + FROZEN_ORB_DURATION; // timeout gi.linkentity(orb); orb->nextthink = level.time + FRAMETIME; diff --git a/src/combat/abilities/magicbolt.c b/src/combat/abilities/magicbolt.c index cabefc83..b6f1b3a9 100644 --- a/src/combat/abilities/magicbolt.c +++ b/src/combat/abilities/magicbolt.c @@ -72,6 +72,8 @@ void fire_magicbolt (edict_t *ent, int damage, int radius_damage, float damage_r bolt->dmg_radius = damage_radius; bolt->radius_dmg = radius_damage; bolt->classname = "magicbolt"; + if (ent->client) + bolt->svflags |= SVF_PROJECTILE; gi.linkentity (bolt); // write a nice effect so everyone knows we've cast a spell diff --git a/src/combat/abilities/mirv.c b/src/combat/abilities/mirv.c index 01240487..67a17a25 100644 --- a/src/combat/abilities/mirv.c +++ b/src/combat/abilities/mirv.c @@ -131,6 +131,8 @@ void fire_mirv_grenade (edict_t *self, vec3_t start, vec3_t aimdir, int damage, grenade->radius_dmg = damage; grenade->dmg_radius = radius; grenade->classname = "mirv grenade"; + if (self->client) + grenade->svflags |= SVF_PROJECTILE; gi.linkentity (grenade); grenade->nextthink = level.time + timer; diff --git a/src/combat/abilities/napalm.c b/src/combat/abilities/napalm.c index d99baef8..f0244d1f 100644 --- a/src/combat/abilities/napalm.c +++ b/src/combat/abilities/napalm.c @@ -102,6 +102,8 @@ void fire_napalm (edict_t *self, vec3_t start, vec3_t aimdir, grenade->radius_dmg = burn_damage; grenade->classname = "napalm"; + if (self->client) + grenade->svflags |= SVF_PROJECTILE; gi.linkentity (grenade); VectorScale (aimdir, speed, grenade->velocity); diff --git a/src/combat/abilities/plasmabolt.c b/src/combat/abilities/plasmabolt.c index bb18bf20..e5cc4100 100644 --- a/src/combat/abilities/plasmabolt.c +++ b/src/combat/abilities/plasmabolt.c @@ -75,6 +75,8 @@ void fire_plasmabolt (edict_t *self, vec3_t start, vec3_t aimdir, int damage, fl bolt->dmg_radius = damage_radius; bolt->dmg = damage; bolt->classname = "plasma bolt"; + if (self->client) + bolt->svflags |= SVF_PROJECTILE; bolt->random = duration; bolt->delay = level.time + 10.0; gi.linkentity(bolt); @@ -121,4 +123,4 @@ void Cmd_Plasmabolt_f (edict_t *ent) gi.multicast (ent->s.origin, MULTICAST_PVS); gi.sound(ent, CHAN_ITEM, gi.soundindex("abilities/holybolt2.wav"), 1, ATTN_NORM, 0); -} \ No newline at end of file +} diff --git a/src/combat/abilities/playermonster/playertocacodemon.c b/src/combat/abilities/playermonster/playertocacodemon.c index 346d31fa..58405fc5 100644 --- a/src/combat/abilities/playermonster/playertocacodemon.c +++ b/src/combat/abilities/playermonster/playertocacodemon.c @@ -69,6 +69,8 @@ void fire_skull(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, skull->radius_dmg = damage; skull->dmg_radius = damage_radius; skull->classname = "skull"; + if (self->client) + skull->svflags |= SVF_PROJECTILE; skull->s.sound = gi.soundindex("weapons/bfg__l1a.wav"); skull->delay = level.time + 10; skull->think = bskull_think; diff --git a/src/combat/abilities/spike.c b/src/combat/abilities/spike.c index a899a444..4a3736bd 100644 --- a/src/combat/abilities/spike.c +++ b/src/combat/abilities/spike.c @@ -46,6 +46,8 @@ void fire_spike (edict_t *self, vec3_t start, vec3_t dir, int damage, float stun bolt->dmg = damage; bolt->dmg_radius = stun_length; bolt->classname = "spike"; + if (self->client) + bolt->svflags |= SVF_PROJECTILE; gi.linkentity (bolt); // cloak a player-owned spike in PvM if there are too many entities nearby @@ -152,4 +154,4 @@ void Cmd_Spike_f (edict_t *ent) if (ent->myskills.abilities[SPIKE].disable) return; player_fire_spike(ent); -} \ No newline at end of file +} diff --git a/src/combat/abilities/spikegrenade.c b/src/combat/abilities/spikegrenade.c index e0a191b9..5f2417b3 100644 --- a/src/combat/abilities/spikegrenade.c +++ b/src/combat/abilities/spikegrenade.c @@ -68,6 +68,9 @@ void fire_spikey (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed bolt->think = G_FreeEdict; bolt->dmg = damage; bolt->classname = "spikey"; + bolt->owner = self; + if (self->client) + bolt->svflags |= SVF_PROJECTILE; gi.linkentity (bolt); } diff --git a/src/combat/common/damage.c b/src/combat/common/damage.c index e2bf31cf..f8043ab8 100644 --- a/src/combat/common/damage.c +++ b/src/combat/common/damage.c @@ -150,7 +150,14 @@ qboolean IsMorphedPlayer(const edict_t *ent) { } qboolean IsMonster(const edict_t* ent) { - return (ent->mtype && (ent->mtype <= M_TANK || ent->mtype == M_SHAMBLER)); + return (ent->mtype && (ent->mtype <= M_TANK || ent->mtype == M_SUPERTANK || ent->mtype == M_BOSS5 || ent->mtype == M_SHAMBLER + || ent->mtype == M_REDMUTANT || ent->mtype == M_RUNNERTANK || ent->mtype == M_GUNCMDR + || ent->mtype == M_DAEDALUS || ent->mtype == M_GLADB || ent->mtype == M_GLADC + || ent->mtype == M_STALKER || ent->mtype == M_GEKK || ent->mtype == M_ARACHNID + || ent->mtype == M_MEDIC_COMMANDER || ent->mtype == M_BOSS2 || ent->mtype == M_BOSS2_SMALL || ent->mtype == M_CARRIER + || ent->mtype == M_WIDOW || ent->mtype == M_WIDOW2 + || ent->mtype == M_FIXBOT || ent->mtype == M_FIXBOT_BOSS || ent->mtype == M_ROGUE_TURRET + || ent->mtype == M_CHICK_HEAT)); } float vrx_get_pack_modifier(const edict_t *ent) { diff --git a/src/combat/common/v_misc.c b/src/combat/common/v_misc.c index e81c9125..7594bde4 100644 --- a/src/combat/common/v_misc.c +++ b/src/combat/common/v_misc.c @@ -242,6 +242,45 @@ int vrx_remove_all_monsters(edict_t *monster_owner) { } +static enum dronespawn_t vrx_pvm_random_drone_type(void) +{ + static const enum dronespawn_t pvm_drone_types[] = + { + DS_GUNNER, + DS_PARASITE, + DS_BITCH, + DS_BRAIN, + DS_MEDIC, + DS_TANK, + DS_MUTANT, + DS_GLADIATOR, + DS_BERSERK, + DS_INFANTRY, + DS_FLYER, + DS_FLOATER, + DS_HOVER, + DS_SHAMBLER, + DS_REDMUTANT, + DS_RUNNERTANK, + DS_GUNCMDR, + DS_DAEDALUS, + DS_GLADB, + DS_GLADC, + DS_STALKER, + DS_GEKK, + DS_BITCH_HEAT, + DS_ARACHNID, + DS_MEDIC_COMMANDER, + DS_FIXBOT, + DS_BOSS2_SMALL, + DS_JANITOR, + DS_MINIGUARDIAN + }; + + const int count = (int)(sizeof(pvm_drone_types) / sizeof(pvm_drone_types[0])); + return pvm_drone_types[GetRandom(0, count - 1)]; +} + void vrx_pvm_spawn_monsters(edict_t* self, int max_monsters, int total_monsters) { int max_spawn_this_cycle = GetRandom(0, max_monsters - total_monsters); @@ -252,10 +291,7 @@ void vrx_pvm_spawn_monsters(edict_t* self, int max_monsters, int total_monsters) max_spawn_this_cycle = max(max_spawn_this_cycle, 3); while (total_monsters < max_monsters && max_spawn_this_cycle > 0) { - int rnd; - do { - rnd = GetRandom(1, 15); // az: don't spawn soldiers - } while (rnd == 10); + enum dronespawn_t rnd = vrx_pvm_random_drone_type(); edict_t* scan; if ((scan = vrx_create_new_drone(self, rnd, true, true, self->monsterinfo.scale)) != NULL) { @@ -596,6 +632,7 @@ int vrx_GetMonsterCost(int mtype) { cost = M_GUNNER_COST; break; case M_CHICK: + case M_CHICK_HEAT: cost = M_CHICK_COST; break; case M_PARASITE: @@ -604,9 +641,13 @@ int vrx_GetMonsterCost(int mtype) { case M_MEDIC: cost = M_MEDIC_COST; break; + case M_MEDIC_COMMANDER: + cost = M_TANK_COST; + break; case M_BRAIN: cost = M_BRAIN_COST; break; + case M_RUNNERTANK: case M_TANK: cost = M_TANK_COST; break; @@ -616,7 +657,49 @@ int vrx_GetMonsterCost(int mtype) { case M_SHAMBLER: cost = M_TANK_COST; //using tank atm break; + case M_REDMUTANT: + cost = M_MUTANT_COST; + break; + case M_GUNCMDR: + cost = M_GUNNER_COST; + break; + case M_DAEDALUS: + cost = M_HOVER_COST; + break; + case M_GLADB: + case M_GLADC: + cost = M_DEFAULT_COST; + break; + case M_STALKER: + cost = M_DEFAULT_COST; + break; + case M_GEKK: + cost = M_MUTANT_COST; + break; + case M_ARACHNID: + cost = M_DEFAULT_COST; + break; + case M_CARRIER: + case M_WIDOW: + case M_WIDOW2: + case M_FIXBOT_BOSS: + case M_BOSS2: + cost = M_COMMANDER_COST; + break; + case M_BOSS2_SMALL: + cost = M_HOVER_COST; + break; + case M_FIXBOT: + cost = M_HOVER_COST; + break; + case M_ROGUE_TURRET: + cost = M_DEFAULT_COST; + break; + case M_JANITOR: + cost = M_TANK_COST; + break; case M_SUPERTANK: + case M_BOSS5: cost = M_SUPERTANK_COST; break; case M_COMMANDER: @@ -652,6 +735,7 @@ int vrx_GetMonsterControlCost(int mtype) { cost = M_GUNNER_CONTROL_COST; break; case M_CHICK: + case M_CHICK_HEAT: cost = M_CHICK_CONTROL_COST; break; case M_PARASITE: @@ -660,6 +744,9 @@ int vrx_GetMonsterControlCost(int mtype) { case M_MEDIC: cost = M_MEDIC_CONTROL_COST; break; + case M_MEDIC_COMMANDER: + cost = M_TANK_CONTROL_COST; + break; case M_BRAIN: cost = M_BRAIN_CONTROL_COST; break; @@ -669,10 +756,50 @@ int vrx_GetMonsterControlCost(int mtype) { case M_SHAMBLER: cost = M_TANK_CONTROL_COST; //using tank atm break; + case M_REDMUTANT: + cost = M_MUTANT_CONTROL_COST; + break; + case M_RUNNERTANK: + cost = M_TANK_CONTROL_COST; + break; + case M_GUNCMDR: + cost = M_TANK_CONTROL_COST; + break; + case M_GLADB: + case M_GLADC: + cost = M_GLADIATOR_CONTROL_COST; + break; + case M_STALKER: + cost = M_BERSERKER_CONTROL_COST; + break; + case M_GEKK: + cost = M_MUTANT_CONTROL_COST; + break; + case M_ARACHNID: + cost = M_GLADIATOR_CONTROL_COST; + break; + case M_CARRIER: + case M_WIDOW: + case M_WIDOW2: + case M_FIXBOT_BOSS: + case M_BOSS2: + cost = M_JORG_CONTROL_COST; + break; + case M_BOSS2_SMALL: + cost = M_HOVER_CONTROL_COST; + break; + case M_FIXBOT: + cost = M_HOVER_CONTROL_COST; + break; + case M_ROGUE_TURRET: + cost = M_DEFAULT_CONTROL_COST; + break; case M_HOVER: + case M_DAEDALUS: cost = M_HOVER_CONTROL_COST; break; case M_SUPERTANK: + case M_BOSS5: cost = M_SUPERTANK_CONTROL_COST; break; case M_COMMANDER: @@ -786,7 +913,7 @@ void ThrowDeadlyGib(edict_t* self, char* modelname, vec3_t origin, vec3_t dir, i gib->creator = self; gib->die = deadly_gib_die; gib->touch = shrapnel_touch; - if (type == GIB_ORGANIC) + if (!(type & GIB_METALLIC)) gib->movetype = MOVETYPE_TOSS; else gib->movetype = MOVETYPE_BOUNCE; @@ -1163,4 +1290,4 @@ qboolean vrx_spawn_nonessential_ent(vec3_t org) return true; //gi.dprintf("don't spawn non-essential entity!\n"); return false; -} \ No newline at end of file +} diff --git a/src/combat/weapons/g_weapon.c b/src/combat/weapons/g_weapon.c index 7cd95193..f1572449 100644 --- a/src/combat/weapons/g_weapon.c +++ b/src/combat/weapons/g_weapon.c @@ -45,7 +45,6 @@ void check_dodge (edict_t *self, vec3_t start, vec3_t dir, int speed, int radius { vec3_t end; vec3_t v; - const vec3_t zvec = {0,0,0}; trace_t tr; float eta; edict_t *blip = NULL; @@ -62,8 +61,8 @@ void check_dodge (edict_t *self, vec3_t start, vec3_t dir, int speed, int radius tr.ent->monsterinfo.eta = level.time + eta; //gi.dprintf("ETA for impact is %.1f\n", tr.ent->monsterinfo.eta); tr.ent->monsterinfo.attacker = self; - VectorCopy(zvec, self->monsterinfo.dir); - tr.ent->monsterinfo.radius = 0; + VectorCopy(tr.endpos, tr.ent->monsterinfo.dir); + tr.ent->monsterinfo.radius = radius; } else @@ -512,7 +511,8 @@ void blaster_touch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t * if (other->takedamage) { WeaponStun(self->owner, other, self->style); - T_Damage (other, self, self->owner, self->velocity, self->s.origin, plane->normal, self->dmg, 0, DAMAGE_ENERGY, self->style); + T_Damage (other, self, self->owner, self->velocity, self->s.origin, + plane ? plane->normal : vec3_origin, self->dmg, 0, DAMAGE_ENERGY, self->style); } else { @@ -542,6 +542,135 @@ void blaster_touch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t * G_FreeEdict (self); } +static void blaster2_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf) +{ + if (other == self->owner) + return; + + if (surf && (surf->flags & SURF_SKY)) + { + G_FreeEdict(self); + return; + } + + if (self->owner && self->owner->client) + PlayerNoise(self->owner, self->s.origin, PNOISE_IMPACT); + + if (other->takedamage) + { + if (self->dmg >= 5) + T_RadiusDamage(self, self->owner, 2 * self->dmg, other, self->dmg_radius, MOD_BLASTER); + + T_Damage(other, self, self->owner, self->velocity, self->s.origin, + plane ? plane->normal : vec3_origin, self->dmg, 1, DAMAGE_ENERGY, MOD_BLASTER); + } + else + { + if (self->dmg >= 5) + T_RadiusDamage(self, self->owner, 2 * self->dmg, NULL, self->dmg_radius, MOD_BLASTER); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BLASTER2); + gi.WritePosition(self->s.origin); + gi.WriteDir(plane ? plane->normal : vec3_origin); + gi.multicast(self->s.origin, MULTICAST_PHS); + } + + G_FreeEdict(self); +} + +void fire_blaster2(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, qboolean hyper) +{ + edict_t *bolt; + trace_t tr; + + self->lastsound = level.framenum; + + VectorNormalize(dir); + + bolt = G_Spawn(); + VectorCopy(start, bolt->s.origin); + VectorCopy(start, bolt->s.old_origin); + vectoangles(dir, bolt->s.angles); + VectorScale(dir, speed, bolt->velocity); + bolt->movetype = MOVETYPE_FLYMISSILE; + bolt->clipmask = MASK_SHOT; + bolt->solid = SOLID_BBOX; + bolt->s.effects |= effect; + if (effect) + bolt->s.effects |= EF_TRACKER; + bolt->s.modelindex = gi.modelindex("models/objects/laser/tris.md2"); + bolt->s.skinnum = 2; + bolt->s.sound = gi.soundindex("misc/lasfly.wav"); + bolt->owner = self; + bolt->touch = blaster2_touch; + bolt->nextthink = level.time + 2.0; + bolt->think = G_FreeEdict; + bolt->dmg = damage; + bolt->dmg_radius = hyper ? 64 : 128; + bolt->classname = "bolt"; + if (self->client) + bolt->svflags |= SVF_PROJECTILE; + VectorClear(bolt->mins); + VectorClear(bolt->maxs); + gi.linkentity(bolt); + + if (self->client) + check_dodge(self, bolt->s.origin, dir, speed, 0); + + tr = gi.trace(self->s.origin, NULL, NULL, bolt->s.origin, bolt, MASK_SHOT); + if (tr.fraction < 1.0) + { + VectorMA(bolt->s.origin, -10, dir, bolt->s.origin); + bolt->touch(bolt, tr.ent, NULL, NULL); + } +} + +void fire_blueblaster(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect) +{ + edict_t *bolt; + trace_t tr; + + self->lastsound = level.framenum; + + VectorNormalize(dir); + + bolt = G_Spawn(); + VectorCopy(start, bolt->s.origin); + VectorCopy(start, bolt->s.old_origin); + vectoangles(dir, bolt->s.angles); + VectorScale(dir, speed, bolt->velocity); + bolt->movetype = MOVETYPE_FLYMISSILE; + bolt->clipmask = MASK_SHOT; + bolt->solid = SOLID_BBOX; + bolt->s.effects |= effect; + bolt->s.modelindex = gi.modelindex("models/objects/laser/tris.md2"); + bolt->s.skinnum = 1; + bolt->s.sound = gi.soundindex("misc/lasfly.wav"); + bolt->owner = self; + bolt->style = MOD_HYPERBLASTER; + bolt->touch = blaster_touch; + bolt->nextthink = level.time + 2.0; + bolt->think = G_FreeEdict; + bolt->dmg = damage; + bolt->classname = "bolt"; + if (self->client) + bolt->svflags |= SVF_PROJECTILE; + VectorClear(bolt->mins); + VectorClear(bolt->maxs); + gi.linkentity(bolt); + + if (self->client) + check_dodge(self, bolt->s.origin, dir, speed, 0); + + tr = gi.trace(self->s.origin, NULL, NULL, bolt->s.origin, bolt, MASK_SHOT); + if (tr.fraction < 1.0) + { + VectorMA(bolt->s.origin, -10, dir, bolt->s.origin); + bolt->touch(bolt, tr.ent, NULL, NULL); + } +} + void fire_blaster (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int proj_type, int mod, float duration, qboolean bounce) { edict_t *bolt; @@ -604,6 +733,8 @@ void fire_blaster (edict_t *self, vec3_t start, vec3_t dir, int damage, int spee bolt->think = G_FreeEdict; bolt->dmg = damage; bolt->classname = "bolt"; + if (self->client) + bolt->svflags |= SVF_PROJECTILE; gi.linkentity (bolt); // call monster's dodge function @@ -853,21 +984,79 @@ static void Grenade_Touch (edict_t *ent, edict_t *other, cplane_t *plane, csurfa //Cluster_Explode(ent); } +#define GRENADE_VERTICAL_BOOST 200.0f + +static float grenade_throwing_pitch(vec3_t start, vec3_t end, float speed, float vertical_boost) +{ + const float gravity = sv_gravity->value; + const float dist = Get2dDistance(start, end); + const float height = end[2] - start[2]; + const float effective_speed = sqrtf(speed * speed + vertical_boost * vertical_boost); + const double speed_sq = (double)effective_speed * effective_speed; + const double discriminant = speed_sq * speed_sq - (double)gravity * + ((double)gravity * dist * dist + 2.0 * height * speed_sq); + float pitch, boost_angle; + + if (speed <= 0 || gravity <= 0 || dist < 1.0f || discriminant < 0) + return -999; + + pitch = (float)(-atan((speed_sq - sqrt(discriminant)) / ((double)gravity * dist)) * (180.0 / M_PI)); + boost_angle = (float)(atan2(vertical_boost, speed) * (180.0 / M_PI)); + return pitch + boost_angle; +} + +static qboolean monster_adjust_grenade_aim(edict_t *self, vec3_t start, int speed, vec3_t aimdir) +{ + float dist, flat_len, pitch; + vec3_t angles, flat, target; + + if (!(self->svflags & SVF_MONSTER) || !G_EntExists(self->enemy) || speed <= 100) + return false; + + if (!M_MonsterFindClearShot(self, start, target)) + G_EntMidPoint(self->enemy, target); + + dist = Get2dDistance(start, target); + VectorCopy(aimdir, flat); + flat[2] = 0; + flat_len = VectorNormalize(flat); + if (dist < 1.0f || flat_len < 0.01f) + return false; + + target[0] = start[0] + flat[0] * dist; + target[1] = start[1] + flat[1] * dist; + target[2] = start[2] + aimdir[2] / flat_len * dist; + + pitch = grenade_throwing_pitch(start, target, speed, GRENADE_VERTICAL_BOOST); + if (pitch < -90) + return false; + + vectoangles(aimdir, angles); + angles[PITCH] = pitch; + AngleVectors(angles, aimdir, NULL, NULL); + VectorNormalize(aimdir); + return true; +} + void fire_grenade (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, float timer, float damage_radius, int radius_damage) { edict_t *grenade; vec3_t dir; vec3_t forward, right, up; + vec3_t adjusted_aim; // calling entity made a sound, used to alert monsters self->lastsound = level.framenum; - vectoangles (aimdir, dir); + VectorCopy(aimdir, adjusted_aim); + monster_adjust_grenade_aim(self, start, speed, adjusted_aim); + + vectoangles (adjusted_aim, dir); AngleVectors (dir, forward, right, up); grenade = G_Spawn(); VectorCopy (start, grenade->s.origin); - VectorScale (aimdir, speed, grenade->velocity); + VectorScale (adjusted_aim, speed, grenade->velocity); VectorMA (grenade->velocity, 200 + crandom() * 10.0, up, grenade->velocity); VectorMA (grenade->velocity, crandom() * 10.0, right, grenade->velocity); VectorSet (grenade->avelocity, 300, 300, 300); @@ -904,6 +1093,7 @@ void fire_grenade (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int s grenade->radius_dmg = radius_damage; grenade->dmg_radius = damage_radius; grenade->classname = "grenade"; + grenade->svflags |= SVF_PROJECTILE; gi.linkentity (grenade); } @@ -912,16 +1102,20 @@ edict_t *fire_grenade2 (edict_t *self, vec3_t start, vec3_t aimdir, int damage, edict_t *grenade; vec3_t dir; vec3_t forward, right, up; + vec3_t adjusted_aim; // calling entity made a sound, used to alert monsters self->lastsound = level.framenum; - vectoangles (aimdir, dir); + VectorCopy(aimdir, adjusted_aim); + monster_adjust_grenade_aim(self, start, speed, adjusted_aim); + + vectoangles (adjusted_aim, dir); AngleVectors (dir, forward, right, up); grenade = G_Spawn(); VectorCopy (start, grenade->s.origin); - VectorScale (aimdir, speed, grenade->velocity); + VectorScale (adjusted_aim, speed, grenade->velocity); VectorMA (grenade->velocity, 200 + crandom() * 10.0, up, grenade->velocity); VectorMA (grenade->velocity, crandom() * 10.0, right, grenade->velocity); VectorSet (grenade->avelocity, 300, 300, 300); @@ -946,6 +1140,7 @@ edict_t *fire_grenade2 (edict_t *self, vec3_t start, vec3_t aimdir, int damage, grenade->radius_dmg = radius_damage; grenade->dmg_radius = damage_radius; grenade->classname = "hgrenade"; + grenade->svflags |= SVF_PROJECTILE; if (held) grenade->spawnflags = 3; else @@ -1116,6 +1311,8 @@ void fire_rocket (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed rocket->dmg_radius = damage_radius; rocket->s.sound = gi.soundindex ("weapons/rockfly.wav"); rocket->classname = "rocket"; + if (self->client) + rocket->svflags |= SVF_PROJECTILE; if (self->client) check_dodge (self, rocket->s.origin, dir, speed, damage_radius); @@ -1226,6 +1423,8 @@ void fire_lockon_rocket (edict_t *self, vec3_t start, vec3_t dir, int damage, in rocket->dmg_radius = damage_radius; rocket->s.sound = gi.soundindex ("weapons/rockfly.wav"); rocket->classname = "lockon rocket"; + if (self->client) + rocket->svflags |= SVF_PROJECTILE; if (self->client) check_dodge (self, rocket->s.origin, dir, speed, damage_radius); @@ -1343,6 +1542,8 @@ void fire_smartrocket (edict_t *self, edict_t *target, vec3_t start, vec3_t dir, rocket->dmg_radius = damage_radius; rocket->s.sound = gi.soundindex ("weapons/rockfly.wav"); rocket->classname = "smartrocket"; + if (self->client) + rocket->svflags |= SVF_PROJECTILE; gi.linkentity (rocket); } @@ -1823,6 +2024,8 @@ void fire_bfg (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, f bfg->dmg = damage; bfg->dmg_radius = damage_radius; bfg->classname = "bfg blast"; + if (self->client) + bfg->svflags |= SVF_PROJECTILE; bfg->s.sound = gi.soundindex ("weapons/bfg__l1a.wav"); bfg->delay = level.time + 10.0; // initial time to expire @@ -1874,6 +2077,8 @@ void spawn_grenades(edict_t *ent, vec3_t origin, float time, int damage, int num grenade->dmg = damage; grenade->dmg_radius = 100; grenade->radius_dmg = damage; + if (ent->client) + grenade->svflags |= SVF_PROJECTILE; VectorSet(grenade->mins,-8,-8,-8); VectorSet(grenade->maxs,8,8,8); grenade->touch = Grenade_Touch; diff --git a/src/combat/weapons/mp/g_weapon_mp.c b/src/combat/weapons/mp/g_weapon_mp.c index 8d292957..8e914cbf 100644 --- a/src/combat/weapons/mp/g_weapon_mp.c +++ b/src/combat/weapons/mp/g_weapon_mp.c @@ -520,6 +520,8 @@ void fire_ionripper (edict_t *self, vec3_t start, vec3_t dir, int damage, int sp ion->think = ionripper_sparks; ion->dmg = damage; ion->dmg_radius = 100; + if (self->client) + ion->svflags |= SVF_PROJECTILE; gi.linkentity (ion); if (self->client) @@ -602,6 +604,8 @@ void fire_flechette (edict_t *self, vec3_t start, vec3_t dir, int damage, int sp flechette->think = G_FreeEdict; flechette->dmg = damage; flechette->dmg_radius = kick; + if (self->client) + flechette->svflags |= SVF_PROJECTILE; gi.linkentity(flechette); if (self->client) @@ -795,6 +799,8 @@ void fire_plasma (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed plasma->s.modelindex = gi.modelindex ("sprites/s_photon.sp2"); plasma->s.effects |= EF_PLASMA | EF_ANIM_ALLFAST; + if (self->client) + plasma->svflags |= SVF_PROJECTILE; if (self->client) check_dodge (self, plasma->s.origin, dir, speed, damage_radius); @@ -1040,6 +1046,8 @@ void fire_trap (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int spee trap->dmg = damage; trap->dmg_radius = damage_radius; trap->classname = "htrap"; + if (self->client) + trap->svflags |= SVF_PROJECTILE; trap->s.sound = gi.soundindex("weapons/traploop.wav"); trap->spawnflags = held ? 3 : 1; @@ -1294,6 +1302,8 @@ void fire_prox(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed prox->dmg = damage; prox->dmg_radius = damage_radius; prox->classname = "prox"; + if (self->client) + prox->svflags |= SVF_PROJECTILE; gi.linkentity(prox); } @@ -1623,6 +1633,8 @@ void fire_tesla(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int spee tesla->classname = "tesla"; tesla->clipmask = MASK_SHOT | CONTENTS_SLIME | CONTENTS_LAVA; tesla->mtype = M_TESLA; + if (self->client) + tesla->svflags |= SVF_PROJECTILE; gi.linkentity(tesla); diff --git a/src/combat/weapons/w_sword.c b/src/combat/weapons/w_sword.c index e2b97c88..4e7fd9b7 100644 --- a/src/combat/weapons/w_sword.c +++ b/src/combat/weapons/w_sword.c @@ -368,6 +368,8 @@ void fire_lance (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int bur lance->dmg = damage; lance->radius_dmg = burn_damage; lance->classname = "lance"; + if (self->client) + lance->svflags |= SVF_PROJECTILE; lance->delay = level.time + 10.0; //gi.setmodel(lance, "models/objects/javelin/tris.md2"); // lance->s.skinnum = 1; diff --git a/src/entities/drone/baron_fire.c b/src/entities/drone/baron_fire.c index 9bcca0c6..378969bd 100644 --- a/src/entities/drone/baron_fire.c +++ b/src/entities/drone/baron_fire.c @@ -623,6 +623,7 @@ void baron_fire_die(edict_t* self, edict_t* inflictor, edict_t* attacker, int da gi.sound(self, CHAN_VOICE, sound_death2, 1, ATTN_IDLE, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); self->monsterinfo.currentmove = &baron_fire_move_death; } diff --git a/src/entities/drone/drone_ai.c b/src/entities/drone/drone_ai.c index 118b764d..9cc2618f 100644 --- a/src/entities/drone/drone_ai.c +++ b/src/entities/drone/drone_ai.c @@ -201,6 +201,7 @@ void ai_charge (edict_t *self, float dist) { edict_t *target; vec3_t v; + float ofs; if (!self || !self->inuse) return; @@ -245,6 +246,26 @@ void ai_charge (edict_t *self, float dist) VectorSubtract (self->enemy->s.origin, self->s.origin, v); self->ideal_yaw = vectoyaw(v); M_ChangeYaw (self); + + if (self->monsterinfo.aiflags & AI_ALTERNATE_FLY) + { + if (self->monsterinfo.attack_state == AS_SLIDING) + { + ofs = self->monsterinfo.lefty ? 90 : -90; + if (M_walkmove(self, self->ideal_yaw + ofs, dist)) + return; + + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + M_walkmove(self, self->ideal_yaw - ofs, dist); + } + else + { + M_walkmove(self, self->s.angles[YAW], dist); + } + } + + if (entdist(self, self->enemy) <= MELEE_DISTANCE * 2.5f) + M_ChangeYaw(self); } /* @@ -257,11 +278,13 @@ Strafe sideways, but stay at aproximately the same range void ai_run_slide(edict_t *self, float distance) { float ofs; + vec3_t v; if (!self->enemy) return; - self->ideal_yaw = self->enemy->s.angles[YAW]; + VectorSubtract (self->enemy->s.origin, self->s.origin, v); + self->ideal_yaw = vectoyaw(v); M_ChangeYaw (self); if (self->monsterinfo.lefty) @@ -702,7 +725,12 @@ void drone_ai_idle (edict_t *self) { // change skin if we are being healed by someone else self->s.skinnum &= ~1; - if (self->mtype != M_COMMANDER) + if (self->mtype != M_COMMANDER && self->mtype != M_GUNCMDR + && self->mtype != M_DAEDALUS && self->mtype != M_GLADB && self->mtype != M_GLADC + && self->mtype != M_CHICK_HEAT && self->mtype != M_MEDIC_COMMANDER + && self->mtype != M_SOLDIER && self->mtype != M_SOLDIER_RIPPER + && self->mtype != M_SOLDIER_BLUEBLASTER && self->mtype != M_SOLDIER_LASER + && self->mtype != M_STALKER && self->mtype != M_BOSS5) self->s.skinnum &= ~2; } @@ -739,6 +767,9 @@ void drone_ai_idle (edict_t *self) // called when a the drone finds a new target void drone_newtarget(edict_t* self) { + if (self->monsterinfo.aiflags & AI_ALTERNATE_FLY) + self->monsterinfo.fly_position_time = 0.0f; + // if monster is not standing ground or enemy is a player if (!(self->monsterinfo.aiflags & AI_STAND_GROUND) || (self->enemy && self->enemy->inuse && self->enemy->client)) @@ -885,7 +916,7 @@ void drone_ai_stand (edict_t *self, float dist) if (DRONE_DEBUG) gi.dprintf("drone_ai_stand()\n"); // used for slight position adjustments for animations - if (dist) + if (dist || (self->monsterinfo.aiflags & AI_ALTERNATE_FLY)) M_walkmove(self, self->s.angles[YAW], dist); if (!self->enemy) @@ -1347,6 +1378,42 @@ void drone_ai_run_slide (edict_t *self, float dist) M_walkmove (self, self->ideal_yaw - ofs, dist); } +void drone_set_dodge_side(edict_t *self, vec3_t impact) +{ + vec3_t right, diff; + + if (!self) + return; + + AngleVectors(self->s.angles, NULL, right, NULL); + VectorSubtract(impact, self->s.origin, diff); + + if (DotProduct(right, diff) < 0) + self->monsterinfo.lefty = 0; + else + self->monsterinfo.lefty = 1; +} + +void drone_ai_dodge_slide(edict_t *self, float dist) +{ + float ofs; + vec3_t v; + + if (!G_EntIsAlive(self->enemy)) + return; + + VectorSubtract(self->enemy->s.origin, self->s.origin, v); + self->ideal_yaw = vectoyaw(v); + M_ChangeYaw(self); + + ofs = self->monsterinfo.lefty ? 90 : -90; + if (M_walkmove(self, self->ideal_yaw + ofs, dist)) + return; + + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + M_walkmove(self, self->ideal_yaw - ofs, dist); +} + void TeleportForward (edict_t *ent, vec3_t vec, float dist); void drone_cleargoal (edict_t *self) @@ -1903,7 +1970,12 @@ void drone_ai_run1 (edict_t *self, float dist) { // change skin if we are being healed by someone else self->s.skinnum &= ~1; - if (self->mtype != M_COMMANDER) + if (self->mtype != M_COMMANDER && self->mtype != M_GUNCMDR + && self->mtype != M_DAEDALUS && self->mtype != M_GLADB && self->mtype != M_GLADC + && self->mtype != M_CHICK_HEAT && self->mtype != M_MEDIC_COMMANDER + && self->mtype != M_SOLDIER && self->mtype != M_SOLDIER_RIPPER + && self->mtype != M_SOLDIER_BLUEBLASTER && self->mtype != M_SOLDIER_LASER + && self->mtype != M_STALKER && self->mtype != M_BOSS5) self->s.skinnum &= ~2; } @@ -2158,18 +2230,143 @@ void drone_togglelight (edict_t *self) } } +#define DRONE_PROJECTILE_DODGE_RANGE 512 +#define DRONE_PROJECTILE_DODGE_MAX 256 +#define DRONE_PROJECTILE_DODGE_MIN_ETA FRAMETIME +#define DRONE_PROJECTILE_DODGE_MAX_ETA 2.5f +#define DRONE_PROJECTILE_DODGE_DIRECT_LEAD 0.5f +#define DRONE_PROJECTILE_DODGE_RADIUS_LEAD 0.45f + +static qboolean drone_projectile_infront(edict_t *self, edict_t *projectile) +{ + vec3_t forward, vec; + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(projectile->s.origin, self->s.origin, vec); + VectorNormalize(vec); + + return DotProduct(vec, forward) > 0.35f; +} + +static qboolean drone_projectile_is_explosive(edict_t *projectile) +{ + if (!projectile->classname) + return projectile->dmg_radius > 0; + + if (projectile->dmg_radius > 0) + return true; + + return !Q_strcasecmp(projectile->classname, "rocket") || + !Q_strcasecmp(projectile->classname, "smartrocket") || + !Q_strcasecmp(projectile->classname, "lockon rocket") || + !Q_strcasecmp(projectile->classname, "grenade") || + !Q_strcasecmp(projectile->classname, "hgrenade") || + !Q_strcasecmp(projectile->classname, "prox") || + !Q_strcasecmp(projectile->classname, "htrap") || + !Q_strcasecmp(projectile->classname, "tesla") || + !Q_strcasecmp(projectile->classname, "bfg blast"); +} + +static edict_t *drone_projectile_attacker(edict_t *projectile) +{ + if (G_EntExists(projectile->owner)) + return projectile->owner; + if (G_EntExists(projectile->creator)) + return projectile->creator; + if (G_EntExists(projectile->activator)) + return projectile->activator; + if (G_EntExists(projectile->teammaster)) + return projectile->teammaster; + + return NULL; +} + +static void drone_check_active_projectile_dodge(edict_t *self) +{ + edict_t *touch[DRONE_PROJECTILE_DODGE_MAX]; + vec3_t mins, maxs, end; + int area_pass, i, num; + + if (level.time < self->monsterinfo.dodge_time) + return; + + VectorSet(mins, self->absmin[0] - DRONE_PROJECTILE_DODGE_RANGE, + self->absmin[1] - DRONE_PROJECTILE_DODGE_RANGE, + self->absmin[2] - DRONE_PROJECTILE_DODGE_RANGE); + VectorSet(maxs, self->absmax[0] + DRONE_PROJECTILE_DODGE_RANGE, + self->absmax[1] + DRONE_PROJECTILE_DODGE_RANGE, + self->absmax[2] + DRONE_PROJECTILE_DODGE_RANGE); + + for (area_pass = 0; area_pass < 2; ++area_pass) + { + num = gi.BoxEdicts(mins, maxs, touch, DRONE_PROJECTILE_DODGE_MAX, area_pass ? AREA_TRIGGERS : AREA_SOLID); + for (i = 0; i < num; ++i) + { + edict_t *projectile = touch[i]; + edict_t *attacker; + trace_t tr; + float speed, eta; + int radius; + + if (!projectile || !projectile->inuse) + continue; + if (!(projectile->svflags & SVF_PROJECTILE)) + continue; + attacker = drone_projectile_attacker(projectile); + if (!G_EntExists(attacker) || attacker == self) + continue; + if (OnSameTeam(self, attacker)) + continue; + if (VectorLength(projectile->velocity) < 16) + continue; + if (!drone_projectile_infront(self, projectile)) + continue; + + VectorAdd(projectile->s.origin, projectile->velocity, end); + tr = gi.trace(projectile->s.origin, projectile->mins, projectile->maxs, end, projectile, projectile->clipmask ? projectile->clipmask : MASK_SHOT); + if (tr.ent != self) + continue; + + speed = VectorLength(projectile->velocity); + if (speed < 1) + continue; + + VectorSubtract(tr.endpos, projectile->s.origin, end); + eta = VectorLength(end) / speed; + if (eta < DRONE_PROJECTILE_DODGE_MIN_ETA || eta > DRONE_PROJECTILE_DODGE_MAX_ETA) + continue; + + radius = drone_projectile_is_explosive(projectile) ? (int)projectile->dmg_radius : 0; + if ((self->monsterinfo.aiflags & AI_DODGE) && self->monsterinfo.eta <= level.time + eta) + continue; + + self->monsterinfo.aiflags |= AI_DODGE; + self->monsterinfo.eta = level.time + eta; + self->monsterinfo.attacker = attacker; + VectorCopy(tr.endpos, self->monsterinfo.dir); + self->monsterinfo.radius = radius; + return; + } + } +} + void drone_dodgeprojectiles (edict_t *self) { const qboolean alive = self->health > 0; - const qboolean can_dodge = self->monsterinfo.dodge && (self->monsterinfo.aiflags & AI_DODGE); const qboolean stand_ground = (self->monsterinfo.aiflags & AI_STAND_GROUND); // dodge incoming projectiles - if (alive && can_dodge && !stand_ground) // don't dodge if we are holding position + if (alive && self->monsterinfo.dodge && !stand_ground) { - if (((self->monsterinfo.radius > 0) && ((level.time + 0.3) > self->monsterinfo.eta)) - || (level.time + FRAMETIME) > self->monsterinfo.eta) { - self->monsterinfo.dodge(self, self->monsterinfo.attacker, self->monsterinfo.dir, self->monsterinfo.radius); - self->monsterinfo.aiflags &= ~AI_DODGE; + drone_check_active_projectile_dodge(self); + + if (self->monsterinfo.aiflags & AI_DODGE) { + const float eta = self->monsterinfo.eta - level.time; + const float lead = (self->monsterinfo.radius > 0) ? DRONE_PROJECTILE_DODGE_RADIUS_LEAD : DRONE_PROJECTILE_DODGE_DIRECT_LEAD; + + if (eta < 0 || eta <= lead) { + self->monsterinfo.dodge(self, self->monsterinfo.attacker, self->monsterinfo.dir, self->monsterinfo.radius); + self->monsterinfo.aiflags &= ~AI_DODGE; + } } } } @@ -2271,7 +2468,8 @@ void M_UpdateLastSight (edict_t *self) return; // is the goal entity visible? - if (!visible(self, goal)) + if (!visible(self, goal) && + (!(self->monsterinfo.aiflags & AI_ALTERNATE_FLY) || goal != self->enemy || !M_MonsterHasCombatSight(self, goal))) return; // last sight position has not been reached yet @@ -2406,7 +2604,7 @@ void drone_think (edict_t *self) // don't slide if (self->groundentity) VectorClear(self->velocity); - else if (self->flags & FL_FLY) + else if ((self->flags & FL_FLY) && !(self->monsterinfo.aiflags & AI_ALTERNATE_FLY)) { self->velocity[0] *= 0.8; if (self->velocity[0] < 1) diff --git a/src/entities/drone/drone_arachnid.c b/src/entities/drone/drone_arachnid.c new file mode 100644 index 00000000..0415f5c7 --- /dev/null +++ b/src/entities/drone/drone_arachnid.c @@ -0,0 +1,420 @@ +/* +============================================================================== + +ARACHNID + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_arachnid.h" + +static int sound_pain; +static int sound_death; +static int sound_sight; +static int sound_step; +static int sound_charge; +static int sound_melee; +static int sound_melee_hit; + +#define ARACHNID_DEFAULT_SCALE 0.75f +#define ARACHNID_INVASION_SCALE 0.60f + +static void arachnid_stand(edict_t *self); +static void arachnid_run(edict_t *self); + +static void arachnid_footstep(edict_t *self) +{ + gi.sound(self, CHAN_BODY, sound_step, 0.5, ATTN_IDLE, 0); +} + +static void arachnid_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); +} + +mframe_t arachnid_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t arachnid_move_stand = { FRAME_idle1, FRAME_idle13, arachnid_frames_stand, NULL }; + +static void arachnid_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &arachnid_move_stand; +} + +mframe_t arachnid_frames_walk[] = +{ + drone_ai_walk, 8, arachnid_footstep, + drone_ai_walk, 8, NULL, + drone_ai_walk, 8, NULL, + drone_ai_walk, 8, NULL, + drone_ai_walk, 8, NULL, + drone_ai_walk, 8, arachnid_footstep, + drone_ai_walk, 8, NULL, + drone_ai_walk, 8, NULL, + drone_ai_walk, 8, NULL, + drone_ai_walk, 8, NULL +}; +mmove_t arachnid_move_walk = { FRAME_walk1, FRAME_walk10, arachnid_frames_walk, NULL }; + +static void arachnid_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &arachnid_move_walk; +} + +mframe_t arachnid_frames_run[] = +{ + drone_ai_run, 13, arachnid_footstep, + drone_ai_run, 13, NULL, + drone_ai_run, 13, NULL, + drone_ai_run, 13, NULL, + drone_ai_run, 13, NULL, + drone_ai_run, 13, arachnid_footstep, + drone_ai_run, 13, NULL, + drone_ai_run, 13, NULL, + drone_ai_run, 13, NULL, + drone_ai_run, 13, NULL +}; +mmove_t arachnid_move_run = { FRAME_walk1, FRAME_walk10, arachnid_frames_run, NULL }; + +static void arachnid_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &arachnid_move_stand; + else + self->monsterinfo.currentmove = &arachnid_move_run; +} + +static void arachnid_charge_rail(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + gi.sound(self, CHAN_WEAPON, sound_charge, 1, ATTN_NORM, 0); + VectorCopy(self->enemy->s.origin, self->pos1); + self->pos1[2] += self->enemy->viewheight; +} + +static void arachnid_rail(edict_t *self) +{ + int damage; + int flash_number; + vec3_t start, forward, right, offset, dir; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + damage = M_RAILGUN_DMG_BASE + M_RAILGUN_DMG_ADDON * drone_damagelevel(self); + if (M_RAILGUN_DMG_MAX && damage > M_RAILGUN_DMG_MAX) + damage = M_RAILGUN_DMG_MAX; + + switch (self->s.frame) + { + case FRAME_rails7: + flash_number = MZ2_ARACHNID_RAIL2; + break; + case FRAME_rails_up2: + case FRAME_rails_up9: + flash_number = MZ2_ARACHNID_RAIL_UP1; + break; + case FRAME_rails_up5: + case FRAME_rails_up11: + flash_number = MZ2_ARACHNID_RAIL_UP2; + break; + case FRAME_rails3: + default: + flash_number = MZ2_ARACHNID_RAIL1; + break; + } + + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[flash_number], offset); + if (self->s.scale) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); + VectorSubtract(self->pos1, start, dir); + VectorNormalize(dir); + + monster_fire_railgun(self, start, dir, damage, 100, flash_number); + M_DelayNextAttack(self, 0, true); +} + +mframe_t arachnid_frames_attack1[] = +{ + ai_charge, 0, arachnid_charge_rail, + ai_charge, 0, arachnid_rail, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, arachnid_charge_rail, + ai_charge, 0, arachnid_rail, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, arachnid_charge_rail, + ai_charge, 0, NULL +}; +mmove_t arachnid_move_attack1 = { FRAME_rails2, FRAME_rails11, arachnid_frames_attack1, arachnid_run }; + +mframe_t arachnid_frames_attack_up1[] = +{ + ai_charge, 0, arachnid_charge_rail, + ai_charge, 0, arachnid_rail, + ai_charge, 0, NULL, + ai_charge, 0, arachnid_charge_rail, + ai_charge, 0, arachnid_rail, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, arachnid_charge_rail, + ai_charge, 0, arachnid_rail, + ai_charge, 0, arachnid_charge_rail, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t arachnid_move_attack_up1 = { FRAME_rails_up1, FRAME_rails_up13, arachnid_frames_attack_up1, arachnid_run }; + +static void arachnid_melee_charge(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, true, true) || entdist(self, self->enemy) > 96) + return; + + gi.sound(self, CHAN_WEAPON, sound_melee, 1, ATTN_NORM, 0); +} + +static void arachnid_melee_hit(edict_t *self) +{ + int damage; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + if (entdist(self, self->enemy) > 96) + { + self->monsterinfo.melee_finished = level.time + 1.0; + return; + } + + damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + if (M_MELEE_DMG_MAX && damage > M_MELEE_DMG_MAX) + damage = M_MELEE_DMG_MAX; + + if (M_MeleeAttack(self, self->enemy, 96, damage, 100)) + gi.sound(self, CHAN_WEAPON, sound_melee_hit, 1, ATTN_NORM, 0); + else + self->monsterinfo.melee_finished = level.time + 1.0; +} + +mframe_t arachnid_frames_melee[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, arachnid_melee_charge, + ai_charge, 0, arachnid_melee_hit, + ai_charge, 0, arachnid_melee_hit, + ai_charge, 0, arachnid_melee_charge, + ai_charge, 0, arachnid_melee_hit, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, arachnid_melee_charge, + ai_charge, 0, NULL, + ai_charge, 0, arachnid_melee_hit, + ai_charge, 0, NULL +}; +mmove_t arachnid_move_melee = { FRAME_melee_atk1, FRAME_melee_atk12, arachnid_frames_melee, arachnid_run }; + +static void arachnid_attack(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + if (self->monsterinfo.melee_finished < level.time && entdist(self, self->enemy) < MELEE_DISTANCE) + self->monsterinfo.currentmove = &arachnid_move_melee; + else if ((self->enemy->s.origin[2] - self->s.origin[2]) > 150) + self->monsterinfo.currentmove = &arachnid_move_attack_up1; + else + self->monsterinfo.currentmove = &arachnid_move_attack1; + + M_DelayNextAttack(self, 0, true); +} + +static void arachnid_melee(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, true, true) || entdist(self, self->enemy) > 96) + self->monsterinfo.currentmove = &arachnid_move_attack1; + else + self->monsterinfo.currentmove = &arachnid_move_melee; + M_DelayNextAttack(self, 0, true); +} + +mframe_t arachnid_frames_pain1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t arachnid_move_pain1 = { FRAME_pain11, FRAME_pain15, arachnid_frames_pain1, arachnid_run }; + +mframe_t arachnid_frames_pain2[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t arachnid_move_pain2 = { FRAME_pain21, FRAME_pain26, arachnid_frames_pain2, arachnid_run }; + +static void arachnid_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + if (self->health < (self->max_health / 2)) + self->s.skinnum = 1; + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0; + gi.sound(self, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + + if (random() < 0.5) + self->monsterinfo.currentmove = &arachnid_move_pain1; + else + self->monsterinfo.currentmove = &arachnid_move_pain2; +} + +static void arachnid_dead(edict_t *self) +{ + VectorSet(self->mins, -16, -16, -24); + VectorSet(self->maxs, 16, 16, -8); + self->movetype = MOVETYPE_TOSS; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); + M_PrepBodyRemoval(self); +} + +mframe_t arachnid_frames_death[] = +{ + ai_move, 0, NULL, + ai_move, -1.23, NULL, + ai_move, -1.23, NULL, + ai_move, -1.23, NULL, + ai_move, -1.23, NULL, + ai_move, -1.64, NULL, + ai_move, -1.64, NULL, + ai_move, -2.45, NULL, + ai_move, -8.63, NULL, + ai_move, -4.0, NULL, + ai_move, -4.5, NULL, + ai_move, -6.8, NULL, + ai_move, -8.0, NULL, + ai_move, -5.4, NULL, + ai_move, -3.4, NULL, + ai_move, -1.9, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t arachnid_move_death = { FRAME_death1, FRAME_death20, arachnid_frames_death, arachnid_dead }; + +static void arachnid_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + M_Notify(self); + + if (self->health <= self->gib_health) + { + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); + vrx_throw_drone_gibs(self, damage); + M_Remove(self, false, false); + return; + } + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); + self->monsterinfo.currentmove = &arachnid_move_death; + + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; +} + +void init_drone_arachnid(edict_t *self) +{ + sound_step = gi.soundindex("insane/insane11.wav"); + sound_charge = gi.soundindex("gladiator/railgun.wav"); + sound_melee = gi.soundindex("gladiator/melee3.wav"); + sound_melee_hit = gi.soundindex("gladiator/melee2.wav"); + sound_pain = gi.soundindex("arachnid/pain.wav"); + sound_death = gi.soundindex("arachnid/death.wav"); + sound_sight = gi.soundindex("arachnid/sight.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/arachnid/tris.md2"); + if (invasion->value) + { + VectorSet(self->mins, -28, -28, -18); + VectorSet(self->maxs, 28, 28, 34); + self->s.scale = ARACHNID_INVASION_SCALE; + } + else + { + VectorSet(self->mins, -36, -36, -18); + VectorSet(self->maxs, 36, 36, 42); + self->s.scale = ARACHNID_DEFAULT_SCALE; + } + + self->health = M_ARACHNID_INITIAL_HEALTH + M_ARACHNID_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -200; + self->mass = 450; + self->mtype = M_ARACHNID; + self->monsterinfo.control_cost = M_GLADIATOR_CONTROL_COST; + self->monsterinfo.cost = M_DEFAULT_COST; + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.power_armor_power = M_ARACHNID_INITIAL_ARMOR + M_ARACHNID_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + self->monsterinfo.pain_chance = 0.2f; + self->item = FindItemByClassname("ammo_slugs"); + + self->pain = arachnid_pain; + self->die = arachnid_die; + self->monsterinfo.stand = arachnid_stand; + self->monsterinfo.walk = arachnid_walk; + self->monsterinfo.run = arachnid_run; + self->monsterinfo.attack = arachnid_attack; + self->monsterinfo.melee = arachnid_melee; + self->monsterinfo.sight = arachnid_sight; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &arachnid_move_stand; + self->monsterinfo.scale = MODEL_SCALE * self->s.scale; + self->nextthink = level.time + FRAMETIME; +} diff --git a/src/entities/drone/drone_berserk.c b/src/entities/drone/drone_berserk.c index 8634a934..323ce09f 100644 --- a/src/entities/drone/drone_berserk.c +++ b/src/entities/drone/drone_berserk.c @@ -17,6 +17,13 @@ static int sound_punch; static int sound_sight; static int sound_search; +static void berserk_ai_dodge_slide(edict_t *self, float dist); +static void berserk_duck_up(edict_t *self); + +#define BERSERK_CLOSE_MELEE_RANGE 96 +#define BERSERK_SLAM_MELEE_RANGE 128 +#define BERSERK_SLAM_COOLDOWN 3.0f + void berserk_sight (edict_t *self, edict_t *other) { @@ -119,6 +126,8 @@ mmove_t berserk_move_run1 = {FRAME_run1, FRAME_run6, berserk_frames_run1, NULL}; void berserk_run (edict_t *self) { + berserk_duck_up(self); + if (self->monsterinfo.aiflags & AI_STAND_GROUND) self->monsterinfo.currentmove = &berserk_move_stand; else @@ -143,11 +152,13 @@ void berserk_attack_spike (edict_t *self) void berserk_swing (edict_t *self) { //gi.dprintf("played sound at %d on frame %d\n", level.framenum, self->s.frame); - gi.sound (self, CHAN_WEAPON, sound_punch, 1, ATTN_STATIC, 0); + gi.sound (self, CHAN_WEAPON, sound_punch, 1, ATTN_NORM, 0); // doesn't make noises on static on repro - testing } mframe_t berserk_frames_attack_spike [] = { + ai_charge, 0, NULL, + ai_charge, 0, NULL, ai_charge, 0, berserk_swing, ai_charge, 0, berserk_attack_spike, ai_charge, 0, NULL, @@ -155,7 +166,7 @@ mframe_t berserk_frames_attack_spike [] = ai_charge, 0, NULL, ai_charge, 0, NULL }; -mmove_t berserk_move_attack_spike = {FRAME_att_c3, FRAME_att_c8, berserk_frames_attack_spike, berserk_run}; +mmove_t berserk_move_attack_spike = {FRAME_att_c1, FRAME_att_c8, berserk_frames_attack_spike, berserk_run}; void berserk_attack_club (edict_t *self) { @@ -173,25 +184,55 @@ void berserk_attack_club (edict_t *self) mframe_t berserk_frames_attack_club [] = { + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, ai_charge, 0, berserk_swing, ai_charge, 0, NULL, ai_charge, 0, berserk_attack_club, ai_charge, 0, berserk_attack_club, ai_charge, 0, berserk_attack_club, + ai_charge, 0, NULL, + ai_charge, 0, NULL, ai_charge, 0, NULL }; -mmove_t berserk_move_attack_club = {FRAME_att_b10, FRAME_att_b15, berserk_frames_attack_club, berserk_run}; +mmove_t berserk_move_attack_club = {FRAME_att_c9, FRAME_att_c20, berserk_frames_attack_club, berserk_run}; + +static void berserk_run_attack_speed(edict_t *self) +{ + if (G_EntExists(self->enemy) && entdist(self, self->enemy) <= BERSERK_CLOSE_MELEE_RANGE) + self->monsterinfo.nextframe = self->s.frame + 6; +} + +static void berserk_run_swing(edict_t *self) +{ + berserk_swing(self); + self->monsterinfo.melee_finished = level.time + 0.6f; +} mframe_t berserk_frames_runattack1 [] = { - drone_ai_run, 35, berserk_swing, - drone_ai_run, 35, NULL, - drone_ai_run, 35, berserk_attack_club, - drone_ai_run, 35, berserk_attack_club, - drone_ai_run, 35, berserk_attack_club, - drone_ai_run, 35, NULL + drone_ai_run, 21, berserk_run_attack_speed, + drone_ai_run, 11, berserk_run_attack_speed, + drone_ai_run, 21, berserk_run_attack_speed, + drone_ai_run, 25, berserk_run_attack_speed, + drone_ai_run, 18, berserk_run_attack_speed, + drone_ai_run, 19, berserk_run_attack_speed, + drone_ai_run, 21, NULL, + drone_ai_run, 11, NULL, + drone_ai_run, 21, NULL, + drone_ai_run, 25, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 19, NULL, + drone_ai_run, 21, berserk_run_swing, + drone_ai_run, 11, NULL, + drone_ai_run, 21, berserk_attack_club, + drone_ai_run, 25, berserk_attack_club, + drone_ai_run, 18, berserk_attack_club, + drone_ai_run, 19, NULL }; -mmove_t berserk_move_runattack1 = {FRAME_r_att13, FRAME_r_att18, berserk_frames_runattack1, berserk_run}; +mmove_t berserk_move_runattack1 = {FRAME_r_att1, FRAME_r_att18, berserk_frames_runattack1, berserk_run}; void berserk_attack_strike (edict_t *self) @@ -240,6 +281,18 @@ mframe_t berserk_frames_attack_strike [] = mmove_t berserk_move_attack_strike = {FRAME_slam5, FRAME_slam10, berserk_frames_attack_strike, berserk_run}; +static qboolean berserk_can_slam(edict_t *self, float dist) +{ + return self->groundentity && level.time >= self->timestamp && + dist > BERSERK_CLOSE_MELEE_RANGE && dist <= BERSERK_SLAM_MELEE_RANGE; +} + +static void berserk_start_slam(edict_t *self) +{ + self->timestamp = level.time + BERSERK_SLAM_COOLDOWN; + self->monsterinfo.currentmove = &berserk_move_attack_strike; +} + void berserk_dead (edict_t *self) { VectorSet (self->mins, -16, -16, -24); @@ -250,6 +303,13 @@ void berserk_dead (edict_t *self) M_PrepBodyRemoval(self); } +static void berserk_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t berserk_frames_death1 [] = { ai_move, 0, NULL, @@ -258,7 +318,7 @@ mframe_t berserk_frames_death1 [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, berserk_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -275,7 +335,7 @@ mframe_t berserk_frames_death2 [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, berserk_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -317,6 +377,179 @@ mframe_t berserk_frames_pain_long[] = }; mmove_t berserk_move_pain_long = { FRAME_painb1, FRAME_painb20, berserk_frames_pain_long, berserk_run }; +#define BERSERK_SCALE(self) ((self)->s.scale > 0 ? (self)->s.scale : 1.0f) +#define BERSERK_STAND_MAX_Z_SCALED(self) (32.0f * BERSERK_SCALE(self)) +#define BERSERK_DUCK_MAX_Z_SCALED(self) (0.0f * BERSERK_SCALE(self)) + +static void berserk_duck_down(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_DUCKED) + return; + if (!self->groundentity) + return; + + self->monsterinfo.aiflags |= AI_DUCKED; + self->maxs[2] = BERSERK_DUCK_MAX_Z_SCALED(self); + self->takedamage = DAMAGE_YES; + gi.linkentity(self); +} + +static void berserk_duck_hold(edict_t *self) +{ + if (self->monsterinfo.pausetime > level.time) + self->monsterinfo.nextframe = self->s.frame; +} + +static void berserk_duck_up(edict_t *self) +{ + vec3_t oldmaxs; + trace_t tr; + + if (!(self->monsterinfo.aiflags & AI_DUCKED) && self->maxs[2] == BERSERK_STAND_MAX_Z_SCALED(self)) + return; + + VectorCopy(self->maxs, oldmaxs); + self->maxs[2] = BERSERK_STAND_MAX_Z_SCALED(self); + + tr = gi.trace(self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_MONSTERSOLID); + + if (tr.startsolid || tr.allsolid) + { + VectorCopy(oldmaxs, self->maxs); + self->monsterinfo.aiflags |= AI_DUCKED; + return; + } + + self->monsterinfo.aiflags &= ~AI_DUCKED; + self->takedamage = DAMAGE_AIM; + gi.linkentity(self); +} + +mframe_t berserk_frames_dodge_slide[] = +{ + berserk_ai_dodge_slide, 21, NULL, + berserk_ai_dodge_slide, 11, NULL, + berserk_ai_dodge_slide, 21, NULL, + berserk_ai_dodge_slide, 25, NULL, + berserk_ai_dodge_slide, 18, NULL, + berserk_ai_dodge_slide, 19, NULL +}; +mmove_t berserk_move_dodge_slide = { FRAME_run1, FRAME_run6, berserk_frames_dodge_slide, berserk_run }; + +mframe_t berserk_frames_dodge_duck[] = +{ + ai_move, 21, berserk_duck_down, + ai_move, 28, NULL, + ai_move, 20, NULL, + ai_move, 12, NULL, + ai_move, 7, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, berserk_duck_hold, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, berserk_duck_up, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t berserk_move_dodge_duck = { FRAME_fall2, FRAME_fall18, berserk_frames_dodge_duck, berserk_run }; + +mframe_t berserk_frames_dodge_duck_short[] = +{ + ai_move, 0, berserk_duck_down, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, berserk_duck_hold, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, berserk_duck_up, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t berserk_move_dodge_duck_short = { FRAME_duck1, FRAME_duck10, berserk_frames_dodge_duck_short, berserk_run }; + +static qboolean berserk_is_dodge_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &berserk_move_dodge_slide || + self->monsterinfo.currentmove == &berserk_move_dodge_duck || + self->monsterinfo.currentmove == &berserk_move_dodge_duck_short; +} + +static void berserk_ai_dodge_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(self->monsterinfo.attacker)) + self->enemy = self->monsterinfo.attacker; + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_dodge_slide(self, dist); +} + +void berserk_dodge(edict_t *self, edict_t *attacker, vec3_t dir, int radius) +{ + if (level.time < self->monsterinfo.dodge_time) + return; + if (!attacker) + return; + if (OnSameTeam(self, attacker)) + return; + if (berserk_is_dodge_move(self) || + self->monsterinfo.currentmove == &berserk_move_attack_strike || + self->monsterinfo.currentmove == &berserk_move_pain_long) + return; + if (!self->groundentity) + return; + + if (!G_EntIsAlive(self->enemy)) + { + if (!G_EntIsAlive(attacker)) + return; + self->enemy = attacker; + } + self->monsterinfo.attacker = attacker; + + if (random() > 0.5f) + return; + + if (radius || random() < 0.05f) + { + self->monsterinfo.pausetime = level.time + 0.5f; + + if (radius && random() < 0.5f) + { + self->monsterinfo.currentmove = &berserk_move_dodge_duck_short; + berserk_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.0f; + } + else + { + self->monsterinfo.currentmove = &berserk_move_dodge_duck; + berserk_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.7f; + } + + return; + } + + if (random() < 0.25f) + { + self->monsterinfo.pausetime = level.time + 0.5f; + self->monsterinfo.currentmove = &berserk_move_dodge_duck_short; + berserk_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.0f; + return; + } + + drone_set_dodge_side(self, dir); + self->monsterinfo.currentmove = &berserk_move_dodge_slide; + self->monsterinfo.dodge_time = level.time + 0.4f + random() * 1.6f; +} + void berserk_pain(edict_t* self, edict_t* other, float kick, int damage) { const double rng = random(); @@ -325,7 +558,8 @@ void berserk_pain(edict_t* self, edict_t* other, float kick, int damage) // we're already in a pain state if (self->monsterinfo.currentmove == &berserk_move_pain_long || - self->monsterinfo.currentmove == &berserk_move_pain_short) + self->monsterinfo.currentmove == &berserk_move_pain_short || + berserk_is_dodge_move(self)) return; // monster players don't get pain state induced @@ -355,8 +589,6 @@ void berserk_pain(edict_t* self, edict_t* other, float kick, int damage) void berserk_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; - M_Notify(self); #ifdef OLD_NOLAG_STYLE @@ -372,14 +604,7 @@ void berserk_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); #else @@ -398,6 +623,7 @@ void berserk_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama gi.sound (self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); if (damage >= 50) self->monsterinfo.currentmove = &berserk_move_death1; @@ -416,23 +642,31 @@ void berserk_attack (edict_t *self) float r = random(); const float dist = entdist(self, self->enemy); - if (dist > 128) + if (dist > 160) return; if (self->monsterinfo.aiflags & AI_STAND_GROUND) { - if (random() <= 0.3 && dist <= 96) + if (dist <= BERSERK_CLOSE_MELEE_RANGE && r <= 0.45f) + self->monsterinfo.currentmove = &berserk_move_attack_spike; + else if (dist <= BERSERK_CLOSE_MELEE_RANGE) self->monsterinfo.currentmove = &berserk_move_attack_club; + else if (berserk_can_slam(self, dist)) + berserk_start_slam(self); else - self->monsterinfo.currentmove = &berserk_move_attack_strike; + return; } - else if (dist <= 96) + else if (dist <= BERSERK_CLOSE_MELEE_RANGE) { - if (random() <= 0.2) - self->monsterinfo.currentmove = &berserk_move_attack_strike; + if (r <= 0.45f) + self->monsterinfo.currentmove = &berserk_move_attack_spike; else self->monsterinfo.currentmove = &berserk_move_runattack1; } + else if (berserk_can_slam(self, dist)) + berserk_start_slam(self); + else if (self->monsterinfo.currentmove == &berserk_move_run1) + self->monsterinfo.currentmove = &berserk_move_runattack1; else return; @@ -441,7 +675,26 @@ void berserk_attack (edict_t *self) void berserk_melee (edict_t *self) { + float dist; + + if (!G_EntExists(self->enemy)) + return; + + dist = entdist(self, self->enemy); + if (dist > BERSERK_SLAM_MELEE_RANGE) + return; + if (dist <= BERSERK_CLOSE_MELEE_RANGE && random() <= 0.45f) + self->monsterinfo.currentmove = &berserk_move_attack_spike; + else if (dist <= BERSERK_CLOSE_MELEE_RANGE) + self->monsterinfo.currentmove = &berserk_move_attack_club; + else if (berserk_can_slam(self, dist)) + berserk_start_slam(self); + else + return; + + self->monsterinfo.melee_finished = level.time + 0.4; + self->monsterinfo.attack_finished = level.time + 0.6; } /*QUAKED monster_berserk (1 .5 0) (-16 -16 -24) (16 16 32) Ambush Trigger_Spawn Sight @@ -482,11 +735,11 @@ void init_drone_berserk (edict_t *self) self->monsterinfo.stand = berserk_stand; self->monsterinfo.walk = berserk_walk; self->monsterinfo.run = berserk_run; - //self->monsterinfo.dodge = NULL; + self->monsterinfo.dodge = berserk_dodge; self->monsterinfo.attack = berserk_attack; self->monsterinfo.melee = berserk_melee; self->monsterinfo.sight = berserk_sight; - //self->monsterinfo.search = berserk_search; + self->monsterinfo.idle = berserk_search; self->monsterinfo.currentmove = &berserk_move_stand; self->monsterinfo.scale = MODEL_SCALE; diff --git a/src/entities/drone/drone_bitch.c b/src/entities/drone/drone_bitch.c index e564f0f3..f6aae1b1 100644 --- a/src/entities/drone/drone_bitch.c +++ b/src/entities/drone/drone_bitch.c @@ -15,6 +15,9 @@ void mychick_reslash(edict_t *self); void mychick_rerocket(edict_t *self); void mychick_attack1(edict_t *self); void mychick_continue (edict_t *self); +extern mmove_t mychick_move_start_attack1; +extern mmove_t mychick_move_attack1; +extern mmove_t mychick_move_end_attack1; static int sound_missile_prelaunch; static int sound_missile_launch; @@ -178,6 +181,31 @@ mframe_t mychick_frames_run [] = }; mmove_t mychick_move_run = {FRAME_walk11, FRAME_walk20, mychick_frames_run, NULL}; +static void mychick_ai_dodge_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(self->monsterinfo.attacker)) + self->enemy = self->monsterinfo.attacker; + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_dodge_slide(self, dist); +} + +mframe_t mychick_frames_dodge_slide[] = +{ + mychick_ai_dodge_slide, 16, NULL, + mychick_ai_dodge_slide, 16, NULL, + mychick_ai_dodge_slide, 14, NULL, + mychick_ai_dodge_slide, 13, NULL, + mychick_ai_dodge_slide, 15, NULL, + mychick_ai_dodge_slide, 12, NULL, + mychick_ai_dodge_slide, 11, NULL, + mychick_ai_dodge_slide, 10, NULL, + mychick_ai_dodge_slide, 8, NULL, + mychick_ai_dodge_slide, 6, NULL +}; +mmove_t mychick_move_dodge_slide = {FRAME_walk11, FRAME_walk20, mychick_frames_dodge_slide, mychick_run}; + void mychick_run (edict_t *self) { if (self->monsterinfo.aiflags & AI_STAND_GROUND) @@ -192,7 +220,7 @@ void mychick_dead (edict_t *self) { // gi.dprintf("mychick_dead()\n"); VectorSet (self->mins, -16, -16, 0); - VectorSet (self->maxs, 16, 16, 16); + VectorSet (self->maxs, 16, 16, 8); self->movetype = MOVETYPE_TOSS; self->svflags |= SVF_DEADMONSTER; //self->nextthink = 0; @@ -200,6 +228,13 @@ void mychick_dead (edict_t *self) M_PrepBodyRemoval(self); } +static void mychick_shrink(edict_t *self) +{ + self->maxs[2] = 12; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t mychick_frames_death2 [] = { ai_move, -6, NULL, @@ -222,7 +257,7 @@ mframe_t mychick_frames_death2 [] = ai_move, -3, NULL, ai_move, -5, NULL, ai_move, 4, NULL, - ai_move, 15, NULL, + ai_move, 15, mychick_shrink, ai_move, 14, NULL, ai_move, 1, NULL }; @@ -234,7 +269,7 @@ mframe_t mychick_frames_death1 [] = ai_move, 0, NULL, ai_move, -7, NULL, ai_move, 4, NULL, - ai_move, 11, NULL, + ai_move, 11, mychick_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -265,14 +300,7 @@ void mychick_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); @@ -291,6 +319,7 @@ void mychick_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama // regular death self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); //level.total_monsters--; n = randomMT() % 2; @@ -313,6 +342,10 @@ void mychick_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama } } +#define MYCHICK_STAND_SCALE(self) ((self)->s.scale > 0 ? (self)->s.scale : 1.0f) +#define MYCHICK_STAND_MAX_Z(self) (56 * MYCHICK_STAND_SCALE(self)) +#define MYCHICK_DUCK_MAX_Z 0 + void mychick_duck_down (edict_t *self) { if (self->monsterinfo.aiflags & AI_DUCKED) @@ -321,29 +354,54 @@ void mychick_duck_down (edict_t *self) return; self->monsterinfo.aiflags |= AI_DUCKED; - self->maxs[2] = 0; + self->maxs[2] = MYCHICK_DUCK_MAX_Z; self->takedamage = DAMAGE_YES; - gi.linkentity (self); + gi.linkentity(self); } void mychick_duck_up (edict_t *self) { + vec3_t oldmaxs; + trace_t tr; + + if (!(self->monsterinfo.aiflags & AI_DUCKED) && + self->maxs[2] == MYCHICK_STAND_MAX_Z(self)) + return; + + VectorCopy(self->maxs, oldmaxs); + self->maxs[2] = MYCHICK_STAND_MAX_Z(self); + + tr = gi.trace(self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_MONSTERSOLID); + + if (tr.startsolid || tr.allsolid) + { + VectorCopy(oldmaxs, self->maxs); + self->monsterinfo.aiflags |= AI_DUCKED; + return; + } + self->monsterinfo.aiflags &= ~AI_DUCKED; - self->maxs[2] = 32; self->takedamage = DAMAGE_AIM; - VectorClear(self->velocity); - gi.linkentity (self); + gi.linkentity(self); +} + +void mychick_duck_hold (edict_t *self) +{ + if (self->monsterinfo.pausetime > level.time) + self->monsterinfo.nextframe = self->s.frame; } mframe_t mychick_frames_duck [] = { ai_move, 0, mychick_duck_down, - ai_move, 0, NULL, - ai_move, 0, mychick_duck_up, - ai_move, 0, NULL, - ai_move, 0, NULL + ai_move, 1, NULL, + ai_move, 4, mychick_duck_hold, + ai_move, -4, NULL, + ai_move, -5, mychick_duck_up, + ai_move, 3, NULL, + ai_move, 1, NULL }; -mmove_t mychick_move_duck = {FRAME_duck03, FRAME_duck07, mychick_frames_duck, mychick_run}; +mmove_t mychick_move_duck = {FRAME_duck01, FRAME_duck07, mychick_frames_duck, mychick_run}; void mychick_jump_takeoff (edict_t *self) { @@ -382,11 +440,17 @@ void mychick_jump_hold (edict_t *self) } } +static void mychick_jump_hold_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + mychick_jump_hold(self); +} + mframe_t mychick_frames_leap [] = { ai_move, 0, mychick_jump_takeoff, ai_move, 0, NULL, - ai_move, 0, mychick_jump_hold, + mychick_jump_hold_ai, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -400,29 +464,79 @@ void mychick_leap (edict_t *self) self->monsterinfo.currentmove = &mychick_move_leap; } -void mychick_dodge (edict_t *self, edict_t *attacker, vec3_t dir, int radius) +static qboolean mychick_is_dodge_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &mychick_move_duck || + self->monsterinfo.currentmove == &mychick_move_leap || + self->monsterinfo.currentmove == &mychick_move_dodge_slide; +} + +static qboolean mychick_is_uninterruptible_attack(edict_t *self) +{ + return self->monsterinfo.currentmove == &mychick_move_start_attack1 || + self->monsterinfo.currentmove == &mychick_move_attack1; +} + +static qboolean mychick_dodge_hit_low(edict_t *self, vec3_t dir) +{ + const float duck_height = self->absmax[2] - 33; + + return dir[2] > self->absmin[2] && dir[2] <= duck_height; +} + +static void mychick_start_duck(edict_t *self) +{ + self->monsterinfo.pausetime = level.time + 0.5f; + self->monsterinfo.currentmove = &mychick_move_duck; + mychick_duck_down(self); + self->monsterinfo.dodge_time = level.time + 2.0f; +} + +static qboolean mychick_start_sidestep(edict_t *self, vec3_t dir) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + return false; + + drone_set_dodge_side(self, dir); + self->monsterinfo.currentmove = &mychick_move_dodge_slide; + self->monsterinfo.dodge_time = level.time + 1.0f; + return true; +} + +static void mychick_dodge (edict_t *self, edict_t *attacker, vec3_t dir, int radius) { - if (random() > 0.9) - return; - if (!G_GetClient(self)) - return; if (level.time < self->monsterinfo.dodge_time) return; + if (!attacker) + return; if (OnSameTeam(self, attacker)) return; + if (mychick_is_dodge_move(self) || mychick_is_uninterruptible_attack(self)) + return; - if (!self->enemy && G_EntIsAlive(attacker)) + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) self->enemy = attacker; - if (!radius) + + if (random() > 0.9f) + return; + + self->monsterinfo.attacker = attacker; + if (radius) { - self->monsterinfo.currentmove = &mychick_move_duck; - self->monsterinfo.dodge_time = level.time + 2.0; + mychick_start_sidestep(self, dir); + return; } - else + + if (!mychick_dodge_hit_low(self, dir)) { - mychick_leap(self); - self->monsterinfo.dodge_time = level.time + 3.0; + mychick_start_duck(self); + return; } + + if (mychick_start_sidestep(self, dir)) + return; + + mychick_start_duck(self); } void fire_meteor (edict_t *self, vec3_t end, int damage, int radius, int speed); @@ -486,6 +600,9 @@ void myChickFireball (edict_t *self) speed = 650 + 35 * slvl; // spd: myChickFireball MonsterAim(self, M_PROJECTILE_ACC, speed, true, MZ2_CHICK_ROCKET_1, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + fire_fireball(self, start, forward, damage, 125.0, speed, 5, flame_damage); gi.sound(self, CHAN_ITEM, gi.soundindex("abilities/firecast.wav"), 1, ATTN_NORM, 0); @@ -502,8 +619,10 @@ void myChickRocket (edict_t *self) return; } - if (!G_EntExists(self->enemy)) + if (!G_EntExists(self->enemy) && self->mtype != M_CHICK_HEAT) + { return; + } damage = M_ROCKETLAUNCHER_DMG_BASE + M_ROCKETLAUNCHER_DMG_ADDON * drone_damagelevel(self); // dmg: myChickRocket if (M_ROCKETLAUNCHER_DMG_MAX && damage > M_ROCKETLAUNCHER_DMG_MAX) @@ -511,9 +630,17 @@ void myChickRocket (edict_t *self) speed = M_ROCKETLAUNCHER_SPEED_BASE + M_ROCKETLAUNCHER_SPEED_ADDON * drone_damagelevel(self); // spd: myChickRocket if (M_ROCKETLAUNCHER_SPEED_MAX && speed > M_ROCKETLAUNCHER_SPEED_MAX) speed = M_ROCKETLAUNCHER_SPEED_MAX; - + MonsterAim(self, M_PROJECTILE_ACC, speed, true, MZ2_CHICK_ROCKET_1, forward, start); - monster_fire_rocket (self, start, forward, damage, speed, MZ2_CHICK_ROCKET_1); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + if (self->mtype == M_CHICK_HEAT) + { + monster_fire_heat(self, start, forward, damage, speed, MZ2_CHICK_ROCKET_1, 0.095f); + } + else + monster_fire_rocket (self, start, forward, damage, speed, MZ2_CHICK_ROCKET_1); } void myChickRail (edict_t *self) @@ -527,6 +654,9 @@ void myChickRail (edict_t *self) damage = 50 + 10 * drone_damagelevel(self); // dmg: myChickRailWorld MonsterAim(self, 0.33, 0, false, MZ2_CHICK_ROCKET_1, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + monster_fire_railgun (self, start, forward, damage, damage, MZ2_GLADIATOR_RAILGUN_1); } @@ -591,7 +721,7 @@ mmove_t mychick_move_end_attack1 = {FRAME_attak128, FRAME_attak132, mychick_fram void mychick_rerocket(edict_t *self) { - if (G_ValidTarget(self, self->enemy, true, true)) + if (G_ValidTarget(self, self->enemy, true, true) && M_MonsterHasClearShotFromFlash(self, MZ2_CHICK_ROCKET_1)) { if (random() <= 0.8 && (entdist(self, self->enemy) <= 512 || (self->monsterinfo.aiflags & AI_STAND_GROUND))) self->monsterinfo.currentmove = &mychick_move_attack1; @@ -626,7 +756,7 @@ void mychick_runandshoot (edict_t *self) void mychick_continue (edict_t *self) { if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.9) - && (entdist(self, self->enemy) <= 512)) + && (entdist(self, self->enemy) <= 512) && M_MonsterHasClearShotFromFlash(self, MZ2_CHICK_ROCKET_1)) { self->monsterinfo.currentmove = &mychick_move_runandshoot; @@ -724,7 +854,9 @@ void chick_fire_attack (edict_t *self) } else { - if (self->monsterinfo.aiflags & AI_STAND_GROUND) + if (!M_MonsterHasClearShotFromFlash(self, MZ2_CHICK_ROCKET_1)) + self->monsterinfo.currentmove = &mychick_move_slash; + else if (self->monsterinfo.aiflags & AI_STAND_GROUND) self->monsterinfo.currentmove = &mychick_move_attack1; else mychick_runandshoot(self); @@ -746,6 +878,9 @@ void mychick_attack(edict_t *self) return; } + if (!M_MonsterHasClearShotFromFlash(self, MZ2_CHICK_ROCKET_1)) + return; + if (self->monsterinfo.aiflags & AI_STAND_GROUND) self->monsterinfo.currentmove = &mychick_move_attack1; else @@ -757,7 +892,10 @@ void mychick_attack(edict_t *self) void mychick_sight(edict_t *self, edict_t *other) { - gi.sound (self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + if (random() < 0.5) + gi.sound (self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + else + gi.sound (self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); } mframe_t mychick_frames_jump [] = @@ -831,12 +969,13 @@ void mychick_pain(edict_t* self, edict_t* other, float kick, int damage) { const double rng = random(); if (self->health < (self->max_health / 2)) - self->s.skinnum = 1; + self->s.skinnum |= 1; // we're already in a pain state if (self->monsterinfo.currentmove == &mychick_move_pain_long || self->monsterinfo.currentmove == &mychick_move_pain_short1 || - self->monsterinfo.currentmove == &mychick_move_pain_short2) + self->monsterinfo.currentmove == &mychick_move_pain_short2 || + mychick_is_dodge_move(self)) return; // monster players don't get pain state induced @@ -949,3 +1088,14 @@ void init_drone_bitch (edict_t *self) // walkmonster_start (self); self->nextthink = level.time + 0.1; } + +void init_drone_bitch_heat (edict_t *self) +{ + init_drone_bitch(self); + self->mtype = M_CHICK_HEAT; + self->s.skinnum = 2; + self->health = M_CHICK_HEAT_INITIAL_HEALTH + M_CHICK_HEAT_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->monsterinfo.power_armor_power = M_CHICK_HEAT_INITIAL_ARMOR + M_CHICK_HEAT_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; +} diff --git a/src/entities/drone/drone_boss2.c b/src/entities/drone/drone_boss2.c new file mode 100644 index 00000000..5bc52473 --- /dev/null +++ b/src/entities/drone/drone_boss2.c @@ -0,0 +1,828 @@ +/* +============================================================================== + +boss2 + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_boss2.h" + +#define BOSS2_VARIANT_MG 0 +#define BOSS2_VARIANT_HYPER 1 +#define BOSS2_VARIANT_SMALL 2 +#define BOSS2_ROCKET_SPEED 750 +#define BOSS2_INVASION_SCALE 0.75f +#define BOSS2_INVASION_MOVE_SCALE 1.5f +#define BOSS2_SMALL_DEATH_GRAVITY 0.30f +#define BOSS2_SMALL_DEATH_FALL_SPEED -40.0f + +static int sound_pain1; +static int sound_pain2; +static int sound_pain3; +static int sound_death; +static int sound_search1; + +void drone_ai_stand(edict_t *self, float dist); +void drone_ai_walk(edict_t *self, float dist); +void drone_ai_run(edict_t *self, float dist); + +static void boss2_run(edict_t *self); +static void boss2_attack_mg(edict_t *self); +static void boss2_reattack_mg(edict_t *self); + +static qboolean boss2_is_small(const edict_t *self) +{ + return self->style == BOSS2_VARIANT_SMALL; +} + +static qboolean boss2_is_boss(const edict_t *self) +{ + return self->mtype == M_BOSS2 && !boss2_is_small(self); +} + +static qboolean boss2_is_hyper(const edict_t *self) +{ + return self->style == BOSS2_VARIANT_HYPER || boss2_is_small(self); +} + +static float boss2_move_scale(const edict_t *self) +{ + return (invasion->value && !boss2_is_small(self)) ? BOSS2_INVASION_MOVE_SCALE : 1.0f; +} + +static void boss2_ai_walk(edict_t *self, float dist) +{ + drone_ai_walk(self, dist * boss2_move_scale(self)); +} + +static void boss2_ai_run(edict_t *self, float dist) +{ + drone_ai_run(self, dist * boss2_move_scale(self)); +} + +static void boss2_set_fly_parameters(edict_t *self) +{ + float speed_scale = boss2_move_scale(self); + + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_acceleration = 20.0f * speed_scale; + self->monsterinfo.fly_speed = 120.0f * speed_scale; + if (boss2_is_boss(self)) + { + self->monsterinfo.fly_min_distance = 220.0f; + self->monsterinfo.fly_max_distance = 750.0f; + } + else + { + self->monsterinfo.fly_min_distance = 250.0f; + self->monsterinfo.fly_max_distance = 450.0f; + } +} + +static float boss2_voice_attenuation(const edict_t *self) +{ + return boss2_is_small(self) ? ATTN_NORM : ATTN_NONE; +} + +static void boss2_project_flash(edict_t *self, int flash, vec3_t start) +{ + vec3_t forward, right; + vec3_t offset; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[flash], offset); + if (self->s.scale && self->s.scale != 1.0f) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static void boss2_predict_aim(edict_t *self, vec3_t start, int speed, float lead, vec3_t dir) +{ + vec3_t target; + float dist; + float time; + + if (!G_ValidTarget(self, self->enemy, false, true)) + { + AngleVectors(self->s.angles, dir, NULL, NULL); + return; + } + + G_EntMidPoint(self->enemy, target); + if (speed > 0) + { + VectorSubtract(target, start, dir); + dist = VectorLength(dir); + time = dist / speed + lead; + if (time < 0) + time = 0; + VectorMA(target, time, self->enemy->velocity, target); + } + + VectorSubtract(target, start, dir); + if (!VectorNormalize(dir)) + AngleVectors(self->s.angles, dir, NULL, NULL); +} + +static void boss2_search(edict_t *self) +{ + if (random() < 0.5f) + gi.sound(self, CHAN_VOICE, sound_search1, 1, boss2_voice_attenuation(self), 0); +} + +static void boss2_emit_random_explosion(edict_t *self, int effect) +{ + vec3_t org; + + VectorAdd(self->s.origin, self->mins, org); + org[0] += random() * self->size[0]; + org[1] += random() * self->size[1]; + org[2] += random() * self->size[2]; + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(effect); + gi.WritePosition(org); + gi.multicast(org, MULTICAST_PVS); +} + +static void boss2_explosion_sequence_think(edict_t *self) +{ + edict_t *owner = self->owner; + int effect; + + if (level.time > self->timestamp || !owner || !owner->inuse + || owner->deadflag != DEAD_DEAD || owner->s.modelindex != self->style) + { + G_FreeEdict(self); + return; + } + if (self->count >= self->dmg) + { + G_FreeEdict(self); + return; + } + + effect = (self->count++ % 4) ? TE_EXPLOSION1_NL : TE_EXPLOSION1; + boss2_emit_random_explosion(owner, effect); + self->nextthink = level.time + 0.2f + random() * 0.25f; +} + +static void boss2_start_explosion_sequence(edict_t *self) +{ + edict_t *exploder; + + if (self->count) + return; + self->count = 1; + + boss2_emit_random_explosion(self, TE_EXPLOSION1_BIG); + + exploder = G_Spawn(); + exploder->classname = "boss2_exploder"; + exploder->svflags |= SVF_NOCLIENT; + exploder->solid = SOLID_NOT; + exploder->owner = self; + exploder->style = self->s.modelindex; + exploder->count = 1; + exploder->dmg = boss2_is_small(self) ? 4 : 13; + exploder->think = boss2_explosion_sequence_think; + exploder->nextthink = level.time + 0.2f + random() * 0.25f; + exploder->timestamp = level.time + (boss2_is_small(self) ? 1.5f : 5.0f); +} + +static void boss2_dead(edict_t *self) +{ + vrx_throw_drone_gibs(self, 500); + + boss2_emit_random_explosion(self, TE_EXPLOSION1_BIG); + M_Remove(self, false, false); +} + +static void boss2_fire_predictive_rocket(edict_t *self, int flash, float lead) +{ + vec3_t start, dir; + + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + boss2_project_flash(self, flash, start); + boss2_predict_aim(self, start, BOSS2_ROCKET_SPEED, lead, dir); + monster_fire_rocket(self, start, dir, 50, BOSS2_ROCKET_SPEED, flash); +} + +static void Boss2PredictiveRocket(edict_t *self) +{ + boss2_fire_predictive_rocket(self, MZ2_BOSS2_ROCKET_1, -0.10f); + boss2_fire_predictive_rocket(self, MZ2_BOSS2_ROCKET_2, -0.05f); + boss2_fire_predictive_rocket(self, MZ2_BOSS2_ROCKET_3, 0.05f); + boss2_fire_predictive_rocket(self, MZ2_BOSS2_ROCKET_4, 0.10f); +} + +static void boss2_fire_spread_rocket(edict_t *self, int flash, float zofs, float rightofs) +{ + vec3_t forward, right; + vec3_t start, dir, target; + + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + AngleVectors(self->s.angles, forward, right, NULL); + G_ProjectSource(self->s.origin, monster_flash_offset[flash], forward, right, start); + VectorCopy(self->enemy->s.origin, target); + target[2] += zofs; + VectorSubtract(target, start, dir); + if (!VectorNormalize(dir)) + VectorCopy(forward, dir); + VectorMA(dir, rightofs, right, dir); + VectorNormalize(dir); + monster_fire_rocket(self, start, dir, 50, 500, flash); +} + +static void Boss2Rocket(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + if (self->enemy->client && random() < 0.9f) + { + Boss2PredictiveRocket(self); + return; + } + + boss2_fire_spread_rocket(self, MZ2_BOSS2_ROCKET_1, -15, 0.4f); + boss2_fire_spread_rocket(self, MZ2_BOSS2_ROCKET_2, 0, 0.025f); + boss2_fire_spread_rocket(self, MZ2_BOSS2_ROCKET_3, 0, -0.025f); + boss2_fire_spread_rocket(self, MZ2_BOSS2_ROCKET_4, -15, -0.4f); +} + +static void Boss2Rocket64(edict_t *self) +{ + vec3_t forward, right; + vec3_t start, dir, target; + float scale; + + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + AngleVectors(self->s.angles, forward, right, NULL); + boss2_project_flash(self, MZ2_BOSS2_ROCKET_1, start); + + scale = self->s.scale ? self->s.scale : 1.0f; + start[2] += 10.0f * scale; + VectorMA(start, -2.0f * scale, right, start); + VectorMA(start, -((self->count++ % 4) * 8.0f * scale), right, start); + + if (self->enemy->client && random() < 0.9f) + boss2_predict_aim(self, start, BOSS2_ROCKET_SPEED, -0.3f, dir); + else + { + VectorCopy(self->enemy->s.origin, target); + target[2] -= 15; + VectorSubtract(target, start, dir); + if (!VectorNormalize(dir)) + VectorCopy(forward, dir); + } + + monster_fire_rocket(self, start, dir, 35, BOSS2_ROCKET_SPEED, MZ2_BOSS2_ROCKET_1); +} + +static void boss2_firebullet(edict_t *self, int flash) +{ + vec3_t start, dir; + + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + boss2_project_flash(self, flash, start); + boss2_predict_aim(self, start, 0, 0, dir); + monster_fire_bullet(self, start, dir, 6, 4, DEFAULT_BULLET_HSPREAD * 3, DEFAULT_BULLET_VSPREAD, flash); +} + +static void Boss2MachineGun(edict_t *self) +{ + boss2_firebullet(self, MZ2_BOSS2_MACHINEGUN_L1); + boss2_firebullet(self, MZ2_BOSS2_MACHINEGUN_R1); +} + +static void Boss2HyperBlaster(edict_t *self) +{ + int flash; + int effect; + vec3_t start, dir; + + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + flash = (self->s.frame & 1) ? MZ2_BOSS2_MACHINEGUN_L2 : MZ2_BOSS2_MACHINEGUN_R2; + effect = (self->s.frame % 4) ? 0 : EF_HYPERBLASTER; + boss2_project_flash(self, flash, start); + boss2_predict_aim(self, start, 1000, 0, dir); + monster_fire_blaster(self, start, dir, 2, 1000, effect, BLASTER_PROJ_BOLT, 2.0f, false, flash); +} + +static void Boss2HyperBlasterReattack(edict_t *self) +{ + Boss2HyperBlaster(self); + boss2_reattack_mg(self); +} + +static mframe_t boss2_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +static mmove_t boss2_move_stand = { FRAME_stand30, FRAME_stand50, boss2_frames_stand, NULL }; + +static mframe_t boss2_frames_walk[] = +{ + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL, + boss2_ai_walk, 10, NULL +}; +static mmove_t boss2_move_walk = { FRAME_walk1, FRAME_walk20, boss2_frames_walk, NULL }; + +static mframe_t boss2_frames_run[] = +{ + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL, + boss2_ai_run, 10, NULL +}; +static mmove_t boss2_move_run = { FRAME_walk1, FRAME_walk20, boss2_frames_run, NULL }; + +static mframe_t boss2_frames_attack_pre_mg[] = +{ + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, boss2_attack_mg +}; +static mmove_t boss2_move_attack_pre_mg = { FRAME_attack1, FRAME_attack9, boss2_frames_attack_pre_mg, NULL }; + +static mframe_t boss2_frames_attack_mg[] = +{ + ai_charge, 2, Boss2MachineGun, + ai_charge, 2, Boss2MachineGun, + ai_charge, 2, Boss2MachineGun, + ai_charge, 2, Boss2MachineGun, + ai_charge, 2, Boss2MachineGun, + ai_charge, 2, boss2_reattack_mg +}; +static mmove_t boss2_move_attack_mg = { FRAME_attack10, FRAME_attack15, boss2_frames_attack_mg, NULL }; + +static mframe_t boss2_frames_attack_hb[] = +{ + ai_charge, 2, Boss2HyperBlaster, + ai_charge, 2, Boss2HyperBlaster, + ai_charge, 2, Boss2HyperBlaster, + ai_charge, 2, Boss2HyperBlaster, + ai_charge, 2, Boss2HyperBlaster, + ai_charge, 2, Boss2HyperBlasterReattack +}; +static mmove_t boss2_move_attack_hb = { FRAME_attack10, FRAME_attack15, boss2_frames_attack_hb, NULL }; + +static mframe_t boss2_frames_attack_post_mg[] = +{ + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL +}; +static mmove_t boss2_move_attack_post_mg = { FRAME_attack16, FRAME_attack19, boss2_frames_attack_post_mg, boss2_run }; + +static mframe_t boss2_frames_attack_rocket[] = +{ + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_move, -5, Boss2Rocket, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL +}; +static mmove_t boss2_move_attack_rocket = { FRAME_attack20, FRAME_attack40, boss2_frames_attack_rocket, boss2_run }; + +static mframe_t boss2_frames_attack_rocket2[] = +{ + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, NULL, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, NULL, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, Boss2Rocket64, + ai_charge, 2, Boss2Rocket64 +}; +static mmove_t boss2_move_attack_rocket2 = { FRAME_attack20, FRAME_attack39, boss2_frames_attack_rocket2, boss2_run }; + +static mframe_t boss2_frames_pain_heavy[] = +{ + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL +}; +static mmove_t boss2_move_pain_heavy = { FRAME_pain2, FRAME_pain19, boss2_frames_pain_heavy, boss2_run }; + +static mframe_t boss2_frames_pain_light[] = +{ + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL +}; +static mmove_t boss2_move_pain_light = { FRAME_pain20, FRAME_pain23, boss2_frames_pain_light, boss2_run }; + +static void boss2_shrink(edict_t *self) +{ + self->maxs[2] = boss2_is_small(self) ? 30 : 50; + gi.linkentity(self); +} + +static mframe_t boss2_frames_death[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, boss2_shrink, + ai_move, 0, NULL, +}; +static mmove_t boss2_move_death = { FRAME_death2, FRAME_death10, boss2_frames_death, boss2_dead }; + +static mframe_t boss2_frames_deathboss[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, boss2_shrink, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +static mmove_t boss2_move_deathboss = { FRAME_death2, FRAME_death50, boss2_frames_deathboss, boss2_dead }; + +static void boss2_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &boss2_move_stand; +} + +static void boss2_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &boss2_move_stand; + else + self->monsterinfo.currentmove = &boss2_move_run; +} + +static void boss2_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &boss2_move_walk; +} + +static void boss2_attack(edict_t *self) +{ + vec3_t delta; + float range; + + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + VectorSubtract(self->enemy->s.origin, self->s.origin, delta); + range = VectorLength(delta); + + if (range <= 125 || random() <= 0.6f) + self->monsterinfo.currentmove = boss2_is_hyper(self) ? &boss2_move_attack_hb : &boss2_move_attack_pre_mg; + else + self->monsterinfo.currentmove = boss2_is_hyper(self) ? &boss2_move_attack_rocket2 : &boss2_move_attack_rocket; + + M_DelayNextAttack(self, 1.0f + random() * 0.5f, true); +} + +static void boss2_attack_mg(edict_t *self) +{ + self->monsterinfo.currentmove = boss2_is_hyper(self) ? &boss2_move_attack_hb : &boss2_move_attack_mg; +} + +static void boss2_reattack_mg(edict_t *self) +{ + if (G_ValidTarget(self, self->enemy, false, true) && infront(self, self->enemy) && random() <= 0.7f) + boss2_attack_mg(self); + else + self->monsterinfo.currentmove = &boss2_move_attack_post_mg; +} + +static void boss2_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + (void)other; + (void)kick; + + if (self->health < (self->max_health / 2)) + self->s.skinnum = 1; + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0f; + if (damage < 10) + gi.sound(self, CHAN_VOICE, sound_pain3, 1, ATTN_NORM, 0); + else if (damage < 30) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + if (damage < 30) + self->monsterinfo.currentmove = &boss2_move_pain_light; + else + self->monsterinfo.currentmove = &boss2_move_pain_heavy; +} + +static void boss2_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + (void)inflictor; + (void)attacker; + (void)damage; + (void)point; + + M_Notify(self); + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + self->s.sound = 0; + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_NO; + vrx_update_drone_death_skin(self); + self->count = 0; + self->monsterinfo.aiflags &= ~AI_ALTERNATE_FLY; + self->monsterinfo.attack_state = AS_STRAIGHT; + VectorClear(self->velocity); + VectorClear(self->avelocity); + self->ideal_yaw = self->s.angles[YAW]; + + if (boss2_is_small(self)) + { + self->flags &= ~FL_FLY; + self->movetype = MOVETYPE_TOSS; + self->gravity = BOSS2_SMALL_DEATH_GRAVITY; + self->velocity[2] = BOSS2_SMALL_DEATH_FALL_SPEED; + } + + boss2_start_explosion_sequence(self); + self->monsterinfo.currentmove = boss2_is_small(self) ? &boss2_move_death : &boss2_move_deathboss; +} + +static void init_drone_boss2_common(edict_t *self, int variant) +{ + qboolean hyper = (variant == BOSS2_VARIANT_HYPER || variant == BOSS2_VARIANT_SMALL); + qboolean small = (variant == BOSS2_VARIANT_SMALL); + + sound_pain1 = gi.soundindex("bosshovr/bhvpain1.wav"); + sound_pain2 = gi.soundindex("bosshovr/bhvpain2.wav"); + sound_pain3 = gi.soundindex("bosshovr/bhvpain3.wav"); + sound_death = gi.soundindex("bosshovr/bhvdeth1.wav"); + sound_search1 = gi.soundindex("bosshovr/bhvunqv1.wav"); + gi.soundindex("tank/rocket.wav"); + gi.soundindex(hyper ? "flyer/flyatck3.wav" : "infantry/infatck1.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/boss2/tris.md2"); + if (small) + { + VectorSet(self->mins, -34, -34, 0); + VectorSet(self->maxs, 34, 34, 48); + } + else if (invasion->value) + { + VectorSet(self->mins, -42, -42, 0); + VectorSet(self->maxs, 42, 42, 60); + } + else + { + VectorSet(self->mins, -56, -56, 0); + VectorSet(self->maxs, 56, 56, 80); + } + + gi.modelindex("models/monsters/boss2/gibs/chaingun.md2"); + gi.modelindex("models/monsters/boss2/gibs/chest.md2"); + gi.modelindex("models/monsters/boss2/gibs/cpu.md2"); + gi.modelindex("models/monsters/boss2/gibs/engine.md2"); + gi.modelindex("models/monsters/boss2/gibs/head.md2"); + gi.modelindex("models/monsters/boss2/gibs/larm.md2"); + gi.modelindex("models/monsters/boss2/gibs/rarm.md2"); + gi.modelindex("models/monsters/boss2/gibs/rocket.md2"); + gi.modelindex("models/monsters/boss2/gibs/spine.md2"); + gi.modelindex("models/monsters/boss2/gibs/wing.md2"); + + if (small) + self->health = M_BOSS2_SMALL_INITIAL_HEALTH + M_BOSS2_SMALL_ADDON_HEALTH * self->monsterinfo.level; + else + self->health = M_BOSS2_INITIAL_HEALTH + M_BOSS2_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -200; + self->mass = small ? 1000 : 2000; + self->mtype = small ? M_BOSS2_SMALL : M_BOSS2; + self->style = variant; + self->yaw_speed = small ? 80 : 50; + self->flags |= FL_FLY | FL_IMMUNE_LASER; + self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; + boss2_set_fly_parameters(self); + self->s.sound = gi.soundindex("bosshovr/bhvengn1.wav"); + self->s.scale = small ? 0.6f : (invasion->value ? BOSS2_INVASION_SCALE : 1.0f); + + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + if (small) + self->monsterinfo.power_armor_power = M_BOSS2_SMALL_INITIAL_ARMOR + M_BOSS2_SMALL_ADDON_ARMOR * self->monsterinfo.level; + else + self->monsterinfo.power_armor_power = M_BOSS2_INITIAL_ARMOR + M_BOSS2_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = small ? M_HOVER_CONTROL_COST : M_JORG_CONTROL_COST; + self->monsterinfo.cost = small ? M_HOVER_COST : M_COMMANDER_COST; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.sight_range = 1024; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + + self->pain = boss2_pain; + self->die = boss2_die; + self->monsterinfo.stand = boss2_stand; + self->monsterinfo.walk = boss2_walk; + self->monsterinfo.run = boss2_run; + self->monsterinfo.attack = boss2_attack; + self->monsterinfo.idle = boss2_search; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &boss2_move_stand; + self->monsterinfo.scale = MODEL_SCALE * self->s.scale; + self->nextthink = level.time + FRAMETIME; + + if (!small && !invasion->value) + G_PrintGreenText(va("A level %d hornet%s has spawned!", self->monsterinfo.level, hyper ? " hyper" : "")); +} + +void init_drone_boss2(edict_t *self) +{ + init_drone_boss2_common(self, BOSS2_VARIANT_MG); +} + +void init_drone_boss2_hyper(edict_t *self) +{ + init_drone_boss2_common(self, BOSS2_VARIANT_HYPER); +} + +void init_drone_boss2_small(edict_t *self) +{ + init_drone_boss2_common(self, BOSS2_VARIANT_SMALL); +} diff --git a/src/entities/drone/drone_brain.c b/src/entities/drone/drone_brain.c index f8b4fe80..1eadb31f 100644 --- a/src/entities/drone/drone_brain.c +++ b/src/entities/drone/drone_brain.c @@ -27,6 +27,7 @@ static int sound_thud; void mybrain_attack (edict_t *self); void mybrain_attack3 (edict_t *self); +static qboolean mybrain_is_dodge_move(edict_t *self); void mybrain_sight (edict_t *self, edict_t *other) { @@ -241,7 +242,8 @@ void mybrain_pain(edict_t* self, edict_t* other, float kick, int damage) if (self->monsterinfo.currentmove == &mybrain_move_pain_long || self->monsterinfo.currentmove == &mybrain_move_pain_short1 || self->monsterinfo.currentmove == &mybrain_move_pain_short2 || - self->monsterinfo.currentmove == &mybrain_move_defense) + self->monsterinfo.currentmove == &mybrain_move_defense || + mybrain_is_dodge_move(self)) return; // monster players don't get pain state induced @@ -299,6 +301,12 @@ void mybrain_duck_up (edict_t *self) gi.linkentity (self); } +void mybrain_duck_hold (edict_t *self) +{ + if (self->monsterinfo.pausetime > level.time) + self->monsterinfo.nextframe = self->s.frame; +} + void mybrain_jump_takeoff (edict_t *self) { vec3_t v; @@ -333,16 +341,22 @@ void mybrain_jump_hold (edict_t *self) self->monsterinfo.aiflags |= AI_HOLD_FRAME; } +static void mybrain_jump_hold_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + mybrain_jump_hold(self); +} + mframe_t mybrain_frames_duck [] = { - ai_move, 0, mybrain_duck_down, - ai_move, 0, NULL,//mybrain_duck_down, - ai_move, 0, NULL, + ai_move, 1, mybrain_duck_down, + ai_move, 1, NULL, + ai_move, 1, mybrain_duck_hold, ai_move, 0, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, ai_move, 0, mybrain_duck_up, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL + ai_move, -1, NULL }; mmove_t mybrain_move_duck = {FRAME_duck01, FRAME_duck08, mybrain_frames_duck, mybrain_run}; @@ -350,7 +364,7 @@ mframe_t mybrain_frames_jump [] = { ai_move, 0, NULL, ai_move, 0, mybrain_jump_takeoff, - ai_move, 0, mybrain_jump_hold, + mybrain_jump_hold_ai, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -437,12 +451,18 @@ void mybrain_jumpattack_hold (edict_t *self) } } +static void mybrain_jumpattack_hold_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + mybrain_jumpattack_hold(self); +} + mframe_t mybrain_frames_jumpattack [] = { ai_move, 0, NULL, ai_move, 0, mybrain_jumpattack_takeoff, - ai_move, 0, mybrain_jumpattack_hold, + mybrain_jumpattack_hold_ai, 0, NULL, ai_move, 0, NULL, ai_move, 0, mybrain_jumpattack_landing, ai_move, 0, NULL, @@ -457,22 +477,34 @@ void mybrain_jump (edict_t *self) self->monsterinfo.currentmove = &mybrain_move_jump; } +static qboolean mybrain_is_dodge_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &mybrain_move_duck || + self->monsterinfo.currentmove == &mybrain_move_jump || + self->monsterinfo.currentmove == &mybrain_move_jumpattack; +} + void mybrain_dodge (edict_t *self, edict_t *attacker, vec3_t dir, int radius) { if (random() > 0.9) return; - if (!G_GetClient(self)) - return; if (level.time < self->monsterinfo.dodge_time) return; + if (!attacker) + return; if (OnSameTeam(self, attacker)) return; + if (mybrain_is_dodge_move(self)) + return; - if (!self->enemy && G_EntIsAlive(attacker)) + self->monsterinfo.attacker = attacker; + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) self->enemy = attacker; if (!radius) { + self->monsterinfo.pausetime = level.time + 0.5; self->monsterinfo.currentmove = &mybrain_move_duck; + mybrain_duck_down(self); self->monsterinfo.dodge_time = level.time + 2.0; } else @@ -482,11 +514,18 @@ void mybrain_dodge (edict_t *self, edict_t *attacker, vec3_t dir, int radius) } } +static void mybrain_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t mybrain_frames_death2 [] = { ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, mybrain_shrink, ai_move, 9, NULL, ai_move, 0, NULL }; @@ -497,7 +536,7 @@ mframe_t mybrain_frames_death1 [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, -2, NULL, - ai_move, 9, NULL, + ai_move, 9, mybrain_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -724,9 +763,188 @@ mmove_t mybrain_move_attack3 = {FRAME_attak205, FRAME_attak217, mybrain_frames_a void mybrain_attack3 (edict_t *self) { + gi.sound(self, CHAN_WEAPON, sound_tentacles_extend, 1, ATTN_NORM, 0); self->monsterinfo.currentmove = &mybrain_move_attack3; } +static const vec3_t brain_reye[] = +{ + {0.746700f, 0.238370f, 34.167690f}, + {-1.076390f, 0.238370f, 33.386372f}, + {-1.335500f, 5.334300f, 32.177170f}, + {-0.175360f, 8.846370f, 30.635479f}, + {-2.757590f, 7.804610f, 30.150860f}, + {-5.575090f, 5.152840f, 30.056160f}, + {-7.017550f, 3.262470f, 30.552521f}, + {-7.915740f, 0.638800f, 33.176189f}, + {-3.915390f, 8.285730f, 33.976349f}, + {-0.913540f, 10.933030f, 34.141811f}, + {-0.369900f, 8.923900f, 34.189079f} +}; + +static const vec3_t brain_leye[] = +{ + {-3.364710f, 0.327750f, 33.938381f}, + {-5.140450f, 0.493480f, 32.659851f}, + {-5.341980f, 5.646980f, 31.277901f}, + {-4.134480f, 9.277440f, 29.925621f}, + {-6.598340f, 6.815090f, 29.322620f}, + {-8.610840f, 2.529650f, 29.251591f}, + {-9.231360f, 0.093280f, 29.747959f}, + {-11.004110f, 1.936930f, 32.395260f}, + {-7.878310f, 7.648190f, 33.148151f}, + {-4.947370f, 11.430050f, 33.313610f}, + {-4.332820f, 9.444570f, 33.526340f} +}; + +static int mybrain_eye_frame_index(edict_t *self) +{ + int frame_index = self->s.frame - FRAME_walk101; + + if (frame_index < 0 || frame_index >= 11) + frame_index = 0; + + return frame_index; +} + +static void mybrain_project_eye_origin(edict_t *self, qboolean left_eye, vec3_t start) +{ + vec3_t forward, right, up; + const vec3_t *eye_offsets = left_eye ? brain_leye : brain_reye; + int frame_index = mybrain_eye_frame_index(self); + + AngleVectors(self->s.angles, forward, right, up); + VectorCopy(self->s.origin, start); + VectorMA(start, eye_offsets[frame_index][0], right, start); + VectorMA(start, eye_offsets[frame_index][1], forward, start); + VectorMA(start, eye_offsets[frame_index][2], up, start); +} + +static qboolean mybrain_trace_eye_target(edict_t *self, vec3_t start, vec3_t target) +{ + trace_t tr; + + tr = gi.trace(start, NULL, NULL, target, self, MASK_SHOT); + return tr.ent && tr.ent == self->enemy; +} + +static void mybrain_aim_eye_laser(edict_t *self, vec3_t start, vec3_t aim) +{ + vec3_t target, predicted; + float offset; + trace_t tr; + + VectorCopy(self->enemy->s.origin, target); + if (!mybrain_trace_eye_target(self, start, target)) + { + G_EntViewPoint(self->enemy, target); + if (!mybrain_trace_eye_target(self, start, target)) + G_EntMidPoint(self->enemy, target); + } + + offset = 0.10f + random() * 0.10f; + VectorMA(target, -offset, self->enemy->velocity, predicted); + tr = gi.trace(start, NULL, NULL, predicted, self, MASK_SOLID); + if (tr.fraction < 0.9f) + VectorCopy(target, predicted); + + VectorSubtract(predicted, start, aim); + VectorNormalize(aim); +} + +static qboolean mybrain_has_eye_laser_shot(edict_t *self, qboolean left_eye) +{ + vec3_t start; + + mybrain_project_eye_origin(self, left_eye, start); + return M_MonsterHasClearShotFrom(self, start); +} + +static qboolean mybrain_has_laser_shot(edict_t *self) +{ + return mybrain_has_eye_laser_shot(self, false) || mybrain_has_eye_laser_shot(self, true); +} + +static void mybrain_eye_laser_update(edict_t *laser, qboolean left_eye) +{ + edict_t *self; + vec3_t start, aim; + qboolean do_damage; + + self = laser->owner; + if (!self || !self->inuse || !G_EntExists(self->enemy)) + { + laser->spawnflags |= DABEAM_SPAWNED; + return; + } + + mybrain_project_eye_origin(self, left_eye, start); + if (!M_MonsterHasClearShotFrom(self, start)) + { + laser->spawnflags |= DABEAM_SPAWNED; + return; + } + + mybrain_aim_eye_laser(self, start, aim); + + VectorCopy(start, laser->s.origin); + VectorCopy(aim, laser->movedir); + + do_damage = !(laser->spawnflags & DABEAM_SPAWNED); + dabeam_update(laser, do_damage); + laser->spawnflags |= DABEAM_SPAWNED; +} + +static void mybrain_right_eye_laser_update(edict_t *laser) +{ + mybrain_eye_laser_update(laser, false); +} + +static void mybrain_left_eye_laser_update(edict_t *laser) +{ + mybrain_eye_laser_update(laser, true); +} + +static void mybrain_laserbeam(edict_t *self) +{ + int damage; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_DABEAM_DMG_BASE + M_DABEAM_DMG_ADDON * drone_damagelevel(self); + if (M_DABEAM_DMG_MAX && damage > M_DABEAM_DMG_MAX) + damage = M_DABEAM_DMG_MAX; + + if (mybrain_has_eye_laser_shot(self, false)) + monster_fire_dabeam(self, damage, false, mybrain_right_eye_laser_update); + if (mybrain_has_eye_laser_shot(self, true)) + monster_fire_dabeam(self, damage, true, mybrain_left_eye_laser_update); +} + +static void mybrain_laserbeam_reattack(edict_t *self) +{ + if (G_EntExists(self->enemy) && visible(self, self->enemy) && self->enemy->health > 0 + && mybrain_has_laser_shot(self) && random() < 0.5) + self->monsterinfo.nextframe = FRAME_walk101; +} + +mframe_t mybrain_frames_attack4[] = +{ + ai_charge, 9, mybrain_laserbeam, + ai_charge, 2, mybrain_laserbeam, + ai_charge, 3, mybrain_laserbeam, + ai_charge, 3, mybrain_laserbeam, + ai_charge, 1, mybrain_laserbeam, + ai_charge, 0, mybrain_laserbeam, + ai_charge, 0, mybrain_laserbeam, + ai_charge, 10, mybrain_laserbeam, + ai_charge, -4, mybrain_laserbeam, + ai_charge, -1, mybrain_laserbeam, + ai_charge, 2, mybrain_laserbeam_reattack +}; +mmove_t mybrain_move_attack4 = {FRAME_walk101, FRAME_walk111, mybrain_frames_attack4, mybrain_run}; + void mybrain_melee (edict_t *self) { if (entdist(self, self->enemy) < MELEE_DISTANCE) @@ -741,11 +959,16 @@ void mybrain_melee (edict_t *self) void mybrain_attack (edict_t *self) { float dist; + qboolean has_los; dist = entdist(self, self->enemy); + has_los = visible(self, self->enemy); + // laser attack + if (has_los && dist >= 192 && dist <= 640 && mybrain_has_laser_shot(self) && random() < 0.15) + self->monsterinfo.currentmove = &mybrain_move_attack4; // jump to our enemy if he's close and on even ground - if ((dist > 256) && (self->enemy->absmin[2]+18 >= self->absmin[2]) + else if ((dist > 256) && (self->enemy->absmin[2]+18 >= self->absmin[2]) && (self->enemy->absmin[2]-18 <= self->absmin[2]) && !(self->monsterinfo.aiflags & AI_STAND_GROUND)) self->monsterinfo.currentmove = &mybrain_move_jumpattack; @@ -819,8 +1042,6 @@ void mybrain_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *s void mybrain_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; - M_Notify(self); // reduce lag by removing the entity right away @@ -840,14 +1061,7 @@ void mybrain_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); #else @@ -866,6 +1080,7 @@ void mybrain_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama gi.sound (self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); if (random() <= 0.5) self->monsterinfo.currentmove = &mybrain_move_death1; else @@ -936,8 +1151,7 @@ void init_drone_brain (edict_t *self) self->monsterinfo.attack = mybrain_attack; self->monsterinfo.melee = mybrain_melee; self->monsterinfo.sight = mybrain_sight; -// self->monsterinfo.search = mybrain_search; - //self->monsterinfo.idle = mybrain_idle; + self->monsterinfo.idle = mybrain_search; self->monsterinfo.jumpup = 64; self->monsterinfo.jumpdn = 512; diff --git a/src/entities/drone/drone_carrier.c b/src/entities/drone/drone_carrier.c new file mode 100644 index 00000000..f0171491 --- /dev/null +++ b/src/entities/drone/drone_carrier.c @@ -0,0 +1,1008 @@ +/* +============================================================================== + +carrier + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_rogue_carrier.h" + +#define CARRIER_SUMMON_COUNT 4 +#define CARRIER_SUMMON_COOLDOWN 6.0f +#define CARRIER_HEAT_TURN_FRACTION 0.085f +#define CARRIER_NO_SPAWN_Z -99999.0f +#define CARRIER_NO_SPAWN_YAW -99999.0f +#define CARRIER_YAW_SPEED 20.0f +#define CARRIER_SPAWN_YAW_SPEED 30.0f +#define CARRIER_AI_SPAWNING 0x00400000 +#define CARRIER_RAIL_REFIRE_CHANCE 0.65f +#define CARRIER_RAIL_MAX_REFIRES 1 +#define CARRIER_DEFAULT_SCALE 0.75f +#define CARRIER_INVASION_SCALE 0.50f // 0.60 was too big for spambox + +static int sound_pain1; +static int sound_pain2; +static int sound_pain3; +static int sound_death; +static int sound_sight; +static int sound_rail; +static int sound_spawn; + +void drone_ai_stand(edict_t *self, float dist); +void drone_ai_run(edict_t *self, float dist); +void drone_ai_walk(edict_t *self, float dist); +qboolean drone_findtarget(edict_t *self, qboolean force); + +static void carrier_stand(edict_t *self); +static void carrier_walk(edict_t *self); +static void carrier_run(edict_t *self); +static void carrier_attack(edict_t *self); +static void carrier_attack_grenade(edict_t *self); +static void carrier_reattack_grenade(edict_t *self); +static void carrier_attack_rail(edict_t *self); +static void carrier_reattack_rail(edict_t *self); +static void carrier_turn_to_spawn_yaw(edict_t *self); + +static void carrier_project_flash(edict_t *self, int flash, vec3_t forward, vec3_t start) +{ + vec3_t right, offset; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[flash], offset); + if (self->s.scale && self->s.scale != 1.0f) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static void carrier_set_fly_parameters(edict_t *self) +{ + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_acceleration = 20.0f; + self->monsterinfo.fly_speed = 120.0f; + self->monsterinfo.fly_above = false; + self->monsterinfo.fly_min_distance = 375.0f; + self->monsterinfo.fly_max_distance = 650.0f; +} + +static void carrier_spawn_ai(edict_t *self, float dist) +{ + if (!self || !self->inuse) + return; + + if (que_typeexists(self->curses, CURSE_FROZEN)) + return; + + VectorClear(self->velocity); + VectorClear(self->avelocity); + if (self->monsterinfo.aiflags & CARRIER_AI_SPAWNING) + carrier_turn_to_spawn_yaw(self); + (void)dist; +} + +static void carrier_restore_spawn_steering(edict_t *self) +{ + if (self->monsterinfo.aiflags & CARRIER_AI_SPAWNING) + { + if (self->delay > 0.0f) + { + self->yaw_speed = self->delay; + self->delay = 0.0f; + } + else + self->yaw_speed = CARRIER_YAW_SPEED; + } + + self->monsterinfo.aiflags &= ~(AI_HOLD_FRAME | CARRIER_AI_SPAWNING); + VectorClear(self->avelocity); +} + +static const int carrier_summons[CARRIER_SUMMON_COUNT] = +{ + M_DAEDALUS, + M_FLYER, + M_HOVER, + M_FLOATER +}; + +static void carrier_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); +} + +static void carrier_explode(edict_t *self) +{ + vec3_t org; + + VectorCopy(self->s.origin, org); + org[0] += crandom() * self->maxs[0]; + org[1] += crandom() * self->maxs[1]; + org[2] += crandom() * self->maxs[2]; + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_EXPLOSION1_BIG); + gi.WritePosition(org); + gi.multicast(self->s.origin, MULTICAST_PVS); +} + +static void carrier_dead(edict_t *self) +{ + vrx_throw_drone_gibs(self, 500); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_EXPLOSION1_BIG); + gi.WritePosition(self->s.origin); + gi.multicast(self->s.origin, MULTICAST_PVS); + + M_Remove(self, false, false); +} + +static void carrier_fire_heat(edict_t *self) +{ + int damage; + int speed; + vec3_t forward, start; + const int flashes[4] = + { + MZ2_CARRIER_ROCKET_1, + MZ2_CARRIER_ROCKET_2, + MZ2_CARRIER_ROCKET_3, + MZ2_CARRIER_ROCKET_4 + }; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_ROCKETLAUNCHER_DMG_BASE + M_ROCKETLAUNCHER_DMG_ADDON * drone_damagelevel(self); + if (M_ROCKETLAUNCHER_DMG_MAX && damage > M_ROCKETLAUNCHER_DMG_MAX) + damage = M_ROCKETLAUNCHER_DMG_MAX; + speed = M_ROCKETLAUNCHER_SPEED_BASE + M_ROCKETLAUNCHER_SPEED_ADDON * drone_damagelevel(self); + if (M_ROCKETLAUNCHER_SPEED_MAX && speed > M_ROCKETLAUNCHER_SPEED_MAX) + speed = M_ROCKETLAUNCHER_SPEED_MAX; + + for (int i = 0; i < 4; i++) + { + carrier_project_flash(self, flashes[i], forward, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, true, flashes[i], forward, start); + monster_fire_heat(self, start, forward, damage, speed, flashes[i], CARRIER_HEAT_TURN_FRACTION); + } +} + +static void carrier_fire_bullets(edict_t *self) +{ + int damage; + vec3_t forward, start; + const int flashes[2][2] = + { + { MZ2_CARRIER_MACHINEGUN_L1, MZ2_CARRIER_MACHINEGUN_R1 }, + { MZ2_CARRIER_MACHINEGUN_L2, MZ2_CARRIER_MACHINEGUN_R2 } + }; + const int flash_row = self->monsterinfo.lefty ? 1 : 0; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_MACHINEGUN_DMG_BASE + M_MACHINEGUN_DMG_ADDON * drone_damagelevel(self); + if (M_MACHINEGUN_DMG_MAX && damage > M_MACHINEGUN_DMG_MAX) + damage = M_MACHINEGUN_DMG_MAX; + + for (int i = 0; i < 2; i++) + { + carrier_project_flash(self, flashes[flash_row][i], forward, start); + MonsterAim(self, M_PROJECTILE_ACC, 0, false, -1, forward, start); + monster_fire_bullet(self, start, forward, damage, damage, DEFAULT_BULLET_HSPREAD, DEFAULT_BULLET_VSPREAD, flashes[flash_row][i]); + } + + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; +} + +static void carrier_fire_grenade(edict_t *self) +{ + int damage; + int speed; + vec3_t forward, right, up, start, target, aim, offset; + float direction; + float spread_r, spread_u; + int mytime; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_GRENADELAUNCHER_DMG_BASE + M_GRENADELAUNCHER_DMG_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_DMG_MAX && damage > M_GRENADELAUNCHER_DMG_MAX) + damage = M_GRENADELAUNCHER_DMG_MAX; + speed = M_GRENADELAUNCHER_SPEED_BASE + M_GRENADELAUNCHER_SPEED_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_SPEED_MAX && speed > M_GRENADELAUNCHER_SPEED_MAX) + speed = M_GRENADELAUNCHER_SPEED_MAX; + + AngleVectors(self->s.angles, forward, right, up); + VectorCopy(monster_flash_offset[MZ2_CARRIER_GRENADE], offset); + if (self->s.scale && self->s.scale != 1.0f) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); + + VectorCopy(self->enemy->s.origin, target); + target[2] += self->enemy->viewheight; + VectorSubtract(target, start, aim); + VectorNormalize(aim); + + direction = (random() < 0.5f) ? -1.0f : 1.0f; + mytime = (int)((level.time - self->timestamp) / 0.4f); + switch (mytime) + { + case 0: + spread_r = 0.15f * direction; + spread_u = 0.1f - 0.1f * direction; + break; + case 1: + spread_r = 0.0f; + spread_u = 0.1f; + break; + case 2: + spread_r = -0.15f * direction; + spread_u = 0.1f + 0.1f * direction; + break; + case 3: + spread_r = 0.0f; + spread_u = 0.1f; + break; + default: + spread_r = 0.0f; + spread_u = 0.0f; + break; + } + + VectorMA(aim, spread_r, right, aim); + VectorMA(aim, spread_u, up, aim); + VectorNormalize(aim); + if (aim[2] > 0.15f) + aim[2] = 0.15f; + else if (aim[2] < -0.5f) + aim[2] = -0.5f; + + monster_fire_grenade(self, start, aim, damage, speed, MZ2_CARRIER_GRENADE); +} + +static void carrier_fire_rail(edict_t *self) +{ + int damage; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_RAILGUN_DMG_BASE + M_RAILGUN_DMG_ADDON * drone_damagelevel(self); + if (M_RAILGUN_DMG_MAX && damage > M_RAILGUN_DMG_MAX) + damage = M_RAILGUN_DMG_MAX; + + carrier_project_flash(self, MZ2_CARRIER_RAILGUN, forward, start); + if (self->pos2[2] > CARRIER_NO_SPAWN_Z + 1.0f) + { + VectorSubtract(self->pos2, start, forward); + VectorNormalize(forward); + } + else + MonsterAim(self, 0.25f, 0, false, MZ2_CARRIER_RAILGUN, forward, start); + gi.sound(self, CHAN_WEAPON, sound_rail, 1, ATTN_NORM, 0); + monster_fire_railgun(self, start, forward, damage, damage, MZ2_CARRIER_RAILGUN); +} + +static void carrier_save_rail_target(edict_t *self) +{ + if (!G_EntExists(self->enemy)) + return; + + VectorCopy(self->enemy->s.origin, self->pos2); + self->pos2[2] += self->enemy->viewheight; +} + +static qboolean carrier_valid_spawn_spot(edict_t *self, vec3_t mins, vec3_t maxs, vec3_t spot) +{ + if (G_IsValidLocation(self, spot, mins, maxs)) + return true; + + spot[2] += 32; + if (G_IsValidLocation(self, spot, mins, maxs)) + return true; + + spot[2] -= 64; + return G_IsValidLocation(self, spot, mins, maxs); +} + +static void carrier_spawn_basis(edict_t *self, vec3_t forward, vec3_t right) +{ + vec3_t angles; + + VectorCopy(self->s.angles, angles); + if ((self->monsterinfo.aiflags & CARRIER_AI_SPAWNING) + && self->angle > CARRIER_NO_SPAWN_YAW + 1.0f) + angles[YAW] = self->angle; + + AngleVectors(angles, forward, right, NULL); +} + +static int carrier_spawn_type(int index) +{ + return carrier_summons[index % CARRIER_SUMMON_COUNT]; +} + +static void carrier_spawn_bounds(int mtype, vec3_t mins, vec3_t maxs) +{ + switch (mtype) + { + case M_FLYER: + VectorSet(mins, -16, -16, -24); + VectorSet(maxs, 16, 16, 8); + break; + case M_FLOATER: + VectorSet(mins, -24, -24, -24); + VectorSet(maxs, 24, 24, 40); + break; + case M_DAEDALUS: + case M_HOVER: + default: + VectorSet(mins, -24, -24, -24); + VectorSet(maxs, 24, 24, 32); + break; + } +} + +static qboolean carrier_find_spawn_spot(edict_t *self, vec3_t mins, vec3_t maxs, int index, vec3_t spot) +{ + vec3_t forward, right; + static const vec3_t local_offsets[8] = + { + { 105.0f, 0.0f, -58.0f }, + { 150.0f, 0.0f, -58.0f }, + { 200.0f, 0.0f, -58.0f }, + { 140.0f, 72.0f, -58.0f }, + { 140.0f, -72.0f, -58.0f }, + { 190.0f, 96.0f, -48.0f }, + { 190.0f, -96.0f, -48.0f }, + { 220.0f, 0.0f, -32.0f } + }; + static const float ring_dirs[8][2] = + { + { 1.0f, 0.0f }, + { 0.707f, 0.707f }, + { 0.0f, 1.0f }, + { -0.707f, 0.707f }, + { -1.0f, 0.0f }, + { -0.707f,-0.707f }, + { 0.0f, -1.0f }, + { 0.707f,-0.707f } + }; + static const float radii[3] = { 160.0f, 240.0f, 320.0f }; + static const float z_offsets[3] = { -58.0f, -32.0f, 8.0f }; + + carrier_spawn_basis(self, forward, right); + + for (int i = 0; i < 8; i++) + { + const int offset_index = i; + + VectorCopy(self->s.origin, spot); + VectorMA(spot, local_offsets[offset_index][0], forward, spot); + VectorMA(spot, local_offsets[offset_index][1], right, spot); + spot[2] += local_offsets[offset_index][2]; + if (carrier_valid_spawn_spot(self, mins, maxs, spot)) + return true; + } + + for (int radius_index = 0; radius_index < 3; radius_index++) + { + for (int z_index = 0; z_index < 3; z_index++) + { + for (int dir_index = 0; dir_index < 8; dir_index++) + { + const int rotated_index = dir_index; + + VectorCopy(self->s.origin, spot); + VectorMA(spot, radii[radius_index] * ring_dirs[rotated_index][0], forward, spot); + VectorMA(spot, radii[radius_index] * ring_dirs[rotated_index][1], right, spot); + spot[2] += z_offsets[z_index]; + if (carrier_valid_spawn_spot(self, mins, maxs, spot)) + return true; + } + } + } + + return false; +} + +static void carrier_cleanup_failed_spawn(edict_t *owner, edict_t *spawned) +{ + if (owner && owner->client) + layout_remove_tracked_entity(&owner->client->layout, spawned); + + DroneList_Remove(spawned); + AI_EnemyRemoved(spawned); + G_FreeEdict(spawned); +} + +static void carrier_setup_invasion_spawn(edict_t *spawned) +{ + if (!invasion->value) + return; + + spawned->monsterinfo.aiflags &= ~AI_STAND_GROUND; + spawned->monsterinfo.aiflags |= AI_FIND_NAVI; + spawned->prev_navi = NULL; + spawned->goalentity = NULL; +} + +static void carrier_start_spawned_monster(edict_t *self, edict_t *spawned) +{ + const qboolean force_start = invasion->value || pvm->value; + + if (G_ValidTarget(spawned, self->enemy, !force_start, true)) + { + spawned->enemy = self->enemy; + VectorCopy(self->enemy->s.origin, spawned->monsterinfo.last_sighting); + } + else if (force_start) + drone_findtarget(spawned, true); + + if ((spawned->enemy || spawned->goalentity) && spawned->monsterinfo.run) + spawned->monsterinfo.run(spawned); + else if (spawned->monsterinfo.stand) + spawned->monsterinfo.stand(spawned); +} + +static qboolean carrier_spawn_monster(edict_t *self, int index) +{ + edict_t *owner; + edict_t *spawned; + vec3_t saved_spot; + vec3_t spot; + + owner = (self->activator && self->activator->inuse) ? self->activator : self; + spawned = G_Spawn(); + spawned->mtype = carrier_spawn_type(index); + spawned->activator = owner; + spawned->monsterinfo.level = self->monsterinfo.level; + + if (!M_Initialize(owner, spawned, 0.0f)) + { + G_FreeEdict(spawned); + return false; + } + + VectorCopy(self->pos1, saved_spot); + if (saved_spot[2] > CARRIER_NO_SPAWN_Z + 1.0f && G_IsValidLocation(self, saved_spot, spawned->mins, spawned->maxs)) + VectorCopy(saved_spot, spot); + else if (!carrier_find_spawn_spot(self, spawned->mins, spawned->maxs, index, spot)) + { + carrier_cleanup_failed_spawn(owner, spawned); + return false; + } + + spawned->monsterinfo.cost = 0; + spawned->s.effects |= EF_PLASMA; + VectorCopy(spot, spawned->s.origin); + VectorCopy(spot, spawned->s.old_origin); + VectorCopy(self->s.angles, spawned->s.angles); + spawned->nextthink = level.time + FRAMETIME; + spawned->monsterinfo.attack_finished = level.time + 1.0; + carrier_setup_invasion_spawn(spawned); + + gi.linkentity(spawned); + owner->num_monsters += spawned->monsterinfo.control_cost; + owner->num_monsters_real++; + + if (sound_spawn) + gi.sound(self, CHAN_BODY, sound_spawn, 1, ATTN_NORM, 0); + carrier_start_spawned_monster(self, spawned); + + return true; +} + +static qboolean carrier_spawngrow(edict_t *self, int index) +{ + vec3_t mins, maxs, size, spot; + float radius; + + carrier_spawn_bounds(carrier_spawn_type(index), mins, maxs); + VectorSubtract(maxs, mins, size); + radius = VectorLength(size) * 0.5f; + + VectorSet(self->pos1, 0, 0, CARRIER_NO_SPAWN_Z); + if (!carrier_find_spawn_spot(self, mins, maxs, index, spot)) + return false; + + VectorCopy(spot, self->pos1); + SpawnGrow_Spawn(spot, radius, radius * 2.0f); + return true; +} + +static float carrier_yaw_delta(float a, float b) +{ + float delta = anglemod(a - b); + + if (delta > 180.0f) + delta = 360.0f - delta; + return delta; +} + +static qboolean carrier_set_spawn_base_yaw(edict_t *self) +{ + vec3_t to_enemy; + + if (!G_EntExists(self->enemy)) + return false; + + VectorSubtract(self->enemy->s.origin, self->s.origin, to_enemy); + self->move_angles[YAW] = vectoyaw(to_enemy); + return true; +} + +static void carrier_set_spawn_yaw(edict_t *self) +{ + static const float yaw_offsets[CARRIER_SUMMON_COUNT] = { -30.0f, 0.0f, 30.0f, 0.0f }; + + self->angle = anglemod(self->move_angles[YAW] + yaw_offsets[self->count % CARRIER_SUMMON_COUNT]); + self->ideal_yaw = self->angle; + self->teleport_time = level.time + 1.0f; +} + +static void carrier_turn_to_spawn_yaw(edict_t *self) +{ + if (self->angle <= CARRIER_NO_SPAWN_YAW + 1.0f) + return; + + VectorClear(self->velocity); + VectorClear(self->avelocity); + self->ideal_yaw = self->angle; + M_ChangeYaw(self); +} + +static void carrier_prep_spawn(edict_t *self) +{ + self->count = 0; + self->timestamp = level.time; + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + if (!(self->monsterinfo.aiflags & CARRIER_AI_SPAWNING)) + self->delay = self->yaw_speed > 0.0f ? self->yaw_speed : CARRIER_YAW_SPEED; + self->monsterinfo.aiflags |= CARRIER_AI_SPAWNING; + self->yaw_speed = CARRIER_SPAWN_YAW_SPEED; + self->angle = CARRIER_NO_SPAWN_YAW; + VectorClear(self->velocity); + VectorClear(self->avelocity); + VectorSet(self->pos1, 0, 0, CARRIER_NO_SPAWN_Z); + if (carrier_set_spawn_base_yaw(self)) + carrier_set_spawn_yaw(self); +} + +static void carrier_start_spawn(edict_t *self) +{ + if (self->angle <= CARRIER_NO_SPAWN_YAW + 1.0f + && carrier_set_spawn_base_yaw(self)) + carrier_set_spawn_yaw(self); +} + +static void carrier_ready_spawn(edict_t *self) +{ + if (self->angle <= CARRIER_NO_SPAWN_YAW + 1.0f) + { + if (!carrier_set_spawn_base_yaw(self)) + { + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + return; + } + carrier_set_spawn_yaw(self); + } + + if (carrier_yaw_delta(self->s.angles[YAW], self->angle) > 0.5f + && level.time < self->teleport_time) + { + self->monsterinfo.aiflags |= AI_HOLD_FRAME; + return; + } + + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + carrier_spawngrow(self, self->count); +} + +static void carrier_spawn_check(edict_t *self) +{ + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + if (self->count < CARRIER_SUMMON_COUNT) + { + carrier_spawn_monster(self, self->count); + self->count++; + } + + if (self->count < CARRIER_SUMMON_COUNT) + { + self->angle = CARRIER_NO_SPAWN_YAW; + VectorSet(self->pos1, 0, 0, CARRIER_NO_SPAWN_Z); + carrier_set_spawn_yaw(self); + self->monsterinfo.nextframe = FRAME_spawn08; + } + else + self->monsterinfo.melee_finished = level.time + CARRIER_SUMMON_COOLDOWN; +} + +static void carrier_done_spawn(edict_t *self) +{ + self->count = 0; + carrier_restore_spawn_steering(self); + self->angle = CARRIER_NO_SPAWN_YAW; + VectorSet(self->pos1, 0, 0, CARRIER_NO_SPAWN_Z); +} + +mframe_t carrier_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t carrier_move_stand = { FRAME_search01, FRAME_search13, carrier_frames_stand, NULL }; + +static void carrier_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &carrier_move_stand; +} + +mframe_t carrier_frames_run[] = +{ + drone_ai_run, 12, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 20, NULL, + drone_ai_run, 20, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 12, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL +}; +mmove_t carrier_move_run = { FRAME_search01, FRAME_search13, carrier_frames_run, NULL }; + +static void carrier_run(edict_t *self) +{ + carrier_restore_spawn_steering(self); + + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &carrier_move_stand; + else + self->monsterinfo.currentmove = &carrier_move_run; +} + +static void carrier_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &carrier_move_run; +} + +mframe_t carrier_frames_attack_mg[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, carrier_fire_bullets, + ai_charge, 0, carrier_fire_bullets, + ai_charge, 0, carrier_fire_bullets, + ai_charge, 0, NULL +}; +mmove_t carrier_move_attack_mg = { FRAME_firea06, FRAME_firea11, carrier_frames_attack_mg, carrier_run }; + +mframe_t carrier_frames_attack_heat[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, carrier_fire_heat, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t carrier_move_attack_heat = { FRAME_fireb01, FRAME_fireb04, carrier_frames_attack_heat, carrier_run }; + +mframe_t carrier_frames_attack_pre_grenade[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, carrier_attack_grenade +}; +mmove_t carrier_move_attack_pre_grenade = { FRAME_fireb01, FRAME_fireb06, carrier_frames_attack_pre_grenade, NULL }; + +mframe_t carrier_frames_attack_grenade[] = +{ + ai_charge, -15, carrier_fire_grenade, + ai_charge, 4, NULL, + ai_charge, 4, NULL, + ai_charge, 4, carrier_reattack_grenade +}; +mmove_t carrier_move_attack_grenade = { FRAME_fireb07, FRAME_fireb10, carrier_frames_attack_grenade, NULL }; + +mframe_t carrier_frames_attack_post_grenade[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t carrier_move_attack_post_grenade = { FRAME_fireb11, FRAME_fireb16, carrier_frames_attack_post_grenade, carrier_run }; + +mframe_t carrier_frames_attack_rail[] = +{ + ai_charge, 2, NULL, + ai_charge, 2, carrier_save_rail_target, + ai_charge, 2, NULL, + ai_charge, -20, carrier_fire_rail, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, carrier_reattack_rail +}; +mmove_t carrier_move_attack_rail = { FRAME_search01, FRAME_search09, carrier_frames_attack_rail, carrier_run }; + +mframe_t carrier_frames_spawn[] = +{ + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, carrier_prep_spawn, + carrier_spawn_ai, -2, carrier_start_spawn, + carrier_spawn_ai, -2, carrier_ready_spawn, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -10, carrier_spawn_check, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, NULL, + carrier_spawn_ai, -2, carrier_done_spawn +}; +mmove_t carrier_move_spawn = { FRAME_spawn01, FRAME_spawn18, carrier_frames_spawn, carrier_run }; + +static void carrier_attack_grenade(edict_t *self) +{ + self->timestamp = level.time; + self->monsterinfo.currentmove = &carrier_move_attack_grenade; +} + +static void carrier_reattack_grenade(edict_t *self) +{ + if (G_EntExists(self->enemy) && infront(self, self->enemy) + && self->timestamp + 1.3f > level.time) + { + self->monsterinfo.currentmove = &carrier_move_attack_grenade; + return; + } + + self->monsterinfo.currentmove = &carrier_move_attack_post_grenade; +} + +static void carrier_begin_spawn(edict_t *self) +{ + self->monsterinfo.currentmove = &carrier_move_spawn; +} + +static void carrier_attack_rail(edict_t *self) +{ + self->count = 0; + self->timestamp = level.time; + VectorSet(self->pos2, 0, 0, CARRIER_NO_SPAWN_Z); + self->monsterinfo.currentmove = &carrier_move_attack_rail; +} + +static void carrier_reattack_rail(edict_t *self) +{ + if (self->count < CARRIER_RAIL_MAX_REFIRES + && G_ValidTarget(self, self->enemy, true, true) + && infront(self, self->enemy) + && random() < CARRIER_RAIL_REFIRE_CHANCE) + { + self->count++; + VectorSet(self->pos2, 0, 0, CARRIER_NO_SPAWN_Z); + self->monsterinfo.currentmove = &carrier_move_attack_rail; + self->monsterinfo.nextframe = FRAME_search01; + M_DelayNextAttack(self, 0.0f, true); + return; + } + + self->count = 0; + self->monsterinfo.attack_finished = level.time + 1.0f; +} + +static void carrier_attack(edict_t *self) +{ + float r; + + if (!G_EntExists(self->enemy)) + return; + + r = random(); + if (level.time >= self->monsterinfo.melee_finished && r < 0.25f) + carrier_begin_spawn(self); + else if (r < 0.50f) + self->monsterinfo.currentmove = &carrier_move_attack_heat; + else if (r < 0.80f) + carrier_attack_rail(self); + else if (r < 0.92f) + self->monsterinfo.currentmove = &carrier_move_attack_pre_grenade; + else + self->monsterinfo.currentmove = &carrier_move_attack_mg; + + M_DelayNextAttack(self, 1.0f + random(), true); +} + +mframe_t carrier_frames_pain_heavy[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t carrier_move_pain_heavy = { FRAME_death01, FRAME_death10, carrier_frames_pain_heavy, carrier_run }; + +mframe_t carrier_frames_pain_light[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t carrier_move_pain_light = { FRAME_spawn01, FRAME_spawn04, carrier_frames_pain_light, carrier_run }; + +static void carrier_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + carrier_restore_spawn_steering(self); + + if (self->health < (self->max_health / 2)) + self->s.skinnum = 1; + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 5.0; + if (damage < 10) + gi.sound(self, CHAN_VOICE, sound_pain3, 1, ATTN_NORM, 0); + else if (damage < 30) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + + if (damage >= 30) + self->monsterinfo.currentmove = &carrier_move_pain_heavy; + else if (damage >= 10 && random() < 0.5f) + self->monsterinfo.currentmove = &carrier_move_pain_light; +} + +mframe_t carrier_frames_death[] = +{ + ai_move, 0, carrier_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, carrier_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, carrier_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, carrier_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, carrier_explode +}; +mmove_t carrier_move_death = { FRAME_death01, FRAME_death16, carrier_frames_death, carrier_dead }; + +static void carrier_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + M_Notify(self); + carrier_restore_spawn_steering(self); + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); + self->monsterinfo.currentmove = &carrier_move_death; +} + +void init_drone_carrier(edict_t *self) +{ + sound_pain1 = gi.soundindex("carrier/pain_md.wav"); + sound_pain2 = gi.soundindex("carrier/pain_lg.wav"); + sound_pain3 = gi.soundindex("carrier/pain_sm.wav"); + sound_death = gi.soundindex("carrier/death.wav"); + sound_sight = gi.soundindex("carrier/sight.wav"); + sound_rail = gi.soundindex("gladiator/railgun.wav"); + sound_spawn = gi.soundindex("medic_commander/monsterspawn1.wav"); + self->s.sound = gi.soundindex("bosshovr/bhvengn1.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/carrier/tris.md2"); + if (invasion->value) + { + self->s.scale = CARRIER_INVASION_SCALE; + VectorSet(self->mins, -34, -34, -20); + VectorSet(self->maxs, 34, 34, 68); + } + else + { + self->s.scale = CARRIER_DEFAULT_SCALE; + VectorSet(self->mins, -56, -56, -44); + VectorSet(self->maxs, 56, 56, 44); + } + + gi.modelindex("models/items/spawngro3/tris.md2"); + gi.modelindex("models/monsters/flyer/tris.md2"); + gi.modelindex("models/monsters/float/tris.md2"); + gi.modelindex("models/monsters/hover/tris.md2"); + + self->health = M_CARRIER_INITIAL_HEALTH + M_CARRIER_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -2000; + self->mass = 1000; + self->mtype = M_CARRIER; + self->flags |= FL_FLY; + self->yaw_speed = CARRIER_YAW_SPEED; + self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; + carrier_set_fly_parameters(self); + + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.power_armor_power = M_CARRIER_INITIAL_ARMOR + M_CARRIER_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_JORG_CONTROL_COST; + self->monsterinfo.cost = M_COMMANDER_COST; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + + self->pain = carrier_pain; + self->die = carrier_die; + self->monsterinfo.stand = carrier_stand; + self->monsterinfo.walk = carrier_walk; + self->monsterinfo.run = carrier_run; + self->monsterinfo.attack = carrier_attack; + self->monsterinfo.sight = carrier_sight; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &carrier_move_stand; + self->monsterinfo.scale = MODEL_SCALE; + self->nextthink = level.time + FRAMETIME; + + if (!invasion->value) + G_PrintGreenText(va("A level %d carrier has spawned!", self->monsterinfo.level)); +} diff --git a/src/entities/drone/drone_daedalus.c b/src/entities/drone/drone_daedalus.c new file mode 100644 index 00000000..5a14592b --- /dev/null +++ b/src/entities/drone/drone_daedalus.c @@ -0,0 +1,277 @@ +/* +============================================================================== + +DAEDALUS + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_hover.h" + +static int sound_pain1; +static int sound_pain2; +static int sound_death1; +static int sound_death2; +static int sound_sight; +static int sound_search1; +static int sound_search2; + +extern mmove_t hover_move_stand; +extern mmove_t hover_move_walk; +extern mmove_t hover_move_run; +extern mmove_t hover_move_pain1; +extern mmove_t hover_move_pain2; +extern mmove_t hover_move_pain3; +extern mmove_t hover_move_death1; + +static void daedalus_run(edict_t *self); +static void daedalus_reattack(edict_t *self); +static void daedalus_fire_grenade(edict_t *self); + +static void daedalus_set_fly_parameters(edict_t *self) +{ + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_acceleration = 20.0f; + self->monsterinfo.fly_speed = 120.0f; + self->monsterinfo.fly_min_distance = 270.0f; + self->monsterinfo.fly_max_distance = 390.0f; +} + +static void daedalus_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); +} + +static void daedalus_search(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + + if (random() < 0.5) + gi.sound(self, CHAN_VOICE, sound_search1, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_search2, 1, ATTN_NORM, 0); +} + +static void daedalus_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &hover_move_stand; +} + +static void daedalus_walk(edict_t *self) +{ + self->monsterinfo.currentmove = &hover_move_walk; +} + +static void daedalus_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &hover_move_stand; + else + self->monsterinfo.currentmove = &hover_move_run; +} + +mframe_t daedalus_frames_attack[] = +{ + ai_charge, -10, daedalus_fire_grenade, + ai_charge, -10, daedalus_fire_grenade, + ai_charge, 0, daedalus_reattack +}; +mmove_t daedalus_move_attack = { FRAME_attak104, FRAME_attak106, daedalus_frames_attack, NULL }; + +mframe_t daedalus_frames_attack_slide[] = +{ + ai_charge, 10, daedalus_fire_grenade, + ai_charge, 10, daedalus_fire_grenade, + ai_charge, 10, daedalus_reattack +}; +mmove_t daedalus_move_attack_slide = { FRAME_attak104, FRAME_attak106, daedalus_frames_attack_slide, NULL }; + +mframe_t daedalus_frames_end_attack[] = +{ + ai_charge, 1, NULL, + ai_charge, 1, NULL +}; +mmove_t daedalus_move_end_attack = { FRAME_attak107, FRAME_attak108, daedalus_frames_end_attack, daedalus_run }; + +static void daedalus_fire_grenade(edict_t *self) +{ + int damage, speed, flash_number; + vec3_t forward, right, start, offset; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_GRENADELAUNCHER_DMG_BASE + M_GRENADELAUNCHER_DMG_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_DMG_MAX && damage > M_GRENADELAUNCHER_DMG_MAX) + damage = M_GRENADELAUNCHER_DMG_MAX; + + speed = M_GRENADELAUNCHER_SPEED_BASE + M_GRENADELAUNCHER_SPEED_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_SPEED_MAX && speed > M_GRENADELAUNCHER_SPEED_MAX) + speed = M_GRENADELAUNCHER_SPEED_MAX; + + AngleVectors(self->s.angles, forward, right, NULL); + if (self->s.frame == FRAME_attak104) + VectorSet(offset, 1.7, 7.0, 11.3); + else + VectorSet(offset, 1.7, -7.0, 11.3); + G_ProjectSource(self->s.origin, offset, forward, right, start); + + flash_number = MZ2_GUNCMDR_GRENADE_MORTAR_1; + MonsterAim(self, M_PROJECTILE_ACC, speed, false, -1, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + { + M_MonsterBlockedShot(self, 0.4f); + return; + } + monster_fire_grenade(self, start, forward, damage, speed, flash_number); +} + +static void daedalus_reattack(edict_t *self) +{ + if (G_ValidTarget(self, self->enemy, true, true) && random() <= 0.4) + { + self->s.frame = FRAME_attak104; + return; + } + + self->monsterinfo.attack_finished = level.time + 1.0; + if (!(self->monsterinfo.aiflags & AI_STAND_GROUND)) + self->monsterinfo.currentmove = &daedalus_move_end_attack; +} + +static void daedalus_attack(edict_t *self) +{ + if (random() < 0.65f) + { + self->monsterinfo.attack_state = AS_STRAIGHT; + self->monsterinfo.currentmove = &daedalus_move_attack; + } + else + { + if (random() <= 0.5f) + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + self->monsterinfo.attack_state = AS_SLIDING; + self->monsterinfo.currentmove = &daedalus_move_attack_slide; + } +} + +static void daedalus_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + if (self->health < (self->max_health / 2)) + self->s.skinnum |= 1; + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0; + + if (random() < 0.5) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + + if (damage <= 25) + { + if (random() < 0.5) + self->monsterinfo.currentmove = &hover_move_pain3; + else + self->monsterinfo.currentmove = &hover_move_pain2; + } + else + { + if (random() < 0.3f) + self->monsterinfo.currentmove = &hover_move_pain1; + else + self->monsterinfo.currentmove = &hover_move_pain2; + } +} + +static void daedalus_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + qboolean overkill; + + M_Notify(self); + overkill = self->health <= self->gib_health; + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + + if (overkill) + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); + else if (random() < 0.5) + gi.sound(self, CHAN_VOICE, sound_death1, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_death2, 1, ATTN_NORM, 0); + + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); + self->flags &= ~FL_FLY; + self->movetype = MOVETYPE_TOSS; + self->gravity = 1.0; + if (self->velocity[2] > -120) + self->velocity[2] = -120; + self->monsterinfo.currentmove = &hover_move_death1; + + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; +} + +void init_drone_daedalus(edict_t *self) +{ + sound_pain1 = gi.soundindex("daedalus/daedpain1.wav"); + sound_pain2 = gi.soundindex("daedalus/daedpain2.wav"); + sound_death1 = gi.soundindex("daedalus/daeddeth1.wav"); + sound_death2 = gi.soundindex("daedalus/daeddeth2.wav"); + sound_sight = gi.soundindex("daedalus/daedsght1.wav"); + sound_search1 = gi.soundindex("daedalus/daedsrch1.wav"); + sound_search2 = gi.soundindex("daedalus/daedsrch2.wav"); + + gi.soundindex("hover/hovatck1.wav"); + self->s.sound = gi.soundindex("hover/hovidle1.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/hover/tris.md2"); + VectorSet(self->mins, -24, -24, -24); + VectorSet(self->maxs, 24, 24, 32); + self->s.skinnum = 2; + + self->health = M_DAEDALUS_INITIAL_HEALTH + M_DAEDALUS_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -100; + self->mass = 225; + + self->mtype = M_DAEDALUS; + self->flags |= FL_FLY; + self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; + daedalus_set_fly_parameters(self); + self->monsterinfo.power_armor_power = M_DAEDALUS_INITIAL_ARMOR + M_DAEDALUS_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_HOVER_CONTROL_COST; + self->monsterinfo.cost = M_HOVER_COST; + self->item = FindItemByClassname("ammo_grenades"); + + self->pain = daedalus_pain; + self->die = daedalus_die; + self->monsterinfo.stand = daedalus_stand; + self->monsterinfo.walk = daedalus_walk; + self->monsterinfo.run = daedalus_run; + self->monsterinfo.attack = daedalus_attack; + self->monsterinfo.sight = daedalus_sight; + self->monsterinfo.idle = daedalus_search; + self->monsterinfo.pain_chance = 0.2f; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &hover_move_stand; + self->monsterinfo.scale = MODEL_SCALE; +} diff --git a/src/entities/drone/drone_fixbot.c b/src/entities/drone/drone_fixbot.c new file mode 100644 index 00000000..c448d753 --- /dev/null +++ b/src/entities/drone/drone_fixbot.c @@ -0,0 +1,1454 @@ +/* +============================================================================== + +fixbot + +============================================================================== +*/ + +#include "g_local.h" + +#define FIXBOT_FRAME_charging_01 0 +#define FIXBOT_FRAME_charging_27 26 +#define FIXBOT_FRAME_charging_31 30 +#define FIXBOT_FRAME_ambient_01 121 +#define FIXBOT_FRAME_ambient_19 139 +#define FIXBOT_FRAME_paina_01 140 +#define FIXBOT_FRAME_paina_06 145 +#define FIXBOT_FRAME_painb_01 146 +#define FIXBOT_FRAME_painb_08 153 +#define FIXBOT_FRAME_freeze_01 181 +#define FIXBOT_FRAME_weldstart_01 188 +#define FIXBOT_FRAME_weldstart_07 194 + +#define FIXBOT_BOSS_TURRET_MAX 6 +#define FIXBOT_BOSS_SPAWN_COOLDOWN 8.0f +#define FIXBOT_BOSS_FAIL_COOLDOWN 2.0f +#define FIXBOT_BLASTER_FLASH MZ2_HOVER_BLASTER_1 +#define FIXBOT_BOSS_DEFAULT_SCALE 2.6f +#define FIXBOT_BOSS_INVASION_SCALE 2.0f +#define FIXBOT_BOSS_INVASION_MOVE_SCALE 1.5f +#define FIXBOT_SPAWN_YAW_SPEED 12.0f +#define FIXBOT_SPAWN_PITCH_SPEED 12.0f +#define FIXBOT_SPAWN_AIM_TIMEOUT 2.0f +#define FIXBOT_SPAWN_AIM_EPSILON 5.0f +#define FIXBOT_NO_SPAWN_YAW -99999.0f + +static int sound_pain; +static int sound_die; +static int sound_pew; +static int sound_ionripper; +static int sound_weld; +static int sound_spawn; + +void drone_ai_stand(edict_t *self, float dist); +void drone_ai_walk(edict_t *self, float dist); +void drone_ai_run(edict_t *self, float dist); +void rogue_turret_force_ready(edict_t *self); + +static void fixbot_stand(edict_t *self); +static void fixbot_walk(edict_t *self); +static void fixbot_run(edict_t *self); +static void fixbot_attack(edict_t *self); +static void fixbot_try_start_spawn(edict_t *self); +static mmove_t fixbot_move_spawn; + +static qboolean fixbot_is_boss(edict_t *self) +{ + return self->mtype == M_FIXBOT_BOSS; +} + +static float fixbot_move_scale(edict_t *self) +{ + return (fixbot_is_boss(self) && invasion->value) ? FIXBOT_BOSS_INVASION_MOVE_SCALE : 1.0f; +} + +static void fixbot_ai_run(edict_t *self, float dist) +{ + drone_ai_run(self, dist * fixbot_move_scale(self)); +} + +static void fixbot_ai_walk(edict_t *self, float dist) +{ + drone_ai_walk(self, dist * fixbot_move_scale(self)); +} + +static void fixbot_set_fly_parameters(edict_t *self) +{ + float speed_scale = fixbot_move_scale(self); + + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_acceleration = 20.0f * speed_scale; + self->monsterinfo.fly_speed = 120.0f * speed_scale; + if (fixbot_is_boss(self)) + { + self->monsterinfo.fly_min_distance = 220.0f; + self->monsterinfo.fly_max_distance = 650.0f; + } + else + { + self->monsterinfo.fly_min_distance = 250.0f; + self->monsterinfo.fly_max_distance = 450.0f; + } +} + +static int fixbot_count_live_turrets(edict_t *self) +{ + int count = 0; + edict_t *ent = DroneList_Iterate(); + + while (ent) + { + if (G_EntExists(ent) && ent->owner == self && ent->mtype == M_ROGUE_TURRET) + count++; + ent = DroneList_Next(ent); + } + + return count; +} + +static void fixbot_remove_turrets(edict_t *self) +{ + edict_t *ent = DroneList_Iterate(); + + while (ent) + { + edict_t *next = DroneList_Next(ent); + + if (G_EntExists(ent) && ent->owner == self && ent->mtype == M_ROGUE_TURRET) + { + if (ent->die) + ent->die(ent, self, self, ent->health + 100, ent->s.origin); + else + M_Remove(ent, false, true); + } + + ent = next; + } +} + +static qboolean fixbot_turret_position_clear(edict_t *self, vec3_t position) +{ + edict_t *ent = NULL; + + if (gi.pointcontents(position) & MASK_WATER) + return false; + + while ((ent = findradius(ent, position, 144)) != NULL) + { + if (!ent->inuse || ent == self) + continue; + if (ent->mtype == M_ROGUE_TURRET) + return false; + if ((ent->client || (ent->svflags & SVF_MONSTER)) && ent->health > 0 && ent->solid != SOLID_NOT) + return false; + if (ent->solid == SOLID_BSP || (ent->solid == SOLID_BBOX && ent->takedamage)) + return false; + } + + return true; +} + +static float fixbot_angle_delta(float a, float b) +{ + float delta = anglemod(a - b); + + if (delta > 180.0f) + delta = 360.0f - delta; + return delta; +} + +static qboolean fixbot_set_spawn_base_yaw(edict_t *self) +{ + vec3_t to_enemy; + + if (!G_EntExists(self->enemy)) + return false; + + VectorSubtract(self->enemy->s.origin, self->s.origin, to_enemy); + self->move_angles[YAW] = vectoyaw(to_enemy); + return true; +} + +static void fixbot_set_spawn_yaw(edict_t *self) +{ + static const float yaw_offsets[] = { -35.0f, 35.0f, 0.0f, -70.0f, 70.0f, -110.0f, 110.0f, 180.0f }; + int index; + + index = fixbot_count_live_turrets(self) % (int)(sizeof(yaw_offsets) / sizeof(yaw_offsets[0])); + self->angle = anglemod(self->move_angles[YAW] + yaw_offsets[index]); + self->ideal_yaw = self->angle; + self->teleport_time = level.time + FIXBOT_SPAWN_AIM_TIMEOUT; +} + +static void fixbot_turn_to_spawn_yaw(edict_t *self) +{ + float old_yaw_speed; + + if (self->angle <= FIXBOT_NO_SPAWN_YAW + 1.0f) + return; + + VectorClear(self->velocity); + VectorClear(self->avelocity); + self->ideal_yaw = self->angle; + old_yaw_speed = self->yaw_speed; + self->yaw_speed = FIXBOT_SPAWN_YAW_SPEED; + M_ChangeYaw(self); + self->yaw_speed = old_yaw_speed; +} + +static qboolean fixbot_find_turret_spawn_position(edict_t *self, vec3_t position, vec3_t direction) +{ + vec3_t start; + vec3_t base_angles; + vec3_t initial_forward; + vec3_t best_pos; + vec3_t best_dir; + float best_dist = 0; + qboolean found = false; + qboolean best_front = false; + const float trace_distance = 1000.0f; + vec3_t turret_mins = { -12, -12, -12 }; + vec3_t turret_maxs = { 12, 12, 12 }; + + VectorCopy(self->s.origin, start); + start[2] += 16; + VectorCopy(self->s.angles, base_angles); + if (self->angle > FIXBOT_NO_SPAWN_YAW + 1.0f) + base_angles[YAW] = self->angle; + base_angles[PITCH] = 0.0f; + AngleVectors(base_angles, initial_forward, NULL, NULL); + VectorClear(best_pos); + VectorClear(best_dir); + + for (int attempt = 0; attempt < 12; attempt++) + { + vec3_t angles; + vec3_t forward; + vec3_t end; + vec3_t candidate; + vec3_t normal; + vec3_t to_pos; + trace_t tr; + float dist; + qboolean in_front; + qboolean better = false; + + VectorCopy(base_angles, angles); + if (attempt == 0) + { + VectorCopy(initial_forward, forward); + } + else + { + if (attempt <= 6) + { + angles[YAW] += (float)(attempt - 1) * 30.0f - 75.0f; + angles[PITCH] = -15.0f; + } + else + { + angles[YAW] += (float)(attempt - 7) * 60.0f - 150.0f; + angles[PITCH] += crandom() * 15.0f - 15.0f; + } + + while (angles[YAW] < 0) + angles[YAW] += 360; + while (angles[YAW] >= 360) + angles[YAW] -= 360; + AngleVectors(angles, forward, NULL, NULL); + } + + VectorMA(start, trace_distance, forward, end); + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID); + if (tr.fraction >= 1.0) + continue; + if (tr.ent && tr.ent != world && tr.ent->solid == SOLID_BBOX) + continue; + + VectorCopy(tr.plane.normal, normal); + if (fabs(normal[2]) > 0.9f) + { + normal[2] = (normal[2] > 0) ? 0.7f : -0.7f; + VectorNormalize(normal); + } + + VectorMA(tr.endpos, 16.0f, normal, candidate); + if (!G_IsValidLocation(self, candidate, turret_mins, turret_maxs)) + continue; + if (!fixbot_turret_position_clear(self, candidate)) + continue; + if (!G_IsClearPath(self, MASK_SOLID, start, candidate)) + continue; + + VectorSubtract(candidate, self->s.origin, to_pos); + dist = VectorLength(to_pos); + if (dist <= 56.0f) + continue; + VectorNormalize(to_pos); + in_front = DotProduct(to_pos, initial_forward) > 0.1f; + + if (!found) + better = true; + else if (in_front && !best_front) + better = true; + else if (in_front == best_front && dist < best_dist) + better = true; + + if (better) + { + VectorCopy(candidate, best_pos); + VectorCopy(normal, best_dir); + best_dist = dist; + best_front = in_front; + found = true; + } + } + + if (!found) + { + const float distances[] = { 120.0f, 180.0f, 240.0f, 320.0f }; + const float yaws[] = { 0.0f, -45.0f, 45.0f, -90.0f, 90.0f, -135.0f, 135.0f, 180.0f }; + + for (int d = 0; d < (int)(sizeof(distances) / sizeof(distances[0])); d++) + { + for (int y = 0; y < (int)(sizeof(yaws) / sizeof(yaws[0])); y++) + { + vec3_t angles; + vec3_t forward; + vec3_t probe; + vec3_t down; + vec3_t candidate; + vec3_t to_pos; + trace_t tr; + + VectorCopy(base_angles, angles); + angles[YAW] += yaws[y]; + while (angles[YAW] < 0) + angles[YAW] += 360; + while (angles[YAW] >= 360) + angles[YAW] -= 360; + angles[PITCH] = 0; + AngleVectors(angles, forward, NULL, NULL); + + VectorMA(self->s.origin, distances[d], forward, probe); + probe[2] += 96.0f; + VectorCopy(probe, down); + down[2] -= 384.0f; + tr = gi.trace(probe, NULL, NULL, down, self, MASK_SOLID); + if (tr.startsolid || tr.allsolid || tr.fraction >= 1.0f) + continue; + if (tr.surface && (tr.surface->flags & SURF_SKY)) + continue; + if (tr.plane.normal[2] < 0.7f) + continue; + + VectorCopy(tr.endpos, candidate); + candidate[2] -= turret_mins[2]; + if (!G_IsValidLocation(self, candidate, turret_mins, turret_maxs)) + continue; + if (!fixbot_turret_position_clear(self, candidate)) + continue; + if (!G_IsClearPath(self, MASK_SOLID, start, candidate)) + continue; + + VectorSubtract(candidate, self->s.origin, to_pos); + if (VectorLength(to_pos) <= 56.0f) + continue; + + VectorCopy(candidate, position); + VectorSet(direction, 0, 0, 1); + return true; + } + } + + VectorClear(position); + VectorClear(direction); + return false; + } + + VectorCopy(best_pos, position); + VectorCopy(best_dir, direction); + return true; +} + +static qboolean fixbot_plasma_valid_target(edict_t *self, edict_t *target) +{ + if (!G_EntExists(self->owner)) + return false; + if (target == self->owner) + return false; + if (!G_ValidTargetEnt(self->owner, target, true)) + return false; + if (OnSameTeam(self->owner, target)) + return false; + if (!visible(self, target)) + return false; + return true; +} + +static void fixbot_plasma_turn_toward(edict_t *self, edict_t *target) +{ + vec3_t dir; + float turn_fraction; + + if (!fixbot_plasma_valid_target(self, target)) + return; + + VectorSubtract(target->s.origin, self->s.origin, dir); + if (!VectorNormalize(dir)) + return; + + turn_fraction = self->accel; + if (turn_fraction < 0) + turn_fraction = 0; + else if (turn_fraction > 1) + turn_fraction = 1; + + VectorScale(self->movedir, 1.0f - turn_fraction, self->movedir); + VectorMA(self->movedir, turn_fraction, dir, self->movedir); + VectorNormalize(self->movedir); + vectoangles(self->movedir, self->s.angles); + self->enemy = target; +} + +static edict_t *fixbot_plasma_acquire_target(edict_t *self) +{ + edict_t *target = NULL; + edict_t *acquire = NULL; + vec3_t forward; + vec3_t dir; + float best_dot = -1.0f; + float best_dist = 0.0f; + + if (fixbot_plasma_valid_target(self, self->enemy)) + return self->enemy; + self->enemy = NULL; + + AngleVectors(self->s.angles, forward, NULL, NULL); + + while ((target = findradius(target, self->s.origin, 1024)) != NULL) + { + float dot; + float dist; + + if (!fixbot_plasma_valid_target(self, target)) + continue; + + VectorSubtract(target->s.origin, self->s.origin, dir); + dist = VectorNormalize(dir); + dot = DotProduct(dir, forward); + + if (!acquire || dot > best_dot || (dot == best_dot && dist < best_dist)) + { + acquire = target; + best_dot = dot; + best_dist = dist; + } + } + + return acquire; +} + +static void fixbot_plasma_think(edict_t *self) +{ + edict_t *target; + vec3_t upward; + + if (!G_EntExists(self->owner) || level.time >= self->delay) + { + BecomeExplosion1(self); + return; + } + + if (self->timestamp > level.time) + { + VectorSet(upward, 0, 0, self->movedir[2] > 0.3f ? 0.10f : 0.02f); + VectorAdd(self->movedir, upward, self->movedir); + VectorNormalize(self->movedir); + vectoangles(self->movedir, self->s.angles); + VectorScale(self->movedir, self->speed, self->velocity); + if (random() > 0.5f) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BLASTER); + gi.WritePosition(self->s.origin); + gi.WriteDir(vec3_origin); + gi.multicast(self->s.origin, MULTICAST_PVS); + } + self->nextthink = level.time + FRAMETIME; + return; + } + + target = fixbot_plasma_acquire_target(self); + if (target) + fixbot_plasma_turn_toward(self, target); + + VectorScale(self->movedir, self->speed, self->velocity); + self->nextthink = level.time + FRAMETIME; +} + +static void fixbot_plasma_touch(edict_t *ent, edict_t *other, cplane_t *plane, csurface_t *surf) +{ + vec3_t origin; + vec3_t normal; + + if (other == ent->owner) + return; + + if (!G_EntExists(ent->owner)) + { + G_FreeEdict(ent); + return; + } + + if (surf && (surf->flags & SURF_SKY)) + { + ent->timestamp = 0; + if (plane) + VectorMA(ent->s.origin, 16.0f, plane->normal, ent->s.origin); + if (ent->movedir[2] > 0) + { + ent->movedir[2] *= -0.35f; + VectorNormalize(ent->movedir); + } + if (fixbot_plasma_valid_target(ent, ent->enemy)) + fixbot_plasma_turn_toward(ent, ent->enemy); + ent->nextthink = level.time + FRAMETIME; + return; + } + + VectorMA(ent->s.origin, -0.02f, ent->velocity, origin); + if (plane) + VectorCopy(plane->normal, normal); + else + VectorClear(normal); + + if (other->takedamage) + T_Damage(other, ent, ent->owner, ent->velocity, ent->s.origin, normal, ent->dmg, 0, DAMAGE_ENERGY, MOD_PHALANX); + + T_RadiusDamage(ent, ent->owner, ent->radius_dmg, other, ent->dmg_radius, MOD_PHALANX); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_PLASMA_EXPLOSION); + gi.WritePosition(origin); + gi.multicast(ent->s.origin, MULTICAST_PVS); + + G_FreeEdict(ent); +} + +static void fixbot_fire_plasma_shot(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, float radius, int radius_damage, float turn_fraction) +{ + edict_t *plasma; + + damage = vrx_increase_monster_damage_by_talent(self->activator, damage); + if (speed < 1) + speed = 1; + + VectorNormalize(dir); + self->lastsound = level.framenum; + + plasma = G_Spawn(); + VectorCopy(start, plasma->s.origin); + VectorCopy(start, plasma->s.old_origin); + VectorCopy(dir, plasma->movedir); + vectoangles(dir, plasma->s.angles); + VectorScale(dir, speed, plasma->velocity); + plasma->movetype = MOVETYPE_FLYMISSILE; + plasma->clipmask = MASK_SHOT; + plasma->solid = SOLID_BBOX; + VectorSet(plasma->mins, -5, -5, -5); + VectorSet(plasma->maxs, 5, 5, 5); + plasma->owner = self; + plasma->touch = fixbot_plasma_touch; + plasma->think = fixbot_plasma_think; + plasma->nextthink = level.time + FRAMETIME; + plasma->speed = speed; + plasma->accel = turn_fraction; + plasma->dmg = damage; + plasma->radius_dmg = radius_damage; + plasma->dmg_radius = radius; + plasma->s.sound = gi.soundindex("weapons/rockfly.wav"); + plasma->s.modelindex = gi.modelindex("sprites/s_photon.sp2"); + plasma->s.effects |= EF_PLASMA | EF_ANIM_ALLFAST; + plasma->s.scale = 0.75f; + plasma->svflags |= SVF_PROJECTILE; + plasma->timestamp = level.time + (fixbot_is_boss(self) ? 0.7f : 1.0f); + plasma->delay = level.time + 8.0f; + + if (G_ValidTarget(self, self->enemy, true, true)) + plasma->enemy = self->enemy; + + gi.linkentity(plasma); +} + +static qboolean fixbot_has_open_sky(edict_t *self) +{ + vec3_t start; + vec3_t end; + trace_t tr; + + VectorCopy(self->s.origin, start); + start[2] += 20.0f; + VectorCopy(start, end); + end[2] += 2000.0f; + + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID); + return tr.surface && (tr.surface->flags & SURF_SKY); +} + +static void fixbot_fire_ionripper_spread(edict_t *self, vec3_t start, vec3_t forward, vec3_t right) +{ + int damage; + int speed; + int shots = fixbot_is_boss(self) ? 5 : 3; + vec3_t target; + vec3_t dir; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + damage = M_IONRIPPER_DMG_BASE + M_IONRIPPER_DMG_ADDON * drone_damagelevel(self); + if (M_IONRIPPER_DMG_MAX && damage > M_IONRIPPER_DMG_MAX) + damage = M_IONRIPPER_DMG_MAX; + if (damage <= 0) + damage = fixbot_is_boss(self) ? 20 : 12; + + speed = M_IONRIPPER_SPEED_BASE + M_IONRIPPER_SPEED_ADDON * drone_damagelevel(self); + if (M_IONRIPPER_SPEED_MAX && speed > M_IONRIPPER_SPEED_MAX) + speed = M_IONRIPPER_SPEED_MAX; + if (speed <= 0) + speed = fixbot_is_boss(self) ? 750 : 650; + + G_EntMidPoint(self->enemy, target); + VectorSubtract(target, start, dir); + if (!VectorNormalize(dir)) + VectorCopy(forward, dir); + + if (shots == 5) + { + vec3_t dir1, dir2, dir3, dir4, dir5; + + VectorCopy(dir, dir1); + VectorMA(dir1, 0.16f, right, dir1); + VectorNormalize(dir1); + VectorCopy(dir, dir2); + VectorMA(dir2, 0.12f, right, dir2); + VectorNormalize(dir2); + VectorCopy(dir, dir3); + VectorCopy(dir, dir4); + VectorMA(dir4, -0.12f, right, dir4); + VectorNormalize(dir4); + VectorCopy(dir, dir5); + VectorMA(dir5, -0.16f, right, dir5); + VectorNormalize(dir5); + + monster_fire_ionripper(self, start, dir1, damage, speed, EF_IONRIPPER, -1); + monster_fire_ionripper(self, start, dir2, damage, speed, EF_IONRIPPER, -1); + monster_fire_ionripper(self, start, dir3, damage, speed, EF_IONRIPPER, -1); + monster_fire_ionripper(self, start, dir4, damage, speed, EF_IONRIPPER, -1); + monster_fire_ionripper(self, start, dir5, damage, speed, EF_IONRIPPER, -1); + } + else + { + vec3_t dir1, dir2, dir3; + + VectorCopy(dir, dir1); + VectorMA(dir1, 0.12f, right, dir1); + VectorNormalize(dir1); + VectorCopy(dir, dir2); + VectorCopy(dir, dir3); + VectorMA(dir3, -0.12f, right, dir3); + VectorNormalize(dir3); + + monster_fire_ionripper(self, start, dir1, damage, speed, EF_IONRIPPER, -1); + monster_fire_ionripper(self, start, dir2, damage, speed, EF_IONRIPPER, -1); + monster_fire_ionripper(self, start, dir3, damage, speed, EF_IONRIPPER, -1); + } + + gi.sound(self, CHAN_WEAPON, sound_ionripper ? sound_ionripper : sound_pew, 1, ATTN_NORM, 0); +} + +static void fixbot_fire_plasma(edict_t *self, float offset) +{ + int damage; + int speed; + int radius_damage; + float turn_fraction; + vec3_t forward; + vec3_t right; + vec3_t up; + vec3_t start; + vec3_t dir; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + AngleVectors(self->s.angles, forward, right, up); + VectorCopy(self->s.origin, start); + VectorMA(start, 25.0f, forward, start); + VectorMA(start, offset, right, start); + VectorMA(start, 50.0f, up, start); + + if (!fixbot_has_open_sky(self)) + { + fixbot_fire_ionripper_spread(self, start, forward, right); + return; + } + + VectorCopy(forward, dir); + VectorMA(dir, 0.55f, up, dir); + VectorNormalize(dir); + + damage = (fixbot_is_boss(self) ? 24 : 20) + (fixbot_is_boss(self) ? 4 : 3) * drone_damagelevel(self); + if (damage > (fixbot_is_boss(self) ? 120 : 90)) + damage = fixbot_is_boss(self) ? 120 : 90; + + speed = fixbot_is_boss(self) ? (520 + (int)(random() * 120.0f)) : (380 + (int)(random() * 100.0f)); + radius_damage = (int)(damage * 1.75f); + turn_fraction = fixbot_is_boss(self) ? 0.085f : 0.065f; + + if (fixbot_is_boss(self)) + { + vec3_t start1, start2, start3; + vec3_t dir1, dir2, dir3; + + VectorCopy(start, start1); + VectorMA(start1, 25.0f, right, start1); + VectorMA(start1, 15.0f, up, start1); + VectorCopy(start, start2); + VectorCopy(start, start3); + VectorMA(start3, -25.0f, right, start3); + VectorMA(start3, 15.0f, up, start3); + + VectorCopy(dir, dir1); + VectorMA(dir1, 0.10f, right, dir1); + VectorMA(dir1, 0.05f, up, dir1); + VectorNormalize(dir1); + VectorCopy(dir, dir2); + VectorCopy(dir, dir3); + VectorMA(dir3, -0.10f, right, dir3); + VectorMA(dir3, 0.05f, up, dir3); + VectorNormalize(dir3); + + fixbot_fire_plasma_shot(self, start1, dir1, damage, speed, 150.0f, radius_damage, turn_fraction); + fixbot_fire_plasma_shot(self, start2, dir2, damage, speed, 150.0f, radius_damage, turn_fraction); + fixbot_fire_plasma_shot(self, start3, dir3, damage, speed, 150.0f, radius_damage, turn_fraction); + } + else + { + fixbot_fire_plasma_shot(self, start, dir, damage, speed, 150.0f, radius_damage, turn_fraction); + } + + gi.sound(self, CHAN_WEAPON, sound_pew, 1, ATTN_NORM, 0); +} + +static void fixbot_fire_blaster(edict_t *self) +{ + int damage; + int speed = 1000; + int effect; + vec3_t forward; + vec3_t base_forward; + vec3_t right; + vec3_t start; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + damage = (fixbot_is_boss(self) ? 10 : 7) + (fixbot_is_boss(self) ? 3 : 2) * self->monsterinfo.level; + if (damage > (fixbot_is_boss(self) ? 80 : 50)) + damage = fixbot_is_boss(self) ? 80 : 50; + + effect = (self->s.frame & 3) ? 0 : EF_BLASTER; + AngleVectors(self->s.angles, forward, right, NULL); + G_ProjectSource(self->s.origin, monster_flash_offset[FIXBOT_BLASTER_FLASH], forward, right, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, -1, forward, start); + VectorCopy(forward, base_forward); + monster_fire_blaster(self, start, forward, damage, speed, effect, BLASTER_PROJ_BOLT, 2.0f, false, FIXBOT_BLASTER_FLASH); + + if (fixbot_is_boss(self) && (self->s.frame & 1)) + { + VectorCopy(base_forward, forward); + VectorMA(forward, 0.10f, right, forward); + VectorNormalize(forward); + monster_fire_blaster(self, start, forward, damage, speed, 0, BLASTER_PROJ_BOLT, 2.0f, false, FIXBOT_BLASTER_FLASH); + + VectorCopy(base_forward, forward); + VectorMA(forward, -0.10f, right, forward); + VectorNormalize(forward); + monster_fire_blaster(self, start, forward, damage, speed, 0, BLASTER_PROJ_BOLT, 2.0f, false, FIXBOT_BLASTER_FLASH); + } + + gi.sound(self, CHAN_WEAPON, sound_pew, 1, ATTN_NORM, 0); +} + +static void fixbot_reattack(edict_t *self) +{ + if (G_ValidTarget(self, self->enemy, true, true)) + { + if (random() < (fixbot_is_boss(self) ? 0.16f : 0.12f)) + { + fixbot_fire_plasma(self, 0.0f); + self->monsterinfo.nextframe = FIXBOT_FRAME_charging_27; + return; + } + + if (random() < (fixbot_is_boss(self) ? 0.80f : 0.55f)) + { + self->monsterinfo.nextframe = FIXBOT_FRAME_charging_27; + return; + } + } + + M_DelayNextAttack(self, fixbot_is_boss(self) ? 0.4f : 0.8f, true); +} + +static void fixbot_update_spawn_probe(edict_t *self) +{ + static const float scan_yaws[] = { 0.0f, -35.0f, 35.0f, -70.0f, 70.0f, -110.0f, 110.0f, -150.0f, 150.0f, 180.0f }; + vec3_t start; + vec3_t angles; + vec3_t forward; + vec3_t end; + trace_t tr; + int index; + + VectorCopy(self->s.origin, start); + start[2] += 16.0f; + + index = (self->s.frame - FIXBOT_FRAME_weldstart_01) % (int)(sizeof(scan_yaws) / sizeof(scan_yaws[0])); + if (index < 0) + index = 0; + + VectorCopy(self->s.angles, angles); + if (self->angle > FIXBOT_NO_SPAWN_YAW + 1.0f) + angles[YAW] = self->angle; + angles[YAW] += scan_yaws[index]; + angles[PITCH] = -10.0f; + while (angles[YAW] < 0) + angles[YAW] += 360; + while (angles[YAW] >= 360) + angles[YAW] -= 360; + + AngleVectors(angles, forward, NULL, NULL); + VectorMA(start, 1000.0f, forward, end); + tr = gi.trace(start, NULL, NULL, end, self, MASK_SOLID); + + if (tr.fraction < 1.0f) + VectorCopy(tr.endpos, self->pos2); + else + VectorCopy(end, self->pos2); +} + +static qboolean fixbot_try_select_turret_position(edict_t *self) +{ + if (fixbot_find_turret_spawn_position(self, self->pos1, self->pos2)) + return true; + + VectorClear(self->pos1); + fixbot_update_spawn_probe(self); + return false; +} + +static qboolean fixbot_get_spawn_target(edict_t *self, vec3_t target) +{ + if (VectorLength(self->pos1) >= 1) + VectorCopy(self->pos1, target); + else + VectorCopy(self->pos2, target); + + return VectorLength(target) >= 1; +} + +static float fixbot_turn_angle(float current, float ideal, float speed) +{ + float move; + float step; + + current = anglemod(current); + ideal = anglemod(ideal); + if (current == ideal) + return current; + + move = ideal - current; + if (ideal > current) + { + if (move >= 180.0f) + move -= 360.0f; + } + else + { + if (move <= -180.0f) + move += 360.0f; + } + + step = speed * FRAMETIME * 10.0f; + if (move > step) + move = step; + else if (move < -step) + move = -step; + + return anglemod(current + move); +} + +static qboolean fixbot_facing_spawn_target(edict_t *self) +{ + vec3_t dir; + vec3_t angles; + vec3_t target; + + if (!fixbot_get_spawn_target(self, target)) + return false; + + VectorSubtract(target, self->s.origin, dir); + if (VectorLength(dir) <= 1) + return false; + + vectoangles(dir, angles); + return fixbot_angle_delta(self->s.angles[YAW], angles[YAW]) <= FIXBOT_SPAWN_AIM_EPSILON; +} + +static qboolean fixbot_aim_at_spawn_target(edict_t *self) +{ + vec3_t dir; + vec3_t angles; + vec3_t target; + float old_yaw_speed; + qboolean building; + + if (!fixbot_get_spawn_target(self, target)) + return false; + + VectorSubtract(target, self->s.origin, dir); + if (VectorLength(dir) <= 1) + return false; + + vectoangles(dir, angles); + building = self->monsterinfo.currentmove == &fixbot_move_spawn; + self->ideal_yaw = angles[YAW]; + if (building) + self->s.angles[PITCH] = fixbot_turn_angle(self->s.angles[PITCH], angles[PITCH], FIXBOT_SPAWN_PITCH_SPEED); + else + self->s.angles[PITCH] = angles[PITCH]; + old_yaw_speed = self->yaw_speed; + if (building) + self->yaw_speed = FIXBOT_SPAWN_YAW_SPEED; + M_ChangeYaw(self); + self->yaw_speed = old_yaw_speed; + return true; +} + +static void fixbot_prep_spawn(edict_t *self) +{ + VectorClear(self->pos1); + VectorClear(self->pos2); + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + self->angle = FIXBOT_NO_SPAWN_YAW; + self->teleport_time = level.time; + + if (!fixbot_is_boss(self) || level.time < self->monsterinfo.melee_finished) + { + self->monsterinfo.currentmove = NULL; + fixbot_run(self); + return; + } + + if (fixbot_count_live_turrets(self) >= FIXBOT_BOSS_TURRET_MAX) + { + self->monsterinfo.melee_finished = level.time + FIXBOT_BOSS_FAIL_COOLDOWN; + self->monsterinfo.currentmove = NULL; + fixbot_run(self); + return; + } + + if (fixbot_set_spawn_base_yaw(self)) + fixbot_set_spawn_yaw(self); + fixbot_try_select_turret_position(self); + fixbot_aim_at_spawn_target(self); + self->s.effects |= EF_HYPERBLASTER | EF_PLASMA; + gi.sound(self, CHAN_WEAPON, sound_weld, 1, ATTN_NORM, 0); +} + +static void fixbot_spawn_laser_off(edict_t *self) +{ + if (self->beam && self->beam->inuse) + G_FreeEdict(self->beam); + self->beam = NULL; +} + +static void fixbot_fire_spawn_laser(edict_t *self) +{ + edict_t *laser; + vec3_t forward; + vec3_t start; + vec3_t target; + + if (!fixbot_get_spawn_target(self, target)) + return; + + laser = self->beam; + if (!laser || !laser->inuse) + { + laser = G_Spawn(); + if (!laser) + return; + self->beam = laser; + laser->movetype = MOVETYPE_NONE; + laser->solid = SOLID_NOT; + laser->s.renderfx = RF_BEAM | RF_TRANSLUCENT; + laser->s.modelindex = 1; + laser->s.frame = 2; + laser->owner = self; + laser->classname = "fixbot_spawn_laser"; + laser->s.sound = gi.soundindex("misc/lasfly.wav"); + } + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 16.0f, forward, start); + VectorCopy(start, laser->s.origin); + VectorCopy(target, laser->s.old_origin); + VectorCopy(start, laser->pos1); + VectorCopy(target, laser->pos2); + laser->s.skinnum = fixbot_is_boss(self) ? 0xf0f0f0f0 : 0xf2f2f0f0; + laser->think = G_FreeEdict; + laser->nextthink = level.time + 0.2f; + gi.linkentity(laser); +} + +static void fixbot_spawn_effect(edict_t *self) +{ + vec3_t target; + qboolean has_position; + + has_position = VectorLength(self->pos1) >= 1; + if (!has_position) + has_position = fixbot_try_select_turret_position(self); + + if (!fixbot_get_spawn_target(self, target)) + return; + + fixbot_fire_spawn_laser(self); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_WELDING_SPARKS); + gi.WriteByte(has_position ? (fixbot_is_boss(self) ? 18 : 10) : 5); + gi.WritePosition(target); + gi.WriteDir(vec3_origin); + gi.WriteByte(has_position ? (fixbot_is_boss(self) ? 0xf0 : 0xe0) : 0xd0); + gi.multicast(target, MULTICAST_PVS); +} + +static void fixbot_spawn_aim_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + if (VectorLength(self->pos1) < 1) + fixbot_try_select_turret_position(self); + if (!fixbot_aim_at_spawn_target(self)) + fixbot_turn_to_spawn_yaw(self); + fixbot_fire_spawn_laser(self); +} + +static qboolean fixbot_spawn_turret(edict_t *self) +{ + edict_t *spawned; + edict_t *summoner; + vec3_t dir; + vec3_t angles; + + if (VectorLength(self->pos1) < 1) + return false; + if (fixbot_count_live_turrets(self) >= FIXBOT_BOSS_TURRET_MAX) + return false; + + spawned = G_Spawn(); + spawned->mtype = M_ROGUE_TURRET; + spawned->activator = self; + spawned->owner = self; + spawned->monsterinfo.level = self->monsterinfo.level; + + summoner = G_GetSummoner(self); + if (summoner) + spawned->creator = summoner; + + if (!M_Initialize(self, spawned, 0.0f)) + { + G_FreeEdict(spawned); + return false; + } + + spawned->activator = self; + spawned->owner = self; + if (summoner) + spawned->creator = summoner; + spawned->monsterinfo.control_cost = 0; + spawned->monsterinfo.cost = 0; + spawned->movetype = MOVETYPE_NONE; + spawned->gravity = 0; + spawned->health = spawned->max_health; + spawned->monsterinfo.power_armor_power = spawned->monsterinfo.max_armor; + spawned->monsterinfo.pausetime = 0; + spawned->monsterinfo.attack_finished = level.time; + spawned->monsterinfo.melee_finished = level.time; + VectorCopy(self->pos1, spawned->s.origin); + VectorCopy(self->pos1, spawned->s.old_origin); + + if (G_ValidTarget(self, self->enemy, false, true)) + VectorSubtract(self->enemy->s.origin, self->pos1, dir); + else + VectorSubtract(self->s.origin, self->pos1, dir); + if (VectorLength(dir) < 1) + VectorSet(dir, 1, 0, 0); + vectoangles(dir, angles); + VectorCopy(angles, spawned->s.angles); + + if (G_ValidTarget(spawned, self->enemy, false, true)) + spawned->enemy = self->enemy; + + gi.linkentity(spawned); + rogue_turret_force_ready(spawned); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_TELEPORT_EFFECT); + gi.WritePosition(self->pos1); + gi.multicast(self->pos1, MULTICAST_PVS); + if (sound_spawn) + gi.sound(self, CHAN_AUTO, sound_spawn, 1, ATTN_NORM, 0); + + return true; +} + +static void fixbot_finish_spawn(edict_t *self) +{ + if (VectorLength(self->pos1) < 1) + fixbot_try_select_turret_position(self); + + fixbot_aim_at_spawn_target(self); + fixbot_fire_spawn_laser(self); + if (!fixbot_facing_spawn_target(self) && level.time < self->teleport_time) + { + self->monsterinfo.aiflags |= AI_HOLD_FRAME; + return; + } + + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + if (fixbot_spawn_turret(self)) + self->monsterinfo.melee_finished = level.time + FIXBOT_BOSS_SPAWN_COOLDOWN; + else + self->monsterinfo.melee_finished = level.time + FIXBOT_BOSS_FAIL_COOLDOWN; + + VectorClear(self->pos1); + VectorClear(self->pos2); + self->angle = FIXBOT_NO_SPAWN_YAW; + fixbot_spawn_laser_off(self); + self->s.effects &= ~(EF_HYPERBLASTER | EF_PLASMA); +} + +static mframe_t fixbot_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +static mmove_t fixbot_move_stand = { FIXBOT_FRAME_ambient_01, FIXBOT_FRAME_ambient_19, fixbot_frames_stand, fixbot_run }; + +static void fixbot_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &fixbot_move_stand; +} + +static mframe_t fixbot_frames_run[] = +{ + fixbot_ai_run, 10, fixbot_try_start_spawn +}; +static mmove_t fixbot_move_run = { FIXBOT_FRAME_freeze_01, FIXBOT_FRAME_freeze_01, fixbot_frames_run, NULL }; + +static void fixbot_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &fixbot_move_stand; + else + self->monsterinfo.currentmove = &fixbot_move_run; +} + +static mframe_t fixbot_frames_walk[] = +{ + fixbot_ai_walk, 5, NULL +}; +static mmove_t fixbot_move_walk = { FIXBOT_FRAME_freeze_01, FIXBOT_FRAME_freeze_01, fixbot_frames_walk, NULL }; + +static void fixbot_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &fixbot_move_walk; +} + +static mframe_t fixbot_frames_attack[] = +{ + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, -10, NULL, + ai_charge, 0, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, -10, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, NULL, + ai_charge, -10, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, NULL, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, fixbot_fire_blaster, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, fixbot_reattack +}; +static mmove_t fixbot_move_attack = { FIXBOT_FRAME_charging_01, FIXBOT_FRAME_charging_31, fixbot_frames_attack, fixbot_run }; + +static mframe_t fixbot_frames_spawn[] = +{ + ai_move, 0, fixbot_prep_spawn, + fixbot_spawn_aim_ai, 0, fixbot_spawn_effect, + fixbot_spawn_aim_ai, 0, fixbot_spawn_effect, + fixbot_spawn_aim_ai, 0, fixbot_spawn_effect, + fixbot_spawn_aim_ai, 0, fixbot_spawn_effect, + ai_move, 0, fixbot_finish_spawn, + ai_move, 0, NULL +}; +static mmove_t fixbot_move_spawn = { FIXBOT_FRAME_weldstart_01, FIXBOT_FRAME_weldstart_07, fixbot_frames_spawn, fixbot_run }; + +static void fixbot_try_start_spawn(edict_t *self) +{ + if (!fixbot_is_boss(self)) + return; + if (level.time < self->monsterinfo.melee_finished) + return; + if (fixbot_count_live_turrets(self) >= FIXBOT_BOSS_TURRET_MAX) + return; + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + self->monsterinfo.currentmove = &fixbot_move_spawn; +} + +static void fixbot_attack(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + + if (fixbot_is_boss(self) && level.time >= self->monsterinfo.melee_finished + && fixbot_count_live_turrets(self) < FIXBOT_BOSS_TURRET_MAX) + { + self->monsterinfo.currentmove = &fixbot_move_spawn; + return; + } + + self->monsterinfo.lefty = random() <= 0.5f; + self->monsterinfo.currentmove = &fixbot_move_attack; +} + +static mframe_t fixbot_frames_paina[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +static mmove_t fixbot_move_paina = { FIXBOT_FRAME_paina_01, FIXBOT_FRAME_paina_06, fixbot_frames_paina, fixbot_run }; + +static mframe_t fixbot_frames_painb[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +static mmove_t fixbot_move_painb = { FIXBOT_FRAME_painb_01, FIXBOT_FRAME_painb_08, fixbot_frames_painb, fixbot_run }; + +static mframe_t fixbot_frames_pain3[] = +{ + ai_move, -1, NULL +}; +static mmove_t fixbot_move_pain3 = { FIXBOT_FRAME_freeze_01, FIXBOT_FRAME_freeze_01, fixbot_frames_pain3, fixbot_run }; + +static void fixbot_dead(edict_t *self); + +static mframe_t fixbot_frames_death1[] = +{ + ai_move, 0, NULL +}; +static mmove_t fixbot_move_death1 = { FIXBOT_FRAME_freeze_01, FIXBOT_FRAME_freeze_01, fixbot_frames_death1, fixbot_dead }; + +static void fixbot_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0f; + gi.sound(self, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0); + + if (fixbot_is_boss(self) && self->monsterinfo.currentmove == &fixbot_move_spawn) + return; + + if (damage <= 10) + self->monsterinfo.currentmove = &fixbot_move_pain3; + else if (damage <= 25) + self->monsterinfo.currentmove = &fixbot_move_painb; + else + self->monsterinfo.currentmove = &fixbot_move_paina; +} + +static void fixbot_dead(edict_t *self) +{ + fixbot_remove_turrets(self); + fixbot_spawn_laser_off(self); + + vrx_throw_drone_gibs(self, self->dmg ? self->dmg : 120); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_EXPLOSION1); + gi.WritePosition(self->s.origin); + gi.multicast(self->s.origin, MULTICAST_PVS); + + M_Remove(self, false, false); +} + +static void fixbot_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + M_Notify(self); + + if (self->deadflag == DEAD_DEAD) + return; + + gi.sound(self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_NO; + self->s.sound = 0; + self->dmg = damage; + self->monsterinfo.currentmove = &fixbot_move_death1; +} + +static void init_drone_fixbot_common(edict_t *self, qboolean boss) +{ + sound_pain = gi.soundindex("daedalus/daedpain1.wav"); + sound_die = gi.soundindex("daedalus/daeddeth1.wav"); + sound_pew = gi.soundindex("makron/blaster.wav"); + sound_ionripper = gi.soundindex("weapons/rippfire.wav"); + sound_weld = gi.soundindex("misc/welder1.wav"); + sound_spawn = gi.soundindex("makron/popup.wav"); + gi.soundindex("misc/welder2.wav"); + gi.soundindex("misc/welder3.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/fixbot/tris.md2"); + + if (boss) + { + if (invasion->value) + { + VectorSet(self->mins, -30, -30, -24); + VectorSet(self->maxs, 30, 30, 24); + self->s.scale = FIXBOT_BOSS_INVASION_SCALE; + } + else + { + VectorSet(self->mins, -36, -36, -28); + VectorSet(self->maxs, 36, 36, 28); + self->s.scale = FIXBOT_BOSS_DEFAULT_SCALE; + } + self->health = M_FIXBOT_BOSS_INITIAL_HEALTH + M_FIXBOT_BOSS_ADDON_HEALTH * self->monsterinfo.level; + self->monsterinfo.power_armor_power = M_FIXBOT_BOSS_INITIAL_ARMOR + M_FIXBOT_BOSS_ADDON_ARMOR * self->monsterinfo.level; + self->mass = 400; + self->mtype = M_FIXBOT_BOSS; + self->monsterinfo.control_cost = M_JORG_CONTROL_COST; + self->monsterinfo.cost = M_COMMANDER_COST; + } + else + { + VectorSet(self->mins, -24, -24, -18); + VectorSet(self->maxs, 24, 24, 24); + self->health = M_FIXBOT_INITIAL_HEALTH + M_FIXBOT_ADDON_HEALTH * self->monsterinfo.level; + self->monsterinfo.power_armor_power = M_FIXBOT_INITIAL_ARMOR + M_FIXBOT_ADDON_ARMOR * self->monsterinfo.level; + self->mass = 150; + self->s.scale = 1.55f; + self->mtype = M_FIXBOT; + self->monsterinfo.control_cost = M_HOVER_CONTROL_COST; + self->monsterinfo.cost = M_HOVER_COST; + } + + self->gib_health = -100; + self->max_health = self->health; + self->flags |= FL_FLY | FL_NO_KNOCKBACK; + self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; + fixbot_set_fly_parameters(self); + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.sight_range = boss ? 1400 : 1024; + self->monsterinfo.pain_chance = boss ? 0.08f : 0.18f; + self->yaw_speed = boss ? 25 : 30; + + self->pain = fixbot_pain; + self->die = fixbot_die; + + self->monsterinfo.stand = fixbot_stand; + self->monsterinfo.walk = fixbot_walk; + self->monsterinfo.run = fixbot_run; + self->monsterinfo.attack = fixbot_attack; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &fixbot_move_stand; + self->monsterinfo.scale = 1.0f; + + qboolean isBoss = (self->mtype == M_FIXBOT_BOSS); + if (isBoss && !invasion->value) + G_PrintGreenText(va("A level %d fixer has spawned!", self->monsterinfo.level)); +} + +void init_drone_fixbot(edict_t *self) +{ + init_drone_fixbot_common(self, false); +} + +void init_drone_fixbot_boss(edict_t *self) +{ + init_drone_fixbot_common(self, true); +} diff --git a/src/entities/drone/drone_float.c b/src/entities/drone/drone_float.c index d1444931..3208d425 100644 --- a/src/entities/drone/drone_float.c +++ b/src/entities/drone/drone_float.c @@ -42,6 +42,16 @@ void floater_wham (edict_t *self); void floater_zap (edict_t *self); void floater_continue_attack(edict_t* self); +static void floater_set_fly_parameters(edict_t *self) +{ + // Using ranged-only attacks like hover does, so for now flyer and float do not have melee attacks + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_acceleration = 20.0f; + self->monsterinfo.fly_speed = 120.0f; + self->monsterinfo.fly_min_distance = 250.0f; + self->monsterinfo.fly_max_distance = 450.0f; +} + void floater_fire_blaster (edict_t *self) { int damage, speed=2000, effect; @@ -57,6 +67,11 @@ void floater_fire_blaster (edict_t *self) damage = M_HYPERBLASTER_DMG_MAX; MonsterAim(self, M_PROJECTILE_ACC, speed, false, MZ2_FLOAT_BLASTER_1, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + { + M_MonsterBlockedShot(self, 0.35f); + return; + } monster_fire_blaster(self, start, forward, damage, speed, EF_BLASTER, BLASTER_PROJ_BOLT, 2.0, true, MZ2_FLOAT_BLASTER_1); } @@ -219,23 +234,42 @@ mmove_t floater_move_activate = {FRAME_actvat01, FRAME_actvat31, floater_frames_ mframe_t floater_frames_attack1 [] = { - drone_ai_run, 15, NULL, // Blaster attack - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, floater_fire_blaster, // BOOM (0, -25.8, 32.5) -- LOOP Starts - drone_ai_run, 15, floater_fire_blaster, - drone_ai_run, 15, floater_fire_blaster, - drone_ai_run, 15, floater_fire_blaster, - drone_ai_run, 15, floater_fire_blaster, - drone_ai_run, 15, floater_fire_blaster, - drone_ai_run, 15, floater_fire_blaster, - drone_ai_run, 15, floater_continue_attack, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL // -- LOOP Ends + ai_charge, 0, NULL, // Blaster attack + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, floater_fire_blaster, // BOOM (0, -25.8, 32.5) -- LOOP Starts + ai_charge, 0, floater_fire_blaster, + ai_charge, 0, floater_fire_blaster, + ai_charge, 0, floater_fire_blaster, + ai_charge, 0, floater_fire_blaster, + ai_charge, 0, floater_fire_blaster, + ai_charge, 0, floater_fire_blaster, + ai_charge, 0, floater_continue_attack, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL // -- LOOP Ends }; mmove_t floater_move_attack1 = {FRAME_attak101, FRAME_attak114, floater_frames_attack1, floater_run}; +mframe_t floater_frames_attack1a [] = +{ + ai_charge, 10, NULL, // Blaster attack + ai_charge, 10, NULL, + ai_charge, 10, NULL, + ai_charge, 10, floater_fire_blaster, // BOOM (0, -25.8, 32.5) -- LOOP Starts + ai_charge, 10, floater_fire_blaster, + ai_charge, 10, floater_fire_blaster, + ai_charge, 10, floater_fire_blaster, + ai_charge, 10, floater_fire_blaster, + ai_charge, 10, floater_fire_blaster, + ai_charge, 10, floater_fire_blaster, + ai_charge, 10, floater_continue_attack, + ai_charge, 10, NULL, + ai_charge, 10, NULL, + ai_charge, 10, NULL // -- LOOP Ends +}; +mmove_t floater_move_attack1a = {FRAME_attak101, FRAME_attak114, floater_frames_attack1a, floater_run}; + mframe_t floater_frames_attack4[] = @@ -263,6 +297,8 @@ void floater_continue_attack(edict_t* self) if (self->monsterinfo.aiflags & AI_STAND_GROUND) move = &floater_move_attack4; + else if (self->monsterinfo.attack_state == AS_SLIDING) + move = &floater_move_attack1a; else move = &floater_move_attack1; @@ -273,7 +309,7 @@ void floater_continue_attack(edict_t* self) return; } - M_DelayNextAttack(self, 0, true); + M_DelayNextAttack(self, 0, true); } mframe_t floater_frames_attack2 [] = @@ -571,9 +607,22 @@ void floater_zap (edict_t *self) void floater_attack(edict_t *self) { if (self->monsterinfo.aiflags & AI_STAND_GROUND) + { + self->monsterinfo.attack_state = AS_STRAIGHT; self->monsterinfo.currentmove = &floater_move_attack4; - else + } + else if (random() < 0.5f) + { + self->monsterinfo.attack_state = AS_STRAIGHT; self->monsterinfo.currentmove = &floater_move_attack1; + } + else + { + if (random() <= 0.5f) + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + self->monsterinfo.attack_state = AS_SLIDING; + self->monsterinfo.currentmove = &floater_move_attack1a; + } } @@ -599,8 +648,8 @@ void floater_pain (edict_t *self, edict_t *other, float kick, int damage) return; self->pain_debounce_time = level.time + 3; - if (skill->value == 3) - return; // no pain anims in nightmare + if (invasion->value == 2) + return; n = (rand() + 1) % 3; if (n == 0) @@ -634,6 +683,7 @@ void floater_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama gi.multicast (self->s.origin, MULTICAST_PVS); M_Notify(self); + vrx_throw_drone_gibs(self, 55); M_Remove(self, false, false); } @@ -665,6 +715,8 @@ void init_drone_floater (edict_t *self) self->gib_health = -0.6 * BASE_GIB_HEALTH; self->mass = 150; self->mtype = M_FLOATER; + self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; + floater_set_fly_parameters(self); self->monsterinfo.power_armor_power = M_FLOATER_INITIAL_ARMOR + M_FLOATER_ADDON_ARMOR*self->monsterinfo.level; @@ -684,7 +736,9 @@ void init_drone_floater (edict_t *self) self->monsterinfo.run = floater_run; // self->monsterinfo.dodge = floater_dodge; self->monsterinfo.attack = floater_attack; - self->monsterinfo.melee = floater_melee; + // Future melee addon: + // self->monsterinfo.melee = floater_melee; + self->monsterinfo.melee = NULL; self->monsterinfo.sight = floater_sight; self->monsterinfo.idle = floater_idle; diff --git a/src/entities/drone/drone_flyer.c b/src/entities/drone/drone_flyer.c index d47f1a48..6aa5d230 100644 --- a/src/entities/drone/drone_flyer.c +++ b/src/entities/drone/drone_flyer.c @@ -28,6 +28,17 @@ void flyer_melee (edict_t *self); void flyer_setstart (edict_t *self); void flyer_stand (edict_t *self); void flyer_nextmove (edict_t *self); +static void flyer_attack_finished(edict_t *self); + +static void flyer_set_fly_parameters(edict_t *self) +{ + // Using ranged-only attacks like hover does, so for now flyer and float do not have melee attacks + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_acceleration = 15.0f; + self->monsterinfo.fly_speed = 165.0f; + self->monsterinfo.fly_min_distance = 250.0f; + self->monsterinfo.fly_max_distance = 450.0f; +} void flyer_sight (edict_t *self, edict_t *other) @@ -349,7 +360,7 @@ mmove_t flyer_move_bankleft = {FRAME_bankl01, FRAME_bankl07, flyer_frames_bankle void flyer_fire (edict_t *self, int flash_number) { - vec3_t start, forward; + vec3_t start, forward, right, offset; int effect, damage; if ((self->s.frame == FRAME_attak204) || (self->s.frame == FRAME_attak207) || (self->s.frame == FRAME_attak210)) @@ -361,7 +372,17 @@ void flyer_fire (edict_t *self, int flash_number) if (M_HYPERBLASTER_DMG_MAX && damage > M_HYPERBLASTER_DMG_MAX) damage = M_HYPERBLASTER_DMG_MAX; - MonsterAim(self, M_PROJECTILE_ACC, 2000, false, MZ2_FLOAT_BLASTER_1, forward, start); + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[flash_number], offset); + if (self->s.scale) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); + MonsterAim(self, M_PROJECTILE_ACC, 2000, false, -1, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + { + M_MonsterBlockedShot(self, 0.35f); + return; + } monster_fire_blaster(self, start, forward, damage, 2000, effect, BLASTER_PROJ_BOLT, 2.0, false, flash_number); } @@ -375,49 +396,66 @@ void flyer_fireright (edict_t *self) flyer_fire (self, MZ2_FLYER_BLASTER_2); } +static void flyer_reattack_blaster(edict_t *self) +{ + if (G_EntExists(self->enemy) && visible(self, self->enemy) && random() < 0.55f) + { + self->monsterinfo.nextframe = FRAME_attak204; + return; + } + + flyer_attack_finished(self); +} + mframe_t flyer_frames_attack3[] = +{ + ai_charge, 10, NULL, + ai_charge, 10, NULL, + ai_charge, 10, NULL, + ai_charge, 10, flyer_fireleft, // left gun + ai_charge, 10, flyer_fireright, // right gun + ai_charge, 10, flyer_fireleft, // left gun + ai_charge, 10, flyer_fireright, // right gun + ai_charge, 10, flyer_fireleft, // left gun + ai_charge, 10, flyer_fireright, // right gun + ai_charge, 10, flyer_fireleft, // left gun + ai_charge, 10, flyer_fireright, // right gun + ai_charge, 10, NULL, + ai_charge, 10, NULL, + ai_charge, 10, NULL, + ai_charge, -15, flyer_reattack_blaster, + ai_charge, 10, NULL, + ai_charge, 10, NULL +}; +static void flyer_attack_finished(edict_t *self) +{ + self->monsterinfo.attack_finished = level.time + 0.8f + random() * 0.6f; + flyer_run(self); +} + +mmove_t flyer_move_attack3 = { FRAME_attak201, FRAME_attak217, flyer_frames_attack3, flyer_attack_finished }; + +mframe_t flyer_frames_attack2 [] = { ai_charge, 0, NULL, ai_charge, 0, NULL, ai_charge, 0, NULL, - ai_charge, 0, flyer_fireleft, // left gun - ai_charge, 0, flyer_fireright, // right gun - ai_charge, 0, flyer_fireleft, // left gun - ai_charge, 0, flyer_fireright, // right gun - ai_charge, 0, flyer_fireleft, // left gun - ai_charge, 0, flyer_fireright, // right gun - ai_charge, 0, flyer_fireleft, // left gun - ai_charge, 0, flyer_fireright, // right gun - ai_charge, 0, NULL, + ai_charge, -10, flyer_fireleft, // left gun + ai_charge, -10, flyer_fireright, // right gun + ai_charge, -10, flyer_fireleft, // left gun + ai_charge, -10, flyer_fireright, // right gun + ai_charge, -10, flyer_fireleft, // left gun + ai_charge, -10, flyer_fireright, // right gun + ai_charge, -10, flyer_fireleft, // left gun + ai_charge, -10, flyer_fireright, // right gun ai_charge, 0, NULL, ai_charge, 0, NULL, ai_charge, 0, NULL, + ai_charge, -15, flyer_reattack_blaster, ai_charge, 0, NULL, ai_charge, 0, NULL }; -mmove_t flyer_move_attack3 = { FRAME_attak201, FRAME_attak217, flyer_frames_attack3, flyer_run }; - -mframe_t flyer_frames_attack2 [] = -{ - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, flyer_fireleft, // left gun - drone_ai_run, 20, flyer_fireright, // right gun - drone_ai_run, 20, flyer_fireleft, // left gun - drone_ai_run, 20, flyer_fireright, // right gun - drone_ai_run, 20, flyer_fireleft, // left gun - drone_ai_run, 20, flyer_fireright, // right gun - drone_ai_run, 20, flyer_fireleft, // left gun - drone_ai_run, 20, flyer_fireright, // right gun - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL -}; -mmove_t flyer_move_attack2 = {FRAME_attak201, FRAME_attak217, flyer_frames_attack2, flyer_run}; +mmove_t flyer_move_attack2 = {FRAME_attak201, FRAME_attak217, flyer_frames_attack2, flyer_attack_finished}; void flyer_slash_left (edict_t *self) @@ -478,12 +516,23 @@ mmove_t flyer_move_loop_melee = {FRAME_attak107, FRAME_attak118, flyer_frames_lo void flyer_check_melee(edict_t *self) { if (entdist (self, self->enemy) == RANGE_MELEE) + { if (random() <= 0.8) + { + flyer_set_fly_parameters(self); self->monsterinfo.currentmove = &flyer_move_loop_melee; + } else + { + flyer_set_fly_parameters(self); self->monsterinfo.currentmove = &flyer_move_end_melee; + } + } else + { + flyer_set_fly_parameters(self); self->monsterinfo.currentmove = &flyer_move_end_melee; + } } void flyer_loop_melee (edict_t *self) @@ -498,13 +547,28 @@ void flyer_loop_melee (edict_t *self) void flyer_attack (edict_t *self) { + flyer_set_fly_parameters(self); + /* if (random() <= 0.5) self->monsterinfo.currentmove = &flyer_move_attack1; else */ if (self->monsterinfo.aiflags & AI_STAND_GROUND) + { + self->monsterinfo.attack_state = AS_STRAIGHT; self->monsterinfo.currentmove = &flyer_move_attack3; - else + } + else if (random() < 0.5f) + { + self->monsterinfo.attack_state = AS_STRAIGHT; self->monsterinfo.currentmove = &flyer_move_attack2; + } + else + { + if (random() <= 0.5f) + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + self->monsterinfo.attack_state = AS_SLIDING; + self->monsterinfo.currentmove = &flyer_move_attack3; + } } void flyer_setstart (edict_t *self) @@ -528,6 +592,7 @@ void flyer_melee (edict_t *self) // flyer.nextmove = ACTION_attack1; // self->monsterinfo.currentmove = &flyer_move_stop; //self->monsterinfo.currentmove = &flyer_move_start_melee; + flyer_set_fly_parameters(self); } void flyer_pain (edict_t *self, edict_t *other, float kick, int damage) @@ -541,8 +606,8 @@ void flyer_pain (edict_t *self, edict_t *other, float kick, int damage) return; self->pain_debounce_time = level.time + 3; - if (skill->value == 3) - return; // no pain anims in nightmare + if (invasion->value == 2) + return; n = rand() % 3; if (n == 0) @@ -572,6 +637,7 @@ void flyer_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, gi.multicast (self->s.origin, MULTICAST_PVS); M_Notify(self); + vrx_throw_drone_gibs(self, 55); M_Remove(self, false, false); } @@ -591,6 +657,7 @@ void init_drone_flyer (edict_t *self) gi.soundindex ("flyer/flyatck3.wav"); self->s.modelindex = gi.modelindex ("models/monsters/flyer/tris.md2"); + gi.imageindex ("models/monsters/flyer/pain.pcx"); VectorSet (self->mins, -16, -16, -24); VectorSet (self->maxs, 16, 16, 8); self->movetype = MOVETYPE_STEP; @@ -603,6 +670,9 @@ void init_drone_flyer (edict_t *self) self->mtype = M_FLYER; self->flags |= FL_FLY; + self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; + self->monsterinfo.fly_buzzard = true; + flyer_set_fly_parameters(self); self->max_health = self->health; self->monsterinfo.power_armor_power = M_FLYER_INITIAL_ARMOR + M_FLYER_ADDON_ARMOR*self->monsterinfo.level; self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; diff --git a/src/entities/drone/drone_gekk.c b/src/entities/drone/drone_gekk.c new file mode 100644 index 00000000..89472393 --- /dev/null +++ b/src/entities/drone/drone_gekk.c @@ -0,0 +1,1200 @@ +/* +============================================================================== + +GEKK + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_xatrix_gekk.h" + +static int sound_swing; +static int sound_hit; +static int sound_hit2; +static int sound_speet; +static int sound_death; +static int sound_pain1; +static int sound_sight; +static int sound_search; +static int sound_step1; +static int sound_step2; +static int sound_step3; +static int sound_thud; + +void drone_ai_stand(edict_t *self, float dist); +void drone_ai_run(edict_t *self, float dist); +void drone_ai_walk(edict_t *self, float dist); +void drone_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf); + +static void gekk_stand(edict_t *self); +static void gekk_walk(edict_t *self); +static void gekk_run_start(edict_t *self); +static void gekk_run(edict_t *self); +static void gekk_land_to_water(edict_t *self); +static void gekk_water_to_land(edict_t *self); +static void gekk_swim_loop(edict_t *self); +static void gekk_swim_check(edict_t *self); +static void gekk_check_landing(edict_t *self); +static void gekk_stop_skid(edict_t *self); + +extern mmove_t gekk_move_standunderwater; +extern mmove_t gekk_move_swim_loop; +extern mmove_t gekk_move_swim_start; +extern mmove_t gekk_move_leapatk; +extern mmove_t gekk_move_leapatk2; +extern mmove_t gekk_move_run_start; +extern mmove_t gekk_move_attack1; +extern mmove_t gekk_move_attack2; + +extern void fire_acid(edict_t *self, vec3_t start, vec3_t aimdir, int projectile_damage, float radius, + int speed, int acid_damage, float acid_duration, int gas_damage, float gas_radius, float gas_duration); + +static void gekk_set_leap_cooldown(edict_t *self, float base, float extra) +{ + float cooldown = level.time + base + random() * extra; + + if (self->monsterinfo.dodge_time < cooldown) + self->monsterinfo.dodge_time = cooldown; +} + +static void gekk_step(edict_t *self) +{ + const float r = random(); + + if (r < 0.33) + gi.sound(self, CHAN_BODY, sound_step1, 1, ATTN_NORM, 0); + else if (r < 0.66) + gi.sound(self, CHAN_BODY, sound_step2, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_BODY, sound_step3, 1, ATTN_NORM, 0); +} + +static void gekk_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); +} + +static void gekk_search(edict_t *self) +{ + gi.sound(self, CHAN_VOICE, sound_search, 1, ATTN_IDLE, 0); +} + +static void gekk_setskin(edict_t *self) +{ + if (self->health < (self->max_health / 4)) + self->s.skinnum = 2; + else if (self->health < (self->max_health / 2)) + self->s.skinnum = 1; + else + self->s.skinnum = 0; +} + +static void gekk_set_fly_parameters(edict_t *self) +{ + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_acceleration = 25.0f; + self->monsterinfo.fly_speed = 150.0f; + self->monsterinfo.fly_min_distance = 10.0f; + self->monsterinfo.fly_max_distance = 10.0f; +} + +static qboolean gekk_should_swim(edict_t *self) +{ + return self->waterlevel >= WATER_WAIST; +} + +static void gekk_set_water_bounds(edict_t *self) +{ + gekk_set_fly_parameters(self); + if (!self->goalentity) + self->goalentity = world; + self->flags |= FL_SWIM; + self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; + self->yaw_speed = 10; + self->viewheight = 10; + VectorSet(self->mins, -18, -18, -24); + VectorSet(self->maxs, 18, 18, 16); + gi.linkentity(self); +} + +static void gekk_set_land_bounds(edict_t *self) +{ + self->flags &= ~FL_SWIM; + self->monsterinfo.aiflags &= ~AI_ALTERNATE_FLY; + self->monsterinfo.fly_pinned = false; + self->yaw_speed = 20; + self->viewheight = 25; + VectorSet(self->mins, -18, -18, -24); + VectorSet(self->maxs, 18, 18, 24); + gi.linkentity(self); +} + +mframe_t gekk_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t gekk_move_stand = { FRAME_stand_01, FRAME_stand_39, gekk_frames_stand, gekk_stand }; + +static void gekk_stand(edict_t *self) +{ + gekk_setskin(self); + + if (gekk_should_swim(self)) + { + gekk_set_water_bounds(self); + self->monsterinfo.currentmove = &gekk_move_standunderwater; + } + else + { + if (self->flags & FL_SWIM) + gekk_set_land_bounds(self); + self->monsterinfo.currentmove = &gekk_move_stand; + } +} + +mframe_t gekk_frames_walk[] = +{ + drone_ai_walk, 3.849f, gekk_swim_check, + drone_ai_walk, 19.606f, NULL, + drone_ai_walk, 25.583f, NULL, + drone_ai_walk, 34.625f, gekk_step, + drone_ai_walk, 27.365f, NULL, + drone_ai_walk, 28.480f, NULL +}; +mmove_t gekk_move_walk = { FRAME_run_01, FRAME_run_06, gekk_frames_walk, gekk_walk }; + +static void gekk_walk(edict_t *self) +{ + gekk_setskin(self); + + if (gekk_should_swim(self)) + { + gekk_land_to_water(self); + return; + } + if (self->flags & FL_SWIM) + gekk_set_land_bounds(self); + + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &gekk_move_walk; +} + +mframe_t gekk_frames_run[] = +{ + drone_ai_run, 3.849f, gekk_swim_check, + drone_ai_run, 19.606f, NULL, + drone_ai_run, 25.583f, NULL, + drone_ai_run, 34.625f, gekk_step, + drone_ai_run, 27.365f, NULL, + drone_ai_run, 28.480f, NULL +}; +mmove_t gekk_move_run = { FRAME_run_01, FRAME_run_06, gekk_frames_run, gekk_run }; + +static void gekk_run_start(edict_t *self) +{ + if (gekk_should_swim(self)) + { + gekk_land_to_water(self); + return; + } + + if (self->flags & FL_SWIM) + gekk_set_land_bounds(self); + + self->monsterinfo.currentmove = &gekk_move_run_start; +} + +mframe_t gekk_frames_run_start[] = +{ + drone_ai_run, 0.212f, NULL, + drone_ai_run, 19.753f, NULL +}; +mmove_t gekk_move_run_start = { FRAME_stand_01, FRAME_stand_02, gekk_frames_run_start, gekk_run }; + +static void gekk_run(edict_t *self) +{ + gekk_setskin(self); + + if (gekk_should_swim(self)) + { + gekk_land_to_water(self); + return; + } + if (self->flags & FL_SWIM) + gekk_set_land_bounds(self); + + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &gekk_move_stand; + else + self->monsterinfo.currentmove = &gekk_move_run; +} + +static int gekk_melee_damage(edict_t *self) +{ + int damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + + if (M_MELEE_DMG_MAX && damage > M_MELEE_DMG_MAX) + damage = M_MELEE_DMG_MAX; + + return damage; +} + +static void gekk_bite(edict_t *self) +{ + gi.sound(self, CHAN_WEAPON, sound_hit, 1, ATTN_NORM, 0); + M_MeleeAttack(self, self->enemy, 80, gekk_melee_damage(self), 120); +} + +static void gekk_hit_left(edict_t *self) +{ + gi.sound(self, CHAN_WEAPON, sound_hit, 1, ATTN_NORM, 0); + M_MeleeAttack(self, self->enemy, 96, gekk_melee_damage(self), 180); +} + +static void gekk_hit_right(edict_t *self) +{ + gi.sound(self, CHAN_WEAPON, sound_hit2, 1, ATTN_NORM, 0); + M_MeleeAttack(self, self->enemy, 96, gekk_melee_damage(self), 180); +} + +static void gekk_check_refire(edict_t *self) +{ + if (!G_EntExists(self->enemy) || self->monsterinfo.melee_finished > level.time) + return; + + if (entdist(self, self->enemy) <= 96) + { + if (self->s.frame == FRAME_clawatk3_09) + self->monsterinfo.currentmove = &gekk_move_attack2; + else if (self->s.frame == FRAME_clawatk5_09) + self->monsterinfo.currentmove = &gekk_move_attack1; + } +} + +static void gekk_swim_check(edict_t *self) +{ + if (!gekk_should_swim(self) && (self->flags & FL_SWIM)) + gekk_water_to_land(self); +} + +static void gekk_swim_loop(edict_t *self) +{ + if (!gekk_should_swim(self)) + { + gekk_water_to_land(self); + return; + } + + if (G_EntExists(self->enemy) && self->enemy->waterlevel < WATER_WAIST && random() < 0.3f) + { + gekk_water_to_land(self); + return; + } + + gekk_set_water_bounds(self); + self->monsterinfo.currentmove = &gekk_move_swim_loop; +} + +mframe_t gekk_frames_standunderwater[] = +{ + drone_ai_stand, 14, NULL, + drone_ai_stand, 14, NULL, + drone_ai_stand, 14, NULL, + drone_ai_stand, 14, NULL, + drone_ai_stand, 16, NULL, + drone_ai_stand, 16, NULL, + drone_ai_stand, 16, NULL, + drone_ai_stand, 18, NULL, + drone_ai_stand, 18, NULL, + drone_ai_stand, 18, NULL, + drone_ai_stand, 20, NULL, + drone_ai_stand, 20, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 24, NULL, + drone_ai_stand, 24, NULL, + drone_ai_stand, 26, NULL, + drone_ai_stand, 26, NULL, + drone_ai_stand, 24, NULL, + drone_ai_stand, 24, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 22, NULL, + drone_ai_stand, 18, NULL, + drone_ai_stand, 18, NULL, + drone_ai_stand, 18, NULL, + drone_ai_stand, 18, gekk_swim_check +}; +mmove_t gekk_move_standunderwater = { FRAME_swim_01, FRAME_swim_32, gekk_frames_standunderwater, gekk_stand }; + +mframe_t gekk_frames_swim[] = +{ + drone_ai_run, 14, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 20, NULL, + drone_ai_run, 20, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 24, NULL, + drone_ai_run, 24, NULL, + drone_ai_run, 26, NULL, + drone_ai_run, 26, NULL, + drone_ai_run, 24, NULL, + drone_ai_run, 24, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, gekk_swim_check +}; +mmove_t gekk_move_swim_loop = { FRAME_swim_01, FRAME_swim_32, gekk_frames_swim, gekk_swim_loop }; + +mframe_t gekk_frames_swim_start[] = +{ + drone_ai_run, 14, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, gekk_hit_left, + drone_ai_run, 18, NULL, + drone_ai_run, 20, NULL, + drone_ai_run, 20, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 24, gekk_hit_right, + drone_ai_run, 24, NULL, + drone_ai_run, 26, NULL, + drone_ai_run, 26, NULL, + drone_ai_run, 24, NULL, + drone_ai_run, 24, NULL, + drone_ai_run, 22, gekk_bite, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 22, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, gekk_swim_check +}; +mmove_t gekk_move_swim_start = { FRAME_swim_01, FRAME_swim_32, gekk_frames_swim_start, gekk_swim_loop }; + +static void gekk_land_to_water(edict_t *self) +{ + gekk_set_water_bounds(self); + self->monsterinfo.currentmove = &gekk_move_swim_start; +} + +static void gekk_water_to_land(edict_t *self) +{ + gekk_set_land_bounds(self); + if (G_EntExists(self->enemy)) + { + self->monsterinfo.attack_finished = level.time + 1.5; + self->monsterinfo.currentmove = &gekk_move_leapatk2; + return; + } + + self->monsterinfo.currentmove = self->goalentity ? &gekk_move_run : &gekk_move_stand; +} + +mframe_t gekk_frames_attack[] = +{ + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, gekk_bite, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, gekk_bite, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, gekk_hit_left, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, NULL, + ai_charge, 16, gekk_hit_right, + ai_charge, 16, NULL, + ai_charge, 0, NULL +}; +mmove_t gekk_move_attack = { FRAME_attack_01, FRAME_attack_21, gekk_frames_attack, gekk_run_start }; + +mframe_t gekk_frames_attack1[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, gekk_hit_left, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, gekk_check_refire +}; +mmove_t gekk_move_attack1 = { FRAME_clawatk3_01, FRAME_clawatk3_09, gekk_frames_attack1, gekk_run_start }; + +mframe_t gekk_frames_attack2[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, gekk_hit_left, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, gekk_hit_right, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, gekk_check_refire +}; +mmove_t gekk_move_attack2 = { FRAME_clawatk5_01, FRAME_clawatk5_09, gekk_frames_attack2, gekk_run_start }; + +static void gekk_melee(edict_t *self) +{ + if (gekk_should_swim(self)) + { + gekk_land_to_water(self); + self->monsterinfo.melee_finished = level.time + 1.0; + return; + } + if (self->flags & FL_SWIM) + gekk_set_land_bounds(self); + + if (!G_ValidTarget(self, self->enemy, true, true) || entdist(self, self->enemy) > 120) + { + self->monsterinfo.melee_finished = level.time + 0.5; + gekk_run(self); + return; + } + + float attack_roll = random(); + if (attack_roll > 0.66f) + self->monsterinfo.currentmove = &gekk_move_attack1; + else + self->monsterinfo.currentmove = &gekk_move_attack2; +} + +static qboolean gekk_can_leap(edict_t *self) +{ + vec3_t v; + float distance; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return false; + if (!self->groundentity) + return false; + if (self->monsterinfo.melee_finished > level.time) + return false; + if (self->monsterinfo.dodge_time > level.time) + return false; + if (!visible(self, self->enemy)) + return false; + + VectorSubtract(self->s.origin, self->enemy->s.origin, v); + v[2] = 0; + distance = VectorLength(v); + + if (distance < 100) + { + if (self->absmax[2] <= self->enemy->absmin[2]) + return false; + } + else if (self->absmin[2] + 125 < self->enemy->absmin[2]) + return false; + + if (self->waterlevel > 1 && random() < 0.2f) + return false; + + return true; +} + +static qboolean gekk_use_high_leap(edict_t *self) +{ + vec3_t v; + float distance; + + if (!G_EntExists(self->enemy)) + return false; + if (self->absmin[2] + 125 < self->enemy->absmin[2]) + return false; + + VectorSubtract(self->s.origin, self->enemy->s.origin, v); + v[2] = 0; + distance = VectorLength(v); + if (distance < 100) + return false; + + return random() >= (self->waterlevel > 1 ? 0.2f : 0.9f); +} + +static void gekk_jump_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf) +{ + if (self->health <= 0) + { + self->touch = drone_touch; + return; + } + + if (other && other->takedamage) + return; + + if (!M_CheckBottom(self)) + { + if (self->groundentity) + { + self->monsterinfo.nextframe = FRAME_leapatk_11; + self->touch = drone_touch; + } + return; + } + + self->touch = drone_touch; +} + +static void gekk_jump_takeoff(edict_t *self) +{ + vec3_t forward; + + if (!G_EntExists(self->enemy)) + return; + + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + self->lastsound = level.framenum; + self->s.origin[2] += 1; + self->groundentity = NULL; + AngleVectors(self->s.angles, forward, NULL, NULL); + if (gekk_use_high_leap(self)) + { + VectorScale(forward, 700, self->velocity); + self->velocity[2] = 250; + } + else + { + VectorScale(forward, 250, self->velocity); + self->velocity[2] = 400; + } + self->monsterinfo.aiflags |= AI_DUCKED; + self->monsterinfo.attack_finished = level.time + 3.0; + self->style = 1; + self->touch = gekk_jump_touch; + gekk_set_leap_cooldown(self, 1.0, 0.5); +} + +static void gekk_jump_takeoff2(edict_t *self) +{ + vec3_t forward; + + if (!G_EntExists(self->enemy)) + return; + + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + self->lastsound = level.framenum; + self->s.origin[2] = self->enemy->s.origin[2]; + self->groundentity = NULL; + AngleVectors(self->s.angles, forward, NULL, NULL); + if (gekk_use_high_leap(self)) + { + VectorScale(forward, 300, self->velocity); + self->velocity[2] = 250; + } + else + { + VectorScale(forward, 150, self->velocity); + self->velocity[2] = 300; + } + self->monsterinfo.aiflags |= AI_DUCKED; + self->monsterinfo.attack_finished = level.time + 3.0; + self->style = 1; + self->touch = gekk_jump_touch; + gekk_set_leap_cooldown(self, 1.0, 0.5); +} + +static void gekk_stop_skid(edict_t *self) +{ + if (self->groundentity) + { + VectorClear(self->velocity); + self->monsterinfo.aiflags &= ~AI_DUCKED; + self->touch = drone_touch; + } +} + +static void gekk_check_landing(edict_t *self) +{ + if (self->groundentity) + { + gi.sound(self, CHAN_WEAPON, sound_thud, 1, ATTN_NORM, 0); + self->monsterinfo.aiflags &= ~AI_DUCKED; + self->monsterinfo.attack_finished = 0; + self->touch = drone_touch; + VectorClear(self->velocity); + gekk_set_leap_cooldown(self, 1.5, 1.0); + return; + } + + if (self->waterlevel > 1) + { + self->monsterinfo.aiflags &= ~AI_DUCKED; + self->monsterinfo.nextframe = FRAME_leapatk_11; + self->touch = drone_touch; + gekk_land_to_water(self); + return; + } + + vec3_t forward; + AngleVectors(self->s.angles, forward, NULL, NULL); + if (DotProduct(forward, self->velocity) < 200) + VectorMA(self->velocity, 200, forward, self->velocity); + + if (level.time > self->monsterinfo.attack_finished) + self->monsterinfo.nextframe = FRAME_leapatk_11; + else + self->monsterinfo.nextframe = FRAME_leapatk_12; +} + +static void gekk_spit(edict_t *self) +{ + int acid_level, damage, speed; + float radius; + vec3_t forward, right, start, target, dir, offset; + + if (!G_EntExists(self->enemy)) + return; + + acid_level = drone_damagelevel(self); + if (acid_level > 15) + acid_level = 15; + + damage = ACID_INITIAL_DAMAGE + ACID_ADDON_DAMAGE * acid_level; + speed = ACID_INITIAL_SPEED + ACID_ADDON_SPEED * acid_level; + radius = ACID_INITIAL_RADIUS + ACID_ADDON_RADIUS * acid_level; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorSet(offset, -18, -1, 24); + G_ProjectSource(self->s.origin, offset, forward, right, start); + + VectorCopy(self->enemy->s.origin, target); + target[2] += self->enemy->viewheight; + VectorSubtract(target, start, dir); + VectorNormalize(dir); + + gi.sound(self, CHAN_WEAPON, sound_speet, 1, ATTN_NORM, 0); + fire_acid(self, start, dir, damage, radius, speed, (int)(0.1 * damage), ACID_DURATION, 0, 0, 0); +} + +mframe_t gekk_frames_leapatk[] = +{ + ai_charge, 0, NULL, + ai_charge, -0.387f, NULL, + ai_charge, -1.113f, NULL, + ai_charge, -0.237f, NULL, + ai_charge, 6.720f, gekk_jump_takeoff, + ai_charge, 6.414f, NULL, + ai_charge, 0.163f, NULL, + ai_charge, 28.316f, NULL, + ai_charge, 24.198f, NULL, + ai_charge, 31.742f, NULL, + ai_charge, 35.977f, gekk_check_landing, + ai_charge, 12.303f, gekk_stop_skid, + ai_charge, 20.122f, gekk_stop_skid, + ai_charge, -1.042f, gekk_stop_skid, + ai_charge, 2.556f, gekk_stop_skid, + ai_charge, 0.544f, gekk_stop_skid, + ai_charge, 1.862f, gekk_stop_skid, + ai_charge, 1.224f, gekk_stop_skid, + ai_charge, -0.457f, gekk_swim_check +}; +mmove_t gekk_move_leapatk = { FRAME_leapatk_01, FRAME_leapatk_19, gekk_frames_leapatk, gekk_run_start }; + +mframe_t gekk_frames_leapatk2[] = +{ + ai_charge, 0, NULL, + ai_charge, -0.387f, NULL, + ai_charge, -1.113f, NULL, + ai_charge, -0.237f, NULL, + ai_charge, 6.720f, gekk_jump_takeoff2, + ai_charge, 6.414f, NULL, + ai_charge, 0.163f, NULL, + ai_charge, 28.316f, NULL, + ai_charge, 24.198f, NULL, + ai_charge, 31.742f, NULL, + ai_charge, 35.977f, gekk_check_landing, + ai_charge, 12.303f, gekk_stop_skid, + ai_charge, 20.122f, gekk_stop_skid, + ai_charge, -1.042f, gekk_stop_skid, + ai_charge, 2.556f, gekk_stop_skid, + ai_charge, 0.544f, gekk_stop_skid, + ai_charge, 1.862f, gekk_stop_skid, + ai_charge, 1.224f, gekk_stop_skid, + ai_charge, -0.457f, gekk_swim_check +}; +mmove_t gekk_move_leapatk2 = { FRAME_leapatk_01, FRAME_leapatk_19, gekk_frames_leapatk2, gekk_run_start }; + +mframe_t gekk_frames_spit[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, gekk_spit, + ai_charge, 0, NULL +}; +mmove_t gekk_move_spit = { FRAME_spit_01, FRAME_spit_07, gekk_frames_spit, gekk_run_start }; + +static void gekk_attack(edict_t *self) +{ + if (gekk_should_swim(self)) + { + if (G_EntExists(self->enemy) && self->enemy->waterlevel < WATER_WAIST) + gekk_water_to_land(self); + else + gekk_land_to_water(self); + self->monsterinfo.melee_finished = level.time + 1.0; + M_DelayNextAttack(self, 1.0 + random(), true); + return; + } + if (self->flags & FL_SWIM) + gekk_set_land_bounds(self); + + if (!G_EntExists(self->enemy)) + return; + + float enemy_distance = entdist(self, self->enemy); + if (enemy_distance >= 500.0f) + { + if (random() > 0.5f) + { + self->monsterinfo.currentmove = &gekk_move_spit; + self->monsterinfo.melee_finished = level.time + 0.5; + } + else + { + self->monsterinfo.currentmove = &gekk_move_run_start; + self->monsterinfo.attack_finished = level.time + 2.0; + } + } + else if (random() > 0.7f) + { + self->monsterinfo.currentmove = &gekk_move_spit; + self->monsterinfo.melee_finished = level.time + 0.5; + } + else + { + if (!gekk_can_leap(self) || random() > 0.7f) + { + self->monsterinfo.currentmove = &gekk_move_run_start; + self->monsterinfo.attack_finished = level.time + 1.4; + } + else + { + self->monsterinfo.currentmove = &gekk_move_leapatk; + self->monsterinfo.melee_finished = level.time + 3.0; + } + } + M_DelayNextAttack(self, 1.0 + random(), true); +} + +mframe_t gekk_frames_pain[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t gekk_move_pain = { FRAME_pain_01, FRAME_pain_06, gekk_frames_pain, gekk_run_start }; + +mframe_t gekk_frames_pain1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t gekk_move_pain1 = { FRAME_pain3_01, FRAME_pain3_11, gekk_frames_pain1, gekk_run_start }; + +mframe_t gekk_frames_pain2[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t gekk_move_pain2 = { FRAME_pain4_01, FRAME_pain4_13, gekk_frames_pain2, gekk_run_start }; + +static void gekk_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + gekk_setskin(self); + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0; + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + + if (self->waterlevel >= 2) + self->monsterinfo.currentmove = &gekk_move_pain; + else if (random() < 0.5) + self->monsterinfo.currentmove = &gekk_move_pain1; + else + self->monsterinfo.currentmove = &gekk_move_pain2; +} + +static void gekk_dead(edict_t *self) +{ + self->flags &= ~FL_SWIM; + VectorSet(self->mins, -18, -18, -24); + VectorSet(self->maxs, 18, 18, -8); + self->movetype = MOVETYPE_TOSS; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); + M_PrepBodyRemoval(self); +} + +static void gekk_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + +static void gekk_gibfest(edict_t *self) +{ + if (vrx_throw_drone_gibs(self, 20)) + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); + self->takedamage = DAMAGE_NO; + self->solid = SOLID_NOT; + self->svflags |= SVF_NOCLIENT; + self->deadflag = DEAD_DEAD; + self->activator = NULL; + self->think = G_FreeEdict; + self->nextthink = level.time + FRAMETIME; + gi.unlinkentity(self); +} + +static void gekk_isgibfest(edict_t *self) +{ + if (random() > 0.9f) + gekk_gibfest(self); +} + +mframe_t gekk_frames_death1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, -7, gekk_shrink, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t gekk_move_death1 = { FRAME_death1_01, FRAME_death1_10, gekk_frames_death1, gekk_dead }; + +mframe_t gekk_frames_death3[] = +{ + ai_move, 0, NULL, + ai_move, 0.022f, NULL, + ai_move, 0.169f, NULL, + ai_move, -0.710f, NULL, + ai_move, -13.446f, NULL, + ai_move, -7.654f, gekk_isgibfest, + ai_move, -31.951f, NULL +}; +mmove_t gekk_move_death3 = { FRAME_death3_01, FRAME_death3_07, gekk_frames_death3, gekk_dead }; + +mframe_t gekk_frames_death4[] = +{ + ai_move, 5.103f, NULL, + ai_move, -4.808f, NULL, + ai_move, -10.509f, NULL, + ai_move, -9.899f, NULL, + ai_move, 4.033f, gekk_isgibfest, + ai_move, -5.197f, NULL, + ai_move, -0.919f, NULL, + ai_move, -8.821f, NULL, + ai_move, -5.626f, NULL, + ai_move, -8.865f, gekk_isgibfest, + ai_move, -0.845f, NULL, + ai_move, 1.986f, NULL, + ai_move, 0.170f, NULL, + ai_move, 1.339f, gekk_isgibfest, + ai_move, -0.922f, NULL, + ai_move, 0.818f, NULL, + ai_move, -1.288f, NULL, + ai_move, -1.408f, gekk_isgibfest, + ai_move, -7.787f, NULL, + ai_move, -3.995f, NULL, + ai_move, -4.604f, NULL, + ai_move, -1.715f, gekk_isgibfest, + ai_move, -0.564f, NULL, + ai_move, -0.597f, NULL, + ai_move, 0.074f, NULL, + ai_move, -0.309f, gekk_isgibfest, + ai_move, -0.395f, NULL, + ai_move, -0.501f, NULL, + ai_move, -0.325f, NULL, + ai_move, -0.931f, gekk_isgibfest, + ai_move, -1.433f, NULL, + ai_move, -1.626f, NULL, + ai_move, 4.680f, NULL, + ai_move, 0.560f, NULL, + ai_move, -0.549f, gekk_gibfest +}; +mmove_t gekk_move_death4 = { FRAME_death4_01, FRAME_death4_35, gekk_frames_death4, gekk_dead }; + +mframe_t gekk_frames_wdeath[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t gekk_move_wdeath = { FRAME_wdeath_01, FRAME_wdeath_45, gekk_frames_wdeath, gekk_dead }; + +static void gekk_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + float r; + + M_Notify(self); + + if (self->health <= self->gib_health) + { + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); + vrx_throw_drone_gibs(self, damage); + M_Remove(self, false, false); + return; + } + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + gekk_setskin(self); + if (self->waterlevel >= WATER_WAIST) + { + VectorClear(self->velocity); + self->monsterinfo.aiflags &= ~AI_ALTERNATE_FLY; + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_pinned = false; + gekk_shrink(self); + self->monsterinfo.currentmove = &gekk_move_wdeath; + } + else + { + r = random(); + if (r > 0.66f) + self->monsterinfo.currentmove = &gekk_move_death1; + else if (r > 0.33f) + self->monsterinfo.currentmove = &gekk_move_death3; + else + self->monsterinfo.currentmove = &gekk_move_death4; + } + + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; +} + +void init_drone_gekk(edict_t *self) +{ + sound_swing = gi.soundindex("gek/gk_atck1.wav"); + sound_hit = gi.soundindex("gek/gk_atck2.wav"); + sound_hit2 = gi.soundindex("gek/gk_atck3.wav"); + sound_speet = gi.soundindex("gek/gk_atck4.wav"); + sound_death = gi.soundindex("gek/gk_deth1.wav"); + sound_pain1 = gi.soundindex("gek/gk_pain1.wav"); + sound_sight = gi.soundindex("gek/gk_sght1.wav"); + sound_search = gi.soundindex("gek/gk_idle1.wav"); + sound_step1 = gi.soundindex("gek/gk_step1.wav"); + sound_step2 = gi.soundindex("gek/gk_step2.wav"); + sound_step3 = gi.soundindex("gek/gk_step3.wav"); + sound_thud = gi.soundindex("mutant/thud1.wav"); + gi.soundindex("gek/loogie_hit.wav"); + gi.soundindex("weapons/rocklx1a.wav"); + gi.modelindex("models/objects/loogy/tris.md2"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/gekk/tris.md2"); + VectorSet(self->mins, -18, -18, -24); + VectorSet(self->maxs, 18, 18, 24); + self->viewheight = 25; + + gi.modelindex("models/objects/gekkgib/pelvis/tris.md2"); + gi.modelindex("models/objects/gekkgib/arm/tris.md2"); + gi.modelindex("models/objects/gekkgib/torso/tris.md2"); + gi.modelindex("models/objects/gekkgib/claw/tris.md2"); + gi.modelindex("models/objects/gekkgib/leg/tris.md2"); + gi.modelindex("models/objects/gekkgib/head/tris.md2"); + + self->health = M_GEKK_INITIAL_HEALTH + M_GEKK_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -30; + self->mass = 300; + self->mtype = M_GEKK; + + self->monsterinfo.power_armor_power = M_GEKK_INITIAL_ARMOR + M_GEKK_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.power_armor_type = POWER_ARMOR_SCREEN; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_MUTANT_CONTROL_COST; + self->monsterinfo.cost = M_MUTANT_COST; + self->monsterinfo.jumpdn = 256; + self->monsterinfo.jumpup = 88; + gekk_set_fly_parameters(self); + + self->item = FindItemByClassname("ammo_cells"); + + self->pain = gekk_pain; + self->die = gekk_die; + self->monsterinfo.stand = gekk_stand; + self->monsterinfo.walk = gekk_walk; + self->monsterinfo.run = gekk_run; + self->monsterinfo.attack = gekk_attack; + self->monsterinfo.melee = gekk_melee; + self->monsterinfo.sight = gekk_sight; + self->monsterinfo.idle = gekk_search; + self->monsterinfo.pain_chance = 0.2f; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &gekk_move_stand; + self->monsterinfo.scale = MODEL_SCALE; + self->nextthink = level.time + FRAMETIME; +} diff --git a/src/entities/drone/drone_gibs.c b/src/entities/drone/drone_gibs.c new file mode 100644 index 00000000..363c8b3c --- /dev/null +++ b/src/entities/drone/drone_gibs.c @@ -0,0 +1,597 @@ +#include "g_local.h" + +extern mmove_t infantry_move_death3; + +typedef struct drone_gib_s { + int count; + const char *model; + int type; + float scale; + int frame; +} drone_gib_t; + +typedef struct drone_gib_list_s { + const drone_gib_t *gibs; + int count; +} drone_gib_list_t; + +#define HGIB(name, type) { 1, name, type, 1.0f, 0 } +#define HGIB_N(count, name, type) { count, name, type, 1.0f, 0 } +#define HGIB_SCALE(name, scale, type) { 1, name, type, scale, 0 } +#define HGIB_FRAME(name, frame, type) { 1, name, type, 1.0f, frame } +#define HGIB_COUNT(list) ((int)(sizeof(list) / sizeof((list)[0]))) +#define HGIB_LIST(list) { list, HGIB_COUNT(list) } + +static const drone_gib_t gibs_soldier[] = { + HGIB_N(3, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/objects/gibs/bone2/tris.md2", GIB_ORGANIC), + HGIB("models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/soldier/gibs/arm.md2", GIB_SKINNED), + HGIB("models/monsters/soldier/gibs/gun.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/soldier/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/soldier/gibs/head.md2", GIB_HEAD | GIB_SKINNED), +}; + +static const drone_gib_t gibs_gunner[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/gunner/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/gunner/gibs/garm.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/gunner/gibs/gun.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/gunner/gibs/foot.md2", GIB_SKINNED), + HGIB("models/monsters/gunner/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_guncmdr[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/objects/gibs/gear/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/gunner/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/gunner/gibs/garm.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/gunner/gibs/gun.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/gunner/gibs/foot.md2", GIB_SKINNED), + HGIB("models/monsters/gunner/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_chick[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/bitch/gibs/arm.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/bitch/gibs/foot.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/bitch/gibs/tube.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/bitch/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/bitch/gibs/head.md2", GIB_HEAD | GIB_SKINNED), +}; + +static const drone_gib_t gibs_brain[] = { + HGIB("models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/monsters/brain/gibs/arm.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/brain/gibs/boot.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/brain/gibs/pelvis.md2", GIB_SKINNED), + HGIB("models/monsters/brain/gibs/chest.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/brain/gibs/door.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/brain/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_medic[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB("models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/monsters/medic/gibs/chest.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/medic/gibs/leg.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/medic/gibs/hook.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/medic/gibs/gun.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/medic/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_mutant[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(4, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/monsters/mutant/gibs/hand.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/mutant/gibs/foot.md2", GIB_SKINNED), + HGIB("models/monsters/mutant/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/mutant/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_parasite[] = { + HGIB("models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/parasite/gibs/chest.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/parasite/gibs/bleg.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/parasite/gibs/fleg.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/parasite/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_berserk[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/objects/gibs/gear/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/berserk/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/berserk/gibs/hammer.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/berserk/gibs/thigh.md2", GIB_SKINNED), + HGIB("models/monsters/berserk/gibs/head.md2", GIB_HEAD | GIB_SKINNED), +}; + +static const drone_gib_t gibs_gladiator[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/monsters/gladiatr/gibs/thigh.md2", GIB_SKINNED), + HGIB("models/monsters/gladiatr/gibs/larm.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/gladiatr/gibs/rarm.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/gladiatr/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/gladiatr/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_infantry[] = { + HGIB("models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/infantry/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/infantry/gibs/gun.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/infantry/gibs/foot.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/infantry/gibs/arm.md2", GIB_SKINNED), + HGIB("models/monsters/infantry/gibs/head.md2", GIB_HEAD | GIB_SKINNED), +}; + +static const drone_gib_t gibs_tank[] = { + HGIB("models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/objects/gibs/gear/tris.md2", GIB_METALLIC), + HGIB_N(2, "models/monsters/tank/gibs/foot.md2", GIB_SKINNED | GIB_METALLIC), + HGIB_N(2, "models/monsters/tank/gibs/thigh.md2", GIB_SKINNED | GIB_METALLIC), + HGIB("models/monsters/tank/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/tank/gibs/head.md2", GIB_HEAD | GIB_SKINNED), +}; + +static const drone_gib_t gibs_flyer[] = { + HGIB_N(2, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/flyer/gibs/base.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/flyer/gibs/gun.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/flyer/gibs/wing.md2", GIB_SKINNED), + HGIB("models/monsters/flyer/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_floater[] = { + HGIB_N(2, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB_N(3, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/float/gibs/piece.md2", GIB_SKINNED), + HGIB("models/monsters/float/gibs/gun.md2", GIB_SKINNED), + HGIB("models/monsters/float/gibs/base.md2", GIB_SKINNED), + HGIB("models/monsters/float/gibs/jar.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_hover[] = { + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/monsters/hover/gibs/chest.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/hover/gibs/ring.md2", GIB_SKINNED | GIB_METALLIC), + HGIB_N(2, "models/monsters/hover/gibs/foot.md2", GIB_SKINNED), + HGIB("models/monsters/hover/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_shambler[] = { + HGIB_N(3, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/chest/tris.md2", GIB_ORGANIC), + HGIB("models/objects/gibs/head2/tris.md2", GIB_HEAD), +}; + +static const drone_gib_t gibs_arachnid[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB("models/monsters/gunner/gibs/chest.md2", GIB_METALLIC), + HGIB("models/monsters/gunner/gibs/garm.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB("models/monsters/gladiatr/gibs/rarm.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB("models/monsters/gunner/gibs/foot.md2", GIB_METALLIC), + HGIB("models/monsters/gunner/gibs/head.md2", GIB_METALLIC | GIB_HEAD), +}; + +static const drone_gib_t gibs_gekk[] = { + HGIB("models/objects/gekkgib/pelvis/tris.md2", GIB_ACID), + HGIB_N(2, "models/objects/gekkgib/arm/tris.md2", GIB_ACID), + HGIB("models/objects/gekkgib/torso/tris.md2", GIB_ACID), + HGIB("models/objects/gekkgib/claw/tris.md2", GIB_ACID), + HGIB_N(2, "models/objects/gekkgib/leg/tris.md2", GIB_ACID), + HGIB("models/objects/gekkgib/head/tris.md2", GIB_ACID | GIB_HEAD), +}; + +static const drone_gib_t gibs_stalker[] = { + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/monsters/stalker/gibs/bodya.md2", GIB_SKINNED), + HGIB("models/monsters/stalker/gibs/bodyb.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/stalker/gibs/claw.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/stalker/gibs/leg.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/stalker/gibs/foot.md2", GIB_SKINNED), + HGIB("models/monsters/stalker/gibs/head.md2", GIB_SKINNED | GIB_HEAD), +}; + +static const drone_gib_t gibs_fixbot[] = { + HGIB("models/objects/gibs/sm_metal/tris.md2", GIB_ACID), + HGIB("models/objects/gibs/gear/tris.md2", GIB_METALLIC), +}; + +static const drone_gib_t gibs_supertank[] = { + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/monsters/boss1/gibs/cgun.md2", GIB_SKINNED | GIB_METALLIC), + HGIB("models/monsters/boss1/gibs/chest.md2", GIB_SKINNED), + HGIB("models/monsters/boss1/gibs/core.md2", GIB_SKINNED), + HGIB("models/monsters/boss1/gibs/ltread.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss1/gibs/rgun.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss1/gibs/rtread.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss1/gibs/tube.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss1/gibs/head.md2", GIB_SKINNED | GIB_METALLIC | GIB_HEAD), +}; + +static const drone_gib_t gibs_boss2[] = { + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/monsters/boss2/gibs/chest.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/boss2/gibs/chaingun.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss2/gibs/cpu.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss2/gibs/engine.md2", GIB_SKINNED), + HGIB("models/monsters/boss2/gibs/rocket.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss2/gibs/spine.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/boss2/gibs/wing.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss2/gibs/larm.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss2/gibs/rarm.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_SCALE("models/monsters/boss2/gibs/larm.md2", 2.0f, GIB_SKINNED | GIB_UPRIGHT), + HGIB_SCALE("models/monsters/boss2/gibs/rarm.md2", 2.0f, GIB_SKINNED | GIB_UPRIGHT), + HGIB_SCALE("models/monsters/boss2/gibs/larm.md2", 1.35f, GIB_SKINNED | GIB_UPRIGHT), + HGIB_SCALE("models/monsters/boss2/gibs/rarm.md2", 1.35f, GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss2/gibs/head.md2", GIB_SKINNED | GIB_METALLIC | GIB_HEAD), +}; + +static const drone_gib_t gibs_jorg[] = { + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(2, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/monsters/boss3/jorg/gibs/chest.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/boss3/jorg/gibs/foot.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/boss3/jorg/gibs/gun.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/boss3/jorg/gibs/thigh.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/boss3/jorg/gibs/spine.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_N(4, "models/monsters/boss3/jorg/gibs/tube.md2", GIB_SKINNED), + HGIB_N(6, "models/monsters/boss3/jorg/gibs/spike.md2", GIB_SKINNED), + HGIB("models/monsters/boss3/jorg/gibs/head.md2", GIB_SKINNED | GIB_METALLIC | GIB_HEAD), +}; + +static const drone_gib_t gibs_makron[] = { + HGIB("models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(4, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/objects/gibs/gear/tris.md2", GIB_METALLIC | GIB_HEAD), +}; + +static const drone_gib_t gibs_carrier[] = { + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/monsters/carrier/gibs/base.md2", GIB_SKINNED), + HGIB("models/monsters/carrier/gibs/chest.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/carrier/gibs/gl.md2", GIB_SKINNED), + HGIB("models/monsters/carrier/gibs/lcg.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/carrier/gibs/lwing.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/carrier/gibs/rcg.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB("models/monsters/carrier/gibs/rwing.md2", GIB_SKINNED | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/carrier/gibs/spawner.md2", GIB_SKINNED), + HGIB_N(2, "models/monsters/carrier/gibs/thigh.md2", GIB_SKINNED), + HGIB("models/monsters/carrier/gibs/head.md2", GIB_SKINNED | GIB_METALLIC | GIB_HEAD), +}; + +static const drone_gib_t gibs_guardian[] = { + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(4, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB_N(2, "models/monsters/guardian/gib1.md2", GIB_METALLIC), + HGIB_N(2, "models/monsters/guardian/gib2.md2", GIB_METALLIC), + HGIB_N(2, "models/monsters/guardian/gib3.md2", GIB_METALLIC), + HGIB_N(2, "models/monsters/guardian/gib4.md2", GIB_METALLIC), + HGIB_N(2, "models/monsters/guardian/gib5.md2", GIB_METALLIC), + HGIB_N(2, "models/monsters/guardian/gib6.md2", GIB_METALLIC), + HGIB("models/monsters/guardian/gib7.md2", GIB_METALLIC | GIB_HEAD), +}; + +static const drone_gib_t gibs_widow[] = { + HGIB_N(2, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB("models/monsters/blackwidow/gib1/tris.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB("models/monsters/blackwidow/gib2/tris.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB("models/monsters/blackwidow/gib3/tris.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB("models/monsters/blackwidow/gib4/tris.md2", GIB_METALLIC | GIB_UPRIGHT), +}; + +static const drone_gib_t gibs_widow2[] = { + HGIB_N(2, "models/objects/gibs/bone/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_meat/tris.md2", GIB_ORGANIC), + HGIB_N(3, "models/objects/gibs/sm_metal/tris.md2", GIB_METALLIC), + HGIB_N(3, "models/monsters/blackwidow2/gib1/tris.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB_N(3, "models/monsters/blackwidow2/gib2/tris.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/blackwidow2/gib3/tris.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB("models/monsters/blackwidow2/gib4/tris.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB_N(2, "models/monsters/blackwidow/gib3/tris.md2", GIB_METALLIC | GIB_UPRIGHT), + HGIB("models/objects/gibs/chest/tris.md2", GIB_ORGANIC), + HGIB("models/objects/gibs/head2/tris.md2", GIB_HEAD), +}; + +static const drone_gib_t gibs_rogue_turret[] = { + HGIB_N(4, "models/objects/debris1/tris.md2", GIB_METALLIC | GIB_DEBRIS), + HGIB_FRAME("models/monsters/turret/tris.md2", 14, GIB_SKINNED | GIB_METALLIC | GIB_HEAD | GIB_DEBRIS), +}; + +static const drone_gib_list_t list_soldier = HGIB_LIST(gibs_soldier); +static const drone_gib_list_t list_gunner = HGIB_LIST(gibs_gunner); +static const drone_gib_list_t list_guncmdr = HGIB_LIST(gibs_guncmdr); +static const drone_gib_list_t list_chick = HGIB_LIST(gibs_chick); +static const drone_gib_list_t list_brain = HGIB_LIST(gibs_brain); +static const drone_gib_list_t list_medic = HGIB_LIST(gibs_medic); +static const drone_gib_list_t list_mutant = HGIB_LIST(gibs_mutant); +static const drone_gib_list_t list_parasite = HGIB_LIST(gibs_parasite); +static const drone_gib_list_t list_berserk = HGIB_LIST(gibs_berserk); +static const drone_gib_list_t list_gladiator = HGIB_LIST(gibs_gladiator); +static const drone_gib_list_t list_infantry = HGIB_LIST(gibs_infantry); +static const drone_gib_list_t list_tank = HGIB_LIST(gibs_tank); +static const drone_gib_list_t list_flyer = HGIB_LIST(gibs_flyer); +static const drone_gib_list_t list_floater = HGIB_LIST(gibs_floater); +static const drone_gib_list_t list_hover = HGIB_LIST(gibs_hover); +static const drone_gib_list_t list_shambler = HGIB_LIST(gibs_shambler); +static const drone_gib_list_t list_arachnid = HGIB_LIST(gibs_arachnid); +static const drone_gib_list_t list_gekk = HGIB_LIST(gibs_gekk); +static const drone_gib_list_t list_stalker = HGIB_LIST(gibs_stalker); +static const drone_gib_list_t list_fixbot = HGIB_LIST(gibs_fixbot); +static const drone_gib_list_t list_supertank = HGIB_LIST(gibs_supertank); +static const drone_gib_list_t list_boss2 = HGIB_LIST(gibs_boss2); +static const drone_gib_list_t list_jorg = HGIB_LIST(gibs_jorg); +static const drone_gib_list_t list_makron = HGIB_LIST(gibs_makron); +static const drone_gib_list_t list_carrier = HGIB_LIST(gibs_carrier); +static const drone_gib_list_t list_guardian = HGIB_LIST(gibs_guardian); +static const drone_gib_list_t list_widow = HGIB_LIST(gibs_widow); +static const drone_gib_list_t list_widow2 = HGIB_LIST(gibs_widow2); +static const drone_gib_list_t list_rogue_turret = HGIB_LIST(gibs_rogue_turret); + +static const drone_gib_list_t all_drone_gib_lists[] = { + HGIB_LIST(gibs_soldier), + HGIB_LIST(gibs_gunner), + HGIB_LIST(gibs_guncmdr), + HGIB_LIST(gibs_chick), + HGIB_LIST(gibs_brain), + HGIB_LIST(gibs_medic), + HGIB_LIST(gibs_mutant), + HGIB_LIST(gibs_parasite), + HGIB_LIST(gibs_berserk), + HGIB_LIST(gibs_gladiator), + HGIB_LIST(gibs_infantry), + HGIB_LIST(gibs_tank), + HGIB_LIST(gibs_flyer), + HGIB_LIST(gibs_floater), + HGIB_LIST(gibs_hover), + HGIB_LIST(gibs_shambler), + HGIB_LIST(gibs_arachnid), + HGIB_LIST(gibs_gekk), + HGIB_LIST(gibs_stalker), + HGIB_LIST(gibs_fixbot), + HGIB_LIST(gibs_supertank), + HGIB_LIST(gibs_boss2), + HGIB_LIST(gibs_jorg), + HGIB_LIST(gibs_makron), + HGIB_LIST(gibs_carrier), + HGIB_LIST(gibs_guardian), + HGIB_LIST(gibs_widow), + HGIB_LIST(gibs_widow2), + HGIB_LIST(gibs_rogue_turret), +}; + +static const drone_gib_list_t *vrx_get_drone_gibs(edict_t *self) +{ + switch (self->mtype) + { + case M_SOLDIERLT: + case M_SOLDIER: + case M_SOLDIERSS: + case M_SOLDIER_RIPPER: + case M_SOLDIER_BLUEBLASTER: + case M_SOLDIER_LASER: + return &list_soldier; + case M_GUNNER: + return &list_gunner; + case M_GUNCMDR: + return &list_guncmdr; + case M_CHICK: + case M_CHICK_HEAT: + return &list_chick; + case M_BRAIN: + return &list_brain; + case M_MEDIC: + case M_MEDIC_COMMANDER: + return &list_medic; + case M_MUTANT: + case M_REDMUTANT: + return &list_mutant; + case M_PARASITE: + return &list_parasite; + case M_BERSERK: + return &list_berserk; + case M_GLADIATOR: + case M_GLADB: + case M_GLADC: + return &list_gladiator; + case M_INFANTRY: + return &list_infantry; + case M_TANK: + case M_COMMANDER: + case M_RUNNERTANK: + return &list_tank; + case M_FLYER: + return &list_flyer; + case M_FLOATER: + return &list_floater; + case M_HOVER: + case M_DAEDALUS: + return &list_hover; + case M_SHAMBLER: + return &list_shambler; + case M_ARACHNID: + return &list_arachnid; + case M_GEKK: + return &list_gekk; + case M_STALKER: + return &list_stalker; + case M_FIXBOT: + case M_FIXBOT_BOSS: + return &list_fixbot; + case M_SUPERTANK: + case M_JANITOR: + case M_BOSS5: + return &list_supertank; + case M_BOSS2: + case M_BOSS2_SMALL: + return &list_boss2; + case M_JORG: + return &list_jorg; + case M_MAKRON: + return &list_makron; + case M_CARRIER: + return &list_carrier; + case M_GUARDIAN: + case M_MINIGUARDIAN: + return &list_guardian; + case M_WIDOW: + return &list_widow; + case M_WIDOW2: + return &list_widow2; + case M_ROGUE_TURRET: + return &list_rogue_turret; + default: + return NULL; + } +} + +static int vrx_get_drone_gib_skinnum(edict_t *self) +{ + if (!self) + return 0; + + if (self->s.renderfx & RF_CUSTOMSKIN) + return 0; + + if (self->mtype == M_ROGUE_TURRET) + return self->s.skinnum; + + return self->s.skinnum / 2; +} + +static const char *vrx_get_drone_gib_model(edict_t *self, const drone_gib_t *gib) +{ + if (self->mtype == M_INFANTRY && (gib->type & GIB_HEAD) && + !strcmp(gib->model, "models/monsters/infantry/gibs/head.md2") && + self->monsterinfo.currentmove != &infantry_move_death3) + return "models/objects/gibs/sm_meat/tris.md2"; + + return gib->model; +} + +static void vrx_throw_drone_gib_list(edict_t *self, int damage, const drone_gib_list_t *list) +{ + edict_t *gib; + int gib_skinnum = vrx_get_drone_gib_skinnum(self); + float self_scale = self->s.scale ? self->s.scale : 1.0f; + const char *model; + int i; + int n; + + for (i = 0; i < list->count; i++) + { + model = vrx_get_drone_gib_model(self, &list->gibs[i]); + + for (n = 0; n < list->gibs[i].count; n++) + { + gib = ThrowGibEx(self, (char *)model, damage, list->gibs[i].type, + list->gibs[i].scale * self_scale); + if (gib) + { + if (list->gibs[i].type & GIB_SKINNED) + gib->s.skinnum = gib_skinnum; + if (list->gibs[i].frame) + gib->s.frame = list->gibs[i].frame; + } + } + } +} + +void vrx_precache_drone_gibs(void) +{ + int i; + int j; + + for (i = 0; i < HGIB_COUNT(all_drone_gib_lists); i++) + { + for (j = 0; j < all_drone_gib_lists[i].count; j++) + gi.modelindex((char *)all_drone_gib_lists[i].gibs[j].model); + } + gi.modelindex("models/monsters/tank/gibs/barm.md2"); +} + +qboolean vrx_throw_drone_gibs(edict_t *self, int damage) +{ + const drone_gib_list_t *list; + + if (!self || !self->inuse || nolag->value) + return false; + if (!vrx_spawn_nonessential_ent(self->s.origin)) + return false; + + list = vrx_get_drone_gibs(self); + if (!list) + return false; + + vrx_throw_drone_gib_list(self, damage, list); + + if ((self->mtype == M_TANK || self->mtype == M_COMMANDER || self->mtype == M_RUNNERTANK) && !self->style) + { + edict_t *arm = ThrowGibEx(self, "models/monsters/tank/gibs/barm.md2", damage, GIB_SKINNED | GIB_UPRIGHT, + self->s.scale ? self->s.scale : 1.0f); + if (arm) + arm->s.skinnum = vrx_get_drone_gib_skinnum(self); + } + + return true; +} + +void vrx_drop_tank_death_arm(edict_t *self, int damage) +{ + edict_t *arm; + vec3_t forward; + vec3_t right; + vec3_t up; + + if (!self || self->style) + return; + + self->style = 1; + + if (nolag->value || !vrx_spawn_nonessential_ent(self->s.origin)) + return; + + AngleVectors(self->s.angles, forward, right, up); + arm = ThrowGibEx(self, "models/monsters/tank/gibs/barm.md2", damage, GIB_SKINNED | GIB_UPRIGHT, + self->s.scale ? self->s.scale : 1.0f); + if (!arm) + return; + + arm->s.skinnum = vrx_get_drone_gib_skinnum(self); + VectorMA(self->s.origin, -16, right, arm->s.origin); + VectorMA(arm->s.origin, 23, up, arm->s.origin); + VectorCopy(arm->s.origin, arm->s.old_origin); + VectorScale(up, 100, arm->velocity); + VectorMA(arm->velocity, -120, right, arm->velocity); + VectorCopy(self->s.angles, arm->s.angles); + arm->s.angles[ROLL] = -90; + arm->avelocity[0] = crandom() * 15; + arm->avelocity[1] = crandom() * 15; + arm->avelocity[2] = 180; + gi.linkentity(arm); +} + +#undef HGIB +#undef HGIB_N +#undef HGIB_SCALE +#undef HGIB_FRAME +#undef HGIB_COUNT +#undef HGIB_LIST diff --git a/src/entities/drone/drone_gladiator.c b/src/entities/drone/drone_gladiator.c index 85916dcf..780a22b0 100644 --- a/src/entities/drone/drone_gladiator.c +++ b/src/entities/drone/drone_gladiator.c @@ -13,6 +13,8 @@ static int sound_pain1; static int sound_pain2; static int sound_die; static int sound_gun; +static int sound_gunb; +static int sound_gunc; static int sound_cleaver_swing; static int sound_cleaver_hit; static int sound_cleaver_miss; @@ -22,7 +24,10 @@ static int sound_sight; void gladiator_idle (edict_t *self) { - gi.sound (self, CHAN_VOICE, sound_idle, 1, ATTN_IDLE, 0); + if (random() < 0.5) + gi.sound (self, CHAN_VOICE, sound_idle, 1, ATTN_IDLE, 0); + else + gi.sound (self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); } void gladiator_search (edict_t *self) @@ -97,19 +102,32 @@ mframe_t gladiator_frames_pain[] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, +}; +mmove_t gladiator_move_pain = { FRAME_pain2, FRAME_pain5, gladiator_frames_pain, gladiator_walk }; + +mframe_t gladiator_frames_pain_air[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, }; -mmove_t gladiator_move_pain = { FRAME_pain1, FRAME_pain6, gladiator_frames_pain, gladiator_walk }; +mmove_t gladiator_move_pain_air = { FRAME_painup2, FRAME_painup6, gladiator_frames_pain_air, gladiator_walk }; void gladiator_pain(edict_t* self, edict_t* other, float kick, int damage) { if (self->health < (self->max_health / 2)) - self->s.skinnum = 1; + self->s.skinnum = (self->mtype == M_GLADB || self->mtype == M_GLADC) ? 3 : 1; // we're already in a pain state - if (self->monsterinfo.currentmove == &gladiator_move_pain) + if (self->monsterinfo.currentmove == &gladiator_move_pain || + self->monsterinfo.currentmove == &gladiator_move_pain_air) + { + if (self->velocity[2] > 100 && self->monsterinfo.currentmove == &gladiator_move_pain) + self->monsterinfo.currentmove = &gladiator_move_pain_air; return; + } // monster players don't get pain state induced if (G_GetClient(self)) @@ -131,7 +149,10 @@ void gladiator_pain(edict_t* self, edict_t* other, float kick, int damage) gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); } - self->monsterinfo.currentmove = &gladiator_move_pain; + if (self->velocity[2] > 100) + self->monsterinfo.currentmove = &gladiator_move_pain_air; + else + self->monsterinfo.currentmove = &gladiator_move_pain; } void gladiator_run (edict_t *self) @@ -269,6 +290,46 @@ void GladiatorGun (edict_t *self) monster_fire_railgun (self, start, forward, damage, 100, MZ2_GLADIATOR_RAILGUN_1); } +static void GladiatorDisruptor(edict_t *self) +{ + int damage, speed; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + damage = DISRUPTOR_INITIAL_DAMAGE + DISRUPTOR_ADDON_DAMAGE * drone_damagelevel(self); + if (M_DISRUPTOR_DMG_MAX && damage > M_DISRUPTOR_DMG_MAX) + damage = M_DISRUPTOR_DMG_MAX; + speed = DISRUPTOR_INITIAL_SPEED + DISRUPTOR_ADDON_SPEED * drone_damagelevel(self); + if (M_DISRUPTOR_SPEED_MAX && damage > M_DISRUPTOR_SPEED_MAX) + speed = M_DISRUPTOR_SPEED_MAX; + + MonsterAim(self, M_PROJECTILE_ACC, speed, false, MZ2_GLADIATOR_RAILGUN_1, forward, start); + fire_disruptor(self, start, forward, damage, speed, visible(self, self->enemy) ? self->enemy : NULL); +} + +static void GladiatorPlasma(edict_t *self) +{ + int damage, speed, radius_damage; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_PLASMA_DMG_BASE + M_PLASMA_DMG_ADDON * drone_damagelevel(self); + if (M_PLASMA_DMG_MAX && damage > M_PLASMA_DMG_MAX) + damage = M_PLASMA_DMG_MAX; + + speed = M_PLASMA_SPEED_BASE + M_PLASMA_SPEED_ADDON * drone_damagelevel(self); + if (M_PLASMA_SPEED_MAX && speed > M_PLASMA_SPEED_MAX) + speed = M_PLASMA_SPEED_MAX; + + radius_damage = max(1, damage / 2); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, MZ2_GLADIATOR_RAILGUN_1, forward, start); + fire_plasma(self, start, forward, damage, speed, M_PLASMA_DAMAGE_RADIUS, radius_damage); +} + void gladiator_refire (edict_t *self) { // if our enemy is still valid, then continue firing @@ -282,6 +343,30 @@ void gladiator_refire (edict_t *self) self->monsterinfo.attack_finished = level.time + 1.0; } +static void gladiator_refire_disruptor(edict_t *self) +{ + if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.45)) + { + self->s.frame = FRAME_attack4; + GladiatorDisruptor(self); + return; + } + + self->monsterinfo.attack_finished = level.time + 1.0; +} + +static void gladiator_refire_plasma(edict_t *self) +{ + if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.6)) + { + self->s.frame = FRAME_attack4; + GladiatorPlasma(self); + return; + } + + self->monsterinfo.attack_finished = level.time + 1.0; +} + mframe_t gladiator_frames_attack_gun [] = { ai_charge, 0, GladiatorGun, //49 @@ -292,6 +377,26 @@ mframe_t gladiator_frames_attack_gun [] = }; mmove_t gladiator_move_attack_gun = {FRAME_attack4, FRAME_attack8, gladiator_frames_attack_gun, gladiator_run}; +mframe_t gladb_frames_attack_gun[] = +{ + ai_charge, 0, GladiatorDisruptor, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, gladiator_refire_disruptor, +}; +mmove_t gladb_move_attack_gun = { FRAME_attack4, FRAME_attack8, gladb_frames_attack_gun, gladiator_run }; + +mframe_t gladc_frames_attack_gun[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, GladiatorPlasma, + ai_charge, 0, NULL, + ai_charge, 0, GladiatorPlasma, + ai_charge, 0, gladiator_refire_plasma, +}; +mmove_t gladc_move_attack_gun = { FRAME_attack4, FRAME_attack8, gladc_frames_attack_gun, gladiator_run }; + void gladiator_lightning_attack (edict_t *self) { const float r = random(); @@ -332,9 +437,22 @@ void gladiator_attack(edict_t *self) return; } - // charge up the railgun - gi.sound (self, CHAN_WEAPON, sound_gun, 1, ATTN_NORM, 0); - self->monsterinfo.currentmove = &gladiator_move_attack_gun; + if (self->mtype == M_GLADB) + { + gi.sound(self, CHAN_WEAPON, sound_gunb, 1, ATTN_NORM, 0); + self->monsterinfo.currentmove = &gladb_move_attack_gun; + } + else if (self->mtype == M_GLADC) + { + gi.sound(self, CHAN_WEAPON, sound_gunc, 1, ATTN_NORM, 0); + self->monsterinfo.currentmove = &gladc_move_attack_gun; + } + else + { + // charge up the railgun + gi.sound (self, CHAN_WEAPON, sound_gun, 1, ATTN_NORM, 0); + self->monsterinfo.currentmove = &gladiator_move_attack_gun; + } } void gladiator_sight (edict_t *self, edict_t *other) @@ -354,11 +472,18 @@ void gladiator_dead (edict_t *self) M_PrepBodyRemoval(self); } +static void gladiator_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t gladiator_frames_death [] = { ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, gladiator_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -383,8 +508,6 @@ mmove_t gladiator_move_death = {FRAME_death1, FRAME_death22, gladiator_frames_de void gladiator_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; - M_Notify(self); // reduce lag by removing the entity right away @@ -400,13 +523,7 @@ void gladiator_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int da if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); @@ -426,6 +543,7 @@ void gladiator_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int da gi.sound (self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); self->monsterinfo.currentmove = &gladiator_move_death; @@ -442,6 +560,8 @@ void init_drone_gladiator (edict_t *self) { sound_die = gi.soundindex ("gladiator/glddeth2.wav"); sound_gun = gi.soundindex ("gladiator/railgun.wav"); + sound_gunb = gi.soundindex("weapons/disrupt.wav"); + sound_gunc = gi.soundindex("weapons/plasshot.wav"); sound_cleaver_swing = gi.soundindex ("gladiator/melee1.wav"); sound_cleaver_hit = gi.soundindex ("gladiator/melee2.wav"); sound_cleaver_miss = gi.soundindex ("gladiator/melee3.wav"); @@ -482,3 +602,32 @@ void init_drone_gladiator (edict_t *self) self->monsterinfo.currentmove = &gladiator_move_stand; self->monsterinfo.scale = MODEL_SCALE; } + +void init_drone_gladb(edict_t *self) +{ + init_drone_gladiator(self); + // GLADB is the Gladiator Disruptor variant. + self->mtype = M_GLADB; + self->s.skinnum = 2; + self->s.effects |= EF_TRACKER; + self->health = M_GLADB_INITIAL_HEALTH + M_GLADB_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->monsterinfo.power_armor_power = M_GLADB_INITIAL_ARMOR + M_GLADB_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->mass = 350; + self->item = FindItemByClassname("ammo_disruptor"); +} + +void init_drone_gladc(edict_t *self) +{ + init_drone_gladiator(self); + // GLADC is the Gladiator Plasma variant. + self->mtype = M_GLADC; + self->s.skinnum = 2; + self->health = M_GLADC_INITIAL_HEALTH + M_GLADC_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->monsterinfo.power_armor_power = M_GLADC_INITIAL_ARMOR + M_GLADC_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->mass = 350; + self->item = FindItemByClassname("ammo_magslug"); +} diff --git a/src/entities/drone/drone_guardian.c b/src/entities/drone/drone_guardian.c new file mode 100644 index 00000000..346ca7de --- /dev/null +++ b/src/entities/drone/drone_guardian.c @@ -0,0 +1,826 @@ +/* +============================================================================== + +GUARDIAN / miniguardian + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_guardian.h" + +static int sound_step; +static int sound_charge; +static int sound_spin_loop; +static int sound_laser; +static int sound_pew; +static int sound_pain; + +#define GUARDIAN_INVASION_SCALE 0.45f +#define GUARDIAN_INVASION_MOVE_SCALE 1.75f +#define GUARDIAN_SIGHT_ACK_CHANCE 0.30f + +void guardian_run(edict_t *self); + +static float guardian_scale(edict_t *self) +{ + return (self->s.scale > 0.0f) ? self->s.scale : 1.0f; +} + +static void guardian_ai_walk(edict_t *self, float dist) +{ + drone_ai_walk(self, invasion->value ? dist * GUARDIAN_INVASION_MOVE_SCALE : dist); +} + +static void guardian_ai_run(edict_t *self, float dist) +{ + drone_ai_run(self, invasion->value ? dist * GUARDIAN_INVASION_MOVE_SCALE : dist); +} + +static void guardian_project_flash(edict_t *self, int flash, vec3_t forward, vec3_t right, vec3_t start) +{ + vec3_t offset; + + VectorCopy(monster_flash_offset[flash], offset); + if (guardian_scale(self) != 1.0f) + VectorScale(offset, guardian_scale(self), offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static void guardian_project_laser_frame_origin(edict_t *self, vec3_t forward, vec3_t right, vec3_t start) +{ + vec3_t offset; + + if (self->s.frame & 1) + VectorSet(offset, 125, -70, 60); + else + VectorSet(offset, 112, -62, 60); + + if (guardian_scale(self) != 1.0f) + VectorScale(offset, guardian_scale(self), offset); + + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static void guardian_project_laser_origin(edict_t *self, qboolean secondary, vec3_t forward, vec3_t right, vec3_t start) +{ + vec3_t offset; + + if (secondary) + VectorSet(offset, 112, -62, 60); + else + VectorSet(offset, 125, -70, 60); + + if (guardian_scale(self) != 1.0f) + VectorScale(offset, guardian_scale(self), offset); + + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static qboolean guardian_has_origin_shot(edict_t *self, vec3_t start) +{ + return M_MonsterHasClearShotFrom(self, start); +} + +static qboolean guardian_has_laser_shot(edict_t *self, qboolean secondary) +{ + vec3_t forward, right, start; + + AngleVectors(self->s.angles, forward, right, NULL); + guardian_project_laser_origin(self, secondary, forward, right, start); + return guardian_has_origin_shot(self, start); +} + +static qboolean guardian_has_blaster_shot(edict_t *self) +{ + if (self->mtype == M_MINIGUARDIAN) + return M_MonsterHasClearShotFromFlash(self, MZ2_SOLDIER_RIPPER_8); + + return M_MonsterHasClearShotFromFlash(self, MZ2_GUARDIAN_BLASTER); +} + +static qboolean guardian_has_grenade_or_laser_shot(edict_t *self) +{ + if (self->mtype == M_MINIGUARDIAN) + return guardian_has_laser_shot(self, false) || guardian_has_laser_shot(self, true); + + return guardian_has_laser_shot(self, false) || guardian_has_laser_shot(self, true); +} + +static qboolean guardian_has_rocket_shot(edict_t *self, float offset) +{ + vec3_t forward, right, up, start; + + AngleVectors(self->s.angles, forward, right, up); + VectorCopy(self->s.origin, start); + VectorMA(start, -8 * guardian_scale(self), forward, start); + VectorMA(start, offset * guardian_scale(self), right, start); + VectorMA(start, 50 * guardian_scale(self), up, start); + return guardian_has_origin_shot(self, start); +} + +static void guardian_footstep(edict_t *self) +{ + gi.sound(self, CHAN_BODY, sound_step, 1, ATTN_NORM, 0); +} + +static void guardian_sight(edict_t *self, edict_t *other) +{ + (void)other; + + if (random() < GUARDIAN_SIGHT_ACK_CHANCE) + gi.sound(self, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0); +} + +mframe_t guardian_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t guardian_move_stand = {FRAME_idle1, FRAME_idle52, guardian_frames_stand, NULL}; + +void guardian_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &guardian_move_stand; +} + +mframe_t guardian_frames_walk[] = +{ + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, guardian_footstep, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, NULL, + guardian_ai_walk, 8, guardian_footstep, + guardian_ai_walk, 8, NULL +}; +mmove_t guardian_move_walk = {FRAME_walk1, FRAME_walk19, guardian_frames_walk, NULL}; + +void guardian_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &guardian_move_walk; +} + +mframe_t guardian_frames_run[] = +{ + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, guardian_footstep, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, NULL, + guardian_ai_run, 8, guardian_footstep, + guardian_ai_run, 8, NULL +}; +mmove_t guardian_move_run = {FRAME_walk1, FRAME_walk19, guardian_frames_run, NULL}; + +void guardian_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &guardian_move_stand; + else + self->monsterinfo.currentmove = &guardian_move_run; +} + +mframe_t guardian_frames_pain1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guardian_move_pain1 = {FRAME_pain1_1, FRAME_pain1_8, guardian_frames_pain1, guardian_run}; + +void guardian_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + (void)other; + (void)kick; + + if (level.time < self->pain_debounce_time) + return; + if (damage <= 75 && random() > 0.2) + return; + if ((self->s.frame >= FRAME_atk1_spin1 && self->s.frame <= FRAME_atk1_spin15) || + (self->s.frame >= FRAME_atk2_fire1 && self->s.frame <= FRAME_atk2_fire4) || + (self->s.frame >= FRAME_kick_in1 && self->s.frame <= FRAME_kick_in13)) + return; + if (G_GetClient(self) || invasion->value == 2) + return; + + self->pain_debounce_time = level.time + 3.0; + gi.sound(self, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0); + self->monsterinfo.currentmove = &guardian_move_pain1; + self->s.sound = 0; +} + +mframe_t guardian_frames_atk1_out[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guardian_move_atk1_out = {FRAME_atk1_out1, FRAME_atk1_out3, guardian_frames_atk1_out, guardian_run}; + +void guardian_atk1_finish(edict_t *self) +{ + self->monsterinfo.currentmove = &guardian_move_atk1_out; + self->s.sound = 0; +} + +void guardian_atk1_charge(edict_t *self) +{ + self->s.sound = sound_spin_loop; + + if (self->mtype == M_GUARDIAN) + gi.sound(self, CHAN_WEAPON, sound_charge, 1, ATTN_NORM, 0); +} + +void guardian_fire_blaster(edict_t *self) +{ + vec3_t forward, right, start; + int damage, speed, effect; + + if (!G_EntExists(self->enemy)) + return; + + if (self->mtype == M_MINIGUARDIAN) + { + damage = M_IONRIPPER_DMG_BASE + M_IONRIPPER_DMG_ADDON * drone_damagelevel(self); + if (M_IONRIPPER_DMG_MAX && damage > M_IONRIPPER_DMG_MAX) + damage = M_IONRIPPER_DMG_MAX; + speed = M_IONRIPPER_SPEED_BASE + M_IONRIPPER_SPEED_ADDON * drone_damagelevel(self); + if (M_IONRIPPER_SPEED_MAX && speed > M_IONRIPPER_SPEED_MAX) + speed = M_IONRIPPER_SPEED_MAX; + MonsterAim(self, M_PROJECTILE_ACC, speed, false, MZ2_SOLDIER_RIPPER_8, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_ionripper(self, start, forward, damage, speed, EF_IONRIPPER, MZ2_SOLDIER_RIPPER_8); + } + else + { + damage = M_HYPERBLASTER_DMG_BASE + M_HYPERBLASTER_DMG_ADDON * drone_damagelevel(self); + if (M_HYPERBLASTER_DMG_MAX && damage > M_HYPERBLASTER_DMG_MAX) + damage = M_HYPERBLASTER_DMG_MAX; + + speed = 1100; + effect = (self->s.frame % 4) ? 0 : EF_HYPERBLASTER; + AngleVectors(self->s.angles, forward, right, NULL); + guardian_project_flash(self, MZ2_GUARDIAN_BLASTER, forward, right, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, -1, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_blaster(self, start, forward, damage, speed, effect, BLASTER_PROJ_BOLT, 2.0, true, MZ2_GUARDIAN_BLASTER); + } + + if (G_EntExists(self->enemy) && self->s.frame == FRAME_atk1_spin12 && self->timestamp > level.time && visible(self, self->enemy)) + self->s.frame = FRAME_atk1_spin5 - 1; +} + +mframe_t guardian_frames_atk1_spin[] = +{ + ai_charge, 0, guardian_atk1_charge, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_blaster, + ai_charge, 0, guardian_fire_blaster, + ai_charge, 0, guardian_fire_blaster, + ai_charge, 0, guardian_fire_blaster, + ai_charge, 0, guardian_fire_blaster, + ai_charge, 0, guardian_fire_blaster, + ai_charge, 0, guardian_fire_blaster, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guardian_move_atk1_spin = {FRAME_atk1_spin1, FRAME_atk1_spin15, guardian_frames_atk1_spin, guardian_atk1_finish}; + +void guardian_atk1(edict_t *self) +{ + self->monsterinfo.currentmove = &guardian_move_atk1_spin; + self->timestamp = level.time + 0.65 + random() * 1.5; +} + +mframe_t guardian_frames_atk1_in[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guardian_move_atk1_in = {FRAME_atk1_in1, FRAME_atk1_in3, guardian_frames_atk1_in, guardian_atk1}; + +mframe_t guardian_frames_atk2_out[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guardian_footstep, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guardian_move_atk2_out = {FRAME_atk2_out1, FRAME_atk2_out7, guardian_frames_atk2_out, guardian_run}; + +void guardian_atk2_out(edict_t *self) +{ + self->monsterinfo.currentmove = &guardian_move_atk2_out; +} + +static int guardian_grenade_flash(edict_t *self) +{ + return (self->s.frame & 1) ? MZ2_SUPERTANK_GRENADE_1 : MZ2_SUPERTANK_GRENADE_2; +} + +void guardian_grenade(edict_t *self) +{ + vec3_t forward, right, start; + int damage, speed, flash_number; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_GRENADELAUNCHER_DMG_BASE + M_GRENADELAUNCHER_DMG_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_DMG_MAX && damage > M_GRENADELAUNCHER_DMG_MAX) + damage = M_GRENADELAUNCHER_DMG_MAX; + speed = M_GRENADELAUNCHER_SPEED_BASE + M_GRENADELAUNCHER_SPEED_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_SPEED_MAX && speed > M_GRENADELAUNCHER_SPEED_MAX) + speed = M_GRENADELAUNCHER_SPEED_MAX; + + flash_number = guardian_grenade_flash(self); + if (self->mtype == M_MINIGUARDIAN) + { + AngleVectors(self->s.angles, forward, right, NULL); + guardian_project_laser_frame_origin(self, forward, right, start); + flash_number = -1; + } + MonsterAim(self, M_PROJECTILE_ACC, speed, true, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_grenade(self, start, forward, damage, speed, flash_number); +} + +void guardian_laser_fire(edict_t *self) +{ + int damage; + + if (!G_EntExists(self->enemy)) + return; + + gi.sound(self, CHAN_WEAPON, sound_laser, 1, ATTN_NORM, 0); + damage = M_DABEAM_DMG_BASE + M_DABEAM_DMG_ADDON * drone_damagelevel(self); + if (M_DABEAM_DMG_MAX && damage > M_DABEAM_DMG_MAX) + damage = M_DABEAM_DMG_MAX; + if (self->mtype == M_GUARDIAN) + damage += 10 + 2 * drone_damagelevel(self); + if (!guardian_has_laser_shot(self, self->s.frame & 1)) + return; + + monster_fire_dabeam(self, damage, self->s.frame & 1, NULL); +} + +void guardian_fire_attack(edict_t *self) +{ + if (self->mtype == M_MINIGUARDIAN) + guardian_grenade(self); + else + guardian_laser_fire(self); +} + +mframe_t guardian_frames_atk2_fire[] = +{ + ai_charge, 0, guardian_fire_attack, + ai_charge, 0, guardian_fire_attack, + ai_charge, 0, guardian_fire_attack, + ai_charge, 0, guardian_fire_attack +}; +mmove_t guardian_move_atk2_fire = {FRAME_atk2_fire1, FRAME_atk2_fire4, guardian_frames_atk2_fire, guardian_atk2_out}; + +void guardian_atk2(edict_t *self) +{ + self->monsterinfo.currentmove = &guardian_move_atk2_fire; +} + +mframe_t guardian_frames_atk2_in[] = +{ + ai_charge, 0, guardian_footstep, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guardian_footstep, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guardian_footstep, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guardian_move_atk2_in = {FRAME_atk2_in1, FRAME_atk2_in12, guardian_frames_atk2_in, guardian_atk2}; + +void guardian_fire_rocket(edict_t *self, float offset) +{ + vec3_t forward, right, up, start, dir; + int damage, speed; + + if (!G_EntExists(self->enemy)) + return; + + AngleVectors(self->s.angles, forward, right, up); + VectorCopy(self->s.origin, start); + VectorMA(start, -8 * guardian_scale(self), forward, start); + VectorMA(start, offset * guardian_scale(self), right, start); + VectorMA(start, 50 * guardian_scale(self), up, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + speed = M_ROCKETLAUNCHER_SPEED_BASE + M_ROCKETLAUNCHER_SPEED_ADDON * drone_damagelevel(self); + if (M_ROCKETLAUNCHER_SPEED_MAX && speed > M_ROCKETLAUNCHER_SPEED_MAX) + speed = M_ROCKETLAUNCHER_SPEED_MAX; + speed *= 0.9f; + if (speed < 200) + speed = 200; + + damage = M_ROCKETLAUNCHER_DMG_BASE + M_ROCKETLAUNCHER_DMG_ADDON * drone_damagelevel(self); + if (M_ROCKETLAUNCHER_DMG_MAX && damage > M_ROCKETLAUNCHER_DMG_MAX) + damage = M_ROCKETLAUNCHER_DMG_MAX; + + VectorScale(forward, 0.35f, dir); + VectorMA(dir, 0.95f, up, dir); + VectorNormalize(dir); + monster_fire_heat(self, start, dir, damage, speed, MZ2_GUARDIAN_BLASTER, 0.18f); + gi.sound(self, CHAN_WEAPON, sound_pew, 1, 0.5f, 0); +} + +void guardian_fire_rocket_l(edict_t *self) +{ + guardian_fire_rocket(self, -14.0f); +} + +void guardian_fire_rocket_r(edict_t *self) +{ + guardian_fire_rocket(self, 14.0f); +} + +mframe_t guardian_frames_rocket[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_rocket_l, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_rocket_l, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_rocket_r, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_rocket_r, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_rocket_l, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_rocket_l, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_rocket_r, + ai_charge, 0, NULL, + ai_charge, 0, guardian_fire_rocket_r, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guardian_move_rocket = {FRAME_turnl_1, FRAME_turnr_11, guardian_frames_rocket, guardian_run}; + +void guardian_kick(edict_t *self) +{ + vec3_t aim; + + if (!G_EntExists(self->enemy)) + { + self->monsterinfo.melee_finished = level.time + 1.0; + return; + } + + VectorSet(aim, MELEE_DISTANCE, 0, -80); + if (self->mtype == M_GUARDIAN) + aim[0] = 160; + if (!fire_hit(self, aim, (self->mtype == M_GUARDIAN) ? 85 : 30, 700)) + self->monsterinfo.melee_finished = level.time + ((self->mtype == M_GUARDIAN) ? 3.5 : 1.0); +} + +mframe_t guardian_frames_kick[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, guardian_footstep, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guardian_kick, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guardian_footstep, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guardian_move_kick = {FRAME_kick_in1, FRAME_kick_in13, guardian_frames_kick, guardian_run}; + +void guardian_attack(edict_t *self) +{ + float dist; + qboolean can_blaster; + qboolean can_atk2; + qboolean can_rocket; + + if (!G_EntExists(self->enemy)) + return; + + dist = entdist(self, self->enemy); + can_blaster = guardian_has_blaster_shot(self); + can_atk2 = guardian_has_grenade_or_laser_shot(self); + can_rocket = guardian_has_rocket_shot(self, -14.0f) || guardian_has_rocket_shot(self, 14.0f); + + if (self->mtype == M_GUARDIAN && self->monsterinfo.melee_finished < level.time && dist < 160) + self->monsterinfo.currentmove = &guardian_move_kick; + else if (self->mtype != M_GUARDIAN && self->monsterinfo.melee_finished < level.time && dist < 120) + self->monsterinfo.currentmove = &guardian_move_kick; + else if (can_rocket && self->mtype == M_GUARDIAN && dist > 300 && self->count <= 0 && random() < 0.25) + { + self->monsterinfo.currentmove = &guardian_move_rocket; + self->count = 6; + } + else if (can_atk2 && (dist > 512 || (self->mtype == M_GUARDIAN && dist > 300 && random() < 0.5))) + self->monsterinfo.currentmove = &guardian_move_atk2_in; + else if (can_blaster) + self->monsterinfo.currentmove = &guardian_move_atk1_in; + else if (can_atk2) + self->monsterinfo.currentmove = &guardian_move_atk2_in; + else + return; + + if (self->mtype == M_GUARDIAN && self->count > 0) + self->count--; + + M_DelayNextAttack(self, 0, true); +} + +void guardian_explode(edict_t *self) +{ + vec3_t org; + + VectorCopy(self->s.origin, org); + org[0] += crandom() * self->maxs[0]; + org[1] += crandom() * self->maxs[1]; + org[2] += 24 + random() * self->maxs[2]; + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_EXPLOSION1_BIG); + gi.WritePosition(org); + gi.multicast(self->s.origin, MULTICAST_PVS); +} + +void guardian_dead(edict_t *self) +{ + int n; + + for (n = 0; n < 3; n++) + guardian_explode(self); + vrx_throw_drone_gibs(self, 125); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_EXPLOSION1); + gi.WritePosition(self->s.origin); + gi.multicast(self->s.origin, MULTICAST_PVS); + + M_Remove(self, false, false); +} + +mframe_t guardian_frames_deathboss[] = +{ + ai_move, 0, guardian_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guardian_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guardian_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guardian_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guardian_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guardian_explode +}; +mmove_t guardian_move_deathboss = {FRAME_death1, FRAME_death26, guardian_frames_deathboss, guardian_dead}; + +mframe_t guardian_frames_death[] = +{ + ai_move, 0, guardian_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guardian_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guardian_explode +}; +mmove_t guardian_move_death = {FRAME_death1, FRAME_death11, guardian_frames_death, guardian_dead}; + +void guardian_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + (void)inflictor; + (void)attacker; + (void)damage; + (void)point; + + M_Notify(self); + + if (self->deadflag == DEAD_DEAD) + return; + + self->s.sound = 0; + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_NO; + + if (self->mtype == M_GUARDIAN) + self->monsterinfo.currentmove = &guardian_move_deathboss; + else + self->monsterinfo.currentmove = &guardian_move_death; + + DroneList_Remove(self); + + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; +} + +void init_drone_guardian(edict_t *self) +{ + qboolean miniguardian = (self->mtype == M_MINIGUARDIAN); + + sound_step = gi.soundindex("zortemp/step.wav"); + sound_charge = gi.soundindex("weapons/hyprbu1a.wav"); + sound_spin_loop = gi.soundindex("weapons/hyprbl1a.wav"); + sound_laser = gi.soundindex("weapons/laser2.wav"); + sound_pew = gi.soundindex("weapons/rocklf1a.wav"); + sound_pain = gi.soundindex("zortemp/ack.wav"); + + if (!self->mtype) + self->mtype = M_GUARDIAN; + + self->s.modelindex = gi.modelindex("models/monsters/guardian/tris.md2"); + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->monsterinfo.control_cost = miniguardian ? M_TANK_CONTROL_COST : M_JORG_CONTROL_COST; + self->monsterinfo.cost = miniguardian ? 175 : 500; + + if (miniguardian) + { + self->s.skinnum = 2; + self->s.scale = 0.4f; + self->monsterinfo.scale = MODEL_SCALE * 0.4f; + VectorSet(self->mins, -38, -38, -26); + VectorSet(self->maxs, 38, 38, 25); + self->health = M_MINIGUARDIAN_INITIAL_HEALTH + M_MINIGUARDIAN_ADDON_HEALTH * self->monsterinfo.level; + self->monsterinfo.power_armor_power = M_MINIGUARDIAN_INITIAL_ARMOR + M_MINIGUARDIAN_ADDON_ARMOR * self->monsterinfo.level; + self->mass = 340; + } + else + { + if (invasion->value) + { + self->s.scale = GUARDIAN_INVASION_SCALE; + self->monsterinfo.scale = MODEL_SCALE * GUARDIAN_INVASION_SCALE; + VectorSet(self->mins, -44, -44, -30); + VectorSet(self->maxs, 44, 44, 45); + } + else + { + self->monsterinfo.scale = MODEL_SCALE; + VectorSet(self->mins, -96, -96, -66); + VectorSet(self->maxs, 96, 96, 62); + } + self->health = M_GUARDIAN_INITIAL_HEALTH + M_GUARDIAN_ADDON_HEALTH * self->monsterinfo.level; + self->monsterinfo.power_armor_power = M_GUARDIAN_INITIAL_ARMOR + M_GUARDIAN_ADDON_ARMOR * self->monsterinfo.level; + self->mass = 850; + } + + self->max_health = self->health; + self->gib_health = miniguardian ? -3 * BASE_GIB_HEALTH : -6 * BASE_GIB_HEALTH; + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + + self->pain = guardian_pain; + self->die = guardian_die; + self->monsterinfo.stand = guardian_stand; + self->monsterinfo.walk = guardian_walk; + self->monsterinfo.run = guardian_run; + self->monsterinfo.attack = guardian_attack; + self->monsterinfo.sight = guardian_sight; + self->monsterinfo.currentmove = &guardian_move_stand; + + self->nextthink = level.time + FRAMETIME; + gi.linkentity(self); + + if (!miniguardian && !invasion->value) + G_PrintGreenText(va("A level %d guardian has spawned!", self->monsterinfo.level)); +} diff --git a/src/entities/drone/drone_guncmdr.c b/src/entities/drone/drone_guncmdr.c new file mode 100644 index 00000000..1bea4d78 --- /dev/null +++ b/src/entities/drone/drone_guncmdr.c @@ -0,0 +1,1659 @@ +/* +============================================================================== + +GUN COMMANDER + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_gunner.h" + +static int sound_pain; +static int sound_pain2; +static int sound_death; +static int sound_idle; +static int sound_open; +static int sound_search; +static int sound_sight; +static int sound_thud; + +#define GUNCMDR_GRENADE_RANGE 100.0f +#define GUNCMDR_MORTAR_RANGE 525.0f +#define GUNCMDR_CHAINGUN_RUN_RANGE 400.0f +#define GUNCMDR_MORTAR_SPEED 850 +#define GUNCMDR_GRENADE_SPEED 600 +#define GUNCMDR_WALK_SPEED_MULT 2.0f +#define GUNCMDR_INVASION_RUN_SCALE 1.15f + +#define GUNCMDR_SCALE(self) (((self)->s.scale > 0) ? (self)->s.scale : 1.0f) + +#define GUNCMDR_MINS_X_SCALED(self) (-16.0f * GUNCMDR_SCALE(self)) +#define GUNCMDR_MINS_Y_SCALED(self) (-16.0f * GUNCMDR_SCALE(self)) +#define GUNCMDR_MINS_Z_SCALED(self) (-24.0f * GUNCMDR_SCALE(self)) +#define GUNCMDR_MAXS_X_SCALED(self) (16.0f * GUNCMDR_SCALE(self)) +#define GUNCMDR_MAXS_Y_SCALED(self) (16.0f * GUNCMDR_SCALE(self)) +#define GUNCMDR_STAND_MAXZ_SCALED(self) (36.0f * GUNCMDR_SCALE(self)) +#define GUNCMDR_DUCK_MAXZ_SCALED(self) (4.0f * GUNCMDR_SCALE(self)) +#define GUNCMDR_DEAD_MAXZ_SCALED(self) (-8.0f * GUNCMDR_SCALE(self)) +#define GUNCMDR_SHRINK_MAXZ_SCALED(self) (-4.0f * GUNCMDR_SCALE(self)) + +static void guncmdr_stand(edict_t *self); +static void guncmdr_run(edict_t *self); +static void guncmdr_attack(edict_t *self); +static void guncmdr_fire_chain(edict_t *self); +static void guncmdr_refire_chain(edict_t *self); +static void guncmdr_grenade_finished(edict_t *self); +static void guncmdr_grenade_mortar_resume(edict_t *self); +static void guncmdr_resume_back_attack(edict_t *self); +static void guncmdr_duck_down(edict_t *self); +static void guncmdr_duck_up(edict_t *self); +static mmove_t guncmdr_move_duckstep_dodge; +extern mmove_t guncmdr_move_fidget; +extern mmove_t guncmdr_move_death1; +extern mmove_t guncmdr_move_death2; +extern mmove_t guncmdr_move_death6; + +static void guncmdr_ai_dodge_slide(edict_t *self, float dist); +extern mmove_t guncmdr_move_dodge_slide; + +static void guncmdr_ai_run(edict_t *self, float dist) +{ + drone_ai_run(self, invasion->value ? dist * GUNCMDR_INVASION_RUN_SCALE : dist); +} + +static void guncmdr_set_stand_bbox(edict_t *self) +{ + VectorSet(self->mins, + GUNCMDR_MINS_X_SCALED(self), + GUNCMDR_MINS_Y_SCALED(self), + GUNCMDR_MINS_Z_SCALED(self)); + VectorSet(self->maxs, + GUNCMDR_MAXS_X_SCALED(self), + GUNCMDR_MAXS_Y_SCALED(self), + GUNCMDR_STAND_MAXZ_SCALED(self)); +} + +static void guncmdr_idle_sound(edict_t *self) +{ + if (random() < 0.5) + gi.sound(self, CHAN_VOICE, sound_idle, 1, ATTN_IDLE, 0); + else + gi.sound(self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); +} + +static void guncmdr_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); +} + +static void guncmdr_search(edict_t *self) +{ + gi.sound(self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); +} + +static void guncmdr_fidget(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + return; + if (self->enemy) + return; + if (random() <= 0.05) + self->monsterinfo.currentmove = &guncmdr_move_fidget; +} + +mframe_t guncmdr_frames_fidget[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, guncmdr_idle_sound, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + + drone_ai_stand, 0, guncmdr_idle_sound, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t guncmdr_move_fidget = { FRAME_c_stand201, FRAME_c_stand254, guncmdr_frames_fidget, guncmdr_stand }; + +mframe_t guncmdr_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, guncmdr_fidget, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, guncmdr_fidget, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, guncmdr_fidget, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, guncmdr_fidget +}; +mmove_t guncmdr_move_stand = { FRAME_c_stand101, FRAME_c_stand140, guncmdr_frames_stand, NULL }; + +static void guncmdr_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &guncmdr_move_stand; +} + +static void guncmdr_ai_walk(edict_t *self, float dist) +{ + drone_ai_walk(self, dist * GUNCMDR_WALK_SPEED_MULT); +} + +mframe_t guncmdr_frames_walk[] = +{ + guncmdr_ai_walk, 1.5, NULL, + guncmdr_ai_walk, 2.5, NULL, + guncmdr_ai_walk, 3.0, NULL, + guncmdr_ai_walk, 2.5, NULL, + guncmdr_ai_walk, 2.3, NULL, + guncmdr_ai_walk, 3.0, NULL, + guncmdr_ai_walk, 2.8, NULL, + guncmdr_ai_walk, 3.6, NULL, + guncmdr_ai_walk, 2.8, NULL, + guncmdr_ai_walk, 2.5, NULL, + + guncmdr_ai_walk, 2.3, NULL, + guncmdr_ai_walk, 4.3, NULL, + guncmdr_ai_walk, 3.0, NULL, + guncmdr_ai_walk, 1.5, NULL, + guncmdr_ai_walk, 2.5, NULL, + guncmdr_ai_walk, 3.3, NULL, + guncmdr_ai_walk, 2.8, NULL, + guncmdr_ai_walk, 3.0, NULL, + guncmdr_ai_walk, 2.0, NULL, + guncmdr_ai_walk, 2.0, NULL, + + guncmdr_ai_walk, 3.3, NULL, + guncmdr_ai_walk, 3.6, NULL, + guncmdr_ai_walk, 3.4, NULL, + guncmdr_ai_walk, 2.8, NULL +}; +mmove_t guncmdr_move_walk = { FRAME_c_walk101, FRAME_c_walk124, guncmdr_frames_walk, NULL }; + +static void guncmdr_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &guncmdr_move_walk; +} + +mframe_t guncmdr_frames_run[] = +{ + guncmdr_ai_run, 15, NULL, + guncmdr_ai_run, 16, NULL, + guncmdr_ai_run, 20, NULL, + guncmdr_ai_run, 18, NULL, + guncmdr_ai_run, 24, NULL, + guncmdr_ai_run, 13.5, NULL +}; +mmove_t guncmdr_move_run = { FRAME_c_run101, FRAME_c_run106, guncmdr_frames_run, NULL }; + +static void guncmdr_run(edict_t *self) +{ + guncmdr_duck_up(self); + + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &guncmdr_move_stand; + else + self->monsterinfo.currentmove = &guncmdr_move_run; +} + +static void GunnerCmdrFire(edict_t *self) +{ + int damage; + int flash_number; + vec3_t forward, start; + + if (!self->enemy || !self->enemy->inuse) + return; + + if (self->s.frame >= FRAME_c_attack401 && self->s.frame <= FRAME_c_attack505) + flash_number = MZ2_GUNNER_MACHINEGUN_2; + else if (self->s.frame >= FRAME_c_run201 && self->s.frame <= FRAME_c_run206) + flash_number = MZ2_GUNNER_MACHINEGUN_1 + (self->s.frame - FRAME_c_run201); + else if (self->s.frame >= FRAME_c_duckstep01 && self->s.frame <= FRAME_c_duckstep06) + flash_number = MZ2_GUNCMDR_CHAINGUN_2; + else + flash_number = MZ2_GUNNER_MACHINEGUN_1 + ((self->s.frame - FRAME_c_attack107) % 6); + + damage = M_SHOTGUN_DMG_BASE + M_SHOTGUN_DMG_ADDON * drone_damagelevel(self); + if (M_SHOTGUN_DMG_MAX && damage > M_SHOTGUN_DMG_MAX) + damage = M_SHOTGUN_DMG_MAX; + + MonsterAim(self, M_PROJECTILE_ACC, 800, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + damage = vrx_increase_monster_damage_by_talent(self->activator, damage); + fire_flechette(self, start, forward, damage, 800, damage); + + gi.WriteByte(svc_muzzleflash2); + gi.WriteShort(self - g_edicts); + gi.WriteByte(flash_number); + gi.multicast(start, MULTICAST_PVS); +} + +static void guncmdr_opengun(edict_t *self) +{ + gi.sound(self, CHAN_VOICE, sound_open, 1, ATTN_IDLE, 0); +} + +mframe_t guncmdr_frames_attack_chain[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guncmdr_opengun, + ai_charge, 0, NULL +}; +mmove_t guncmdr_move_attack_chain = { FRAME_c_attack101, FRAME_c_attack106, guncmdr_frames_attack_chain, guncmdr_fire_chain }; + +mframe_t guncmdr_frames_fire_chain[] = +{ + ai_charge, 0, GunnerCmdrFire, + ai_charge, 0, GunnerCmdrFire, + ai_charge, 0, GunnerCmdrFire, + ai_charge, 0, GunnerCmdrFire, + ai_charge, 0, GunnerCmdrFire, + ai_charge, 0, GunnerCmdrFire +}; +mmove_t guncmdr_move_fire_chain = { FRAME_c_attack107, FRAME_c_attack112, guncmdr_frames_fire_chain, guncmdr_refire_chain }; + +mframe_t guncmdr_frames_fire_chain_run[] = +{ + guncmdr_ai_run, 15, GunnerCmdrFire, + guncmdr_ai_run, 16, GunnerCmdrFire, + guncmdr_ai_run, 20, GunnerCmdrFire, + guncmdr_ai_run, 18, GunnerCmdrFire, + guncmdr_ai_run, 24, GunnerCmdrFire, + guncmdr_ai_run, 13.5, GunnerCmdrFire +}; +mmove_t guncmdr_move_fire_chain_run = { FRAME_c_run201, FRAME_c_run206, guncmdr_frames_fire_chain_run, guncmdr_refire_chain }; + +mframe_t guncmdr_frames_fire_chain_dodge_right[] = +{ + guncmdr_ai_dodge_slide, 10.2, GunnerCmdrFire, + guncmdr_ai_dodge_slide, 18.0, GunnerCmdrFire, + guncmdr_ai_dodge_slide, 7.0, GunnerCmdrFire, + guncmdr_ai_dodge_slide, 7.2, GunnerCmdrFire, + guncmdr_ai_dodge_slide, -2.0, GunnerCmdrFire +}; +mmove_t guncmdr_move_fire_chain_dodge_right = { FRAME_c_attack401, FRAME_c_attack405, guncmdr_frames_fire_chain_dodge_right, guncmdr_refire_chain }; + +mframe_t guncmdr_frames_fire_chain_dodge_left[] = +{ + guncmdr_ai_dodge_slide, 10.2, GunnerCmdrFire, + guncmdr_ai_dodge_slide, 18.0, GunnerCmdrFire, + guncmdr_ai_dodge_slide, 7.0, GunnerCmdrFire, + guncmdr_ai_dodge_slide, 7.2, GunnerCmdrFire, + guncmdr_ai_dodge_slide, -2.0, GunnerCmdrFire +}; +mmove_t guncmdr_move_fire_chain_dodge_left = { FRAME_c_attack501, FRAME_c_attack505, guncmdr_frames_fire_chain_dodge_left, guncmdr_refire_chain }; + +mframe_t guncmdr_frames_endfire_chain[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guncmdr_opengun, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guncmdr_move_endfire_chain = { FRAME_c_attack118, FRAME_c_attack124, guncmdr_frames_endfire_chain, guncmdr_run }; + +static int guncmdr_grenade_flash(edict_t *self) +{ + if (self->s.frame == FRAME_c_attack205 || self->s.frame == FRAME_c_attack304 || self->s.frame == FRAME_c_attack911) + return MZ2_GUNNER_GRENADE_1; + if (self->s.frame == FRAME_c_attack208 || self->s.frame == FRAME_c_attack307 || self->s.frame == FRAME_c_attack912) + return MZ2_GUNNER_GRENADE_2; + return MZ2_GUNNER_GRENADE_4; +} + +static void GunnerCmdrGrenade(edict_t *self) +{ + int damage, speed, flash_number; + vec3_t forward, start; + + if (!self->enemy || !self->enemy->inuse) + return; + + damage = M_GRENADELAUNCHER_DMG_BASE + M_GRENADELAUNCHER_DMG_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_DMG_MAX && damage > M_GRENADELAUNCHER_DMG_MAX) + damage = M_GRENADELAUNCHER_DMG_MAX; + + if (self->s.frame >= FRAME_c_attack201 && self->s.frame <= FRAME_c_attack221) + speed = GUNCMDR_MORTAR_SPEED; + else + speed = GUNCMDR_GRENADE_SPEED + M_GRENADELAUNCHER_SPEED_ADDON * drone_damagelevel(self); + + if (M_GRENADELAUNCHER_SPEED_MAX && speed > M_GRENADELAUNCHER_SPEED_MAX) + speed = M_GRENADELAUNCHER_SPEED_MAX; + + flash_number = guncmdr_grenade_flash(self); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_grenade(self, start, forward, damage, speed, flash_number); +} + +static void guncmdr_ai_duckstep_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(self->monsterinfo.attacker)) + self->enemy = self->monsterinfo.attacker; + if (!G_EntIsAlive(self->enemy)) + return; + + guncmdr_duck_down(self); + drone_ai_dodge_slide(self, dist); +} + +mframe_t guncmdr_frames_attack_mortar[] = +{ + ai_charge, 0, guncmdr_duck_down, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, GunnerCmdrGrenade, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, GunnerCmdrGrenade, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + + ai_charge, 0, GunnerCmdrGrenade, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, guncmdr_duck_up, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t guncmdr_move_attack_mortar = { FRAME_c_attack201, FRAME_c_attack221, guncmdr_frames_attack_mortar, guncmdr_grenade_finished }; + +static void guncmdr_grenade_mortar_resume(edict_t *self) +{ + self->monsterinfo.currentmove = &guncmdr_move_attack_mortar; + self->s.frame = self->count; +} + +mframe_t guncmdr_frames_attack_mortar_dodge[] = +{ + guncmdr_ai_duckstep_slide, 11, NULL, + guncmdr_ai_duckstep_slide, 12, GunnerCmdrFire, + guncmdr_ai_duckstep_slide, 16, NULL, + guncmdr_ai_duckstep_slide, 16, GunnerCmdrFire, + guncmdr_ai_duckstep_slide, 12, NULL, + guncmdr_ai_duckstep_slide, 11, GunnerCmdrFire +}; +mmove_t guncmdr_move_attack_mortar_dodge = { FRAME_c_duckstep01, FRAME_c_duckstep06, guncmdr_frames_attack_mortar_dodge, guncmdr_grenade_mortar_resume }; + +mframe_t guncmdr_frames_attack_back[] = +{ + ai_charge, -2, NULL, + ai_charge, -1.5, NULL, + ai_charge, -0.5, GunnerCmdrGrenade, + ai_charge, -6.0, NULL, + ai_charge, -4, NULL, + ai_charge, -2.5, GunnerCmdrGrenade, + ai_charge, -7.0, NULL, + ai_charge, -3.5, NULL, + ai_charge, -1.1, GunnerCmdrGrenade, + + ai_charge, -4.6, NULL, + ai_charge, 1.9, NULL, + ai_charge, 1.0, NULL, + ai_charge, -4.5, NULL, + ai_charge, 3.2, NULL, + ai_charge, 4.4, NULL, + ai_charge, -6.5, NULL, + ai_charge, -6.1, NULL, + ai_charge, 3.0, NULL, + ai_charge, -0.7, NULL, + ai_charge, -1.0, NULL +}; +mmove_t guncmdr_move_attack_grenade_back = { FRAME_c_attack302, FRAME_c_attack321, guncmdr_frames_attack_back, guncmdr_grenade_finished }; + +static void guncmdr_resume_back_attack(edict_t *self) +{ + self->monsterinfo.currentmove = &guncmdr_move_attack_grenade_back; + self->s.frame = self->count; +} + +mframe_t guncmdr_frames_attack_grenade_back_dodge_right[] = +{ + guncmdr_ai_dodge_slide, 10.2, NULL, + guncmdr_ai_dodge_slide, 18.0, NULL, + guncmdr_ai_dodge_slide, 7.0, NULL, + guncmdr_ai_dodge_slide, 7.2, NULL, + guncmdr_ai_dodge_slide, -2.0, NULL +}; +mmove_t guncmdr_move_attack_grenade_back_dodge_right = { FRAME_c_attack601, FRAME_c_attack605, guncmdr_frames_attack_grenade_back_dodge_right, guncmdr_resume_back_attack }; + +mframe_t guncmdr_frames_attack_grenade_back_dodge_left[] = +{ + guncmdr_ai_dodge_slide, 10.2, NULL, + guncmdr_ai_dodge_slide, 18.0, NULL, + guncmdr_ai_dodge_slide, 7.0, NULL, + guncmdr_ai_dodge_slide, 7.2, NULL, + guncmdr_ai_dodge_slide, -2.0, NULL +}; +mmove_t guncmdr_move_attack_grenade_back_dodge_left = { FRAME_c_attack701, FRAME_c_attack705, guncmdr_frames_attack_grenade_back_dodge_left, guncmdr_resume_back_attack }; + +static void guncmdr_kick(edict_t *self) +{ + int damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + vec3_t aim; + + if (M_MELEE_DMG_MAX && damage > M_MELEE_DMG_MAX) + damage = M_MELEE_DMG_MAX; + + VectorSet(aim, MELEE_DISTANCE, 0, -32); + fire_hit(self, aim, damage, 400); +} + +static void guncmdr_kick_finished(edict_t *self) +{ + self->monsterinfo.melee_finished = level.time + 3.0; + if (G_ValidTarget(self, self->enemy, true, true)) + guncmdr_attack(self); + else + guncmdr_run(self); +} + +mframe_t guncmdr_frames_attack_kick[] = +{ + ai_charge, -7.7, NULL, + ai_charge, -4.9, NULL, + ai_charge, 12.6, guncmdr_kick, + ai_charge, 0, NULL, + ai_charge, -3.0, NULL, + ai_charge, 0, NULL, + ai_charge, -4.1, NULL, + ai_charge, 8.6, NULL +}; +mmove_t guncmdr_move_attack_kick = { FRAME_c_attack801, FRAME_c_attack808, guncmdr_frames_attack_kick, guncmdr_kick_finished }; + +static void guncmdr_duck_down(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_DUCKED) + return; + if (!self->groundentity) + return; + + self->monsterinfo.aiflags |= AI_DUCKED; + self->maxs[2] = GUNCMDR_DUCK_MAXZ_SCALED(self); + self->takedamage = DAMAGE_YES; + gi.linkentity(self); +} + +static void guncmdr_duck_up(edict_t *self) +{ + vec3_t oldmaxs; + trace_t tr; + + if (!(self->monsterinfo.aiflags & AI_DUCKED) && + self->maxs[2] == GUNCMDR_STAND_MAXZ_SCALED(self)) + return; + + VectorCopy(self->maxs, oldmaxs); + self->maxs[2] = GUNCMDR_STAND_MAXZ_SCALED(self); + + tr = gi.trace(self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_MONSTERSOLID); + if (tr.startsolid || tr.allsolid) + { + VectorCopy(oldmaxs, self->maxs); + self->monsterinfo.aiflags |= AI_DUCKED; + return; + } + + self->monsterinfo.aiflags &= ~AI_DUCKED; + self->takedamage = DAMAGE_AIM; + gi.linkentity(self); +} + +static void guncmdr_duck_hold(edict_t *self) +{ + if (self->monsterinfo.pausetime > level.time) + self->monsterinfo.nextframe = self->s.frame; +} + +mframe_t guncmdr_frames_duck_attack[] = +{ + ai_move, 3.6, NULL, + ai_move, 5.6, guncmdr_duck_down, + ai_move, 8.4, NULL, + ai_move, 2.0, guncmdr_duck_hold, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + + ai_charge, 0, GunnerCmdrGrenade, + ai_charge, 9.5, GunnerCmdrGrenade, + ai_charge, -1.5, GunnerCmdrGrenade, + ai_charge, 0, NULL, + ai_charge, 0, guncmdr_duck_up, + ai_charge, 0, NULL, + ai_charge, 11, NULL, + ai_charge, 2.0, NULL, + ai_charge, 5.6, NULL +}; +mmove_t guncmdr_move_duck_attack = { FRAME_c_attack901, FRAME_c_attack919, guncmdr_frames_duck_attack, guncmdr_run }; + +static void guncmdr_finish_duckstep_dodge(edict_t *self) +{ + if (self->monsterinfo.nextattack > 0 && G_ValidTarget(self, self->enemy, true, true)) + { + self->monsterinfo.nextattack--; + self->monsterinfo.currentmove = &guncmdr_move_duckstep_dodge; + return; + } + + self->monsterinfo.nextattack = 0; + guncmdr_duck_up(self); + guncmdr_run(self); +} + +mframe_t guncmdr_frames_duckstep_dodge[] = +{ + guncmdr_ai_duckstep_slide, 11, NULL, + guncmdr_ai_duckstep_slide, 12, GunnerCmdrFire, + guncmdr_ai_duckstep_slide, 16, NULL, + guncmdr_ai_duckstep_slide, 16, GunnerCmdrFire, + guncmdr_ai_duckstep_slide, 12, NULL, + guncmdr_ai_duckstep_slide, 11, GunnerCmdrFire +}; +static mmove_t guncmdr_move_duckstep_dodge = { FRAME_c_duckstep01, FRAME_c_duckstep06, guncmdr_frames_duckstep_dodge, guncmdr_finish_duckstep_dodge }; + +static void guncmdr_jump_now(edict_t *self) +{ + vec3_t forward; + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->velocity, 150, forward, self->velocity); + self->velocity[2] += 350; +} + +static void guncmdr_jump2_now(edict_t *self) +{ + vec3_t forward; + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->velocity, 150, forward, self->velocity); + self->velocity[2] += 400; +} + +static void guncmdr_jump_wait_land(edict_t *self) +{ + if (!self->groundentity && level.time < self->monsterinfo.pausetime) + self->monsterinfo.nextframe = self->s.frame; + else + self->monsterinfo.nextframe = self->s.frame + 1; +} + +static void guncmdr_jump_wait_land_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + guncmdr_jump_wait_land(self); +} + +mframe_t guncmdr_frames_jump[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guncmdr_jump_now, + ai_move, 0, NULL, + ai_move, 0, NULL, + guncmdr_jump_wait_land_ai, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_jump = { FRAME_c_jump01, FRAME_c_jump10, guncmdr_frames_jump, guncmdr_run }; + +mframe_t guncmdr_frames_jump2[] = +{ + ai_move, -8, NULL, + ai_move, -4, NULL, + ai_move, -4, NULL, + ai_move, 0, guncmdr_jump2_now, + ai_move, 0, NULL, + ai_move, 0, NULL, + guncmdr_jump_wait_land_ai, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_jump2 = { FRAME_c_jump01, FRAME_c_jump10, guncmdr_frames_jump2, guncmdr_run }; + +static qboolean guncmdr_try_sidestep(edict_t *self); + +static qboolean guncmdr_is_dodge_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &guncmdr_move_fire_chain_dodge_left || + self->monsterinfo.currentmove == &guncmdr_move_fire_chain_dodge_right || + self->monsterinfo.currentmove == &guncmdr_move_attack_grenade_back_dodge_left || + self->monsterinfo.currentmove == &guncmdr_move_attack_grenade_back_dodge_right || + self->monsterinfo.currentmove == &guncmdr_move_attack_mortar_dodge || + self->monsterinfo.currentmove == &guncmdr_move_duckstep_dodge || + self->monsterinfo.currentmove == &guncmdr_move_dodge_slide || + self->monsterinfo.currentmove == &guncmdr_move_jump || + self->monsterinfo.currentmove == &guncmdr_move_jump2 || + self->monsterinfo.currentmove == &guncmdr_move_duck_attack; +} + +static qboolean guncmdr_dodge_hit_low(edict_t *self, vec3_t dir) +{ + const float duck_height = self->absmax[2] - 33; + + return dir[2] > self->absmin[2] && dir[2] <= duck_height; +} + +static void guncmdr_start_duck_dodge(edict_t *self) +{ + if (!self->groundentity) + return; + + self->monsterinfo.nextattack = 0; + self->monsterinfo.pausetime = level.time + 0.75f; + self->monsterinfo.currentmove = &guncmdr_move_duck_attack; + guncmdr_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.8f; +} + +static qboolean guncmdr_start_duckstep_dodge(edict_t *self) +{ + if (!self->groundentity) + return false; + + self->monsterinfo.nextattack = random() < 0.55f ? 1 : 0; + self->monsterinfo.currentmove = &guncmdr_move_duckstep_dodge; + guncmdr_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.6f; + return true; +} + +static qboolean guncmdr_start_mortar_dodge(edict_t *self) +{ + if (!self->groundentity || !G_ValidTarget(self, self->enemy, true, true)) + return false; + + if (entdist(self, self->enemy) < GUNCMDR_CHAINGUN_RUN_RANGE) + return false; + + self->monsterinfo.nextattack = 0; + self->monsterinfo.currentmove = &guncmdr_move_attack_mortar; + guncmdr_duck_down(self); + self->monsterinfo.dodge_time = level.time + 2.0f; + return true; +} + +static void guncmdr_dodge(edict_t *self, edict_t *attacker, vec3_t dir, int radius) +{ + if (level.time < self->monsterinfo.dodge_time) + return; + if (!attacker) + return; + if (OnSameTeam(self, attacker)) + return; + + self->monsterinfo.attacker = attacker; + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) + self->enemy = attacker; + + if (guncmdr_is_dodge_move(self)) + { + if (self->monsterinfo.currentmove == &guncmdr_move_duck_attack && + (radius || guncmdr_dodge_hit_low(self, dir))) + { + drone_set_dodge_side(self, dir); + guncmdr_start_duckstep_dodge(self); + } + return; + } + + if (random() > 0.8) + return; + + drone_set_dodge_side(self, dir); + + if (guncmdr_try_sidestep(self)) + { + self->monsterinfo.dodge_time = level.time + 1.2f; + return; + } + + if (radius && self->groundentity) + { + if (random() < 0.35f && guncmdr_start_duckstep_dodge(self)) + return; + if (random() < 0.35f && guncmdr_start_mortar_dodge(self)) + return; + self->monsterinfo.pausetime = level.time + 2.0; + self->monsterinfo.currentmove = &guncmdr_move_jump; + self->monsterinfo.dodge_time = level.time + 3.0; + return; + } + + if (!radius && !guncmdr_dodge_hit_low(self, dir) && self->groundentity) + { + if (random() < 0.45f && guncmdr_start_duckstep_dodge(self)) + return; + if (random() < 0.35f && guncmdr_start_mortar_dodge(self)) + return; + guncmdr_start_duck_dodge(self); + return; + } + + if (guncmdr_start_duckstep_dodge(self)) + return; + + if (self->groundentity) + { + self->monsterinfo.currentmove = &guncmdr_move_dodge_slide; + self->monsterinfo.dodge_time = level.time + 1.0; + return; + } +} + +static qboolean guncmdr_can_proactive_dodge(edict_t *self, float dist) +{ + return !(self->monsterinfo.aiflags & AI_STAND_GROUND) + && self->groundentity + && dist > 160 + && level.time >= self->monsterinfo.dodge_time; +} + +static qboolean guncmdr_can_chain(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_GUNNER_MACHINEGUN_1); +} + +static qboolean guncmdr_can_dodge_chain(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_GUNNER_MACHINEGUN_2); +} + +static qboolean guncmdr_can_grenade(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_GUNNER_GRENADE_1); +} + +static void guncmdr_start_fire_chain_dodge(edict_t *self) +{ + guncmdr_duck_up(self); + self->monsterinfo.lefty = !self->monsterinfo.lefty; + + if (self->monsterinfo.lefty) + self->monsterinfo.currentmove = &guncmdr_move_fire_chain_dodge_left; + else + self->monsterinfo.currentmove = &guncmdr_move_fire_chain_dodge_right; + + self->monsterinfo.dodge_time = level.time + 1.0; +} + +static void guncmdr_attack(edict_t *self) +{ + float dist; + float zdiff; + float r; + qboolean can_chain; + qboolean can_grenade; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + guncmdr_duck_up(self); + + dist = entdist(self, self->enemy); + zdiff = fabs(self->s.origin[2] - self->enemy->s.origin[2]); + r = random(); + can_chain = guncmdr_can_chain(self); + can_grenade = guncmdr_can_grenade(self); + + if (dist <= MELEE_DISTANCE && self->monsterinfo.melee_finished < level.time) + self->monsterinfo.currentmove = &guncmdr_move_attack_kick; + else if (can_chain && guncmdr_can_proactive_dodge(self, dist) && r < 0.55) + guncmdr_start_fire_chain_dodge(self); + else if (can_grenade && (dist >= GUNCMDR_MORTAR_RANGE || zdiff > 96) && r < 0.75) + { + self->monsterinfo.currentmove = &guncmdr_move_attack_mortar; + guncmdr_duck_down(self); + } + else if (can_grenade && self->groundentity && dist > 96 && dist < GUNCMDR_CHAINGUN_RUN_RANGE && r < 0.05) + { + self->monsterinfo.pausetime = level.time + 0.75f; + self->monsterinfo.currentmove = &guncmdr_move_duck_attack; + guncmdr_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.8f; + } + else if (can_grenade && !(self->monsterinfo.aiflags & AI_STAND_GROUND) && dist > GUNCMDR_GRENADE_RANGE && r < 0.90) + self->monsterinfo.currentmove = &guncmdr_move_attack_grenade_back; + else if (can_chain) + self->monsterinfo.currentmove = &guncmdr_move_attack_chain; + else if (can_grenade) + { + self->monsterinfo.currentmove = &guncmdr_move_attack_grenade_back; + } + else + return; + + M_DelayNextAttack(self, 0, true); +} + +static void guncmdr_fire_chain(edict_t *self) +{ + if (!(self->monsterinfo.aiflags & AI_STAND_GROUND) && G_ValidTarget(self, self->enemy, true, true)) + { + float dist = entdist(self, self->enemy); + + if (guncmdr_can_dodge_chain(self) && guncmdr_can_proactive_dodge(self, dist)) + guncmdr_start_fire_chain_dodge(self); + else if (guncmdr_can_chain(self) && dist > GUNCMDR_CHAINGUN_RUN_RANGE) + self->monsterinfo.currentmove = &guncmdr_move_fire_chain_run; + else if (guncmdr_can_chain(self)) + self->monsterinfo.currentmove = &guncmdr_move_fire_chain; + else + self->monsterinfo.currentmove = &guncmdr_move_endfire_chain; + } + else if (guncmdr_can_chain(self)) + self->monsterinfo.currentmove = &guncmdr_move_fire_chain; + else + self->monsterinfo.currentmove = &guncmdr_move_endfire_chain; +} + +static void guncmdr_refire_chain(edict_t *self) +{ + guncmdr_duck_up(self); + + if (G_ValidTarget(self, self->enemy, true, true) && visible(self, self->enemy) && random() <= 0.75) + { + float dist = entdist(self, self->enemy); + + if (guncmdr_can_dodge_chain(self) && guncmdr_can_proactive_dodge(self, dist) && random() < 0.65) + guncmdr_start_fire_chain_dodge(self); + else if (guncmdr_can_chain(self) && !(self->monsterinfo.aiflags & AI_STAND_GROUND) && dist > GUNCMDR_CHAINGUN_RUN_RANGE) + self->monsterinfo.currentmove = &guncmdr_move_fire_chain_run; + else if (guncmdr_can_chain(self)) + self->monsterinfo.currentmove = &guncmdr_move_fire_chain; + else + self->monsterinfo.currentmove = &guncmdr_move_endfire_chain; + } + else + self->monsterinfo.currentmove = &guncmdr_move_endfire_chain; + + self->monsterinfo.attack_finished = level.time + 0.5; +} + +static void guncmdr_grenade_finished(edict_t *self) +{ + guncmdr_duck_up(self); + self->monsterinfo.attack_finished = level.time + 1.0; + guncmdr_run(self); +} + +mframe_t guncmdr_frames_pain1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_pain1 = { FRAME_c_pain101, FRAME_c_pain104, guncmdr_frames_pain1, guncmdr_run }; + +mframe_t guncmdr_frames_pain2[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_pain2 = { FRAME_c_pain201, FRAME_c_pain204, guncmdr_frames_pain2, guncmdr_run }; + +mframe_t guncmdr_frames_pain3[] = +{ + ai_move, -3, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_pain3 = { FRAME_c_pain301, FRAME_c_pain304, guncmdr_frames_pain3, guncmdr_run }; + +static void guncmdr_pain5_to_death1(edict_t *self) +{ + if (self->health <= 0) + self->monsterinfo.currentmove = &guncmdr_move_death1; +} + +static void guncmdr_pain5_to_death2(edict_t *self) +{ + if (self->health <= 0 && random() < 0.5f) + self->monsterinfo.currentmove = &guncmdr_move_death2; +} + +mframe_t guncmdr_frames_pain4[] = +{ + ai_move, -17.1, NULL, + ai_move, -3.2, NULL, + ai_move, 0.9, NULL, + ai_move, 3.6, NULL, + ai_move, -2.6, NULL, + ai_move, 1.0, NULL, + ai_move, -5.1, NULL, + ai_move, -6.7, NULL, + ai_move, -8.8, NULL, + ai_move, 0, NULL, + + ai_move, 0, NULL, + ai_move, -2.1, NULL, + ai_move, -2.3, NULL, + ai_move, -2.5, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_pain4 = { FRAME_c_pain401, FRAME_c_pain415, guncmdr_frames_pain4, guncmdr_run }; + +mframe_t guncmdr_frames_pain5[] = +{ + ai_move, -29, NULL, + ai_move, -5, NULL, + ai_move, -5, NULL, + ai_move, -3, NULL, + ai_move, 0, NULL, + ai_move, 0, guncmdr_pain5_to_death2, + ai_move, 9, NULL, + ai_move, 3, NULL, + ai_move, 0, guncmdr_pain5_to_death1, + ai_move, 0, NULL, + + ai_move, 0, NULL, + ai_move, -4.6, NULL, + ai_move, -4.8, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 9.5, NULL, + ai_move, 3.4, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + + ai_move, -2.4, NULL, + ai_move, -9.0, NULL, + ai_move, -5.0, NULL, + ai_move, -3.6, NULL +}; +mmove_t guncmdr_move_pain5 = { FRAME_c_pain501, FRAME_c_pain524, guncmdr_frames_pain5, guncmdr_run }; + +static void guncmdr_pain6_to_death6(edict_t *self) +{ + if (self->health <= 0) + self->monsterinfo.currentmove = &guncmdr_move_death6; +} + +mframe_t guncmdr_frames_pain6[] = +{ + ai_move, 16, NULL, + ai_move, 16, NULL, + ai_move, 12, NULL, + ai_move, 5.5, guncmdr_duck_down, + ai_move, 3.0, NULL, + ai_move, -4.7, NULL, + ai_move, -6.0, guncmdr_pain6_to_death6, + ai_move, 0, NULL, + ai_move, 1.8, NULL, + ai_move, 0.7, NULL, + + ai_move, 0, NULL, + ai_move, -2.1, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + + ai_move, 0, NULL, + ai_move, -6.1, NULL, + ai_move, 10.5, NULL, + ai_move, 4.3, NULL, + ai_move, 4.7, guncmdr_duck_up, + ai_move, 1.4, NULL, + ai_move, 0, NULL, + ai_move, -3.2, NULL, + ai_move, 2.3, NULL, + ai_move, -4.4, NULL, + + ai_move, -4.4, NULL, + ai_move, -2.4, NULL +}; +mmove_t guncmdr_move_pain6 = { FRAME_c_pain601, FRAME_c_pain632, guncmdr_frames_pain6, guncmdr_run }; + +mframe_t guncmdr_frames_pain7[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_pain7 = { FRAME_c_pain701, FRAME_c_pain714, guncmdr_frames_pain7, guncmdr_run }; + +static void guncmdr_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + float r, dot = 1.0f; + vec3_t forward, dir; + + if (self->health < (self->max_health / 2)) + self->s.skinnum = 3; + + if (guncmdr_is_dodge_move(self)) + return; + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0; + gi.sound(self, CHAN_VOICE, (random() < 0.5) ? sound_pain : sound_pain2, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + + guncmdr_duck_up(self); + + if (other) + { + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(other->s.origin, self->s.origin, dir); + dir[2] = 0; + if (VectorNormalize(dir) != 0) + dot = DotProduct(dir, forward); + } + + if (damage < 35) + { + r = random(); + if (r < 0.25) + self->monsterinfo.currentmove = &guncmdr_move_pain1; + else if (r < 0.50) + self->monsterinfo.currentmove = &guncmdr_move_pain2; + else if (r < 0.75) + self->monsterinfo.currentmove = &guncmdr_move_pain3; + else + self->monsterinfo.currentmove = &guncmdr_move_pain7; + } + else if (dot < -0.40f) + { + self->monsterinfo.currentmove = &guncmdr_move_pain6; + guncmdr_duck_down(self); + self->pain_debounce_time += 1.5; + } + else + { + if (random() < 0.5) + self->monsterinfo.currentmove = &guncmdr_move_pain4; + else + self->monsterinfo.currentmove = &guncmdr_move_pain5; + self->pain_debounce_time += 1.5; + } +} + +static void guncmdr_dead(edict_t *self) +{ + VectorSet(self->mins, + GUNCMDR_MINS_X_SCALED(self), + GUNCMDR_MINS_Y_SCALED(self), + GUNCMDR_MINS_Z_SCALED(self)); + + VectorSet(self->maxs, + GUNCMDR_MAXS_X_SCALED(self), + GUNCMDR_MAXS_Y_SCALED(self), + GUNCMDR_DEAD_MAXZ_SCALED(self)); + + self->movetype = MOVETYPE_TOSS; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); + M_PrepBodyRemoval(self); +} + +static void guncmdr_shrink(edict_t *self) +{ + self->maxs[2] = GUNCMDR_SHRINK_MAXZ_SCALED(self); + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + +static void guncmdr_ai_dodge_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_dodge_slide(self, dist); +} + +mframe_t guncmdr_frames_dodge_slide[] = +{ + guncmdr_ai_dodge_slide, 12, NULL, + guncmdr_ai_dodge_slide, 10, NULL, + guncmdr_ai_dodge_slide, 14, NULL, + guncmdr_ai_dodge_slide, 13, NULL, + guncmdr_ai_dodge_slide, 14, NULL, + guncmdr_ai_dodge_slide, 10, NULL +}; +mmove_t guncmdr_move_dodge_slide = { FRAME_c_run101, FRAME_c_run106, guncmdr_frames_dodge_slide, guncmdr_run }; + +static qboolean guncmdr_try_sidestep(edict_t *self) +{ + if (self->monsterinfo.currentmove == &guncmdr_move_fire_chain || + self->monsterinfo.currentmove == &guncmdr_move_fire_chain_run) + { + if (self->monsterinfo.lefty) + self->monsterinfo.currentmove = &guncmdr_move_fire_chain_dodge_left; + else + self->monsterinfo.currentmove = &guncmdr_move_fire_chain_dodge_right; + + return true; + } + + if (self->monsterinfo.currentmove == &guncmdr_move_attack_grenade_back) + { + self->count = self->s.frame; + + if (self->monsterinfo.lefty) + self->monsterinfo.currentmove = &guncmdr_move_attack_grenade_back_dodge_left; + else + self->monsterinfo.currentmove = &guncmdr_move_attack_grenade_back_dodge_right; + + return true; + } + + if (self->monsterinfo.currentmove == &guncmdr_move_attack_mortar) + { + self->count = self->s.frame; + self->monsterinfo.currentmove = &guncmdr_move_attack_mortar_dodge; + return true; + } + + if (self->monsterinfo.currentmove == &guncmdr_move_run) + { + if (random() < 0.45f) + return guncmdr_start_duckstep_dodge(self); + + self->monsterinfo.currentmove = &guncmdr_move_dodge_slide; + return true; + } + + return false; +} + +static void guncmdr_footstep(edict_t *self) +{ + gi.sound(self, CHAN_BODY, sound_thud, 1, ATTN_NORM, 0); +} + +static void guncmdr_shrink_footstep(edict_t *self) +{ + guncmdr_footstep(self); + guncmdr_shrink(self); +} + +mframe_t guncmdr_frames_death1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 4, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_death1 = { FRAME_c_death101, FRAME_c_death118, guncmdr_frames_death1, guncmdr_dead }; + +mframe_t guncmdr_frames_death2[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_death2 = { FRAME_c_death201, FRAME_c_death204, guncmdr_frames_death2, guncmdr_dead }; + +mframe_t guncmdr_frames_death3[] = +{ + ai_move, 20, NULL, + ai_move, 10, NULL, + ai_move, 10, guncmdr_shrink_footstep, + ai_move, 0, guncmdr_footstep, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_death3 = { FRAME_c_death301, FRAME_c_death321, guncmdr_frames_death3, guncmdr_dead }; + +mframe_t guncmdr_frames_death4[] = +{ + ai_move, -20, NULL, + ai_move, -16, NULL, + ai_move, -26, guncmdr_shrink_footstep, + ai_move, 0, guncmdr_footstep, + ai_move, -12, NULL, + ai_move, 16, NULL, + ai_move, 9.2, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_death4 = { FRAME_c_death401, FRAME_c_death436, guncmdr_frames_death4, guncmdr_dead }; + +mframe_t guncmdr_frames_death5[] = +{ + ai_move, -14, NULL, + ai_move, -2.7, NULL, + ai_move, -2.5, NULL, + ai_move, -4.6, guncmdr_footstep, + ai_move, -4.0, guncmdr_footstep, + ai_move, -1.5, NULL, + ai_move, 2.3, NULL, + ai_move, 2.5, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 3.5, NULL, + ai_move, 12.9, guncmdr_footstep, + ai_move, 3.8, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, -2.1, NULL, + ai_move, -1.3, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 3.4, NULL, + ai_move, 5.7, NULL, + ai_move, 11.2, NULL, + ai_move, 0, guncmdr_footstep +}; +mmove_t guncmdr_move_death5 = { FRAME_c_death501, FRAME_c_death528, guncmdr_frames_death5, guncmdr_dead }; + +mframe_t guncmdr_frames_death6[] = +{ + ai_move, 0, guncmdr_shrink, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_death6 = { FRAME_c_death601, FRAME_c_death614, guncmdr_frames_death6, guncmdr_dead }; + +mframe_t guncmdr_frames_death7[] = +{ + ai_move, 30, NULL, + ai_move, 20, NULL, + ai_move, 16, guncmdr_shrink_footstep, + ai_move, 5, guncmdr_footstep, + ai_move, -6, NULL, + ai_move, -7, guncmdr_footstep, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guncmdr_footstep, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, guncmdr_footstep, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t guncmdr_move_death7 = { FRAME_c_death701, FRAME_c_death730, guncmdr_frames_death7, guncmdr_dead }; + +static void guncmdr_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + int n; + vec3_t forward, dir; + float dot = 0; + edict_t *head; + + M_Notify(self); + +#ifdef OLD_NOLAG_STYLE + if (nolag->value) + { + M_Remove(self, false, true); + return; + } +#endif + + if (self->health <= self->gib_health) + { + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); + vrx_throw_drone_gibs(self, damage); +#ifdef OLD_NOLAG_STYLE + M_Remove(self, false, false); +#else + if (nolag->value) + M_Remove(self, false, true); + else + M_Remove(self, false, false); +#endif + return; + } + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + + gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); + + if (self->monsterinfo.currentmove == &guncmdr_move_pain5 && + self->s.frame < FRAME_c_pain508) + { + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; + return; + } + if (self->monsterinfo.currentmove == &guncmdr_move_pain6 && + self->s.frame < FRAME_c_pain607) + { + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; + return; + } + + if (inflictor) + { + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(inflictor->s.origin, self->s.origin, dir); + dir[2] = 0; + VectorNormalize(dir); + dot = DotProduct(dir, forward); + } + + if (point[2] >= self->s.origin[2] + self->maxs[2] - 8 && self->velocity[2] < 65) + { + if (vrx_spawn_nonessential_ent(self->s.origin)) + { + head = ThrowGibEx(self, "models/monsters/gunner/gibs/head.md2", damage, GIB_SKINNED | GIB_HEAD, + self->s.scale ? self->s.scale : 1.0f); + if (head) + head->s.skinnum /= 2; + } + self->monsterinfo.currentmove = &guncmdr_move_death5; + } + else if (dot < -0.40) + { + n = GetRandom(0, self->monsterinfo.currentmove == &guncmdr_move_pain6 ? 2 : 3); + if (n == 0) + self->monsterinfo.currentmove = &guncmdr_move_death3; + else if (n == 1) + self->monsterinfo.currentmove = &guncmdr_move_death7; + else if (n == 2) + self->monsterinfo.currentmove = &guncmdr_move_death6; + else + self->monsterinfo.currentmove = &guncmdr_move_pain6; + } + else + { + n = GetRandom(0, self->monsterinfo.currentmove == &guncmdr_move_pain5 ? 1 : 2); + if (n == 0) + self->monsterinfo.currentmove = &guncmdr_move_death4; + else + self->monsterinfo.currentmove = &guncmdr_move_pain5; + } + + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; +} + +void init_drone_guncmdr(edict_t *self) +{ + sound_death = gi.soundindex("guncmdr/gcdrdeath1.wav"); + sound_pain = gi.soundindex("guncmdr/gcdrpain2.wav"); + sound_pain2 = gi.soundindex("guncmdr/gcdrpain1.wav"); + sound_idle = gi.soundindex("guncmdr/gcdridle1.wav"); + sound_open = gi.soundindex("guncmdr/gcdratck1.wav"); + sound_search = gi.soundindex("guncmdr/gcdrsrch1.wav"); + sound_sight = gi.soundindex("guncmdr/sight1.wav"); + sound_thud = gi.soundindex("player/land1.wav"); + + gi.soundindex("guncmdr/gcdratck2.wav"); + gi.soundindex("guncmdr/gcdratck3.wav"); + gi.modelindex("models/proj/flechette/tris.md2"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/gunner/tris.md2"); + gi.modelindex("models/monsters/gunner/gibs/chest.md2"); + gi.modelindex("models/monsters/gunner/gibs/foot.md2"); + gi.modelindex("models/monsters/gunner/gibs/garm.md2"); + gi.modelindex("models/monsters/gunner/gibs/gun.md2"); + gi.modelindex("models/monsters/gunner/gibs/head.md2"); + + self->s.scale = 1.25f; + guncmdr_set_stand_bbox(self); // base bbox is -16 -16 -24, 16 16 36 at scale 1.0 + self->s.skinnum = 2; + + self->monsterinfo.control_cost = M_TANK_CONTROL_COST; + self->monsterinfo.cost = M_TANK_COST; + self->health = M_GUNCMDR_INITIAL_HEALTH + M_GUNCMDR_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -175; + self->mass = 255; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.jumpup = 64; + + if (random() > 0.5) + self->item = FindItemByClassname("ammo_bullets"); + else + self->item = FindItemByClassname("ammo_grenades"); + + self->pain = guncmdr_pain; + self->die = guncmdr_die; + + self->monsterinfo.stand = guncmdr_stand; + self->monsterinfo.walk = guncmdr_walk; + self->monsterinfo.run = guncmdr_run; + self->monsterinfo.dodge = guncmdr_dodge; + self->monsterinfo.attack = guncmdr_attack; + self->monsterinfo.sight = guncmdr_sight; + self->monsterinfo.idle = guncmdr_idle_sound; + self->monsterinfo.pain_chance = 0.2f; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.power_armor_power = M_GUNCMDR_INITIAL_ARMOR + M_GUNCMDR_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->mtype = M_GUNCMDR; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &guncmdr_move_stand; + self->monsterinfo.scale = MODEL_SCALE; + self->nextthink = level.time + FRAMETIME; +} diff --git a/src/entities/drone/drone_gunner.c b/src/entities/drone/drone_gunner.c index b9efb675..acb96ca9 100644 --- a/src/entities/drone/drone_gunner.c +++ b/src/entities/drone/drone_gunner.c @@ -19,6 +19,7 @@ static int sound_sight; static int sound_thud; void mygunner_continue (edict_t *self); +void mygunnerrun (edict_t *self); void mygunner_refire_chain(edict_t *self); void mygunner_fire_chain(edict_t *self); void mygunner_delay (edict_t *self); @@ -184,6 +185,29 @@ mframe_t mygunnerframes_run [] = mmove_t mygunnermove_run = {FRAME_run01, FRAME_run08, mygunnerframes_run, NULL}; +static void mygunner_ai_dodge_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(self->monsterinfo.attacker)) + self->enemy = self->monsterinfo.attacker; + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_dodge_slide(self, dist); +} + +mframe_t mygunner_frames_dodge_slide[] = +{ + mygunner_ai_dodge_slide, 26, NULL, + mygunner_ai_dodge_slide, 9, NULL, + mygunner_ai_dodge_slide, 9, NULL, + mygunner_ai_dodge_slide, 9, NULL, + mygunner_ai_dodge_slide, 15, NULL, + mygunner_ai_dodge_slide, 10, NULL, + mygunner_ai_dodge_slide, 13, NULL, + mygunner_ai_dodge_slide, 6, NULL +}; +mmove_t mygunner_move_dodge_slide = {FRAME_run01, FRAME_run08, mygunner_frames_dodge_slide, mygunnerrun}; + void mygunnerrun (edict_t *self) { if (self->monsterinfo.aiflags & AI_STAND_GROUND) @@ -211,12 +235,22 @@ void myGunnerGrenade (edict_t *self) if (M_GRENADELAUNCHER_SPEED_MAX && speed > M_GRENADELAUNCHER_SPEED_MAX) speed = M_GRENADELAUNCHER_SPEED_MAX; - if (self->s.frame == FRAME_attak105) + if (self->s.frame == FRAME_attak105 || self->s.frame == FRAME_attak309) flash_number = MZ2_GUNNER_GRENADE_1; + else if (self->s.frame == FRAME_attak108 || self->s.frame == FRAME_attak312) + flash_number = MZ2_GUNNER_GRENADE_2; + else if (self->s.frame == FRAME_attak111 || self->s.frame == FRAME_attak315) + flash_number = MZ2_GUNNER_GRENADE_3; else flash_number = MZ2_GUNNER_GRENADE_4; + if (self->s.frame >= FRAME_attak301 && self->s.frame <= FRAME_attak324) + flash_number = MZ2_GUNNER_GRENADE2_1 + (MZ2_GUNNER_GRENADE_4 - flash_number); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + monster_fire_grenade(self, start, forward, damage, speed, flash_number); } @@ -256,13 +290,67 @@ mframe_t mygunner_frames_attack_grenade [] = }; mmove_t mygunner_move_attack_grenade = {FRAME_attak105, FRAME_attak113, mygunner_frames_attack_grenade, gunner_refire_grenade}; +mframe_t mygunner_frames_attack_grenade2[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, myGunnerGrenade, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, myGunnerGrenade, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, myGunnerGrenade, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, myGunnerGrenade, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t mygunner_move_attack_grenade2 = {FRAME_attak305, FRAME_attak324, mygunner_frames_attack_grenade2, mygunnerrun}; + +static qboolean mygunner_can_grenade(edict_t *self, qboolean second_set) +{ + return M_MonsterHasClearShotFromFlash(self, second_set ? MZ2_GUNNER_GRENADE2_4 : MZ2_GUNNER_GRENADE_1); +} + +static qboolean mygunner_can_run_grenade(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_GUNNER_GRENADE_4); +} + +static qboolean mygunner_can_chain(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_GUNNER_MACHINEGUN_1); +} + +static qboolean mygunner_start_grenade(edict_t *self) +{ + qboolean can_first = mygunner_can_grenade(self, false); + qboolean can_second = mygunner_can_grenade(self, true); + + if (!can_first && !can_second) + return false; + + if (can_second && (!can_first || random() <= 0.5)) + self->monsterinfo.currentmove = &mygunner_move_attack_grenade2; + else + self->monsterinfo.currentmove = &mygunner_move_attack_grenade; + + return true; +} + void gunner_refire_grenade (edict_t *self) { // continue firing unless enemy is no longer valid or out of range - if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.8) - && (entdist(self, self->enemy) <= 384)) - self->monsterinfo.currentmove = &mygunner_move_attack_grenade; - else + if (!(G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.8) + && (entdist(self, self->enemy) <= 384) && mygunner_start_grenade(self))) self->monsterinfo.currentmove = &mygunner_move_attack_grenade_end; // don't call the attack function again for awhile! @@ -272,8 +360,8 @@ void gunner_refire_grenade (edict_t *self) void gunner_attack_grenade (edict_t *self) { // continue attack sequence unless enemy is no longer valid - if (G_ValidTarget(self, self->enemy, true, true)) - self->monsterinfo.currentmove = &mygunner_move_attack_grenade; + if (G_ValidTarget(self, self->enemy, true, true) && mygunner_start_grenade(self)) + return; else mygunnerrun(self); } @@ -308,7 +396,7 @@ mmove_t mygunner_move_runandshoot = {FRAME_runs01, FRAME_runs06, mygunner_frames void mygunner_continue (edict_t *self) { if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.9) - && (entdist(self, self->enemy) <= 512)) + && (entdist(self, self->enemy) <= 512) && mygunner_can_run_grenade(self)) self->monsterinfo.currentmove = &mygunner_move_runandshoot; else self->monsterinfo.currentmove = &mygunnermove_run; @@ -338,6 +426,8 @@ void myGunnerFire (edict_t *self) damage = M_MACHINEGUN_DMG_MAX; MonsterAim(self, M_HITSCAN_CONT_ACC, 0, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; monster_fire_bullet (self, start, forward, damage, damage, DEFAULT_BULLET_HSPREAD, DEFAULT_BULLET_VSPREAD, flash_number); @@ -405,7 +495,7 @@ void mygunner_refire_chain(edict_t *self) { // keep firing if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.8) - && entdist(self, self->enemy) > 128) + && entdist(self, self->enemy) > 128 && mygunner_can_chain(self)) self->monsterinfo.currentmove = &mygunner_move_fire_chain; else self->monsterinfo.currentmove = &mygunner_move_endfire_chain; @@ -418,9 +508,10 @@ void mygunner_refire_chain(edict_t *self) void gunner_stand_attack (edict_t *self) { - if (entdist(self, self->enemy) <= 384 && random() <= 0.8) - self->monsterinfo.currentmove = &mygunner_move_attack_grenade; - else + if (entdist(self, self->enemy) <= 384 && random() <= 0.8 && mygunner_start_grenade(self)) + return; + + if (mygunner_can_chain(self)) self->monsterinfo.currentmove = &mygunner_move_attack_chain; } @@ -432,20 +523,26 @@ void gunner_attack (edict_t *self) // short range (20% chance grenade, 80% chance run and shoot) if (dist <= 128) { - if (r <= 0.2) - self->monsterinfo.currentmove = &mygunner_move_attack_grenade; - else + if (r <= 0.2 && mygunner_start_grenade(self)) + return; + + if (mygunner_can_run_grenade(self)) self->monsterinfo.currentmove = &mygunner_move_runandshoot; + else if (mygunner_can_chain(self)) + self->monsterinfo.currentmove = &mygunner_move_attack_chain; } // medium range (100% run and shoot) else if (dist <= 512) { - self->monsterinfo.currentmove = &mygunner_move_runandshoot; + if (mygunner_can_run_grenade(self)) + self->monsterinfo.currentmove = &mygunner_move_runandshoot; + else if (mygunner_can_chain(self)) + self->monsterinfo.currentmove = &mygunner_move_attack_chain; } // long range (50% chance chaingun) else { - if (r <= 0.5) + if (r <= 0.5 && mygunner_can_chain(self)) self->monsterinfo.currentmove = &mygunner_move_attack_chain; else self->monsterinfo.attack_finished = level.time + 2.0;// don't attack, try to get closer @@ -485,17 +582,36 @@ void mygunner_duck_up (edict_t *self) gi.linkentity (self); } +void mygunner_duck_hold (edict_t *self) +{ + if (self->monsterinfo.pausetime > level.time) + self->monsterinfo.nextframe = self->s.frame; +} + mframe_t mygunner_frames_duck [] = { - ai_move, 0, mygunner_duck_down, - ai_move, 0, NULL, + ai_move, 1, mygunner_duck_down, + ai_move, 1, NULL, + ai_move, 1, mygunner_duck_hold, ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, mygunner_duck_up, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, 0, mygunner_duck_up, + ai_move, -1, NULL }; -mmove_t mygunner_move_duck = {FRAME_duck03, FRAME_duck07, mygunner_frames_duck, mygunnerrun}; +mmove_t mygunner_move_duck = {FRAME_duck01, FRAME_duck08, mygunner_frames_duck, mygunnerrun}; + +void mygunner_jump_now (edict_t *self) +{ + vec3_t forward; -void mygunner_jump_takeoff (edict_t *self) + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->velocity, 100, forward, self->velocity); + self->velocity[2] += 300; + self->monsterinfo.pausetime = level.time + 2.0; +} + +void mygunner_jump2_now (edict_t *self) { vec3_t v; @@ -503,12 +619,12 @@ void mygunner_jump_takeoff (edict_t *self) VectorSubtract(self->monsterinfo.dir, self->s.origin, v); v[2] = 0; VectorNormalize(v); - VectorScale(v, -200, self->velocity); + VectorScale(v, -150, self->velocity); self->velocity[2] = 400; self->monsterinfo.pausetime = level.time + 2.0; // maximum duration of jump } -void mygunner_jump_hold (edict_t *self) +void mygunner_jump_wait_land (edict_t *self) { vec3_t v; @@ -522,51 +638,109 @@ void mygunner_jump_hold (edict_t *self) // check for landing or jump timeout if (self->groundentity || (level.time > self->monsterinfo.pausetime)) { - self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; VectorClear(self->velocity); + self->monsterinfo.nextframe = self->s.frame + 1; } else { // we're still in the air - self->monsterinfo.aiflags |= AI_HOLD_FRAME; + self->monsterinfo.nextframe = self->s.frame; } } -mframe_t mygunner_frames_leap [] = +static void mygunner_jump_wait_land_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + mygunner_jump_wait_land(self); +} + +mframe_t mygunner_frames_jump [] = { - ai_move, 0, mygunner_jump_takeoff, - ai_move, 0, NULL, - ai_move, 0, mygunner_jump_hold, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, mygunner_jump_now, + ai_move, 0, NULL, + ai_move, 0, NULL, + mygunner_jump_wait_land_ai, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL }; -mmove_t mygunner_move_leap = {FRAME_duck01, FRAME_duck08, mygunner_frames_leap, mygunnerrun}; +mmove_t mygunner_move_jump = {FRAME_jump01, FRAME_jump10, mygunner_frames_jump, mygunnerrun}; + +mframe_t mygunner_frames_jump2 [] = +{ + ai_move, -8, NULL, + ai_move, -4, NULL, + ai_move, -4, NULL, + ai_move, 0, mygunner_jump2_now, + ai_move, 0, NULL, + ai_move, 0, NULL, + mygunner_jump_wait_land_ai, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t mygunner_move_jump2 = {FRAME_jump01, FRAME_jump10, mygunner_frames_jump2, mygunnerrun}; void mygunner_leap (edict_t *self) { if (self->groundentity) - self->monsterinfo.currentmove = &mygunner_move_leap; + self->monsterinfo.currentmove = &mygunner_move_jump2; +} + +static qboolean mygunner_is_dodge_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &mygunner_move_duck || + self->monsterinfo.currentmove == &mygunner_move_jump || + self->monsterinfo.currentmove == &mygunner_move_jump2 || + self->monsterinfo.currentmove == &mygunner_move_dodge_slide; +} + +static qboolean mygunner_is_uninterruptible_attack(edict_t *self) +{ + return self->monsterinfo.currentmove == &mygunner_move_attack_chain || + self->monsterinfo.currentmove == &mygunner_move_fire_chain || + self->monsterinfo.currentmove == &mygunner_move_attack_grenade || + self->monsterinfo.currentmove == &mygunner_move_attack_grenade2; +} + +static qboolean mygunner_dodge_hit_low(edict_t *self, vec3_t dir) +{ + const float duck_height = self->absmax[2] - 33; + + return dir[2] > self->absmin[2] && dir[2] <= duck_height; } void mygunner_dodge (edict_t *self, edict_t *attacker, vec3_t dir, int radius) { if (random() > 0.9) return; - if (!G_GetClient(self)) - return; if (level.time < self->monsterinfo.dodge_time) return; if (OnSameTeam(self, attacker)) return; + if (mygunner_is_dodge_move(self) || mygunner_is_uninterruptible_attack(self)) + return; - if (!self->enemy && G_EntIsAlive(attacker)) + self->monsterinfo.attacker = attacker; + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) self->enemy = attacker; if (!radius) { - self->monsterinfo.currentmove = &mygunner_move_duck; + drone_set_dodge_side(self, dir); + + if (mygunner_dodge_hit_low(self, dir) && !(self->monsterinfo.aiflags & AI_STAND_GROUND)) + { + self->monsterinfo.currentmove = &mygunner_move_dodge_slide; + } + else + { + self->monsterinfo.pausetime = level.time + 0.5; + self->monsterinfo.currentmove = &mygunner_move_duck; + mygunner_duck_down(self); + } self->monsterinfo.dodge_time = level.time + 2.0; } else @@ -635,7 +809,8 @@ void mygunner_pain(edict_t* self, edict_t* other, float kick, int damage) // we're already in a pain state if (self->monsterinfo.currentmove == &mygunnermove_pain_long1 || self->monsterinfo.currentmove == &mygunnermove_pain_long2 || - self->monsterinfo.currentmove == &mygunnermove_pain_short) + self->monsterinfo.currentmove == &mygunnermove_pain_short || + mygunner_is_dodge_move(self)) return; // monster players don't get pain state induced @@ -680,12 +855,19 @@ void mygunnerdead (edict_t *self) M_PrepBodyRemoval(self); } +static void mygunner_shrink(edict_t *self) +{ + self->maxs[2] = -4; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t mygunnerframes_death [] = { ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, -7, NULL, + ai_move, -7, mygunner_shrink, ai_move, -3, NULL, ai_move, -5, NULL, ai_move, 8, NULL, @@ -698,8 +880,6 @@ mmove_t mygunnermove_death = {FRAME_death01, FRAME_death11, mygunnerframes_death void mygunnerdie (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; - M_Notify(self); #ifdef OLD_NOLAG_STYLE @@ -715,14 +895,7 @@ void mygunnerdie (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); @@ -744,6 +917,7 @@ void mygunnerdie (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama gi.sound (self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); self->monsterinfo.currentmove = &mygunnermove_death; if (self->activator && !self->activator->client) @@ -795,6 +969,7 @@ void init_drone_gunner (edict_t *self) self->monsterinfo.dodge = mygunner_dodge; self->monsterinfo.attack = mygunner_attack; self->monsterinfo.walk = gunner_walk; + self->monsterinfo.idle = mygunnersearch; self->monsterinfo.pain_chance = 0.3f; self->pain = mygunner_pain; diff --git a/src/entities/drone/drone_hover.c b/src/entities/drone/drone_hover.c index ecd5d39e..d699e96f 100644 --- a/src/entities/drone/drone_hover.c +++ b/src/entities/drone/drone_hover.c @@ -41,11 +41,21 @@ void hover_search (edict_t *self) void hover_run (edict_t *self); void hover_stand (edict_t *self); void hover_dead (edict_t *self); +void hover_deadthink (edict_t *self); void hover_attack (edict_t *self); void hover_reattack (edict_t *self); void hover_fire_blaster (edict_t *self); void hover_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point); +static void hover_set_fly_parameters(edict_t *self) +{ + self->monsterinfo.fly_thrusters = false; + self->monsterinfo.fly_acceleration = 20.0f; + self->monsterinfo.fly_speed = 120.0f; + self->monsterinfo.fly_min_distance = 275.0f; //250.0f default value + self->monsterinfo.fly_max_distance = 550.0f; //450.0f default value +} + mframe_t hover_frames_stand [] = { drone_ai_stand, 0, NULL, @@ -295,56 +305,81 @@ mmove_t hover_move_walk = {FRAME_forwrd01, FRAME_forwrd35, hover_frames_walk, NU mframe_t hover_frames_run [] = { - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL, + drone_ai_run, 10, NULL }; mmove_t hover_move_run = {FRAME_forwrd01, FRAME_forwrd35, hover_frames_run, NULL}; +void hover_dying (edict_t *self) +{ + if (self->groundentity) + { + hover_deadthink(self); + return; + } + + if (random() < 0.5) + return; + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_PLAIN_EXPLOSION); + gi.WritePosition(self->s.origin); + gi.multicast(self->s.origin, MULTICAST_PHS); + + if (!vrx_spawn_nonessential_ent(self->s.origin)) + return; + + if (random() < 0.5) + ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", 120, GIB_ORGANIC); + else + ThrowGib(self, "models/objects/gibs/sm_metal/tris.md2", 120, GIB_METALLIC); +} + mframe_t hover_frames_death1 [] = { ai_move, 0, NULL, + ai_move, 0, hover_dying, ai_move, 0, NULL, + ai_move, 0, hover_dying, ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, -10,NULL, + ai_move, 0, hover_dying, + ai_move, -10,hover_dying, ai_move, 3, NULL, - ai_move, 5, NULL, - ai_move, 4, NULL, + ai_move, 5, hover_dying, + ai_move, 4, hover_dying, ai_move, 7, NULL }; mmove_t hover_move_death1 = {FRAME_death101, FRAME_death111, hover_frames_death1, hover_dead}; @@ -388,32 +423,32 @@ mmove_t hover_move_start_attack = {FRAME_attak101, FRAME_attak103, hover_frames_ mframe_t hover_frames_attack2[] = { - ai_charge, 15, hover_fire_blaster, - ai_charge, 15, hover_fire_blaster, - ai_charge, 15, hover_reattack + ai_charge, 10, hover_fire_blaster, + ai_charge, 10, hover_fire_blaster, + ai_charge, 10, hover_reattack }; mmove_t hover_move_attack2 = { FRAME_attak104, FRAME_attak106, hover_frames_attack2, hover_run }; mframe_t hover_frames_attack1 [] = { - drone_ai_run, 15, hover_fire_blaster, - drone_ai_run, 15, hover_fire_blaster, - drone_ai_run, 15, hover_reattack + ai_charge, -10, hover_fire_blaster, + ai_charge, -10, hover_fire_blaster, + ai_charge, 0, hover_reattack }; mmove_t hover_move_attack1 = {FRAME_attak104, FRAME_attak106, hover_frames_attack1, hover_run }; mframe_t hover_frames_end_attack [] = { - drone_ai_run, 15, NULL, - drone_ai_run, 15, NULL + ai_charge, 1, NULL, + ai_charge, 1, NULL }; mmove_t hover_move_end_attack = {FRAME_attak107, FRAME_attak108, hover_frames_end_attack, hover_run}; void hover_reattack (edict_t *self) { // if our enemy is still valid, then continue firing - if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.9)) + if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.6)) { self->s.frame = FRAME_attak104; //hover_fire_blaster(self); @@ -445,8 +480,13 @@ void hover_fire_blaster (edict_t *self) if (M_HYPERBLASTER_DMG_MAX && damage > M_HYPERBLASTER_DMG_MAX) damage = M_HYPERBLASTER_DMG_MAX; - MonsterAim(self, M_PROJECTILE_ACC, speed, true, MZ2_HOVER_BLASTER_1, forward, start); - monster_fire_rocket (self, start, forward, damage, speed, MZ2_HOVER_BLASTER_1); + MonsterAim(self, M_PROJECTILE_ACC, speed, true, MZ2_BOSS2_ROCKET_3, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + { + M_MonsterBlockedShot(self, 0.4f); + return; + } + monster_fire_rocket (self, start, forward, damage, speed, MZ2_BOSS2_ROCKET_3); } void hover_stand (edict_t *self) @@ -475,9 +515,22 @@ void hover_start_attack (edict_t *self) void hover_attack(edict_t *self) { if (self->monsterinfo.aiflags & AI_STAND_GROUND) + { + self->monsterinfo.attack_state = AS_STRAIGHT; self->monsterinfo.currentmove = &hover_move_attack2; - else + } + else if (random() < 0.5f) + { + self->monsterinfo.attack_state = AS_STRAIGHT; self->monsterinfo.currentmove = &hover_move_attack1; + } + else + { + if (random() <= 0.5f) + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + self->monsterinfo.attack_state = AS_SLIDING; + self->monsterinfo.currentmove = &hover_move_attack2; + } } @@ -496,6 +549,11 @@ void hover_pain (edict_t *self, edict_t *other, float kick, int damage) if (G_GetClient(self)) return; + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0f; + // stand animation always gets pain state if (random() <= (1.0f - self->monsterinfo.pain_chance) && self->monsterinfo.currentmove == &hover_move_stand) @@ -517,17 +575,34 @@ void hover_pain (edict_t *self, edict_t *other, float kick, int damage) else { gi.sound (self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); - self->monsterinfo.currentmove = &hover_move_pain1; + if (random() < 0.3f) + self->monsterinfo.currentmove = &hover_move_pain1; + else + self->monsterinfo.currentmove = &hover_move_pain2; } } void hover_deadthink (edict_t *self) { - if (!self->groundentity && level.time < self->timestamp) + vec3_t end; + trace_t tr; + qboolean on_floor; + + on_floor = self->groundentity != NULL; + if (!on_floor) + { + VectorCopy(self->s.origin, end); + end[2] -= 24; + tr = gi.trace(self->s.origin, self->mins, self->maxs, end, self, MASK_SOLID); + on_floor = tr.fraction < 1.0f && tr.plane.normal[2] > 0.7f; + } + + if (!on_floor && level.time < self->timestamp) { self->nextthink = level.time + FRAMETIME; return; } + vrx_throw_drone_gibs(self, 150); BecomeExplosion1(self); } @@ -544,23 +619,10 @@ void hover_dead (edict_t *self) void hover_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; + qboolean overkill; M_Notify(self); - -// check for gib - if (self->health <= self->gib_health) - { - gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - for (n= 0; n < 2; n++) - ThrowGib (self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n= 0; n < 2; n++) - ThrowGib (self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - ThrowHead (self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - self->deadflag = DEAD_DEAD; - M_Remove(self, false, false); - return; - } + overkill = self->health <= self->gib_health; if (self->deadflag == DEAD_DEAD) return; @@ -568,12 +630,20 @@ void hover_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage DroneList_Remove(self); // regular death - if (random() < 0.5) + if (overkill) + gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); + else if (random() < 0.5) gi.sound (self, CHAN_VOICE, sound_death1, 1, ATTN_NORM, 0); else gi.sound (self, CHAN_VOICE, sound_death2, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); + self->flags &= ~FL_FLY; + self->movetype = MOVETYPE_TOSS; + self->gravity = 1.0; + if (self->velocity[2] > -120) + self->velocity[2] = -120; self->monsterinfo.currentmove = &hover_move_death1; } @@ -605,6 +675,8 @@ void init_drone_hover (edict_t *self) self->mtype = M_HOVER; self->flags |= FL_FLY; + self->monsterinfo.aiflags |= AI_ALTERNATE_FLY; + hover_set_fly_parameters(self); self->max_health = self->health; self->monsterinfo.power_armor_power = M_FLOATER_INITIAL_ARMOR + M_FLOATER_ADDON_ARMOR*self->monsterinfo.level; self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; diff --git a/src/entities/drone/drone_infantry.c b/src/entities/drone/drone_infantry.c index 6c05c58c..a2121c32 100644 --- a/src/entities/drone/drone_infantry.c +++ b/src/entities/drone/drone_infantry.c @@ -26,6 +26,30 @@ static int sound_sight; static int sound_search; static int sound_idle; +#define INFANTRY_RUN_ATTACK_MIN_DIST 256 + +static void infantry_run_fire(edict_t *self); +extern mmove_t infantry_move_attack4; + +static void infantry_project_flash(edict_t *self, int flash_number, vec3_t forward, vec3_t start) +{ + vec3_t right, offset; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[flash_number], offset); + if (self->s.scale) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static int infantry_20mm_flash_for_frame(edict_t *self) +{ + if (self->s.frame >= FRAME_run201 && self->s.frame <= FRAME_run208) + return MZ2_INFANTRY_MACHINEGUN_14 + (self->s.frame - FRAME_run201); + if (self->s.frame == FRAME_attak111) + return MZ2_INFANTRY_MACHINEGUN_1; + return MZ2_INFANTRY_MACHINEGUN_2 + (self->s.frame - FRAME_death211); +} mframe_t infantry_frames_stand [] = { @@ -192,14 +216,14 @@ void InfantryMachineGun (edict_t *self) if (self->s.frame == FRAME_attak111) { flash_number = MZ2_INFANTRY_MACHINEGUN_1; - MonsterAim(self, 0.8, 0, false, flash_number, forward, start); + infantry_project_flash(self, flash_number, forward, start); + MonsterAim(self, 0.8, 0, false, -1, forward, start); } else { flash_number = MZ2_INFANTRY_MACHINEGUN_2 + (self->s.frame - FRAME_death211); - AngleVectors (self->s.angles, forward, right, NULL); - G_ProjectSource (self->s.origin, monster_flash_offset[flash_number], forward, right, start); + infantry_project_flash(self, flash_number, forward, start); VectorSubtract (self->s.angles, aimangles[flash_number-MZ2_INFANTRY_MACHINEGUN_2], vec); AngleVectors (vec, forward, NULL, NULL); @@ -211,36 +235,39 @@ void InfantryMachineGun (edict_t *self) void Infantry20mm(edict_t* self) { - vec3_t start, forward, right, vec; + vec3_t start, forward, vec; int damage, flash_number; const float range = M_20MM_RANGE_BASE + M_20MM_RANGE_ADDON * drone_damagelevel(self); + qboolean run_attack = (self->s.frame >= FRAME_run201 && self->s.frame <= FRAME_run208); damage = M_20MM_DMG_BASE + M_20MM_DMG_ADDON * drone_damagelevel(self); if (M_20MM_DMG_MAX && damage > M_20MM_DMG_MAX) damage = M_20MM_DMG_MAX; - if (self->s.frame == FRAME_attak111) + flash_number = infantry_20mm_flash_for_frame(self); + + if (self->s.frame == FRAME_attak111 || run_attack) { - flash_number = MZ2_INFANTRY_MACHINEGUN_1; - MonsterAim(self, M_HITSCAN_CONT_ACC, 0, false, flash_number, forward, start); + infantry_project_flash(self, flash_number, forward, start); + MonsterAim(self, M_HITSCAN_CONT_ACC, 0, false, -1, forward, start); } else { - flash_number = MZ2_INFANTRY_MACHINEGUN_2 + (self->s.frame - FRAME_death211); - - AngleVectors(self->s.angles, forward, right, NULL); - G_ProjectSource(self->s.origin, monster_flash_offset[flash_number], forward, right, start); + infantry_project_flash(self, flash_number, forward, start); VectorSubtract(self->s.angles, aimangles[flash_number - MZ2_INFANTRY_MACHINEGUN_2], vec); AngleVectors(vec, forward, NULL, NULL); } - monster_fire_20mm(self, start, forward, damage, damage, range, MZ_IONRIPPER | MZ_SILENCED); + monster_fire_20mm(self, start, forward, damage, damage, range, flash_number); } void infantry_sight (edict_t *self, edict_t *other) { - gi.sound (self, CHAN_BODY, sound_sight, 1, ATTN_NORM, 0); + if (random() < 0.5) + gi.sound (self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + else + gi.sound (self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); } void infantry_dead (edict_t *self) @@ -253,6 +280,13 @@ void infantry_dead (edict_t *self) M_PrepBodyRemoval(self); } +static void infantry_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t infantry_frames_pain1[] = { ai_move, 0, NULL, @@ -337,7 +371,7 @@ mframe_t infantry_frames_death1 [] = ai_move, -2, NULL, ai_move, 2, NULL, ai_move, 2, NULL, - ai_move, 9, NULL, + ai_move, 9, infantry_shrink, ai_move, 9, NULL, ai_move, 5, NULL, ai_move, -3, NULL, @@ -370,7 +404,7 @@ mframe_t infantry_frames_death2 [] = ai_move, -10, Infantry20mm, ai_move, -7, Infantry20mm, ai_move, -8, Infantry20mm, - ai_move, -6, NULL, + ai_move, -6, infantry_shrink, ai_move, 4, NULL, ai_move, 0, NULL }; @@ -380,7 +414,7 @@ mframe_t infantry_frames_death3 [] = { ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, infantry_shrink, ai_move, -6, NULL, ai_move, -11, NULL, ai_move, -3, NULL, @@ -394,6 +428,8 @@ mmove_t infantry_move_death3 = {FRAME_death301, FRAME_death309, infantry_frames_ void infantry_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { int n; + edict_t *head; + vec3_t head_dir; M_Notify(self); @@ -409,14 +445,7 @@ void infantry_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dam if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - ThrowHead(self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); @@ -437,6 +466,7 @@ void infantry_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dam // regular death self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); n = randomMT() % 3; if (n == 0) @@ -455,6 +485,29 @@ void infantry_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dam gi.sound (self, CHAN_VOICE, sound_die2, 1, ATTN_NORM, 0); } + if (n != 2 && random() <= 0.25f && vrx_spawn_nonessential_ent(self->s.origin)) + { + head = ThrowGibEx(self, "models/monsters/infantry/gibs/head.md2", damage, GIB_ORGANIC, + self->s.scale ? self->s.scale : 1.0f); + if (head) + { + VectorCopy(self->s.angles, head->s.angles); + VectorCopy(self->s.origin, head->s.origin); + head->s.origin[2] += 32; + if (inflictor) + VectorSubtract(self->s.origin, inflictor->s.origin, head_dir); + else + VectorSet(head_dir, crandom(), crandom(), 0.5f); + if (VectorNormalize(head_dir) == 0) + VectorSet(head_dir, 0, 0, 1); + VectorScale(head_dir, 100, head->velocity); + head->velocity[2] = 200; + VectorScale(head->avelocity, 0.15f, head->avelocity); + head->s.skinnum = 0; + gi.linkentity(head); + } + } + if (self->activator && !self->activator->client) { self->activator->num_monsters_real--; @@ -540,6 +593,201 @@ void infantry_fire(edict_t* self) M_DelayNextAttack(self, 0, true); } +static void infantry_ai_dodge_slide(edict_t *self, float dist); + +static void infantry_ai_dodge_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(self->monsterinfo.attacker)) + self->enemy = self->monsterinfo.attacker; + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_dodge_slide(self, dist); +} + +mframe_t infantry_frames_dodge_slide[] = +{ + infantry_ai_dodge_slide, 12, NULL, + infantry_ai_dodge_slide, 10, NULL, + infantry_ai_dodge_slide, 12, NULL, + infantry_ai_dodge_slide, 10, NULL, + infantry_ai_dodge_slide, 12, NULL, + infantry_ai_dodge_slide, 10, NULL, + infantry_ai_dodge_slide, 8, NULL, + infantry_ai_dodge_slide, 6, NULL +}; +mmove_t infantry_move_dodge_slide = { FRAME_run01, FRAME_run08, infantry_frames_dodge_slide, infantry_run }; + +static void infantry_attack4_dodge_ai(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_dodge_slide(self, dist); +} + +static void infantry_resume_attack4(edict_t *self) +{ + self->monsterinfo.currentmove = &infantry_move_attack4; + self->s.frame = self->count; +} + +mframe_t infantry_frames_attack4_dodge[] = +{ + infantry_attack4_dodge_ai, 10, infantry_run_fire, + infantry_attack4_dodge_ai, 12, NULL, + infantry_attack4_dodge_ai, 10, infantry_run_fire, + infantry_attack4_dodge_ai, 9, NULL, + infantry_attack4_dodge_ai, 8, NULL +}; +mmove_t infantry_move_attack4_dodge = { FRAME_run201, FRAME_run205, infantry_frames_attack4_dodge, infantry_resume_attack4 }; + +static qboolean infantry_try_sidestep(edict_t *self) +{ + if (self->monsterinfo.currentmove == &infantry_move_attack4) + { + self->count = self->s.frame; + self->monsterinfo.currentmove = &infantry_move_attack4_dodge; + return true; + } + + if (self->monsterinfo.currentmove == &infantry_move_run) + { + self->monsterinfo.currentmove = &infantry_move_dodge_slide; + return true; + } + + return false; +} + +static qboolean infantry_is_dodge_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &infantry_move_dodge_slide || + self->monsterinfo.currentmove == &infantry_move_attack4_dodge || + self->monsterinfo.currentmove == &infantry_move_duck; +} + +static qboolean infantry_dodge_hit_low(edict_t *self, vec3_t dir) +{ + const float duck_height = self->absmax[2] - 33; + + return dir[2] > self->absmin[2] && dir[2] <= duck_height; +} + +static void infantry_start_duck(edict_t *self) +{ + self->monsterinfo.currentmove = &infantry_move_duck; + infantry_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.2f; +} + +static void infantry_dodge(edict_t *self, edict_t *attacker, vec3_t dir, int radius) +{ + if (level.time < self->monsterinfo.dodge_time) + return; + if (!attacker) + return; + if (OnSameTeam(self, attacker)) + return; + if (infantry_is_dodge_move(self)) + return; + + self->monsterinfo.attacker = attacker; + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) + self->enemy = attacker; + + if (random() > 0.75f) + return; + + // Match remaster: sidestep low shots, duck high direct shots. + if (!radius && !infantry_dodge_hit_low(self, dir)) + { + infantry_start_duck(self); + return; + } + + drone_set_dodge_side(self, dir); + + if (infantry_try_sidestep(self)) + { + self->monsterinfo.dodge_time = level.time + 1.0f; + return; + } + + if (!(self->monsterinfo.aiflags & AI_STAND_GROUND)) + { + self->monsterinfo.currentmove = &infantry_move_dodge_slide; + self->monsterinfo.dodge_time = level.time + 0.9f; + return; + } + + infantry_start_duck(self); +} + +static void infantry_run_attack_ai(edict_t* self, float dist) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + return; + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + ai_charge(self, 0); + M_MoveToGoal(self, dist); +} + +static void infantry_run_fire(edict_t* self) +{ + int range = M_20MM_RANGE_BASE + M_20MM_RANGE_ADDON * drone_damagelevel(self); + + if (M_20MM_RANGE_MAX && range > M_20MM_RANGE_MAX) + range = M_20MM_RANGE_MAX; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + if (entdist(self, self->enemy) > range) + return; + + Infantry20mm(self); + + M_DelayNextAttack(self, 0, true); +} + +extern mmove_t infantry_move_attack1; + +void infantry_attack4_refire(edict_t* self) +{ + if (level.time >= self->monsterinfo.pausetime) + { + self->monsterinfo.currentmove = &infantry_move_attack1; + self->monsterinfo.nextframe = FRAME_attak114; + } + else if ((self->monsterinfo.aiflags & AI_STAND_GROUND) + || !G_ValidTarget(self, self->enemy, true, true) + || entdist(self, self->enemy) < INFANTRY_RUN_ATTACK_MIN_DIST) + { + self->monsterinfo.currentmove = &infantry_move_attack1; + self->monsterinfo.nextframe = FRAME_attak110; + } + else + self->monsterinfo.nextframe = FRAME_run201; + + infantry_run_fire(self); +} + +mframe_t infantry_frames_attack4[] = +{ + infantry_run_attack_ai, 16, infantry_run_fire, + infantry_run_attack_ai, 16, infantry_run_fire, + infantry_run_attack_ai, 13, infantry_run_fire, + infantry_run_attack_ai, 10, infantry_run_fire, + infantry_run_attack_ai, 16, infantry_run_fire, + infantry_run_attack_ai, 16, infantry_run_fire, + infantry_run_attack_ai, 16, infantry_run_fire, + infantry_run_attack_ai, 16, infantry_attack4_refire +}; +mmove_t infantry_move_attack4 = { FRAME_run201, FRAME_run208, infantry_frames_attack4, infantry_run }; + mframe_t infantry_frames_attack1 [] = { //ai_charge, 4, NULL, @@ -650,7 +898,11 @@ void infantry_attack(edict_t* self) maxrange = M_20MM_RANGE_MAX; } else - maxrange = 512; + { + maxrange = M_20MM_RANGE_BASE + M_20MM_RANGE_ADDON * drone_damagelevel(self); + if (M_20MM_RANGE_MAX && maxrange > M_20MM_RANGE_MAX) + maxrange = M_20MM_RANGE_MAX; + } if (range > maxrange) return; @@ -671,6 +923,14 @@ void infantry_attack(edict_t* self) return; } + if (!(self->monsterinfo.aiflags & AI_STAND_GROUND) + && range >= INFANTRY_RUN_ATTACK_MIN_DIST && range <= maxrange) + { + self->monsterinfo.pausetime = level.time + 1.8 + random(); + self->monsterinfo.currentmove = &infantry_move_attack4; + return; + } + // Machinegun attack (default) self->monsterinfo.currentmove = &infantry_move_attack1; } @@ -712,6 +972,7 @@ void init_drone_infantry(edict_t* self) self->mass = 400; + self->s.scale = 1.15f; //don't override previous mtype if (!self->mtype) self->mtype = M_ENFORCER; @@ -748,6 +1009,7 @@ void init_drone_infantry(edict_t* self) self->monsterinfo.sight = infantry_sight; //self->monsterinfo.idle = infantry_fidget; self->monsterinfo.melee = infantry_melee; + self->monsterinfo.dodge = infantry_dodge; gi.linkentity(self); diff --git a/src/entities/drone/drone_jorg.c b/src/entities/drone/drone_jorg.c index f6ad093e..da3415ea 100644 --- a/src/entities/drone/drone_jorg.c +++ b/src/entities/drone/drone_jorg.c @@ -32,7 +32,6 @@ static int sound_death_hit; void BossExplode (edict_t *self); void MakronToss (edict_t *self); -/* void jorg_search (edict_t *self) { float r; @@ -46,7 +45,6 @@ void jorg_search (edict_t *self) else gi.sound (self, CHAN_VOICE, sound_search3, 1, ATTN_NORM, 0); } -*/ void jorgBFG (edict_t *self); @@ -224,6 +222,100 @@ void jorg_run (edict_t *self) self->monsterinfo.currentmove = &jorg_move_run; } +mframe_t jorg_frames_pain3[] = +{ + ai_move, -28, NULL, + ai_move, -6, NULL, + ai_move, -3, jorg_step_left, + ai_move, -9, NULL, + ai_move, 0, jorg_step_right, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, -7, NULL, + ai_move, 1, NULL, + ai_move, -11, NULL, + ai_move, -4, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 10, NULL, + ai_move, 11, NULL, + ai_move, 0, NULL, + ai_move, 10, NULL, + ai_move, 3, NULL, + ai_move, 10, NULL, + ai_move, 7, jorg_step_left, + ai_move, 17, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, jorg_step_right +}; +mmove_t jorg_move_pain3 = { FRAME_pain301, FRAME_pain325, jorg_frames_pain3, jorg_run }; + +mframe_t jorg_frames_pain2[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t jorg_move_pain2 = { FRAME_pain201, FRAME_pain203, jorg_frames_pain2, jorg_run }; + +mframe_t jorg_frames_pain1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t jorg_move_pain1 = { FRAME_pain101, FRAME_pain103, jorg_frames_pain1, jorg_run }; + +void jorg_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + qboolean do_pain3 = false; + + (void)other; + (void)kick; + + if (level.time < self->pain_debounce_time) + return; + + if (damage <= 40 && random() <= 0.6f) + return; + + if (self->s.frame >= FRAME_attak101 && self->s.frame <= FRAME_attak108 && random() <= 0.005f) + return; + + if (self->s.frame >= FRAME_attak109 && self->s.frame <= FRAME_attak114 && random() <= 0.00005f) + return; + + if (self->s.frame >= FRAME_attak201 && self->s.frame <= FRAME_attak208 && random() <= 0.005f) + return; + + self->pain_debounce_time = level.time + 3.0f; + + if (damage > 50) + { + if (damage <= 100) + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + else if (random() <= 0.3f) + { + do_pain3 = true; + gi.sound(self, CHAN_VOICE, sound_pain3, 1, ATTN_NORM, 0); + } + } + + if (invasion->value == 2) + return; + + self->s.sound = 0; + + if (damage <= 50) + self->monsterinfo.currentmove = &jorg_move_pain1; + else if (damage <= 100) + self->monsterinfo.currentmove = &jorg_move_pain2; + else if (do_pain3) + self->monsterinfo.currentmove = &jorg_move_pain3; +} + void jorgBFG_refire (edict_t *self) { if (G_ValidTarget(self, self->enemy, true, true) && entdist(self, self->enemy) <= 768 && random() <= 0.8) @@ -452,6 +544,7 @@ void jorg_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, gi.sound (self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_NO; + vrx_update_drone_death_skin(self); self->s.sound = 0; self->count = 0; self->monsterinfo.currentmove = &jorg_move_death; @@ -593,6 +686,7 @@ void init_drone_jorg (edict_t *self) self->monsterinfo.jumpup = 64; self->monsterinfo.jumpdn = 512; + // vortex bosses have pain animations disabled. //self->pain = jorg_pain; self->die = jorg_die; self->monsterinfo.stand = jorg_stand; @@ -601,7 +695,7 @@ void init_drone_jorg (edict_t *self) self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; //self->monsterinfo.dodge = NULL; self->monsterinfo.attack = jorg_attack; - //self->monsterinfo.search = jorg_search; + self->monsterinfo.idle = jorg_search; //self->monsterinfo.melee = NULL; //self->monsterinfo.sight = NULL; //self->monsterinfo.checkattack = Jorg_CheckAttack; @@ -613,5 +707,6 @@ void init_drone_jorg (edict_t *self) self->nextthink = level.time + 0.1; //walkmonster_start(self); - G_PrintGreenText(va("A level %d jorg has spawned!", self->monsterinfo.level)); + if (!invasion->value) + G_PrintGreenText(va("A level %d jorg has spawned!", self->monsterinfo.level)); } diff --git a/src/entities/drone/drone_makron.c b/src/entities/drone/drone_makron.c index b6cd2503..aee687d9 100644 --- a/src/entities/drone/drone_makron.c +++ b/src/entities/drone/drone_makron.c @@ -9,6 +9,8 @@ Makron -- Final Boss #include "g_local.h" #include "../../quake2/monsterframes/m_boss32.h" +#define MAKRON_INVASION_MOVE_SCALE 0.75f + qboolean visible (const edict_t *self, const edict_t *other); void MakronRailgun (edict_t *self); @@ -34,6 +36,16 @@ static int sound_taunt2; static int sound_taunt3; static int sound_hit; +static void makron_ai_run(edict_t *self, float dist) +{ + drone_ai_run(self, invasion->value ? dist * MAKRON_INVASION_MOVE_SCALE : dist); +} + +static void makron_ai_walk(edict_t *self, float dist) +{ + drone_ai_walk(self, invasion->value ? dist * MAKRON_INVASION_MOVE_SCALE : dist); +} + void makron_taunt (edict_t *self) { float r; @@ -123,16 +135,16 @@ void makron_stand (edict_t *self) mframe_t makron_frames_run [] = { - drone_ai_run, 20, makron_step_left, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, makron_step_right, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL, - drone_ai_run, 20, NULL + makron_ai_run, 20, makron_step_left, + makron_ai_run, 20, NULL, + makron_ai_run, 20, NULL, + makron_ai_run, 20, NULL, + makron_ai_run, 20, makron_step_right, + makron_ai_run, 20, NULL, + makron_ai_run, 20, NULL, + makron_ai_run, 20, NULL, + makron_ai_run, 20, NULL, + makron_ai_run, 20, NULL }; mmove_t makron_move_run = {FRAME_walk204, FRAME_walk213, makron_frames_run, NULL}; @@ -169,16 +181,16 @@ void makron_prerailgun (edict_t *self) mframe_t makron_frames_walk [] = { - drone_ai_walk, 3, makron_step_left, - drone_ai_walk, 12, NULL, - drone_ai_walk, 8, NULL, - drone_ai_walk, 8, NULL, - drone_ai_walk, 8, makron_step_right, - drone_ai_walk, 6, NULL, - drone_ai_walk, 12, NULL, - drone_ai_walk, 9, NULL, - drone_ai_walk, 6, NULL, - drone_ai_walk, 12, NULL + makron_ai_walk, 3, makron_step_left, + makron_ai_walk, 12, NULL, + makron_ai_walk, 8, NULL, + makron_ai_walk, 8, NULL, + makron_ai_walk, 8, makron_step_right, + makron_ai_walk, 6, NULL, + makron_ai_walk, 12, NULL, + makron_ai_walk, 9, NULL, + makron_ai_walk, 6, NULL, + makron_ai_walk, 12, NULL }; mmove_t makron_move_walk = {FRAME_walk204, FRAME_walk213, makron_frames_walk, NULL}; @@ -341,6 +353,103 @@ mframe_t makron_frames_sight [] = }; mmove_t makron_move_sight= {FRAME_active01, FRAME_active13, makron_frames_sight, makron_run}; +mframe_t makron_frames_pain6[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, makron_popup, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, makron_taunt, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t makron_move_pain6 = { FRAME_pain601, FRAME_pain627, makron_frames_pain6, makron_run }; + +mframe_t makron_frames_pain5[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t makron_move_pain5 = { FRAME_pain501, FRAME_pain504, makron_frames_pain5, makron_run }; + +mframe_t makron_frames_pain4[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t makron_move_pain4 = { FRAME_pain401, FRAME_pain404, makron_frames_pain4, makron_run }; + +void makron_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + qboolean do_pain6 = false; + + (void)other; + (void)kick; + + if (self->monsterinfo.currentmove == &makron_move_sight) + return; + + if (level.time < self->pain_debounce_time) + return; + + if (damage <= 25 && random() < 0.2f) + return; + + self->pain_debounce_time = level.time + 3.0f; + + if (damage <= 40) + gi.sound(self, CHAN_VOICE, sound_pain4, 1, ATTN_NORM, 0); + else if (damage <= 110) + gi.sound(self, CHAN_VOICE, sound_pain5, 1, ATTN_NORM, 0); + else if (damage <= 150) + { + if (random() <= 0.45f) + { + do_pain6 = true; + gi.sound(self, CHAN_VOICE, sound_pain6, 1, ATTN_NORM, 0); + } + } + else if (random() <= 0.35f) + { + do_pain6 = true; + gi.sound(self, CHAN_VOICE, sound_pain6, 1, ATTN_NORM, 0); + } + + if (invasion->value == 2) + return; + + if (damage <= 40) + self->monsterinfo.currentmove = &makron_move_pain4; + else if (damage <= 110) + self->monsterinfo.currentmove = &makron_move_pain5; + else if (do_pain6) + self->monsterinfo.currentmove = &makron_move_pain6; +} + void makronBFG (edict_t *self) { vec3_t forward, start; @@ -535,7 +644,7 @@ void makron_torso (edict_t *ent) void makron_dead (edict_t *self) { VectorSet (self->mins, -60, -60, 0); - VectorSet (self->maxs, 60, 60, 72); + VectorSet (self->maxs, 60, 60, 24); self->movetype = MOVETYPE_TOSS; self->svflags |= SVF_DEADMONSTER; self->nextthink = 0; @@ -547,21 +656,12 @@ void makron_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damag { edict_t *tempent; - int n; - self->s.sound = 0; // check for gib if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 1 /*4*/; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_metal/tris.md2", damage, GIB_METALLIC); - ThrowHead(self, "models/objects/gibs/gear/tris.md2", damage, GIB_METALLIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; M_Remove(self, false, false); return; @@ -574,12 +674,17 @@ void makron_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damag gi.sound (self, CHAN_VOICE, sound_death, 1, ATTN_NONE, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + self->svflags |= SVF_DEADMONSTER; + vrx_update_drone_death_skin(self); tempent = G_Spawn(); VectorCopy (self->s.origin, tempent->s.origin); VectorCopy (self->s.angles, tempent->s.angles); tempent->s.origin[1] -= 84; makron_torso (tempent); + VectorSet (self->mins, -60, -60, 0); + VectorSet (self->maxs, 60, 60, 48); + gi.linkentity(self); self->monsterinfo.currentmove = &makron_move_death2; DroneList_Remove(self); @@ -638,6 +743,7 @@ void init_drone_makron (edict_t *self) self->monsterinfo.jumpup = 64; self->monsterinfo.jumpdn = 512; + // vortex bosses have pain animations disabled. //self->pain = makron_pain; self->die = makron_die; self->monsterinfo.stand = makron_stand; diff --git a/src/entities/drone/drone_medic.c b/src/entities/drone/drone_medic.c index 0c14c992..1f93bc7d 100644 --- a/src/entities/drone/drone_medic.c +++ b/src/entities/drone/drone_medic.c @@ -19,14 +19,123 @@ static int sound_hook_launch; static int sound_hook_hit; static int sound_hook_heal; static int sound_hook_retract; +static int commander_sound_idle1; +static int commander_sound_pain1; +static int commander_sound_pain2; +static int commander_sound_die; +static int commander_sound_sight; +static int commander_sound_search; +static int commander_sound_hook_launch; +static int commander_sound_hook_hit; +static int commander_sound_hook_heal; +static int commander_sound_hook_retract; +static int commander_sound_spawn; + +void drone_ai_run_slide(edict_t *self, float dist); +qboolean drone_findtarget(edict_t *self, qboolean force); +void mymedic_run(edict_t *self); +extern mmove_t mymedic_move_attackHyperBlaster; +extern mmove_t mymedic_move_attackBlaster; +extern mmove_t mymedic_move_attackCable; +extern mmove_t medic_commander_move_callReinforcements; +static qboolean mymedic_is_dodge_move(edict_t *self); + +#define MEDIC_COMMANDER_SUMMON_COUNT 2 +#define MEDIC_COMMANDER_SUMMON_COOLDOWN 8.0f +#define SPAWNGROW_LIFESPAN 1.0f void mymedic_refire (edict_t *self); void mymedic_heal (edict_t *self); +void medic_commander_attack(edict_t *self); + +static qboolean medic_is_commander(edict_t *self) +{ + return self && self->mtype == M_MEDIC_COMMANDER; +} + +static int medic_blaster_flash(edict_t *self) +{ + return medic_is_commander(self) ? MZ2_MEDIC_BLASTER_2 : MZ2_MEDIC_BLASTER_1; +} + +static int medic_hyperblaster_flash(edict_t *self) +{ + return medic_is_commander(self) ? MZ2_MEDIC_HYPERBLASTER2_1 : MZ2_MEDIC_HYPERBLASTER1_1; +} + +static qboolean medic_can_blaster(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, medic_blaster_flash(self)); +} + +static qboolean medic_can_hyperblaster(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, medic_hyperblaster_flash(self)); +} + +static int medic_idle_sound(edict_t *self) +{ + return (medic_is_commander(self) && commander_sound_idle1) ? commander_sound_idle1 : sound_idle1; +} + +static int medic_pain_sound1(edict_t *self) +{ + return (medic_is_commander(self) && commander_sound_pain1) ? commander_sound_pain1 : sound_pain1; +} + +static int medic_pain_sound2(edict_t *self) +{ + return (medic_is_commander(self) && commander_sound_pain2) ? commander_sound_pain2 : sound_pain2; +} + +static int medic_die_sound(edict_t *self) +{ + return (medic_is_commander(self) && commander_sound_die) ? commander_sound_die : sound_die; +} + +static int medic_sight_sound(edict_t *self) +{ + return (medic_is_commander(self) && commander_sound_sight) ? commander_sound_sight : sound_sight; +} + +static int medic_search_sound(edict_t *self) +{ + return (medic_is_commander(self) && commander_sound_search) ? commander_sound_search : sound_search; +} + +static int medic_hook_launch_sound(edict_t *self) +{ + return (medic_is_commander(self) && commander_sound_hook_launch) ? commander_sound_hook_launch : sound_hook_launch; +} + +static int medic_hook_retract_sound(edict_t *self) +{ + return (medic_is_commander(self) && commander_sound_hook_retract) ? commander_sound_hook_retract : sound_hook_retract; +} + +static float medic_clampf(float value, float min_value, float max_value) +{ + if (value < min_value) + return min_value; + if (value > max_value) + return max_value; + return value; +} + +static float medic_lerpf(float a, float b, float t) +{ + return a + (b - a) * t; +} void mymedic_idle (edict_t *self) { - gi.sound (self, CHAN_VOICE, sound_idle1, 1, ATTN_IDLE, 0); + gi.sound (self, CHAN_VOICE, medic_idle_sound(self), 1, ATTN_IDLE, 0); + +} +void mymedic_search (edict_t *self) +{ + gi.sound (self, CHAN_VOICE, medic_search_sound(self), 1, ATTN_IDLE, 0); } mframe_t mymedic_frames_stand [] = @@ -166,6 +275,27 @@ mframe_t mymedic_frames_run [] = }; mmove_t mymedic_move_run = {FRAME_run1, FRAME_run6, mymedic_frames_run, NULL}; +static void mymedic_ai_dodge_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(self->monsterinfo.attacker)) + self->enemy = self->monsterinfo.attacker; + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_run_slide(self, dist); +} + +mframe_t mymedic_frames_dodge_slide[] = +{ + mymedic_ai_dodge_slide, 18, NULL, + mymedic_ai_dodge_slide, 22.5, NULL, + mymedic_ai_dodge_slide, 25.4, NULL, + mymedic_ai_dodge_slide, 23.4, NULL, + mymedic_ai_dodge_slide, 24, NULL, + mymedic_ai_dodge_slide, 35.6, NULL +}; +mmove_t mymedic_move_dodge_slide = {FRAME_run1, FRAME_run6, mymedic_frames_dodge_slide, mymedic_run}; + void mymedic_run (edict_t *self) { if (self->monsterinfo.aiflags & AI_STAND_GROUND) @@ -176,40 +306,73 @@ void mymedic_run (edict_t *self) void mymedic_fire_blaster (edict_t *self) { - int effect, damage; - const float speed = 2000; // speed: medic_blaster + int effect, damage, flash_number; + const int speed = 2000; // speed: medic_blaster vec3_t forward, start; qboolean bounce = false; + + if (!G_EntExists(self->enemy)) + return; if ((self->s.frame == FRAME_attack9) || (self->s.frame == FRAME_attack12)) { effect = EF_BLASTER; bounce = true; + flash_number = medic_blaster_flash(self); } else - effect = EF_HYPERBLASTER; + { + int frame_offset = self->s.frame - FRAME_attack19; + + if (frame_offset < 0) + frame_offset = 0; + else if (frame_offset > 11) + frame_offset = 11; + + effect = (self->s.frame % 4) ? 0 : EF_HYPERBLASTER; + flash_number = medic_hyperblaster_flash(self) + frame_offset; + } damage = M_HYPERBLASTER_DMG_BASE + M_HYPERBLASTER_DMG_ADDON * drone_damagelevel(self); if (M_HYPERBLASTER_DMG_MAX && damage > M_HYPERBLASTER_DMG_MAX) damage = M_HYPERBLASTER_DMG_MAX; - MonsterAim(self, M_PROJECTILE_ACC, speed, false, MZ2_MEDIC_BLASTER_1, forward, start); - monster_fire_blaster(self, start, forward, damage, speed, effect, BLASTER_PROJ_BOLT, 2.0, bounce, MZ2_MEDIC_BLASTER_1); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + if (medic_is_commander(self)) + monster_fire_blaster2(self, start, forward, damage, speed, effect, flash_number); + else + monster_fire_blaster(self, start, forward, damage, speed, effect, BLASTER_PROJ_BOLT, 2.0, bounce, flash_number); } void mymedic_fire_bolt (edict_t *self) { - int min, max, damage; + int min, max, damage, flash_number; vec3_t forward, start; min = 4 * drone_damagelevel(self); // dmg.min: medic_fire_bolt max = 50 + 25 * drone_damagelevel(self); // dmg.max: medic_fire_bolt damage = GetRandom(min, max); + flash_number = medic_blaster_flash(self); + + MonsterAim(self, M_PROJECTILE_ACC, 1500, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + if (medic_is_commander(self)) + { + monster_fire_blaster2(self, start, forward, damage, 1500, EF_BLASTER, -1); + gi.sound (self, CHAN_WEAPON, gi.soundindex("weapons/photon.wav"), 1, ATTN_NORM, 0); + return; + } - MonsterAim(self, M_PROJECTILE_ACC, 1500, false, MZ2_MEDIC_BLASTER_1, forward, start); - monster_fire_blaster(self, start, forward, damage, 1500, EF_BLASTER, BLASTER_PROJ_BLAST, 2.0, true, MZ2_MEDIC_BLASTER_1); + // Keep the medic muzzle origin, but don't emit the muzzleflash packet here; + // it can override the secondary blaster sound that should play per bolt. + monster_fire_blaster(self, start, forward, damage, 1500, EF_BLASTER, BLASTER_PROJ_BLAST, 2.0, true, -1); gi.sound (self, CHAN_WEAPON, gi.soundindex("weapons/photon.wav"), 1, ATTN_NORM, 0); } @@ -226,6 +389,13 @@ void mymedic_dead (edict_t *self) M_PrepBodyRemoval(self); } +static void mymedic_shrink(edict_t *self) +{ + self->maxs[2] = -2; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t medic_frames_pain_short[] = { ai_move, 0, NULL, @@ -262,11 +432,12 @@ mmove_t medic_move_pain_long = { FRAME_painb1, FRAME_painb15, medic_frames_pain_ void medic_pain(edict_t* self, edict_t* other, float kick, int damage) { if (self->health < (self->max_health / 2)) - self->s.skinnum = 1; + self->s.skinnum |= 1; // we're already in a pain state if (self->monsterinfo.currentmove == &medic_move_pain_short || - self->monsterinfo.currentmove == &medic_move_pain_long) + self->monsterinfo.currentmove == &medic_move_pain_long || + mymedic_is_dodge_move(self)) return; // monster players don't get pain state induced @@ -284,9 +455,9 @@ void medic_pain(edict_t* self, edict_t* other, float kick, int damage) return; if (random() < 0.5) - gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + gi.sound(self, CHAN_VOICE, medic_pain_sound1(self), 1, ATTN_NORM, 0); else { - gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + gi.sound(self, CHAN_VOICE, medic_pain_sound2(self), 1, ATTN_NORM, 0); } if (self->monsterinfo.currentmove == &medic_move_walk || @@ -302,7 +473,7 @@ mframe_t mymedic_frames_death [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, mymedic_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -333,8 +504,6 @@ mmove_t mymedic_move_death = {FRAME_death1, FRAME_death30, mymedic_frames_death, void mymedic_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; - M_Notify(self); #ifdef OLD_NOLAG_STYLE @@ -350,14 +519,7 @@ void mymedic_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); @@ -376,9 +538,10 @@ void mymedic_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int dama DroneList_Remove(self); // regular death - gi.sound (self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); + gi.sound (self, CHAN_VOICE, medic_die_sound(self), 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); self->monsterinfo.currentmove = &mymedic_move_death; @@ -411,12 +574,18 @@ void mymedic_duck_up (edict_t *self) gi.linkentity (self); } +void mymedic_duck_hold (edict_t *self) +{ + if (self->monsterinfo.pausetime > level.time) + self->monsterinfo.nextframe = self->s.frame; +} + void mymedic_jump_takeoff (edict_t *self) { vec3_t v; - gi.sound (self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + gi.sound (self, CHAN_VOICE, medic_sight_sound(self), 1, ATTN_NORM, 0); VectorSubtract(self->monsterinfo.dir, self->s.origin, v); v[2] = 0; VectorNormalize(v); @@ -449,28 +618,29 @@ void mymedic_jump_hold (edict_t *self) } } +static void mymedic_jump_hold_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + mymedic_jump_hold(self); +} + mframe_t mymedic_frames_duck [] = { - ai_move, 0, mymedic_duck_down, - ai_move, 0, NULL, - ai_move, 0, NULL,//mymedic_duck_down, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, mymedic_duck_up - /* - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL - */ + ai_move, -1, NULL, + ai_move, -1, mymedic_duck_down, + ai_move, -1, mymedic_duck_hold, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, mymedic_duck_up }; -mmove_t mymedic_move_duck = {FRAME_duck1, FRAME_duck7, mymedic_frames_duck, mymedic_run}; +mmove_t mymedic_move_duck = {FRAME_duck2, FRAME_duck14, mymedic_frames_duck, mymedic_run}; mframe_t mymedic_frames_leap [] = { @@ -480,7 +650,7 @@ mframe_t mymedic_frames_leap [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, mymedic_jump_hold, //137 + mymedic_jump_hold_ai, 0, NULL, //137 /* ai_move, 0, NULL, ai_move, 0, NULL, @@ -501,22 +671,63 @@ void mymedic_leap (edict_t *self) self->monsterinfo.currentmove = &mymedic_move_leap; } +static qboolean mymedic_is_dodge_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &mymedic_move_duck || + self->monsterinfo.currentmove == &mymedic_move_leap || + self->monsterinfo.currentmove == &mymedic_move_dodge_slide; +} + +static qboolean mymedic_is_uninterruptible_attack(edict_t *self) +{ + return self->monsterinfo.currentmove == &mymedic_move_attackHyperBlaster || + self->monsterinfo.currentmove == &mymedic_move_attackCable || + self->monsterinfo.currentmove == &mymedic_move_attackBlaster || + self->monsterinfo.currentmove == &medic_commander_move_callReinforcements; +} + +static qboolean mymedic_dodge_hit_low(edict_t *self, vec3_t dir) +{ + const float duck_height = self->absmax[2] - 33; + + return dir[2] > self->absmin[2] && dir[2] <= duck_height; +} + void mymedic_dodge (edict_t *self, edict_t *attacker, vec3_t dir, int radius) { if (random() > 0.9) return; - if (!G_GetClient(self)) - return; if (level.time < self->monsterinfo.dodge_time) return; + if (!attacker) + return; if (OnSameTeam(self, attacker)) return; + if (mymedic_is_dodge_move(self) || mymedic_is_uninterruptible_attack(self)) + return; - if (!self->enemy && G_EntIsAlive(attacker)) + self->monsterinfo.attacker = attacker; + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) self->enemy = attacker; if (!radius) { - self->monsterinfo.currentmove = &mymedic_move_duck; + if (!mymedic_dodge_hit_low(self, dir)) + { + self->monsterinfo.pausetime = level.time + 0.5; + self->monsterinfo.currentmove = &mymedic_move_duck; + mymedic_duck_down(self); + } + else if (!(self->monsterinfo.aiflags & AI_STAND_GROUND)) + { + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + self->monsterinfo.currentmove = &mymedic_move_dodge_slide; + } + else + { + self->monsterinfo.pausetime = level.time + 0.5; + self->monsterinfo.currentmove = &mymedic_move_duck; + mymedic_duck_down(self); + } self->monsterinfo.dodge_time = level.time + 2.0; } else @@ -538,24 +749,28 @@ void medic_checktarget (edict_t *self) mframe_t mymedic_frames_attackHyperBlaster [] = { - ai_charge, 0, mymedic_fire_blaster, //191 - ai_charge, 0, medic_checktarget, + ai_charge, 0, NULL, // attack15 + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, mymedic_fire_blaster, // attack19 ai_charge, 0, mymedic_fire_blaster, - ai_charge, 0, medic_checktarget, - ai_charge, 0, mymedic_fire_blaster, //195 - ai_charge, 0, medic_checktarget, ai_charge, 0, mymedic_fire_blaster, - ai_charge, 0, medic_checktarget, ai_charge, 0, mymedic_fire_blaster, - ai_charge, 0, medic_checktarget, ai_charge, 0, mymedic_fire_blaster, - ai_charge, 0, medic_checktarget, ai_charge, 0, mymedic_fire_blaster, - ai_charge, 0, medic_checktarget, ai_charge, 0, mymedic_fire_blaster, - ai_charge, 0, medic_checktarget //206 + ai_charge, 0, mymedic_fire_blaster, + ai_charge, 0, mymedic_fire_blaster, + ai_charge, 0, mymedic_fire_blaster, + ai_charge, 0, mymedic_fire_blaster, + ai_charge, 0, mymedic_fire_blaster, // attack30 + ai_charge, 0, medic_checktarget, + ai_charge, 0, NULL, + ai_charge, 2, NULL, + ai_charge, 3, NULL }; -mmove_t mymedic_move_attackHyperBlaster = {FRAME_attack15, FRAME_attack30, mymedic_frames_attackHyperBlaster, mymedic_refire}; +mmove_t mymedic_move_attackHyperBlaster = {FRAME_attack15, FRAME_attack34, mymedic_frames_attackHyperBlaster, mymedic_refire}; void mymedic_refire(edict_t* self) { @@ -565,7 +780,7 @@ void mymedic_refire(edict_t* self) { dist = entdist(self, self->enemy); - if (random() <= 0.8 && dist <= 512) + if (random() <= 0.8 && dist <= 512 && medic_can_hyperblaster(self)) { // continue attack self->s.frame = FRAME_attack19; @@ -587,7 +802,7 @@ void mymedic_refire(edict_t* self) void mymedic_continue(edict_t* self) { - if (M_ContinueAttack(self, &mymedic_move_attackHyperBlaster, NULL, 0, 512, 0.8)) + if (medic_can_hyperblaster(self) && M_ContinueAttack(self, &mymedic_move_attackHyperBlaster, NULL, 0, 512, 0.8)) return; // end attack @@ -615,12 +830,12 @@ mmove_t mymedic_move_attackBlaster = {FRAME_attack1, FRAME_attack14, mymedic_fra void mymedic_hook_launch (edict_t *self) { - gi.sound (self, CHAN_WEAPON, sound_hook_launch, 1, ATTN_NORM, 0); + gi.sound (self, CHAN_WEAPON, medic_hook_launch_sound(self), 1, ATTN_NORM, 0); } void mymedic_hook_retract (edict_t *self) { - gi.sound (self, CHAN_WEAPON, sound_hook_retract, 1, ATTN_NORM, 0); + gi.sound (self, CHAN_WEAPON, medic_hook_retract_sound(self), 1, ATTN_NORM, 0); if (!self->enemy) return; @@ -700,7 +915,7 @@ void M_Reanimate (edict_t *ent, edict_t *target, int r_level, float r_modifier, } else if ((!strcmp(target->classname, "bodyque") || !strcmp(target->classname, "player"))) { - const int random=GetRandom(1, 3); + const int random=GetRandom(1, 6); vec3_t start; // if the summoner is a player, check for sufficient monster slots @@ -712,27 +927,43 @@ void M_Reanimate (edict_t *ent, edict_t *target, int r_level, float r_modifier, VectorCopy(target->s.origin, start); // kill the corpse - T_Damage(target, target, target, vec3_origin, target->s.origin, + T_Damage(target, target, target, vec3_origin, target->s.origin, vec3_origin, 10000, 0, DAMAGE_NO_PROTECTION, 0); - //4.2 random soldier type with different weapons - if (random == 1) + // random soldier type with different weapons + switch (random) { + case 1: // blaster e->mtype = M_SOLDIER; e->s.skinnum = 0; - } - else if (random == 2) - { + break; + case 2: // rocket e->mtype = M_SOLDIERLT; e->s.skinnum = 4; - } - else - { + break; + case 3: // shotgun e->mtype = M_SOLDIERSS; e->s.skinnum = 2; + break; + case 4: + // ripper + e->mtype = M_SOLDIER_RIPPER; + e->s.skinnum = 6; + break; + case 5: + // blue blaster + e->mtype = M_SOLDIER_BLUEBLASTER; + e->s.skinnum = 8; + break; + case 6: + default: + // laser + e->mtype = M_SOLDIER_LASER; + e->s.skinnum = 10; + break; } e->activator = ent; @@ -744,7 +975,7 @@ void M_Reanimate (edict_t *ent, edict_t *target, int r_level, float r_modifier, e->s.skinnum |= 1; // injured skin e->monsterinfo.stand(e); - + if (!G_IsValidLocation(target, start, e->mins, e->maxs)) { start[2] += 24; @@ -998,6 +1229,327 @@ mframe_t mymedic_frames_attackCable [] = }; mmove_t mymedic_move_attackCable = {FRAME_attack33, FRAME_attack60, mymedic_frames_attackCable, mymedic_heal}; +static void spawngrow_beam_think(edict_t *self); + +static void spawngrow_beam_pos(edict_t *self, vec3_t pos) +{ + float theta; + float phi; + float radius; + vec3_t dir; + + if (!self->owner || !self->owner->inuse) + { + VectorCopy(self->s.origin, pos); + return; + } + + theta = random() * 2.0f * (float)M_PI; + phi = acosf(crandom()); + dir[0] = sinf(phi) * cosf(theta); + dir[1] = sinf(phi) * sinf(theta); + dir[2] = cosf(phi); + + radius = self->owner->s.scale; + if (radius <= 0.0f) + radius = 1.0f; + radius *= 9.0f; + + VectorMA(self->owner->s.origin, radius, dir, pos); +} + +static void spawngrow_think(edict_t *self) +{ + float t; + int i; + + if (level.time >= self->timestamp) + { + if (self->target_ent && self->target_ent->inuse) + G_FreeEdict(self->target_ent); + G_FreeEdict(self); + return; + } + + for (i = 0; i < 3; i++) + self->s.angles[i] += self->avelocity[i] * FRAMETIME; + + t = 1.0f - ((level.time - self->teleport_time) / self->wait); + t = medic_clampf(t, 0.0f, 1.0f); + self->s.scale = medic_clampf(medic_lerpf(self->decel, self->accel, t) / 16.0f, 0.001f, 16.0f); + self->s.alpha = t * t; + + self->nextthink = level.time + FRAMETIME; +} + +static void spawngrow_beam_think(edict_t *self) +{ + if (!self->owner || !self->owner->inuse) + { + G_FreeEdict(self); + return; + } + + spawngrow_beam_pos(self, self->s.old_origin); + gi.linkentity(self); + self->nextthink = level.time + FRAMETIME; +} + +void SpawnGrow_Spawn(vec3_t startpos, float start_size, float end_size) +{ + edict_t *ent; + edict_t *beam; + + ent = G_Spawn(); + VectorCopy(startpos, ent->s.origin); + VectorSet(ent->s.angles, GetRandom(0, 359), GetRandom(0, 359), GetRandom(0, 359)); + VectorSet(ent->avelocity, + (280.0f + random() * 80.0f) * 2.0f, + (280.0f + random() * 80.0f) * 2.0f, + (280.0f + random() * 80.0f) * 2.0f); + + ent->solid = SOLID_NOT; + ent->movetype = MOVETYPE_NONE; + ent->classname = "spawngro"; + ent->s.modelindex = gi.modelindex("models/items/spawngro3/tris.md2"); + ent->s.skinnum = 1; + ent->s.renderfx |= RF_IR_VISIBLE | RF_TRANSLUCENT; + ent->accel = start_size; + ent->decel = end_size; + ent->think = spawngrow_think; + ent->s.scale = medic_clampf(start_size / 16.0f, 0.001f, 8.0f); + ent->s.alpha = 1.0f; + ent->teleport_time = level.time; + ent->wait = SPAWNGROW_LIFESPAN; + ent->timestamp = level.time + SPAWNGROW_LIFESPAN; + ent->nextthink = level.time + FRAMETIME; + gi.linkentity(ent); + + beam = ent->target_ent = G_Spawn(); + beam->solid = SOLID_NOT; + beam->movetype = MOVETYPE_NONE; + beam->s.modelindex = 1; + beam->s.renderfx = RF_BEAM_LIGHTNING | RF_TRANSLUCENT; + beam->s.frame = 1; + beam->s.skinnum = 0x30303030; + beam->classname = "spawngro_beam"; + beam->owner = ent; + VectorCopy(ent->s.origin, beam->s.origin); + spawngrow_beam_pos(beam, beam->s.old_origin); + beam->think = spawngrow_beam_think; + beam->nextthink = level.time + FRAMETIME; + gi.linkentity(beam); +} + +static void medic_commander_start_spawn(edict_t *self) +{ + if (commander_sound_spawn) + gi.sound(self, CHAN_WEAPON, commander_sound_spawn, 1, ATTN_NORM, 0); + + self->monsterinfo.nextframe = FRAME_attack48; +} + +static void medic_commander_cleanup_failed_spawn(edict_t *owner, edict_t *spawned) +{ + if (owner && owner->client) + layout_remove_tracked_entity(&owner->client->layout, spawned); + + DroneList_Remove(spawned); + AI_EnemyRemoved(spawned); + G_FreeEdict(spawned); +} + +static void medic_commander_setup_invasion_spawn(edict_t *spawned) +{ + if (!invasion->value) + return; + + spawned->monsterinfo.aiflags &= ~AI_STAND_GROUND; + spawned->monsterinfo.aiflags |= AI_FIND_NAVI; + spawned->prev_navi = NULL; + spawned->goalentity = NULL; +} + +static qboolean medic_commander_valid_spawn_spot(edict_t *self, vec3_t mins, vec3_t maxs, vec3_t spot) +{ + if (G_IsValidLocation(self, spot, mins, maxs)) + return true; + + spot[2] += 24; + if (G_IsValidLocation(self, spot, mins, maxs)) + return true; + + spot[2] -= 48; + return G_IsValidLocation(self, spot, mins, maxs); +} + +static qboolean medic_commander_find_spawn_spot(edict_t *self, vec3_t mins, vec3_t maxs, float side, vec3_t spot) +{ + vec3_t forward, right; + + AngleVectors(self->s.angles, forward, right, NULL); + + VectorCopy(self->s.origin, spot); + VectorMA(spot, 96, forward, spot); + VectorMA(spot, side, right, spot); + spot[2] += 8; + if (medic_commander_valid_spawn_spot(self, mins, maxs, spot)) + return true; + + VectorCopy(self->s.origin, spot); + VectorMA(spot, -72, forward, spot); + VectorMA(spot, side, right, spot); + spot[2] += 8; + if (medic_commander_valid_spawn_spot(self, mins, maxs, spot)) + return true; + + VectorCopy(self->s.origin, spot); + VectorMA(spot, 64, forward, spot); + VectorMA(spot, -side, right, spot); + spot[2] += 8; + return medic_commander_valid_spawn_spot(self, mins, maxs, spot); +} + +static int medic_commander_random_soldier_type(void) +{ + static const int soldier_types[] = + { + M_SOLDIER, + M_SOLDIERLT, + M_SOLDIERSS, + M_SOLDIER_RIPPER, + M_SOLDIER_BLUEBLASTER, + M_SOLDIER_LASER + }; + + return soldier_types[GetRandom(0, (int)(sizeof(soldier_types) / sizeof(soldier_types[0])) - 1)]; +} + +static qboolean medic_commander_spawn_soldier(edict_t *self, float side) +{ + edict_t *owner = (self->activator && self->activator->inuse) ? self->activator : self; + edict_t *gunner; + vec3_t spot; + const qboolean force_start = invasion->value || pvm->value; + + if (owner->client && owner->num_monsters + M_GUNNER_CONTROL_COST > MAX_MONSTERS) + return false; + + gunner = G_Spawn(); + gunner->mtype = medic_commander_random_soldier_type(); + gunner->activator = owner; + gunner->monsterinfo.level = self->monsterinfo.level; + + if (!M_Initialize(owner, gunner, 0.0f)) + { + G_FreeEdict(gunner); + return false; + } + + if (!medic_commander_find_spawn_spot(self, gunner->mins, gunner->maxs, side, spot)) + { + medic_commander_cleanup_failed_spawn(owner, gunner); + return false; + } + + gunner->monsterinfo.cost = 0; + gunner->s.effects |= EF_PLASMA; + VectorCopy(spot, gunner->s.origin); + VectorCopy(spot, gunner->s.old_origin); + VectorCopy(self->s.angles, gunner->s.angles); + gunner->nextthink = level.time + FRAMETIME; + gunner->monsterinfo.attack_finished = level.time + 1.0; + medic_commander_setup_invasion_spawn(gunner); + + gi.linkentity(gunner); + + owner->num_monsters += gunner->monsterinfo.control_cost; + owner->num_monsters_real++; + + if (G_ValidTarget(gunner, self->enemy, !force_start, true)) + { + gunner->enemy = self->enemy; + VectorCopy(self->enemy->s.origin, gunner->monsterinfo.last_sighting); + if (gunner->monsterinfo.run) + gunner->monsterinfo.run(gunner); + } + else if (force_start && drone_findtarget(gunner, true) && gunner->monsterinfo.run) + gunner->monsterinfo.run(gunner); + else if (gunner->monsterinfo.stand) + gunner->monsterinfo.stand(gunner); + + return true; +} + +static void medic_commander_spawngrows(edict_t *self) +{ + vec3_t mins, maxs, size, spot, effect_origin; + float radius; + int i; + + VectorSet(mins, -16, -16, -24); + VectorSet(maxs, 16, 16, 32); + VectorSubtract(maxs, mins, size); + radius = VectorLength(size) * 0.5f; + + for (i = 0; i < MEDIC_COMMANDER_SUMMON_COUNT; i++) + { + float side = (i & 1) ? 56 : -56; + + if (!medic_commander_find_spawn_spot(self, mins, maxs, side, spot)) + continue; + + VectorAdd(mins, maxs, effect_origin); + VectorAdd(spot, effect_origin, effect_origin); + SpawnGrow_Spawn(effect_origin, radius, radius * 2.0f); + } +} + +static void medic_commander_finish_spawn(edict_t *self) +{ + int spawned = 0; + int i; + + for (i = 0; i < MEDIC_COMMANDER_SUMMON_COUNT; i++) + { + float side = (i & 1) ? 56 : -56; + + if (medic_commander_spawn_soldier(self, side)) + spawned++; + } + + if (spawned) + self->monsterinfo.melee_finished = level.time + MEDIC_COMMANDER_SUMMON_COOLDOWN; +} + +mframe_t medic_commander_frames_callReinforcements[] = +{ + ai_charge, 2, NULL, //209 + ai_charge, 3, NULL, + ai_charge, 5, NULL, + ai_charge, 4, NULL, + ai_charge, 5, NULL, + ai_charge, 5, NULL, + ai_charge, 6, NULL, + ai_charge, 4, NULL, + ai_charge, 0, NULL, //217 + ai_move, 0, medic_commander_start_spawn, //218 + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, //224 + ai_charge, 0, medic_commander_spawngrows, //225 + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, -15, medic_commander_finish_spawn, //228 + ai_move, -1.5, NULL, + ai_move, -1.2, NULL, + ai_move, -3, NULL +}; +mmove_t medic_commander_move_callReinforcements = {FRAME_attack33, FRAME_attack55, medic_commander_frames_callReinforcements, mymedic_run}; + void drone_wakeallies (edict_t *self); // search for nearby enemies, return true if one is found // this is to make the medic stop healing if there is a higher priority target @@ -1042,6 +1594,8 @@ void mymedic_heal (edict_t *self) void mymedic_attack(edict_t *self) { float dist, r; + qboolean can_blaster; + qboolean can_hyperblaster; if (!self->enemy) return; @@ -1050,6 +1604,8 @@ void mymedic_attack(edict_t *self) dist = entdist(self, self->enemy); r = random(); + can_blaster = medic_can_blaster(self); + can_hyperblaster = medic_can_hyperblaster(self); if ((self->monsterinfo.aiflags & AI_MEDIC) && ((self->enemy->health < 1 || OnSameTeam(self, self->enemy)))) @@ -1061,22 +1617,55 @@ void mymedic_attack(edict_t *self) if (dist <= 256) { - if (r <= 0.2) + if (can_hyperblaster && (r <= 0.2 || !can_blaster)) self->monsterinfo.currentmove = &mymedic_move_attackHyperBlaster; - else + else if (can_blaster) self->monsterinfo.currentmove = &mymedic_move_attackBlaster; + else + return; } else { - if (r <= 0.3) + if (can_blaster && (r <= 0.3 || !can_hyperblaster)) self->monsterinfo.currentmove = &mymedic_move_attackBlaster; - else + else if (can_hyperblaster) self->monsterinfo.currentmove = &mymedic_move_attackHyperBlaster; + else + return; } M_DelayNextAttack(self, 0, true); } +void medic_commander_attack(edict_t *self) +{ + edict_t *owner = (self->activator && self->activator->inuse) ? self->activator : self; + float dist; + + if (!self->enemy || !self->enemy->inuse) + return; + + if (!G_ValidTarget(self, self->enemy, false, true)) + { + mymedic_attack(self); + return; + } + + dist = entdist(self, self->enemy); + + if (dist > 150 + && (!owner->client || owner->num_monsters + M_GUNNER_CONTROL_COST <= MAX_MONSTERS) + && level.time >= self->monsterinfo.melee_finished + && random() < 0.6) + { + self->monsterinfo.currentmove = &medic_commander_move_callReinforcements; + M_DelayNextAttack(self, 0, true); + return; + } + + mymedic_attack(self); +} + void mymedic_melee (edict_t *self) { // just here to keep monster from circle strafing @@ -1084,7 +1673,7 @@ void mymedic_melee (edict_t *self) void mymedic_sight (edict_t *self, edict_t *other) { - gi.sound (self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + gi.sound (self, CHAN_VOICE, medic_sight_sound(self), 1, ATTN_NORM, 0); } /*QUAKED monster_medic (1 .5 0) (-16 -16 -24) (16 16 32) Ambush Trigger_Spawn Sight @@ -1139,8 +1728,7 @@ void init_drone_medic (edict_t *self) self->monsterinfo.sight = mymedic_sight; self->monsterinfo.jumpup = 64; self->monsterinfo.jumpdn = 512; -// self->monsterinfo.idle = mymedic_idle; -// self->monsterinfo.search = mymedic_search; + self->monsterinfo.idle = mymedic_search; // self->monsterinfo.checkattack = mymedic_checkattack; // self->monsterinfo.control_cost = 1; // self->monsterinfo.cost = 150; @@ -1164,3 +1752,35 @@ void init_drone_medic (edict_t *self) self->nextthink = level.time + 0.1; //self->activator->num_monsters += self->monsterinfo.control_cost; } + +void init_drone_medic_commander(edict_t *self) +{ + init_drone_medic(self); + + commander_sound_idle1 = gi.soundindex("medic_commander/medidle.wav"); + commander_sound_pain1 = gi.soundindex("medic_commander/medpain1.wav"); + commander_sound_pain2 = gi.soundindex("medic_commander/medpain2.wav"); + commander_sound_die = gi.soundindex("medic_commander/meddeth.wav"); + commander_sound_sight = gi.soundindex("medic_commander/medsght.wav"); + commander_sound_search = gi.soundindex("medic_commander/medsrch.wav"); + commander_sound_hook_launch = gi.soundindex("medic_commander/medatck2c.wav"); + commander_sound_hook_hit = gi.soundindex("medic_commander/medatck3a.wav"); + commander_sound_hook_heal = gi.soundindex("medic_commander/medatck4a.wav"); + commander_sound_hook_retract = gi.soundindex("medic_commander/medatck5a.wav"); + commander_sound_spawn = gi.soundindex("medic_commander/monsterspawn1.wav"); + gi.soundindex("tank/tnkatck3.wav"); + gi.modelindex("models/items/spawngro3/tris.md2"); + + self->mtype = M_MEDIC_COMMANDER; + self->s.skinnum = 2; + self->mass = 600; + self->yaw_speed = 40; + self->health = M_MEDIC_COMMANDER_INITIAL_HEALTH + M_MEDIC_COMMANDER_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->monsterinfo.power_armor_power = M_MEDIC_COMMANDER_INITIAL_ARMOR + M_MEDIC_COMMANDER_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_TANK_CONTROL_COST; + self->monsterinfo.cost = M_TANK_COST; + self->monsterinfo.attack = medic_commander_attack; + self->monsterinfo.melee_finished = level.time + 1.0; +} diff --git a/src/entities/drone/drone_misc.c b/src/entities/drone/drone_misc.c index 4bc13bd0..6d3fb1a8 100644 --- a/src/entities/drone/drone_misc.c +++ b/src/entities/drone/drone_misc.c @@ -14,16 +14,20 @@ qboolean drone_findtarget (edict_t *self, qboolean force); void init_drone_gunner (edict_t *self); void init_drone_parasite (edict_t *self); void init_drone_bitch (edict_t *self); +void init_drone_bitch_heat (edict_t *self); void init_drone_brain (edict_t *self); void init_drone_medic (edict_t *self); +void init_drone_medic_commander(edict_t *self); void init_drone_tank (edict_t *self); void init_drone_mutant (edict_t *self); void init_drone_decoy (edict_t *self); void init_drone_commander (edict_t *self); void init_drone_supertank (edict_t *self); +void init_drone_boss5 (edict_t *self); void init_drone_jorg (edict_t *self); void init_drone_makron (edict_t *self); void init_drone_soldier (edict_t *self); +void init_drone_guardian (edict_t *self); void init_drone_gladiator (edict_t *self); void init_drone_berserk (edict_t *self); void init_drone_infantry (edict_t *self); @@ -31,6 +35,24 @@ void init_drone_flyer (edict_t* self); void init_drone_floater(edict_t* self); void init_drone_hover(edict_t* self); void init_drone_shambler(edict_t* self); +void init_drone_redmutant(edict_t* self); +void init_drone_runnertank(edict_t* self); +void init_drone_guncmdr(edict_t* self); +void init_drone_daedalus(edict_t* self); +void init_drone_gladb(edict_t* self); +void init_drone_gladc(edict_t* self); +void init_drone_stalker(edict_t* self); +void init_drone_gekk(edict_t* self); +void init_drone_arachnid(edict_t* self); +void init_drone_carrier(edict_t* self); +void init_drone_widow(edict_t* self); +void init_drone_widow2(edict_t* self); +void init_drone_fixbot(edict_t* self); +void init_drone_fixbot_boss(edict_t* self); +void init_drone_rogue_turret(edict_t* self); +void init_drone_boss2(edict_t* self); +void init_drone_boss2_hyper(edict_t* self); +void init_drone_boss2_small(edict_t* self); void init_baron_fire(edict_t* self); void init_skeleton(edict_t* self); void init_golem(edict_t* self); @@ -391,15 +413,26 @@ void drone_ai_checkattack (edict_t *self) // if we see an easier target, go for it if (!visible(self, self->enemy)) { - self->oldenemy = self->enemy; - if (!drone_findtarget(self, false)) - return; - //gi.dprintf("%d going for an easier target\n", self->mtype); + if (!(self->monsterinfo.aiflags & AI_ALTERNATE_FLY) || !M_MonsterHasCombatSight(self, self->enemy)) + { + self->oldenemy = self->enemy; + if (!drone_findtarget(self, false)) + return; + //gi.dprintf("%d going for an easier target\n", self->mtype); + } } //if (!infront(self, self->enemy)) if (!nearfov(self, self->enemy, 0, 60)) { + if ((self->monsterinfo.aiflags & AI_ALTERNATE_FLY) && M_MonsterHasCombatSight(self, self->enemy)) + { + vec3_t dir; + VectorSubtract(self->enemy->s.origin, self->s.origin, dir); + self->ideal_yaw = vectoyaw(dir); + M_ChangeYaw(self); + M_ChangeYaw(self); + } //gi.dprintf("target is not in front\n"); return; } @@ -412,10 +445,13 @@ void drone_ai_checkattack (edict_t *self) if (!tr.ent || tr.ent != self->enemy) { //gi.dprintf("blocked shot\n"); - if (G_ValidTarget(self, tr.ent, false, true)) - self->enemy = tr.ent; - else - return; + if (!(self->monsterinfo.aiflags & AI_ALTERNATE_FLY) || !M_MonsterHasCombatSight(self, self->enemy)) + { + if (G_ValidTarget(self, tr.ent, false, true)) + self->enemy = tr.ent; + else + return; + } } //AngleVectors(self->s.angles, forward, NULL, NULL); //VectorMA(self->s.origin, self->maxs[1]+8, forward , start); @@ -518,7 +554,7 @@ void drone_death (edict_t *self, edict_t *attacker) //4.2 bosses can drop up to 4 runes - if (self->mtype == M_COMMANDER || self->mtype == M_SUPERTANK || self->mtype == M_MAKRON) + if (self->mtype == M_COMMANDER || self->mtype == M_SUPERTANK || self->mtype == M_MAKRON || self->mtype == M_CARRIER) { edict_t *e; float drop_chance = 0.25; @@ -622,6 +658,82 @@ void PassThruEntity (edict_t *self, edict_t *other) gi.linkentity(other); } +#define DRONE_FLYER_SEPARATION_SPEED 650.0f +#define DRONE_ALT_FLY_SEPARATION_SPEED 420.0f +#define DRONE_FLYER_SEPARATION_DELAY 0.75f + +static qboolean drone_alt_fly_can_separate(edict_t *ent) +{ + if (!ent || !G_EntIsAlive(ent)) + return false; + if (ent->mtype != M_FLYER) + return false; + if (!(ent->flags & FL_FLY)) + return false; + if (!(ent->monsterinfo.aiflags & AI_ALTERNATE_FLY)) + return false; + return true; +} + +static qboolean drone_alt_fly_touch_obstacle(edict_t *ent) +{ + if (!ent || !G_EntIsAlive(ent)) + return false; + if (!(ent->svflags & SVF_MONSTER) || ent->solid == SOLID_NOT) + return false; + return true; +} + +static qboolean drone_alt_fly_apply_separation(edict_t *ent, vec3_t dir) +{ + float speed; + + if (ent->monsterinfo.fly_separation_time > level.time) + return false; + + ent->monsterinfo.fly_separation_time = level.time + DRONE_FLYER_SEPARATION_DELAY; + ent->monsterinfo.fly_thrusters = false; + ent->monsterinfo.fly_position_time = 0.0f; + ent->monsterinfo.fly_pinned = false; + speed = (ent->mtype == M_FLYER) ? DRONE_FLYER_SEPARATION_SPEED : DRONE_ALT_FLY_SEPARATION_SPEED; + VectorScale(dir, speed, ent->velocity); + return true; +} + +static qboolean drone_alt_fly_separation_touch(edict_t *self, edict_t *other) +{ + vec3_t dir; + qboolean pushed_self; + + if (!self || !other || self == other) + return false; + if (!drone_alt_fly_can_separate(self) || !drone_alt_fly_touch_obstacle(other)) + return false; + + VectorSubtract(self->s.origin, other->s.origin, dir); + if (VectorNormalize(dir) <= 0.1f) + { + VectorSet(dir, crandom(), crandom(), 0.2f); + if (VectorNormalize(dir) <= 0.1f) + VectorSet(dir, 1.0f, 0.0f, 0.0f); + } + + pushed_self = drone_alt_fly_apply_separation(self, dir); + + if (pushed_self) + { + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_SPLASH); + gi.WriteByte(32); + gi.WritePosition(self->s.origin); + gi.WriteDir(dir); + gi.WriteByte(SPLASH_SPARKS); + gi.multicast(self->s.origin, MULTICAST_PVS); + } + + return pushed_self; +} + void drone_touch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf) { vec3_t forward, right, start, offset; @@ -629,6 +741,9 @@ void drone_touch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t *su //gi.dprintf("drone_touch\n"); V_Touch(self, other, plane, surf); + if (drone_alt_fly_separation_touch(self, other)) + return; + // the monster's owner or allies can push him around //if (G_EntIsAlive(other) && self->activator // && ((other == self->activator) || IsAlly(self->activator, other))) @@ -754,19 +869,44 @@ void vrx_roll_to_make_champion(edict_t *drone, enum dronespawn_t *drone_type) } } +qboolean vrx_drone_spawn_is_boss(enum dronespawn_t drone_type) +{ + switch (drone_type) + { + case DS_COMMANDER: + case DS_MAKRON: + case DS_BARON_FIRE: + case DS_SUPERTANK: + case DS_JORG: + case DS_CARRIER: + case DS_GUARDIAN: + case DS_WIDOW: + case DS_WIDOW2: + case DS_FIXBOT_BOSS: + case DS_BOSS2: + //case DS_BOSS2_HYPER: + case DS_BOSS5: + return true; + default: + return false; + } +} + edict_t *vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn_t drone_type, qboolean worldspawn, qboolean link_now, int bonus_level) { vec3_t forward, right, start, end, offset; trace_t tr; float mult; int talentLevel; + qboolean is_boss_spawn; drone->classname = "drone"; + is_boss_spawn = vrx_drone_spawn_is_boss(drone_type); // set monster level if (worldspawn) { - if (drone_type >= 30)// tank commander, supertank + if (is_boss_spawn) drone->monsterinfo.level = HighestLevelPlayer(); else if (INVASION_OTHERSPAWNS_REMOVED) { @@ -837,6 +977,7 @@ edict_t *vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn case DS_GUNNER: init_drone_gunner(drone); break; case DS_PARASITE: init_drone_parasite(drone); break; case DS_BITCH: init_drone_bitch(drone); break; + case DS_BITCH_HEAT: init_drone_bitch_heat(drone); break; case DS_BRAIN: init_drone_brain(drone); break; case DS_MEDIC: init_drone_medic(drone); break; case DS_TANK: init_drone_tank(drone); break; @@ -844,14 +985,27 @@ edict_t *vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn case DS_GLADIATOR: init_drone_gladiator(drone); break; case DS_BERSERK: init_drone_berserk(drone); break; case DS_SOLDIER: init_drone_soldier(drone); break; + case DS_SOLDIER_RIPPER: drone->mtype = M_SOLDIER_RIPPER; init_drone_soldier(drone); break; + case DS_SOLDIER_BLUEBLASTER: drone->mtype = M_SOLDIER_BLUEBLASTER; init_drone_soldier(drone); break; + case DS_SOLDIER_LASER: drone->mtype = M_SOLDIER_LASER; init_drone_soldier(drone); break; case DS_INFANTRY: init_drone_infantry(drone); break; case DS_FLYER: init_drone_flyer(drone); break; case DS_FLOATER: init_drone_floater(drone); break; case DS_HOVER: init_drone_hover(drone); break; case DS_SHAMBLER: init_drone_shambler(drone); break; + case DS_REDMUTANT: init_drone_redmutant(drone); break; + case DS_RUNNERTANK: init_drone_runnertank(drone); break; + case DS_GUNCMDR: init_drone_guncmdr(drone); break; + case DS_DAEDALUS: init_drone_daedalus(drone); break; case DS_DECOY: init_drone_decoy(drone); break; case DS_SKELETON: init_skeleton(drone); break; case DS_GOLEM: init_golem(drone); break; + case DS_GLADB: init_drone_gladb(drone); break; + case DS_GLADC: init_drone_gladc(drone); break; + case DS_STALKER: init_drone_stalker(drone); break; + case DS_GEKK: init_drone_gekk(drone); break; + case DS_ARACHNID: init_drone_arachnid(drone); break; + case DS_MEDIC_COMMANDER: init_drone_medic_commander(drone); break; // bosses case DS_COMMANDER: init_drone_commander(drone); break; @@ -859,9 +1013,26 @@ edict_t *vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn case DS_BARON_FIRE: init_baron_fire(drone); break; case DS_SUPERTANK: init_drone_supertank(drone); break; case DS_JORG: init_drone_jorg(drone); break; + case DS_CARRIER: init_drone_carrier(drone); break; + case DS_GUARDIAN: init_drone_guardian(drone); break; + case DS_WIDOW: init_drone_widow(drone); break; + case DS_WIDOW2: init_drone_widow2(drone); break; + case DS_FIXBOT_BOSS: init_drone_fixbot_boss(drone); break; + case DS_BOSS2: init_drone_boss2(drone); break; + case DS_BOSS2_HYPER: init_drone_boss2_hyper(drone); break; + case DS_BOSS5: init_drone_boss5(drone); break; + + // special/miniboss-sized normal monsters + case DS_JANITOR: drone->mtype = M_JANITOR; init_drone_supertank(drone); break; + case DS_MINIGUARDIAN: drone->mtype = M_MINIGUARDIAN; init_drone_guardian(drone); break; + case DS_FIXBOT: init_drone_fixbot(drone); break; + case DS_ROGUE_TURRET: init_drone_rogue_turret(drone); break; + case DS_BOSS2_SMALL: init_drone_boss2_small(drone); break; - // default - default: init_drone_gunner(drone); break; + default: + gi.dprintf("WARNING: unknown drone spawn type %d\n", drone_type); + G_FreeEdict(drone); + return NULL; } /* az: init functions might have set up a pain function -- address that here */ @@ -873,7 +1044,7 @@ edict_t *vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn drone->pain = drone_pain; //4.0 gib health based on monster control cost - if (drone_type < 30) + if (!is_boss_spawn) drone->gib_health = -drone->monsterinfo.control_cost * BASE_GIB_HEALTH * M_CONTROL_COST_SCALE; else drone->gib_health = 0;//gib boss immediately @@ -916,7 +1087,7 @@ edict_t *vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn if (worldspawn) { // non-invasion mode monsters or bosses are spawned randomly throughout the map - if (!INVASION_OTHERSPAWNS_REMOVED || drone_type >= 30) // only use designated spawns in invasion mode + if (!INVASION_OTHERSPAWNS_REMOVED || is_boss_spawn) // only use designated spawns in invasion mode { if (link_now && drone->mtype != M_JORG && !vrx_find_random_spawn_point(drone, false)) { @@ -933,7 +1104,7 @@ edict_t *vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn // trigger spree war if a boss successfully spawns in PvP mode if (deathmatch->value && !domination->value && !ctf->value && !invasion->value && !pvm->value - && !ptr->value && !ffa->value && (drone_type >= 30)) + && !ptr->value && !ffa->value && is_boss_spawn) { SPREE_WAR = true; SPREE_TIME = level.time; @@ -1015,7 +1186,7 @@ edict_t *vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn } //4.4 FIXME: should we be doing this if ent is world? - if (!ent->client && (drone_type == 30 || drone_type == 31)) // boss + if (!ent->client && is_boss_spawn) ent->num_sentries++; else { @@ -1454,48 +1625,154 @@ double randfrac(void) { // note: flash_number is used by monsters to determine muzzle location; use -1 if muzzle location is already known, or 0 for non-monsters to estimate muzzle location // aiming vector will be copied to 'forward' and can be used for firing functions -void MonsterAim (edict_t *self, float accuracy, int projectile_speed, qboolean rocket, - int flash_number, vec3_t forward, vec3_t start) +static qboolean M_MonsterTraceCombatSight(edict_t *self, vec3_t start, vec3_t end) { - float velocity, dist, rnd, crnd;//, base_acc = accuracy; - vec3_t target, end; - vec3_t right, offset; - trace_t tr; + if (!gi.inPVS(start, end)) + return false; + + return gi.trace(start, NULL, NULL, end, self, MASK_SOLID).fraction == 1.0f; +} + +static void M_MonsterProjectMuzzleSource(edict_t *self, int flash_number, vec3_t forward, vec3_t right, vec3_t start) +{ + vec3_t offset; - // determine muzzle origin - AngleVectors (self->s.angles, forward, right, NULL); if (self->client && !flash_number) { - VectorSet(offset, 0, 8, self->viewheight-8); - P_ProjectSource (self->client, self->s.origin, offset, forward, right, start); + VectorSet(offset, 0, 8, self->viewheight - 8); + P_ProjectSource(self->client, self->s.origin, offset, forward, right, start); } - else if (flash_number) + else if (flash_number > 0) { - // -1 flash number indicates we've already calculated our muzzle location as 'start' - // otherwise proceed using flash offset - if (flash_number != -1) - { - // monsters have special offsets that determine their exact firing origin - G_ProjectSource(self->s.origin, monster_flash_offset[flash_number], forward, right, start); - // fix for retarded chick muzzle location - if ((self->svflags & SVF_MONSTER) && (start[2] < self->absmin[2] + 32)) - start[2] += 32; - } + G_ProjectSource(self->s.origin, monster_flash_offset[flash_number], forward, right, start); + if ((self->svflags & SVF_MONSTER) && (start[2] < self->absmin[2] + 32)) + start[2] += 32; } - else // can't determine the muzzle origin + else if (self->viewheight) { - // is viewheight set? if so, use that as a starting point - if (self->viewheight) - { - VectorCopy(self->s.origin, start); - start[2] += self->viewheight; - } - else // otherwise, use the mid-point of the bounding box - G_EntMidPoint(self, start); - // move starting point forward - VectorMA(start, self->maxs[1]+8, forward, start); + VectorCopy(self->s.origin, start); + start[2] += self->viewheight; + VectorMA(start, self->maxs[1] + 8, forward, start); + } + else + { + G_EntMidPoint(self, start); + VectorMA(start, self->maxs[1] + 8, forward, start); + } +} + +static qboolean M_MonsterTraceShotToTarget(edict_t *self, vec3_t start, edict_t *target, vec3_t end) +{ + trace_t tr; + + tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT); + return tr.ent && tr.ent == target; +} + +static qboolean M_MonsterFindClearTargetPoint(edict_t *self, vec3_t start, edict_t *target, vec3_t point) +{ + vec3_t test; + + if (!G_EntExists(target)) + return false; + + G_EntMidPoint(target, test); + if (M_MonsterTraceShotToTarget(self, start, target, test)) + { + VectorCopy(test, point); + return true; + } + + G_EntViewPoint(target, test); + if (M_MonsterTraceShotToTarget(self, start, target, test)) + { + VectorCopy(test, point); + return true; } + VectorCopy(target->s.origin, test); + if (M_MonsterTraceShotToTarget(self, start, target, test)) + { + VectorCopy(test, point); + return true; + } + + return false; +} + +qboolean M_MonsterHasCombatSight(edict_t *self, edict_t *other) +{ + vec3_t start, end; + + if (!G_EntExists(other)) + return false; + + if (visible(self, other)) + return true; + + G_EntViewPoint(self, start); + G_EntViewPoint(other, end); + if (M_MonsterTraceCombatSight(self, start, end)) + return true; + + G_EntMidPoint(other, end); + if (M_MonsterTraceCombatSight(self, start, end)) + return true; + + G_EntMidPoint(self, start); + if (M_MonsterTraceCombatSight(self, start, end)) + return true; + + return false; +} + +qboolean M_MonsterHasClearShotFrom(edict_t *self, vec3_t start) +{ + vec3_t end; + + return M_MonsterFindClearShot(self, start, end); +} + +qboolean M_MonsterFindClearShot(edict_t *self, vec3_t start, vec3_t point) +{ + if (!G_EntExists(self->enemy)) + return false; + + return M_MonsterFindClearTargetPoint(self, start, self->enemy, point); +} + +qboolean M_MonsterHasClearShotFromFlash(edict_t *self, int flash_number) +{ + vec3_t forward, right, start; + + AngleVectors(self->s.angles, forward, right, NULL); + M_MonsterProjectMuzzleSource(self, flash_number, forward, right, start); + return M_MonsterHasClearShotFrom(self, start); +} + +void M_MonsterBlockedShot(edict_t *self, float delay) +{ + if (!(self->monsterinfo.aiflags & AI_ALTERNATE_FLY)) + return; + + self->monsterinfo.fly_position_time = 0.0f; + self->monsterinfo.fly_pinned = false; + self->monsterinfo.attack_finished = max(self->monsterinfo.attack_finished, level.time + delay); +} + +void MonsterAim (edict_t *self, float accuracy, int projectile_speed, qboolean rocket, + int flash_number, vec3_t forward, vec3_t start) +{ + float velocity, dist, rnd, crnd;//, base_acc = accuracy; + vec3_t target, end; + vec3_t right; + trace_t tr; + + // determine muzzle origin + AngleVectors (self->s.angles, forward, right, NULL); + if (flash_number != -1) + M_MonsterProjectMuzzleSource(self, flash_number, forward, right, start); + // fire ahead if our enemy is invalid or out of our FOV if (!G_EntExists(self->enemy) || !nearfov(self, self->enemy, 0, 60)) { @@ -1574,6 +1851,7 @@ void MonsterAim (edict_t *self, float accuracy, int projectile_speed, qboolean r accuracy *= 0.2; G_EntMidPoint(self->enemy, target); // 3.58 aim at the ent's actual mid point + M_MonsterFindClearTargetPoint(self, start, self->enemy, target); //VectorCopy(self->enemy->s.origin, target); // miss the shot @@ -1745,7 +2023,12 @@ qboolean M_Regenerate (edict_t *self, int regen_frames, int delay, float mult, q if (!self->client && (self->svflags & SVF_MONSTER) && vrx_has_pain_skin(self) && (self->health >= 0.5 * self->max_health)) { self->s.skinnum &= ~1; - if (self->mtype != M_COMMANDER) + if (self->mtype != M_COMMANDER && self->mtype != M_GUNCMDR + && self->mtype != M_DAEDALUS && self->mtype != M_GLADB && self->mtype != M_GLADC + && self->mtype != M_CHICK_HEAT && self->mtype != M_MEDIC_COMMANDER + && self->mtype != M_SOLDIER && self->mtype != M_SOLDIER_RIPPER + && self->mtype != M_SOLDIER_BLUEBLASTER && self->mtype != M_SOLDIER_LASER + && self->mtype != M_STALKER && self->mtype != M_BOSS5) self->s.skinnum &= ~2; } @@ -2026,24 +2309,65 @@ qboolean M_Initialize (edict_t *ent, edict_t *monster, float dur_bonus) { case M_GUNNER: init_drone_gunner(monster); break; case M_CHICK: init_drone_bitch(monster); break; + case M_CHICK_HEAT: init_drone_bitch_heat(monster); break; case M_BRAIN: init_drone_brain(monster); break; case M_MEDIC: init_drone_medic(monster); break; + case M_MEDIC_COMMANDER: init_drone_medic_commander(monster); break; case M_MUTANT: init_drone_mutant(monster); break; case M_PARASITE: init_drone_parasite(monster); break; case M_TANK: init_drone_tank(monster); break; + case M_SUPERTANK: init_drone_supertank(monster); break; + case M_BOSS5: init_drone_boss5(monster); break; case M_BERSERK: init_drone_berserk(monster); break; - case M_SOLDIER: case M_SOLDIERLT: case M_SOLDIERSS: init_drone_soldier(monster); break; + case M_SOLDIER: case M_SOLDIERLT: case M_SOLDIERSS: + case M_SOLDIER_RIPPER: case M_SOLDIER_BLUEBLASTER: case M_SOLDIER_LASER: + init_drone_soldier(monster); break; case M_GLADIATOR: init_drone_gladiator(monster); break; case M_INFANTRY: init_drone_infantry(monster); break; case M_FLYER: init_drone_flyer(monster); break; case M_FLOATER: init_drone_floater(monster); break; case M_HOVER: init_drone_hover(monster); break; case M_SHAMBLER: init_drone_shambler(monster); break; + case M_REDMUTANT: init_drone_redmutant(monster); break; + case M_RUNNERTANK: init_drone_runnertank(monster); break; + case M_GUNCMDR: init_drone_guncmdr(monster); break; + case M_DAEDALUS: init_drone_daedalus(monster); break; + case M_GLADB: init_drone_gladb(monster); break; + case M_GLADC: init_drone_gladc(monster); break; + case M_STALKER: init_drone_stalker(monster); break; + case M_GEKK: init_drone_gekk(monster); break; + case M_ARACHNID: init_drone_arachnid(monster); break; + case M_BOSS2: init_drone_boss2(monster); break; + case M_BOSS2_SMALL: init_drone_boss2_small(monster); break; + case M_CARRIER: init_drone_carrier(monster); break; + case M_WIDOW: init_drone_widow(monster); break; + case M_WIDOW2: init_drone_widow2(monster); break; + case M_FIXBOT: init_drone_fixbot(monster); break; + case M_FIXBOT_BOSS: init_drone_fixbot_boss(monster); break; + case M_ROGUE_TURRET: init_drone_rogue_turret(monster); break; + case M_GUARDIAN: case M_MINIGUARDIAN: init_drone_guardian(monster); break; + case M_JANITOR: init_drone_supertank(monster); break; case M_SKELETON: init_skeleton(monster); break; case M_GOLEM: init_golem(monster); break; default: return false; } +#ifdef VRX_REPRO + if (monster->s.scale && monster->mtype != M_GUNCMDR && monster->mtype != M_BOSS2_SMALL + && monster->mtype != M_WIDOW && monster->mtype != M_WIDOW2 + && monster->mtype != M_GUARDIAN && monster->mtype != M_MINIGUARDIAN + && !(invasion->value && (monster->mtype == M_CARRIER + || monster->mtype == M_ARACHNID || monster->mtype == M_FIXBOT_BOSS + || monster->mtype == M_SUPERTANK || monster->mtype == M_BOSS5 + || monster->mtype == M_BOSS2))) + { + monster->monsterinfo.scale *= monster->s.scale; + VectorScale(monster->mins, monster->s.scale, monster->mins); + VectorScale(monster->maxs, monster->s.scale, monster->maxs); + monster->mass *= monster->s.scale; + } +#endif + if ( (ent && ent->inuse && ent->client) || (monster->activator && monster->activator->inuse && monster->activator->client) ) // player summons exception. { @@ -2107,6 +2431,9 @@ qboolean M_SetBoundingBox (int mtype, vec3_t boxmin, vec3_t boxmax) case M_SOLDIER: case M_SOLDIERLT: case M_SOLDIERSS: + case M_SOLDIER_RIPPER: + case M_SOLDIER_BLUEBLASTER: + case M_SOLDIER_LASER: case M_GUNNER: case M_BRAIN: VectorSet (boxmin, -16, -16, -24); @@ -2117,6 +2444,7 @@ qboolean M_SetBoundingBox (int mtype, vec3_t boxmin, vec3_t boxmax) VectorSet (boxmax, 16, 16, 0); break; case M_CHICK: + case M_CHICK_HEAT: VectorSet (boxmin, -16, -16, 0); VectorSet (boxmax, 16, 16, 56); break; @@ -2124,11 +2452,153 @@ qboolean M_SetBoundingBox (int mtype, vec3_t boxmin, vec3_t boxmax) VectorSet (boxmin, -24, -24, -16); VectorSet (boxmax, 24, 24, 64); break; + case M_SUPERTANK: + case M_BOSS5: + if (invasion->value) + { + VectorSet(boxmin, -40, -40, 0); + VectorSet(boxmax, 40, 40, 72); + } + else + { + VectorSet(boxmin, -64, -64, 0); + VectorSet(boxmax, 64, 64, 112); + } + break; case M_SHAMBLER: VectorSet(boxmin, -32, -32, -24); VectorSet(boxmax, 32, 32, 64); break; + case M_REDMUTANT: + VectorSet(boxmin, -18, -18, -24); + VectorSet(boxmax, 18, 18, 30); + break; + case M_RUNNERTANK: + VectorSet(boxmin, -28, -28, -14); + VectorSet(boxmax, 28, 28, 56); + break; + case M_GUNCMDR: + VectorSet(boxmin, -20, -20, -30); + VectorSet(boxmax, 20, 20, 45); + break; + case M_DAEDALUS: + VectorSet(boxmin, -24, -24, -24); + VectorSet(boxmax, 24, 24, 32); + break; + case M_STALKER: + VectorSet(boxmin, -28, -28, -18); + VectorSet(boxmax, 28, 28, 18); + break; + case M_GEKK: + VectorSet(boxmin, -18, -18, -24); + VectorSet(boxmax, 18, 18, 24); + break; + case M_ARACHNID: + if (invasion->value) + { + VectorSet(boxmin, -28, -28, -18); + VectorSet(boxmax, 28, 28, 34); + } + else + { + VectorSet(boxmin, -48, -48, -20); + VectorSet(boxmax, 48, 48, 48); + } + break; + case M_BOSS2: + if (invasion->value) + { + VectorSet(boxmin, -42, -42, 0); + VectorSet(boxmax, 42, 42, 60); + } + else + { + VectorSet(boxmin, -56, -56, 0); + VectorSet(boxmax, 56, 56, 80); + } + break; + case M_BOSS2_SMALL: + VectorSet(boxmin, -34, -34, 0); + VectorSet(boxmax, 34, 34, 48); + break; + case M_CARRIER: + if (invasion->value) + { + VectorSet(boxmin, -40, -40, -24); + VectorSet(boxmax, 40, 40, 82); + } + else + { + VectorSet(boxmin, -80, -80, -24); + VectorSet(boxmax, 80, 80, 104); + } + break; + case M_WIDOW: + if (invasion->value) + { + VectorSet(boxmin, -30, -30, 0); + VectorSet(boxmax, 30, 30, 108); + } + else + { + VectorSet(boxmin, -40, -40, 0); + VectorSet(boxmax, 40, 40, 144); + } + break; + case M_WIDOW2: + if (invasion->value) + { + VectorSet(boxmin, -40, -40, 0); + VectorSet(boxmax, 40, 40, 82); + } + else + { + VectorSet(boxmin, -70, -70, 0); + VectorSet(boxmax, 70, 70, 144); + } + break; + case M_FIXBOT: + VectorSet(boxmin, -24, -24, -18); + VectorSet(boxmax, 24, 24, 24); + break; + case M_FIXBOT_BOSS: + if (invasion->value) + { + VectorSet(boxmin, -30, -30, -24); + VectorSet(boxmax, 30, 30, 24); + } + else + { + VectorSet(boxmin, -36, -36, -28); + VectorSet(boxmax, 36, 36, 28); + } + break; + case M_ROGUE_TURRET: + VectorSet(boxmin, -12, -12, -12); + VectorSet(boxmax, 12, 12, 12); + break; + case M_GUARDIAN: + if (invasion->value) + { + VectorSet(boxmin, -44, -44, -30); + VectorSet(boxmax, 44, 44, 45); + } + else + { + VectorSet(boxmin, -96, -96, -66); + VectorSet(boxmax, 96, 96, 62); + } + break; + case M_JANITOR: + VectorSet(boxmin, -38, -38, 0); + VectorSet(boxmax, 38, 38, 67); + break; + case M_MINIGUARDIAN: + VectorSet(boxmin, -38, -38, -26); + VectorSet(boxmax, 38, 38, 25); + break; case M_MEDIC: + case M_MEDIC_COMMANDER: case M_MUTANT: VectorSet (boxmin, -24, -24, -24); VectorSet (boxmax, 24, 24, 32); @@ -2138,6 +2608,8 @@ qboolean M_SetBoundingBox (int mtype, vec3_t boxmin, vec3_t boxmax) VectorSet(boxmax, 16, 16, -8); break; case M_GLADIATOR: + case M_GLADB: + case M_GLADC: VectorSet(boxmin, -24, -24, -24); VectorSet(boxmax, 24, 24, 48); break; @@ -2160,12 +2632,19 @@ char *GetMonsterKindString (int mtype) { case M_BRAIN: return "Brain"; case M_CHICK: return "Praetor"; + case M_CHICK_HEAT: return "Heat Praetor"; case M_MEDIC: return "Medic"; + case M_MEDIC_COMMANDER: return "Medic Commander"; case M_MUTANT: return "Mutant"; case M_PARASITE: return "Parasite"; case M_BERSERK: return "Berserker"; case M_SOLDIER: case M_SOLDIERLT: case M_SOLDIERSS: return "Soldier"; + case M_SOLDIER_RIPPER: return "Ripper Guard"; + case M_SOLDIER_BLUEBLASTER: return "Hyper Guard"; + case M_SOLDIER_LASER: return "Laser Guard"; case M_TANK: return "Tank"; + case M_SUPERTANK: return "Super Tank"; + case M_BOSS5: return "Super Tank Heat"; case M_GUNNER: return "Gunner"; case M_YANGSPIRIT: return "Yang Spirit"; case M_BALANCESPIRIT: return "Balance Spirit"; @@ -2194,6 +2673,26 @@ char *GetMonsterKindString (int mtype) case M_FLOATER: return "Floater"; case M_HOVER: return "Hover"; case M_SHAMBLER: return "Shambler"; + case M_REDMUTANT: return "Red Mutant"; + case M_RUNNERTANK: return "Runner Tank"; + case M_GUNCMDR: return "Gunner Commander"; + case M_DAEDALUS: return "Daedalus"; + case M_GLADB: return "Gladiator Disruptor"; + case M_GLADC: return "Gladiator Plasma"; + case M_STALKER: return "Stalker"; + case M_GEKK: return "Gekk"; + case M_ARACHNID: return "Arachnid"; + case M_BOSS2: return "Hornet"; + case M_BOSS2_SMALL: return "Mini Hornet"; + case M_CARRIER: return "Carrier"; + case M_WIDOW: return "Widow"; + case M_WIDOW2: return "Black Widow"; + case M_FIXBOT: return "Fixbot"; + case M_FIXBOT_BOSS: return "Fixer"; + case M_ROGUE_TURRET: return "Rocket Turret"; + case M_GUARDIAN: return "Guardian"; + case M_JANITOR: return "Janitor"; + case M_MINIGUARDIAN: return "Mini Guardian"; case M_SKELETON: return "Skeleton"; case M_GOLEM: return "Golem"; case M_BARON_FIRE: return "Fire Baron"; @@ -2731,7 +3230,7 @@ void Cmd_Drone_f (edict_t *ent) ent->selected[i]->monsterinfo.sight_range = 256; if (ent->selected[i]->mtype == M_PARASITE) ent->selected[i]->monsterinfo.sight_range = 128; - if (ent->selected[i]->mtype == M_MEDIC) + if (ent->selected[i]->mtype == M_MEDIC || ent->selected[i]->mtype == M_MEDIC_COMMANDER) ent->selected[i]->monsterinfo.sight_range = 256; if (ent->selected[i]->mtype == M_BERSERK) ent->selected[i]->monsterinfo.sight_range = 128; @@ -2778,7 +3277,7 @@ void Cmd_Drone_f (edict_t *ent) if (!Q_strcasecmp(s, "help")) { safe_cprintf(ent, PRINT_HIGH, "Monster summoning:\n"); - safe_cprintf(ent, PRINT_HIGH, "monster [gunner|parasite|brain|praetor|medic|tank|mutant|gladiator|berserker|soldier|enforcer|flyer|floater|hover|shambler\n"); + safe_cprintf(ent, PRINT_HIGH, "monster [gunner|parasite|brain|praetor|praetor_heat|medic|tank|mutant|gladiator|gladb|gladc|berserker|soldier|ripper|hyper|laser|janitor|janitor2|enforcer|flyer|floater|hover|daedalus|stalker|gekk|arachnid|shambler|redmutant|runnertank|guncmdr]\n"); safe_cprintf(ent, PRINT_HIGH, "Monster utility commands:\n"); safe_cprintf(ent, PRINT_HIGH, "monster [remove|command|follow me|count|attack]\n"); return; @@ -2797,37 +3296,76 @@ void Cmd_Drone_f (edict_t *ent) } if (!Q_strcasecmp(s, "gunner")) - vrx_create_new_drone(ent, 1, false, true, 0); + vrx_create_new_drone(ent, DS_GUNNER, false, true, 0); else if (!Q_strcasecmp(s, "parasite")) - vrx_create_new_drone(ent, 2, false, true, 0); + vrx_create_new_drone(ent, DS_PARASITE, false, true, 0); else if (!Q_strcasecmp(s, "brain")) - vrx_create_new_drone(ent, 4, false, true, 0); + vrx_create_new_drone(ent, DS_BRAIN, false, true, 0); else if (!Q_strcasecmp(s, "praetor")) - vrx_create_new_drone(ent, 3, false, true, 0); + vrx_create_new_drone(ent, DS_BITCH, false, true, 0); + else if (!Q_strcasecmp(s, "praetor_heat") || !Q_strcasecmp(s, "praetorheat") + || !Q_strcasecmp(s, "chick_heat") || !Q_strcasecmp(s, "chickheat")) + vrx_create_new_drone(ent, DS_BITCH_HEAT, false, true, 0); else if (!Q_strcasecmp(s, "medic")) - vrx_create_new_drone(ent, 5, false, true, 0); + vrx_create_new_drone(ent, DS_MEDIC, false, true, 0); else if (!Q_strcasecmp(s, "tank")) - vrx_create_new_drone(ent, 6, false, true, 0); + vrx_create_new_drone(ent, DS_TANK, false, true, 0); else if (!Q_strcasecmp(s, "mutant")) - vrx_create_new_drone(ent, 7, false, true, 0); + vrx_create_new_drone(ent, DS_MUTANT, false, true, 0); else if (!Q_strcasecmp(s, "gladiator")/* && ent->myskills.administrator*/) - vrx_create_new_drone(ent, 8, false, true, 0); + vrx_create_new_drone(ent, DS_GLADIATOR, false, true, 0); else if (!Q_strcasecmp(s, "berserker")) - vrx_create_new_drone(ent, 9, false, true, 0); + vrx_create_new_drone(ent, DS_BERSERK, false, true, 0); else if (!Q_strcasecmp(s, "soldier")) - vrx_create_new_drone(ent, 10, false, true, 0); + vrx_create_new_drone(ent, DS_SOLDIER, false, true, 0); + else if (!Q_strcasecmp(s, "soldier_ionripper") || !Q_strcasecmp(s, "soldierripper") || !Q_strcasecmp(s, "rippersoldier") + || !Q_strcasecmp(s, "ripper_guard") || !Q_strcasecmp(s, "ripperguard")) + vrx_create_new_drone(ent, DS_SOLDIER_RIPPER, false, true, 0); + else if (!Q_strcasecmp(s, "soldierhyper") || !Q_strcasecmp(s, "hypersoldier") || !Q_strcasecmp(s, "hyperguard") + || !Q_strcasecmp(s, "hyper") || !Q_strcasecmp(s, "hyper_guard") || !Q_strcasecmp(s, "hyper guard")) + vrx_create_new_drone(ent, DS_SOLDIER_BLUEBLASTER, false, true, 0); + else if (!Q_strcasecmp(s, "soldier_laser") || !Q_strcasecmp(s, "soldierlaser") || !Q_strcasecmp(s, "lasersoldier") + || !Q_strcasecmp(s, "laser") || !Q_strcasecmp(s, "laserguard") || !Q_strcasecmp(s, "laserguard")) + vrx_create_new_drone(ent, DS_SOLDIER_LASER, false, true, 0); + else if (!Q_strcasecmp(s, "janitor")) + vrx_create_new_drone(ent, DS_JANITOR, false, true, 0); + else if (!Q_strcasecmp(s, "miniguardian") || !Q_strcasecmp(s, "mini guardian") || !Q_strcasecmp(s, "mini_guardian")) + vrx_create_new_drone(ent, DS_MINIGUARDIAN, false, true, 0); else if (!Q_strcasecmp(s, "enforcer")) - vrx_create_new_drone(ent, 11, false, true, 0); + vrx_create_new_drone(ent, DS_INFANTRY, false, true, 0); else if (!Q_strcasecmp(s, "flyer")) - vrx_create_new_drone(ent, 12, false, true, 0); + vrx_create_new_drone(ent, DS_FLYER, false, true, 0); else if (!Q_strcasecmp(s, "floater")) - vrx_create_new_drone(ent, 13, false, true, 0); + vrx_create_new_drone(ent, DS_FLOATER, false, true, 0); else if (!Q_strcasecmp(s, "hover")) - vrx_create_new_drone(ent, 14, false, true, 0); + vrx_create_new_drone(ent, DS_HOVER, false, true, 0); + else if (!Q_strcasecmp(s, "fixbot")) + vrx_create_new_drone(ent, DS_FIXBOT, false, true, 0); + else if (!Q_strcasecmp(s, "hornet") || !Q_strcasecmp(s, "mini_hornet") + || !Q_strcasecmp(s, "minihornet") || !Q_strcasecmp(s, "boss2_small")) + vrx_create_new_drone(ent, DS_BOSS2_SMALL, false, true, 0); else if (!Q_strcasecmp(s, "shambler")) - vrx_create_new_drone(ent, 15, false, true, 0); + vrx_create_new_drone(ent, DS_SHAMBLER, false, true, 0); + else if (!Q_strcasecmp(s, "redmutant")) + vrx_create_new_drone(ent, DS_REDMUTANT, false, true, 0); + else if (!Q_strcasecmp(s, "runnertank")) + vrx_create_new_drone(ent, DS_RUNNERTANK, false, true, 0); + else if (!Q_strcasecmp(s, "guncmdr")) + vrx_create_new_drone(ent, DS_GUNCMDR, false, true, 0); + else if (!Q_strcasecmp(s, "daedalus")) + vrx_create_new_drone(ent, DS_DAEDALUS, false, true, 0); + else if (!Q_strcasecmp(s, "gladb") || !Q_strcasecmp(s, "gladiatordisruptor")) + vrx_create_new_drone(ent, DS_GLADB, false, true, 0); + else if (!Q_strcasecmp(s, "gladc") || !Q_strcasecmp(s, "gladiatorplasma")) + vrx_create_new_drone(ent, DS_GLADC, false, true, 0); + else if (!Q_strcasecmp(s, "stalker")) + vrx_create_new_drone(ent, DS_STALKER, false, true, 0); + else if (!Q_strcasecmp(s, "gekk")) + vrx_create_new_drone(ent, DS_GEKK, false, true, 0); + else if (!Q_strcasecmp(s, "arachnid")) + vrx_create_new_drone(ent, DS_ARACHNID, false, true, 0); else if (!Q_strcasecmp(s, "golem") && ent->myskills.administrator) - vrx_create_new_drone(ent, 22, false, true, 0); + vrx_create_new_drone(ent, DS_GOLEM, false, true, 0); //else if (!Q_strcasecmp(s, "baron fire") && ent->myskills.administrator) //vrx_create_new_drone(ent, 32, false, true); //else if (!Q_strcasecmp(s, "jorg")) @@ -3034,4 +3572,4 @@ void M_Touchdown(edict_t* self) gi.sound(self, CHAN_VOICE, gi.soundindex("world/land.wav"), 1, ATTN_IDLE, 0); } self->monsterinfo.touchdown_delay = level.time + 1.0; -} \ No newline at end of file +} diff --git a/src/entities/drone/drone_move.c b/src/entities/drone/drone_move.c index 26584809..eb280ad8 100644 --- a/src/entities/drone/drone_move.c +++ b/src/entities/drone/drone_move.c @@ -336,76 +336,1601 @@ qboolean M_Move (edict_t *ent, vec3_t move, qboolean relink) return true; } +static float M_FlyFloorClearance(const edict_t *ent) +{ + switch (ent->mtype) + { + case M_CARRIER: + return 104.0f; + case M_FIXBOT: + case M_FIXBOT_BOSS: + case M_BOSS2_SMALL: + return 40.0f; + default: + return 16.0f; + } +} + qboolean M_MoveVertical(edict_t* ent, vec3_t dest, vec3_t neworg) { - float dz; // delta Z - float idealZ; // ideal Z position, either a destination waypoint/node or a goal entity - const float zSpeed = 8; // Z movement speed - vec3_t end, goalpos; - trace_t trace; - //qboolean stopOnCollision=false; - VectorCopy(ent->s.origin, neworg); - if (ent->monsterinfo.bump_delay > level.time || ent->monsterinfo.Zchange_delay > level.time) - return true; // don't adjust vertically + float dz; // delta Z + float idealZ; // ideal Z position, either a destination waypoint/node or a goal entity + const float zSpeed = 8; // Z movement speed + const float floorClearance = M_FlyFloorClearance(ent); + vec3_t end, goalpos; + trace_t trace; + //qboolean stopOnCollision=false; + VectorCopy(ent->s.origin, neworg); + if (ent->monsterinfo.bump_delay > level.time || ent->monsterinfo.Zchange_delay > level.time) + return true; // don't adjust vertically + if (dest) + { + idealZ = dest[2] + 16; + VectorCopy(dest, goalpos); + } + else if (ent->goalentity) + { + if (ent->goalentity == world && floorClearance <= 16.0f) + return true; + if (ent->goalentity == world) + { + idealZ = ent->s.origin[2]; + VectorCopy(ent->s.origin, goalpos); + } + else + { + idealZ = ent->goalentity->s.origin[2] + 16; + VectorCopy(ent->goalentity->s.origin, goalpos); + } + } + else if (floorClearance <= 16.0f) + return true; // no goal, no destination - probably idle + else + { + idealZ = ent->s.origin[2]; + VectorCopy(ent->s.origin, goalpos); + } + // if we can see our enemy, stay above him + if (ent->enemy && visible(ent, ent->enemy) && entdist(ent, ent->enemy) <= 256) + idealZ = ent->enemy->absmax[2] + 48; + // goal position is obstructed, so vertical movement should fail if we are obstructed + //else if (!G_IsClearPath(ent, MASK_SHOT, ent->s.origin, goalpos)) + // stopOnCollision = true; + //gi.dprintf("idealZ @ %.0f ", idealZ); + // check floor height + VectorCopy(ent->s.origin, end); + end[2] -= 8192; + trace = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_MONSTERSOLID); + // always stay above the floor, regardless of goal/destination + if (idealZ < trace.endpos[2] + floorClearance) + idealZ = trace.endpos[2] + floorClearance; + //gi.dprintf("floor @ %.0f position @ %.0f\n", trace.endpos[2], ent->s.origin[2]); + dz = ent->s.origin[2] - idealZ; + VectorCopy(ent->s.origin, end); + if (dz > 0) // we are above our target + { + if (dz > zSpeed) + dz = zSpeed; + end[2] -= dz; + } + else if (dz < 0) // we are below our target + { + dz = fabs(dz); + if (dz > zSpeed) + dz = zSpeed; + end[2] += dz; + } + else // we at the correct Z height, so we're done + return true; + trace = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_MONSTERSOLID); + if (trace.fraction == 1) + { + VectorCopy(trace.endpos, neworg); + /*if (relink) + { + gi.linkentity (ent); + G_TouchTriggers (ent); + }*/ + return true; + } + return false; +} + +#define FLY_POSITION_UPDATE_MIN 3.0f +#define FLY_POSITION_UPDATE_MAX 10.0f +#define FLY_TURN_FACTOR_FAST 0.45f +#define FLY_TURN_FACTOR_BASE 0.84f +#define FLY_TURN_FACTOR_SPEED_SCALE 0.08f +#define FLY_PITCH_LERP_SPEED 4.0f +#define FLY_WALL_STUCK_THRESHOLD 1.5f +#define FLY_DESCENT_SPEED_MULTIPLIER 0.6f +#define FLY_TARGET_LEAD_TIME 0.45f +#define FLY_CATCHUP_MARGIN 32.0f +#define FLY_CATCHUP_DISTANCE_SCALE 0.85f +#define FLY_ATTACK_DISTANCE_FRACTION 0.2f +#define FLY_ATTACK_SPEED_SCALE 1.35f +#define FLY_ATTACK_ACCEL_SCALE 1.6f +#define FLY_LOS_PRESERVE_SPEED_SCALE 1.12f +#define FLY_LOS_PRESERVE_ACCEL_SCALE 1.30f +#define FLY_LOS_PRESERVE_TURN_FACTOR 0.32f +#define FLY_LOS_PRESSURE_SNAP_DOT 0.0f +#define FLY_LOS_FULL_SIGHT_FRACTION 0.98f +#define FLY_LOS_SIDE_SWITCH_SPEED 64.0f +#define FLY_LOS_PRESERVE_PROBE 72.0f +#define FLY_LOS_PRESERVE_MIN_FRACTION 0.25f +#define FLY_LOS_PRESERVE_SIDE_WEIGHT 1.05f +#define FLY_LOS_PRESERVE_FWD_WEIGHT 0.75f +#define FLY_LOS_PRESERVE_WALL_WEIGHT 0.35f +#define FLY_LOS_PRESERVE_DOWN_WEIGHT 0.55f +#define FLY_LOS_PREVENT_LEAD_TIME 0.45f +#define FLY_LOS_PREVENT_THRESHOLD 0.86f +#define FLY_LOS_PREVENT_MIN_GAIN 0.10f +#define FLY_LOS_PREVENT_ALLOWED_DROP 0.06f +#define FLY_LOS_PREVENT_PROBE 96.0f +#define FLY_LOS_PREVENT_SIDE_WEIGHT 1.15f +#define FLY_LOS_PREVENT_FWD_WEIGHT 0.85f +#define FLY_LOS_PREVENT_DOWN_WEIGHT 0.35f +#define FLY_LOS_RECOVER_MEMORY_TIME 1.6f +#define FLY_LOS_RECOVER_LAST_SEEN_RANGE 512.0f +#define FLY_LOS_RECOVER_MAX_RANGE 1200.0f +#define FLY_LOS_RECOVER_PROBE 96.0f +#define FLY_LOS_RECOVER_MIN_GAIN 0.08f +#define FLY_LOS_RECOVER_SIDE_WEIGHT 1.20f +#define FLY_LOS_RECOVER_FWD_WEIGHT 0.80f +#define FLY_LOS_RECOVER_DOWN_WEIGHT 0.45f +#define FLY_ATTACK_STRAFE_SPEED_SCALE 0.6f +#define FLY_ATTACK_DRIFT_SPEED_SCALE 0.3f +#define FLY_ATTACK_STRAFE_MIN 32.0f +#define FLY_ATTACK_STRAFE_MAX 128.0f +#define FLY_ATTACK_ORBIT_DISTANCE 48.0f +#define FLY_ATTACK_HEIGHT_VARIANCE 56.0f +#define FLY_TARGET_STRAFE_MIN_SPEED 20.0f +#define FLY_TARGET_STRAFE_DISTANCE_SCALE 0.45f +#define FLY_COMBAT_MIN_HEIGHT 48.0f +#define FLY_COMBAT_HEIGHT_ABOVE_VIEW 24.0f +#define FLY_LOWER_TARGET_DESCENT_HEIGHT 96.0f +#define FLY_LOWER_TARGET_MIN_HEIGHT 16.0f +#define FLY_BLOCKED_DESCENT_HEIGHT 32.0f +#define FLY_BLOCKED_DESCENT_XY_SCALE 0.45f +#define FLY_COMBAT_WALL_STUCK_THRESHOLD 0.35f +#define FLY_STAIR_CLIMB_MIN_HEIGHT 24.0f +#define FLY_STAIR_CLIMB_MAX_HEIGHT 320.0f +#define FLY_STAIR_CLIMB_MAX_RANGE 420.0f +#define FLY_STAIR_CLIMB_PROBE 64.0f +#define FLY_STAIR_CLIMB_MIN_FRACTION 0.60f +#define FLY_STAIR_CLIMB_MIN_GAIN 4.0f +#define FLY_STAIR_CLIMB_FWD_WEIGHT 0.90f +#define FLY_STAIR_CLIMB_UP_WEIGHT 0.75f +#define FLY_CEILING_LOOKAHEAD 256.0f +#define FLY_CEILING_CLEARANCE 8.0f +#define FLY_SEPARATION_RADIUS 112.0f +#define FLY_SEPARATION_MAX_PUSH 96.0f +#define FLY_SEPARATION_Z_SCALE 0.45f +#define FLY_SEPARATION_MAX_NEIGHBORS 24 + +static float fly_frand_range(float min_value, float max_value) +{ + return min_value + random() * (max_value - min_value); +} + +static float fly_clampf(float value, float min_value, float max_value) +{ + if (value < min_value) + return min_value; + if (value > max_value) + return max_value; + return value; +} + +static void fly_scale_add(vec3_t out, vec3_t a, float ascale, vec3_t b, float bscale) +{ + out[0] = a[0] * ascale + b[0] * bscale; + out[1] = a[1] * ascale + b[1] * bscale; + out[2] = a[2] * ascale + b[2] * bscale; +} + +static qboolean fly_vector_valid(vec3_t v) +{ + return isfinite(v[0]) && isfinite(v[1]) && isfinite(v[2]); +} + +static float fly_entity_unit(edict_t *ent, unsigned int salt) +{ + unsigned int n = (unsigned int)(ent - g_edicts + 1); + + n ^= salt * 0x9e3779b9u; + n ^= n >> 16; + n *= 0x7feb352du; + n ^= n >> 15; + n *= 0x846ca68bu; + n ^= n >> 16; + return (float)(n & 0xffffu) / 65535.0f; +} + +static void fly_entity_fallback_dir(edict_t *ent, vec3_t out) +{ + float angle = fly_entity_unit(ent, 0x51u) * M_PI * 2.0f; + + VectorSet(out, cosf(angle), sinf(angle), 0.2f + fly_entity_unit(ent, 0x52u) * 0.35f); + VectorNormalize(out); +} + +static qboolean fly_following_path(edict_t *ent) +{ + if (ent->monsterinfo.aiflags & (AI_FIND_NAVI | AI_COMBAT_POINT | AI_LOST_SIGHT)) + return true; + + if (ent->goalentity && ent->goalentity->inuse) + { + if (ent->goalentity->mtype == INVASION_NAVI || ent->goalentity->mtype == PLAYER_NAVI || + ent->goalentity->mtype == INVASION_PLAYERSPAWN) + return true; + } + + if (ent->enemy && ent->enemy->inuse && invasion->value && ent->enemy->mtype == INVASION_PLAYERSPAWN) + return true; + + return false; +} + +static qboolean fly_has_visible_combat_enemy(edict_t *ent) +{ + if (!ent->enemy || !ent->enemy->inuse) + return false; + + if (invasion->value && ent->enemy->mtype == INVASION_PLAYERSPAWN) + return false; + + if (!M_MonsterHasCombatSight(ent, ent->enemy)) + return false; + + ent->monsterinfo.aiflags &= ~AI_LOST_SIGHT; + ent->monsterinfo.search_frames = 0; + VectorCopy(ent->enemy->s.origin, ent->monsterinfo.last_sighting); + ent->monsterinfo.trail_time = level.time; + return true; +} + +static qboolean fly_has_recent_combat_memory(edict_t *ent); + +static qboolean fly_has_recent_combat_enemy(edict_t *ent) +{ + if (fly_has_visible_combat_enemy(ent)) + return true; + + return fly_has_recent_combat_memory(ent); +} + +static qboolean fly_has_recent_combat_memory(edict_t *ent) +{ + if (!ent->enemy || !ent->enemy->inuse) + return false; + + if (invasion->value && ent->enemy->mtype == INVASION_PLAYERSPAWN) + return false; + + if (ent->monsterinfo.aiflags & AI_LOST_SIGHT) + return false; + + return ent->monsterinfo.search_frames <= 3; +} + +static qboolean fly_has_lost_combat_goal(edict_t *ent, vec3_t dest) +{ + if (!ent->enemy || !ent->enemy->inuse) + return false; + + if (invasion->value && ent->enemy->mtype == INVASION_PLAYERSPAWN) + return false; + + if (!(ent->monsterinfo.aiflags & AI_LOST_SIGHT)) + return false; + + if (dest) + return true; + + if (!VectorCompare(ent->monsterinfo.last_sighting, vec3_origin)) + return true; + + return ent->goalentity && ent->goalentity->inuse && ent->goalentity == ent->enemy; +} + +static qboolean fly_can_separate_from(edict_t *ent, edict_t *other) +{ + if (!other || other == ent || !G_EntIsAlive(other)) + return false; + if (!(other->svflags & SVF_MONSTER) || other->solid == SOLID_NOT) + return false; + if (!(other->flags & (FL_FLY | FL_SWIM))) + return false; + if (!(other->monsterinfo.aiflags & AI_ALTERNATE_FLY)) + return false; + return true; +} + +static qboolean fly_apply_neighbor_separation(edict_t *ent, vec3_t wanted_pos) +{ + edict_t *touch[FLY_SEPARATION_MAX_NEIGHBORS]; + vec3_t mins, maxs, away, separation; + float dist, weight, push; + int i, num; + + VectorSet(mins, + ent->s.origin[0] - FLY_SEPARATION_RADIUS, + ent->s.origin[1] - FLY_SEPARATION_RADIUS, + ent->s.origin[2] - FLY_SEPARATION_RADIUS); + VectorSet(maxs, + ent->s.origin[0] + FLY_SEPARATION_RADIUS, + ent->s.origin[1] + FLY_SEPARATION_RADIUS, + ent->s.origin[2] + FLY_SEPARATION_RADIUS); + + num = gi.BoxEdicts(mins, maxs, touch, FLY_SEPARATION_MAX_NEIGHBORS, AREA_SOLID); + VectorClear(separation); + + for (i = 0; i < num; ++i) + { + edict_t *other = touch[i]; + + if (!fly_can_separate_from(ent, other)) + continue; + + VectorSubtract(ent->s.origin, other->s.origin, away); + away[2] *= FLY_SEPARATION_Z_SCALE; + dist = VectorNormalize(away); + if (dist > FLY_SEPARATION_RADIUS) + continue; + if (dist <= 0.1f) + { + fly_entity_fallback_dir(ent, away); + dist = 1.0f; + } + + weight = (FLY_SEPARATION_RADIUS - dist) / FLY_SEPARATION_RADIUS; + if (ent->enemy && other->enemy == ent->enemy) + weight *= 1.35f; + VectorMA(separation, weight, away, separation); + } + + push = VectorNormalize(separation); + if (push <= 0.1f) + return false; + + push = fly_clampf(push * FLY_SEPARATION_MAX_PUSH, 0.0f, FLY_SEPARATION_MAX_PUSH); + VectorMA(wanted_pos, push, separation, wanted_pos); + return true; +} + +static qboolean fly_use_alternate_step(edict_t *ent, vec3_t dest) +{ + if (!(ent->monsterinfo.aiflags & AI_ALTERNATE_FLY)) + return false; + + if (!fly_has_recent_combat_enemy(ent) && !fly_has_lost_combat_goal(ent, dest)) + return false; + + // Invasion destination movement is used by navis/base pathing and expects an immediate origin step. + if (dest && invasion->value && !fly_has_lost_combat_goal(ent, dest)) + return false; + + return true; +} + +static void fly_reset_alternate_step(edict_t *ent) +{ + VectorClear(ent->velocity); + ent->monsterinfo.fly_pinned = false; + ent->monsterinfo.fly_wall_stuck_time = 0.0f; +} + +static qboolean fly_has_last_sighting(edict_t *ent) +{ + return !VectorCompare(ent->monsterinfo.last_sighting, vec3_origin); +} + +static void fly_face_target(edict_t *ent, vec3_t target_origin) +{ + vec3_t target_dir; + + VectorSubtract(target_origin, ent->s.origin, target_dir); + if (VectorNormalize(target_dir) <= 0.1f) + return; + + ent->ideal_yaw = vectoyaw(target_dir); + M_ChangeYaw(ent); +} + +static qboolean fly_clamp_to_ceiling(edict_t *ent, vec3_t wanted_pos) +{ + vec3_t ceiling_check; + trace_t tr; + float desired_up; + float trace_height; + float highest_origin; + + desired_up = wanted_pos[2] - ent->s.origin[2]; + if (desired_up <= 1.0f) + return false; + + VectorCopy(ent->s.origin, ceiling_check); + trace_height = max(FLY_CEILING_LOOKAHEAD, desired_up + FLY_CEILING_CLEARANCE); + ceiling_check[2] += trace_height; + + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, ceiling_check, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (tr.fraction == 1.0f || tr.startsolid || tr.allsolid) + return false; + + highest_origin = tr.endpos[2] - FLY_CEILING_CLEARANCE; + if (wanted_pos[2] <= highest_origin) + return false; + + wanted_pos[2] = highest_origin; + if (ent->monsterinfo.fly_pinned) + ent->monsterinfo.fly_position_time = 0.0f; + ent->monsterinfo.fly_pinned = false; + return true; +} + +static qboolean fly_can_descend(edict_t *ent) +{ + vec3_t descent_check; + trace_t tr; + + VectorCopy(ent->s.origin, descent_check); + descent_check[2] -= ent->monsterinfo.fly_acceleration; + + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, descent_check, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + return tr.fraction == 1.0f && !tr.startsolid && !tr.allsolid; +} + +static qboolean fly_force_descent(edict_t *ent, vec3_t wanted_dir, float xy_scale) +{ + if (!fly_can_descend(ent)) + return false; + + ent->monsterinfo.fly_position_time = 0.0f; + ent->monsterinfo.fly_pinned = false; + wanted_dir[0] *= xy_scale; + wanted_dir[1] *= xy_scale; + wanted_dir[2] = -FLY_DESCENT_SPEED_MULTIPLIER; + VectorNormalize(wanted_dir); + return true; +} + +static qboolean fly_should_descend_for_lower_path(edict_t *ent, qboolean following_paths, vec3_t towards_origin, vec3_t wanted_pos) +{ + trace_t tr; + + if (!following_paths) + return false; + if (ent->s.origin[2] <= towards_origin[2] + FLY_BLOCKED_DESCENT_HEIGHT) + return false; + + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, wanted_pos, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + return tr.fraction < 1.0f && !tr.startsolid && !tr.allsolid; +} + +static qboolean fly_consider_stair_climb_dir(edict_t *ent, vec3_t candidate, vec3_t target_dir, + float *best_score, vec3_t best_dir) +{ + vec3_t dir, end; + trace_t tr; + float probe_dist, z_gain, score; + + VectorCopy(candidate, dir); + if (VectorNormalize(dir) <= 0.1f) + return false; + + probe_dist = max(FLY_STAIR_CLIMB_PROBE, ent->monsterinfo.fly_acceleration * 2.5f); + VectorMA(ent->s.origin, probe_dist, dir, end); + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (tr.startsolid || tr.allsolid || tr.fraction < FLY_STAIR_CLIMB_MIN_FRACTION) + return false; + + z_gain = tr.endpos[2] - ent->s.origin[2]; + if (z_gain < FLY_STAIR_CLIMB_MIN_GAIN) + return false; + + score = tr.fraction * 36.0f + z_gain * 0.25f + DotProduct(dir, target_dir) * 8.0f; + if (score <= *best_score) + return true; + + *best_score = score; + VectorCopy(dir, best_dir); + return true; +} + +static qboolean fly_try_climb_for_higher_target(edict_t *ent, vec3_t target_origin, vec3_t wanted_dir) +{ + vec3_t to_target, target_dir, candidate, best_dir; + float height_delta, horizontal_dist, best_score; + qboolean found; + + VectorSubtract(target_origin, ent->s.origin, to_target); + height_delta = to_target[2]; + if (height_delta < FLY_STAIR_CLIMB_MIN_HEIGHT || height_delta > FLY_STAIR_CLIMB_MAX_HEIGHT) + return false; + + VectorCopy(to_target, target_dir); + target_dir[2] = 0.0f; + horizontal_dist = VectorNormalize(target_dir); + if (horizontal_dist > FLY_STAIR_CLIMB_MAX_RANGE) + return false; + if (horizontal_dist <= 0.1f) + { + VectorCopy(wanted_dir, target_dir); + target_dir[2] = 0.0f; + if (VectorNormalize(target_dir) <= 0.1f) + return false; + } + + best_score = -9999.0f; + VectorClear(best_dir); + found = false; + + VectorScale(target_dir, FLY_STAIR_CLIMB_FWD_WEIGHT, candidate); + candidate[2] = FLY_STAIR_CLIMB_UP_WEIGHT; + found |= fly_consider_stair_climb_dir(ent, candidate, target_dir, &best_score, best_dir); + + VectorScale(target_dir, FLY_STAIR_CLIMB_FWD_WEIGHT * 0.45f, candidate); + candidate[2] = FLY_STAIR_CLIMB_UP_WEIGHT * 1.25f; + found |= fly_consider_stair_climb_dir(ent, candidate, target_dir, &best_score, best_dir); + + VectorCopy(wanted_dir, candidate); + candidate[2] += FLY_STAIR_CLIMB_UP_WEIGHT; + found |= fly_consider_stair_climb_dir(ent, candidate, target_dir, &best_score, best_dir); + + if (!found) + return false; + + VectorCopy(best_dir, wanted_dir); + ent->monsterinfo.fly_position_time = 0.0f; + ent->monsterinfo.fly_pinned = false; + return true; +} + +static void G_IdealHoverPosition(edict_t *ent, vec3_t out) +{ + float theta, phi, distance_scale; + vec3_t direction; + + VectorClear(out); + + if ((!ent->enemy && !(ent->monsterinfo.aiflags & AI_MEDIC)) || + (fly_following_path(ent) && !fly_has_visible_combat_enemy(ent))) + return; + + theta = random() * M_PI * 2.0f; + if (ent->monsterinfo.fly_above) + phi = acosf(0.7f + random() * 0.3f); + else if (ent->monsterinfo.fly_buzzard || (ent->monsterinfo.aiflags & AI_MEDIC)) + phi = acosf(random()); + else + phi = acosf(random() * 0.7f); + + VectorSet(direction, sinf(phi) * cosf(theta), sinf(phi) * sinf(theta), cosf(phi)); + + distance_scale = fly_frand_range(ent->monsterinfo.fly_min_distance, ent->monsterinfo.fly_max_distance); + VectorScale(direction, distance_scale, out); +} + +static qboolean fly_adjust_visible_combat_goal(edict_t *ent, vec3_t target_origin, vec3_t target_velocity, vec3_t wanted_pos, qboolean visible_combat_enemy) +{ + float current_dist, min_dist, max_dist, desired_dist, attack_dist, radial_speed, catchup_step, strafe_dist, min_height; + float attack_fraction, strafe_variance, orbit_phase, orbit; + float target_lateral_speed, target_lateral_speed_abs; + qboolean attack_drive, sliding_attack, force_drive, strafe_left, lower_target; + vec3_t away, ideal_offset, predicted_target, lateral; + + if (!visible_combat_enemy || ent->monsterinfo.fly_pinned) + return false; + + VectorSubtract(ent->s.origin, target_origin, away); + current_dist = VectorNormalize(away); + if (current_dist <= 0.1f) + return false; + + min_dist = max(0.0f, ent->monsterinfo.fly_min_distance); + max_dist = max(min_dist, ent->monsterinfo.fly_max_distance); + desired_dist = current_dist; + orbit_phase = level.time * (0.55f + fly_entity_unit(ent, 0x70u) * 0.65f) + + fly_entity_unit(ent, 0x71u) * M_PI * 2.0f; + orbit = sinf(orbit_phase); + attack_fraction = FLY_ATTACK_DISTANCE_FRACTION + ((fly_entity_unit(ent, 0x61u) - 0.5f) * 0.22f) + orbit * 0.08f; + attack_dist = fly_clampf(min_dist + ((max_dist - min_dist) * attack_fraction), min_dist, max_dist); + attack_drive = (ent->monsterinfo.attack_state == AS_STRAIGHT || ent->monsterinfo.attack_state == AS_SLIDING); + sliding_attack = (ent->monsterinfo.attack_state == AS_SLIDING); + lower_target = ent->s.origin[2] > target_origin[2] + FLY_LOWER_TARGET_DESCENT_HEIGHT; + force_drive = false; + + if (current_dist > max_dist + FLY_CATCHUP_MARGIN) + { + if (attack_drive) + desired_dist = attack_dist; + else + desired_dist = max(min_dist, max_dist * FLY_CATCHUP_DISTANCE_SCALE); + force_drive = true; + } + else + { + radial_speed = DotProduct(ent->velocity, away) - DotProduct(target_velocity, away); + if (radial_speed > 8.0f && current_dist > min_dist + FLY_CATCHUP_MARGIN) + { + catchup_step = max(FLY_CATCHUP_MARGIN, ent->monsterinfo.fly_speed * 0.35f); + if (attack_drive) + desired_dist = attack_dist; + else + desired_dist = fly_clampf(current_dist - catchup_step, min_dist, max_dist); + force_drive = true; + } + else if (attack_drive) + { + desired_dist = attack_dist; + force_drive = true; + } + else + { + return false; + } + } + + VectorScale(away, desired_dist, ideal_offset); + + if (attack_drive && !(ent->flags & FL_SWIM)) + { + min_height = FLY_COMBAT_MIN_HEIGHT; + if (ent->enemy && ent->enemy->viewheight > 0) + min_height = max(min_height, ent->enemy->viewheight + FLY_COMBAT_HEIGHT_ABOVE_VIEW); + min_height += fly_entity_unit(ent, 0x62u) * FLY_ATTACK_HEIGHT_VARIANCE; + if (ideal_offset[2] < min_height) + ideal_offset[2] = min_height; + } + if (attack_drive && lower_target && !(ent->flags & FL_SWIM)) + { + if (ideal_offset[2] > FLY_LOWER_TARGET_MIN_HEIGHT) + ideal_offset[2] = FLY_LOWER_TARGET_MIN_HEIGHT; + force_drive = true; + } + + if (attack_drive) + { + VectorSet(lateral, -away[1], away[0], 0.0f); + if (VectorNormalize(lateral) > 0.1f) + { + target_lateral_speed = DotProduct(target_velocity, lateral); + target_lateral_speed_abs = fabs(target_lateral_speed); + + if (target_lateral_speed_abs > FLY_TARGET_STRAFE_MIN_SPEED) + { + if (target_lateral_speed < 0.0f) + VectorNegate(lateral, lateral); + strafe_dist = fly_clampf(target_lateral_speed_abs * FLY_TARGET_STRAFE_DISTANCE_SCALE, + FLY_ATTACK_STRAFE_MIN, FLY_ATTACK_STRAFE_MAX); + } + else + { + strafe_left = orbit >= 0.0f; + if (fly_entity_unit(ent, 0x63u) > 0.5f) + strafe_left = !strafe_left; + if (!strafe_left) + VectorNegate(lateral, lateral); + strafe_dist = fly_clampf(ent->monsterinfo.fly_speed * + (sliding_attack ? FLY_ATTACK_STRAFE_SPEED_SCALE : FLY_ATTACK_DRIFT_SPEED_SCALE), + FLY_ATTACK_STRAFE_MIN, FLY_ATTACK_STRAFE_MAX); + } + + strafe_variance = 0.60f + fly_entity_unit(ent, 0x64u) * 0.65f + fabsf(orbit) * 0.35f; + strafe_dist = fly_clampf(strafe_dist * strafe_variance, FLY_ATTACK_STRAFE_MIN, FLY_ATTACK_STRAFE_MAX); + strafe_dist = fly_clampf(strafe_dist + orbit * FLY_ATTACK_ORBIT_DISTANCE, + FLY_ATTACK_STRAFE_MIN, FLY_ATTACK_STRAFE_MAX); + VectorMA(ideal_offset, strafe_dist, lateral, ideal_offset); + } + } + + VectorMA(target_origin, FLY_TARGET_LEAD_TIME, target_velocity, predicted_target); + VectorAdd(predicted_target, ideal_offset, wanted_pos); + VectorCopy(ideal_offset, ent->monsterinfo.fly_ideal_position); + + if (ent->monsterinfo.fly_position_time > level.time + 0.5f) + ent->monsterinfo.fly_position_time = level.time + 0.5f; + + return force_drive; +} + +qboolean SV_flystep_testvisposition(vec3_t start, vec3_t end, vec3_t starta, vec3_t startb, edict_t* ent) +{ + trace_t tr; + + tr = gi.trace(start, NULL, NULL, end, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (tr.fraction == 1.0f) + { + tr = gi.trace(starta, ent->mins, ent->maxs, startb, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (tr.fraction == 1.0f) + return true; + } + + return false; +} + +// Alternate flyers steer directly, so they need local side probes instead of +// Horde's temporary ai_run pursuit goals when LOS is about to be blocked. +static float fly_sight_fraction_to_point_from(edict_t *ent, vec3_t origin, vec3_t point) +{ + vec3_t start; + trace_t tr; + + VectorCopy(origin, start); + if (ent->viewheight) + start[2] += ent->viewheight; + else + start[2] += (ent->mins[2] + ent->maxs[2]) * 0.5f; + + if (!gi.inPVS(start, point)) + return 0.0f; + + tr = gi.trace(start, NULL, NULL, point, ent, MASK_SOLID); + if (tr.startsolid || tr.allsolid) + return 0.0f; + + return fly_clampf(tr.fraction, 0.0f, 1.0f); +} + +static float fly_combat_sight_fraction_from(edict_t *ent, vec3_t origin, edict_t *target) +{ + vec3_t point; + float best, fraction; + + if (!G_EntExists(target)) + return 0.0f; + + best = 0.0f; + + G_EntViewPoint(target, point); + fraction = fly_sight_fraction_to_point_from(ent, origin, point); + if (fraction > best) + best = fraction; + + G_EntMidPoint(target, point); + fraction = fly_sight_fraction_to_point_from(ent, origin, point); + if (fraction > best) + best = fraction; + + VectorCopy(target->s.origin, point); + fraction = fly_sight_fraction_to_point_from(ent, origin, point); + if (fraction > best) + best = fraction; + + return best; +} + +static float fly_predicted_combat_sight_fraction_from(edict_t *ent, vec3_t origin, edict_t *target, vec3_t target_velocity) +{ + vec3_t point; + float best, fraction; + + if (!G_EntExists(target)) + return 0.0f; + + best = 0.0f; + + G_EntViewPoint(target, point); + VectorMA(point, FLY_LOS_PREVENT_LEAD_TIME, target_velocity, point); + fraction = fly_sight_fraction_to_point_from(ent, origin, point); + if (fraction > best) + best = fraction; + + G_EntMidPoint(target, point); + VectorMA(point, FLY_LOS_PREVENT_LEAD_TIME, target_velocity, point); + fraction = fly_sight_fraction_to_point_from(ent, origin, point); + if (fraction > best) + best = fraction; + + VectorMA(target->s.origin, FLY_LOS_PREVENT_LEAD_TIME, target_velocity, point); + fraction = fly_sight_fraction_to_point_from(ent, origin, point); + if (fraction > best) + best = fraction; + + return best; +} + +static qboolean fly_recent_los_recovery_allowed(edict_t *ent, edict_t *target) +{ + if (!G_EntExists(target) || !fly_has_last_sighting(ent)) + return false; + + if (ent->monsterinfo.aiflags & AI_LOST_SIGHT) + return false; + + if (ent->monsterinfo.trail_time <= 0.0f || + level.time > ent->monsterinfo.trail_time + FLY_LOS_RECOVER_MEMORY_TIME) + return false; + + if (entdist(ent, target) > FLY_LOS_RECOVER_MAX_RANGE) + return false; + + if (distance(target->s.origin, ent->monsterinfo.last_sighting) > FLY_LOS_RECOVER_LAST_SEEN_RANGE) + return false; + + return true; +} + +static qboolean fly_los_side_vectors(edict_t *ent, vec3_t target_dir, vec3_t target_velocity, + vec3_t side, vec3_t other_side) +{ + vec3_t lateral; + float lateral_speed; + + VectorSet(lateral, -target_dir[1], target_dir[0], 0.0f); + if (VectorNormalize(lateral) <= 0.1f) + return false; + + lateral_speed = DotProduct(target_velocity, lateral); + if (lateral_speed < -FLY_LOS_SIDE_SWITCH_SPEED || + (fabsf(lateral_speed) <= FLY_LOS_SIDE_SWITCH_SPEED && fly_entity_unit(ent, 0x91u) < 0.5f)) + { + VectorNegate(lateral, lateral); + } + + VectorCopy(lateral, side); + VectorNegate(side, other_side); + return true; +} + +static qboolean fly_consider_los_preserve_dir(edict_t *ent, edict_t *target, vec3_t candidate, + vec3_t wanted_dir, float *best_score, vec3_t best_dir) +{ + vec3_t dir, end; + trace_t tr; + float probe_dist, sight, score; + + VectorCopy(candidate, dir); + if (VectorNormalize(dir) <= 0.1f) + return false; + + probe_dist = max(FLY_LOS_PRESERVE_PROBE, ent->monsterinfo.fly_acceleration * 2.5f); + VectorMA(ent->s.origin, probe_dist, dir, end); + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (tr.startsolid || tr.allsolid || tr.fraction < FLY_LOS_PRESERVE_MIN_FRACTION) + return false; + sight = fly_combat_sight_fraction_from(ent, tr.endpos, target); + if (sight < FLY_LOS_FULL_SIGHT_FRACTION) + return false; + + score = sight * 100.0f + tr.fraction * 28.0f + DotProduct(dir, wanted_dir) * 4.0f; + if (dir[2] < -0.1f) + score += 2.0f; + + if (score <= *best_score) + return true; + + *best_score = score; + VectorCopy(dir, best_dir); + return true; +} + +static qboolean fly_try_preserve_combat_sight(edict_t *ent, vec3_t towards_origin, vec3_t target_velocity, + vec3_t wanted_dir, vec3_t obstacle_normal, vec3_t out_dir) +{ + vec3_t to_target, side, other_side, down, wall_push, candidate, best_dir; + float best_score; + qboolean found; + + if (!ent->enemy || !ent->enemy->inuse) + return false; + + VectorSubtract(towards_origin, ent->s.origin, to_target); + to_target[2] = 0.0f; + if (VectorNormalize(to_target) <= 0.1f) + return false; + + if (!fly_los_side_vectors(ent, to_target, target_velocity, side, other_side)) + return false; + + VectorSet(down, 0.0f, 0.0f, -1.0f); + VectorCopy(obstacle_normal, wall_push); + wall_push[2] = min(wall_push[2], 0.0f); + if (VectorNormalize(wall_push) <= 0.1f) + VectorClear(wall_push); + + best_score = -9999.0f; + VectorClear(best_dir); + found = false; + + VectorScale(side, FLY_LOS_PRESERVE_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_FWD_WEIGHT, to_target, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_WALL_WEIGHT, wall_push, candidate); + found |= fly_consider_los_preserve_dir(ent, ent->enemy, candidate, wanted_dir, &best_score, best_dir); + + VectorScale(side, FLY_LOS_PRESERVE_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_DOWN_WEIGHT, down, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_WALL_WEIGHT, wall_push, candidate); + found |= fly_consider_los_preserve_dir(ent, ent->enemy, candidate, wanted_dir, &best_score, best_dir); + + VectorScale(side, FLY_LOS_PRESERVE_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_WALL_WEIGHT, wall_push, candidate); + found |= fly_consider_los_preserve_dir(ent, ent->enemy, candidate, wanted_dir, &best_score, best_dir); + + VectorScale(other_side, FLY_LOS_PRESERVE_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_FWD_WEIGHT, to_target, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_WALL_WEIGHT, wall_push, candidate); + found |= fly_consider_los_preserve_dir(ent, ent->enemy, candidate, wanted_dir, &best_score, best_dir); + + VectorScale(other_side, FLY_LOS_PRESERVE_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_DOWN_WEIGHT, down, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_WALL_WEIGHT, wall_push, candidate); + found |= fly_consider_los_preserve_dir(ent, ent->enemy, candidate, wanted_dir, &best_score, best_dir); + + VectorScale(to_target, FLY_LOS_PRESERVE_FWD_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_DOWN_WEIGHT, down, candidate); + VectorMA(candidate, FLY_LOS_PRESERVE_WALL_WEIGHT, wall_push, candidate); + found |= fly_consider_los_preserve_dir(ent, ent->enemy, candidate, wanted_dir, &best_score, best_dir); + + if (!found) + return false; + + VectorCopy(best_dir, out_dir); + return true; +} + +static qboolean fly_consider_los_prevent_dir(edict_t *ent, edict_t *target, vec3_t target_velocity, + vec3_t candidate, vec3_t wanted_dir, float current_future_sight, float preference, + float *best_score, vec3_t best_dir) +{ + vec3_t dir, end; + trace_t tr; + float probe_dist, future_sight, actual_sight, score; + + VectorCopy(candidate, dir); + if (VectorNormalize(dir) <= 0.1f) + return false; + + probe_dist = max(FLY_LOS_PRESERVE_PROBE, ent->monsterinfo.fly_acceleration * 3.0f); + VectorMA(ent->s.origin, probe_dist, dir, end); + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (tr.startsolid || tr.allsolid || tr.fraction < FLY_LOS_PRESERVE_MIN_FRACTION) + return false; + + future_sight = fly_predicted_combat_sight_fraction_from(ent, tr.endpos, target, target_velocity); + actual_sight = fly_combat_sight_fraction_from(ent, tr.endpos, target); + if (actual_sight < FLY_LOS_FULL_SIGHT_FRACTION && + future_sight < current_future_sight + FLY_LOS_PREVENT_MIN_GAIN) + return false; + if (future_sight < current_future_sight + FLY_LOS_PREVENT_MIN_GAIN && + future_sight < FLY_LOS_FULL_SIGHT_FRACTION) + return false; + + score = future_sight * 100.0f + actual_sight * 30.0f + tr.fraction * 24.0f + + DotProduct(dir, wanted_dir) * 4.0f + preference; + if (dir[2] < -0.1f) + score += 1.5f; + + if (score <= *best_score) + return true; + + *best_score = score; + VectorCopy(dir, best_dir); + return true; +} + +static qboolean fly_try_prevent_combat_sight_loss(edict_t *ent, vec3_t target_origin, vec3_t target_velocity, + vec3_t wanted_dir, vec3_t out_dir) +{ + vec3_t to_target, side, other_side, down, candidate, best_dir, intended_end; + trace_t tr; + float current_future_sight, intended_future_sight, best_score, side_preference, other_preference, probe_dist; + qboolean found; + + if (!G_EntExists(ent->enemy)) + return false; + + current_future_sight = fly_predicted_combat_sight_fraction_from(ent, ent->s.origin, ent->enemy, target_velocity); + intended_future_sight = current_future_sight; + probe_dist = max(FLY_LOS_PREVENT_PROBE, ent->monsterinfo.fly_acceleration * 3.0f); + VectorMA(ent->s.origin, probe_dist, wanted_dir, intended_end); + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, intended_end, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (!tr.startsolid && !tr.allsolid && tr.fraction >= FLY_LOS_PRESERVE_MIN_FRACTION) + intended_future_sight = fly_predicted_combat_sight_fraction_from(ent, tr.endpos, ent->enemy, target_velocity); + + if (current_future_sight >= FLY_LOS_PREVENT_THRESHOLD && + intended_future_sight >= current_future_sight - FLY_LOS_PREVENT_ALLOWED_DROP) + return false; + current_future_sight = min(current_future_sight, intended_future_sight); + + VectorSubtract(target_origin, ent->s.origin, to_target); + to_target[2] = 0.0f; + if (VectorNormalize(to_target) <= 0.1f) + return false; + + if (!fly_los_side_vectors(ent, to_target, target_velocity, side, other_side)) + return false; + + VectorSet(down, 0.0f, 0.0f, -1.0f); + VectorClear(best_dir); + best_score = -9999.0f; + side_preference = 5.0f + fly_entity_unit(ent, 0x94u); + other_preference = fly_entity_unit(ent, 0x95u); + found = false; + + VectorScale(side, FLY_LOS_PREVENT_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PREVENT_FWD_WEIGHT, to_target, candidate); + found |= fly_consider_los_prevent_dir(ent, ent->enemy, target_velocity, candidate, to_target, + current_future_sight, side_preference, &best_score, best_dir); + + VectorScale(side, FLY_LOS_PREVENT_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PREVENT_FWD_WEIGHT * 0.45f, to_target, candidate); + VectorMA(candidate, FLY_LOS_PREVENT_DOWN_WEIGHT, down, candidate); + found |= fly_consider_los_prevent_dir(ent, ent->enemy, target_velocity, candidate, to_target, + current_future_sight, side_preference * 0.75f, &best_score, best_dir); + + VectorScale(other_side, FLY_LOS_PREVENT_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PREVENT_FWD_WEIGHT, to_target, candidate); + found |= fly_consider_los_prevent_dir(ent, ent->enemy, target_velocity, candidate, to_target, + current_future_sight, other_preference, &best_score, best_dir); + + VectorScale(other_side, FLY_LOS_PREVENT_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_PREVENT_FWD_WEIGHT * 0.45f, to_target, candidate); + VectorMA(candidate, FLY_LOS_PREVENT_DOWN_WEIGHT, down, candidate); + found |= fly_consider_los_prevent_dir(ent, ent->enemy, target_velocity, candidate, to_target, + current_future_sight, other_preference * 0.75f, &best_score, best_dir); + + if (!found) + return false; + + VectorCopy(best_dir, out_dir); + return true; +} + +static qboolean fly_consider_los_recover_dir(edict_t *ent, edict_t *target, vec3_t candidate, + vec3_t wanted_dir, float current_sight, float preference, float *best_score, vec3_t best_dir) +{ + vec3_t dir, end; + trace_t tr; + float probe_dist, sight, score; + + VectorCopy(candidate, dir); + if (VectorNormalize(dir) <= 0.1f) + return false; + + probe_dist = max(FLY_LOS_RECOVER_PROBE, ent->monsterinfo.fly_acceleration * 3.0f); + VectorMA(ent->s.origin, probe_dist, dir, end); + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (tr.startsolid || tr.allsolid || tr.fraction < FLY_LOS_PRESERVE_MIN_FRACTION) + return false; + + sight = fly_combat_sight_fraction_from(ent, tr.endpos, target); + if (sight < FLY_LOS_FULL_SIGHT_FRACTION && sight < current_sight + FLY_LOS_RECOVER_MIN_GAIN) + return false; + + score = sight * 100.0f + tr.fraction * 24.0f + DotProduct(dir, wanted_dir) * 5.0f + preference; + if (dir[2] < -0.1f) + score += 2.0f; + + if (score <= *best_score) + return true; + + *best_score = score; + VectorCopy(dir, best_dir); + return true; +} + +static qboolean fly_try_recover_combat_sight(edict_t *ent, vec3_t target_origin, vec3_t target_velocity, + vec3_t wanted_dir, vec3_t out_dir) +{ + vec3_t to_target, target_dir, side, other_side, down, candidate, point, best_dir; + trace_t tr; + float target_dist, current_sight, side_preference, other_preference; + float block_fraction, temp_dist, side_offset; + qboolean found; + + if (!fly_recent_los_recovery_allowed(ent, ent->enemy)) + return false; + + current_sight = fly_combat_sight_fraction_from(ent, ent->s.origin, ent->enemy); + if (current_sight >= FLY_LOS_FULL_SIGHT_FRACTION) + return false; + + VectorSubtract(target_origin, ent->s.origin, to_target); + target_dist = VectorNormalize(to_target); + if (target_dist <= 0.1f) + return false; + + VectorCopy(to_target, target_dir); + target_dir[2] = 0.0f; + if (VectorNormalize(target_dir) <= 0.1f) + { + VectorCopy(wanted_dir, target_dir); + target_dir[2] = 0.0f; + if (VectorNormalize(target_dir) <= 0.1f) + return false; + } + + if (!fly_los_side_vectors(ent, target_dir, target_velocity, side, other_side)) + return false; + + VectorSet(down, 0.0f, 0.0f, -1.0f); + + found = false; + VectorClear(best_dir); + side_preference = 4.0f + fly_entity_unit(ent, 0x92u); + other_preference = fly_entity_unit(ent, 0x93u); + current_sight = max(current_sight, 0.0f); + { + float best_score = -9999.0f; + + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, target_origin, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (!tr.startsolid && !tr.allsolid && tr.fraction < 1.0f) + { + block_fraction = fly_clampf(tr.fraction, 0.0f, 1.0f); + temp_dist = target_dist * ((block_fraction + 1.0f) * 0.5f); + temp_dist = fly_clampf(temp_dist, FLY_LOS_RECOVER_PROBE, target_dist); + side_offset = max(max(fabsf(ent->maxs[0]), fabsf(ent->maxs[1])) + 24.0f, 32.0f); + + VectorMA(ent->s.origin, temp_dist, target_dir, point); + VectorMA(point, side_offset, side, point); + VectorSubtract(point, ent->s.origin, candidate); + found |= fly_consider_los_recover_dir(ent, ent->enemy, candidate, target_dir, current_sight, + side_preference + 2.0f, &best_score, best_dir); + + VectorMA(ent->s.origin, temp_dist, target_dir, point); + VectorMA(point, side_offset, other_side, point); + VectorSubtract(point, ent->s.origin, candidate); + found |= fly_consider_los_recover_dir(ent, ent->enemy, candidate, target_dir, current_sight, + other_preference + 2.0f, &best_score, best_dir); + } + + VectorScale(side, FLY_LOS_RECOVER_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_RECOVER_FWD_WEIGHT, target_dir, candidate); + found |= fly_consider_los_recover_dir(ent, ent->enemy, candidate, target_dir, current_sight, + side_preference, &best_score, best_dir); + + VectorScale(side, FLY_LOS_RECOVER_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_RECOVER_FWD_WEIGHT * 0.45f, target_dir, candidate); + VectorMA(candidate, FLY_LOS_RECOVER_DOWN_WEIGHT, down, candidate); + found |= fly_consider_los_recover_dir(ent, ent->enemy, candidate, target_dir, current_sight, + side_preference * 0.75f, &best_score, best_dir); + + VectorScale(other_side, FLY_LOS_RECOVER_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_RECOVER_FWD_WEIGHT, target_dir, candidate); + found |= fly_consider_los_recover_dir(ent, ent->enemy, candidate, target_dir, current_sight, + other_preference, &best_score, best_dir); + + VectorScale(other_side, FLY_LOS_RECOVER_SIDE_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_RECOVER_FWD_WEIGHT * 0.45f, target_dir, candidate); + VectorMA(candidate, FLY_LOS_RECOVER_DOWN_WEIGHT, down, candidate); + found |= fly_consider_los_recover_dir(ent, ent->enemy, candidate, target_dir, current_sight, + other_preference * 0.75f, &best_score, best_dir); + + VectorScale(target_dir, FLY_LOS_RECOVER_FWD_WEIGHT, candidate); + VectorMA(candidate, FLY_LOS_RECOVER_DOWN_WEIGHT, down, candidate); + found |= fly_consider_los_recover_dir(ent, ent->enemy, candidate, target_dir, current_sight, + 0.0f, &best_score, best_dir); + } + + if (!found) + return false; + + VectorCopy(best_dir, out_dir); + return true; +} + +qboolean SV_alternate_flystep(edict_t* ent, vec3_t dest, vec3_t move, qboolean relink) +{ + vec3_t dir, towards_origin, towards_velocity, wanted_pos, dest_diff, wanted_dir, final_dir; + vec3_t trace_end, aim_fwd, aim_rgt, aim_up, yaw_angles; + vec3_t box_mins, box_maxs; + trace_t tr; + float current_speed, dist_to_wanted, turn_factor, base_fly_speed, accel, speed_factor, wanted_speed; + qboolean following_paths, have_target, bad_movement_direction, visible_combat_enemy, recent_combat_memory, catchup_goal, combat_attack_drive, los_preserve_drive, los_recover_drive, los_pressure_drive; + + (void)move; + (void)relink; + + if ((ent->flags & FL_SWIM) && ent->waterlevel < WATER_WAIST) + return true; + + if (ent->monsterinfo.fly_speed <= 0.0f || ent->monsterinfo.fly_acceleration <= 0.0f) + return false; + + if (ent->monsterinfo.fly_max_distance < ent->monsterinfo.fly_min_distance) + ent->monsterinfo.fly_max_distance = ent->monsterinfo.fly_min_distance; + + if (ent->monsterinfo.fly_position_time <= level.time || + (ent->enemy && ent->monsterinfo.fly_pinned && !visible(ent, ent->enemy)) || + (ent->enemy && VectorCompare(ent->monsterinfo.fly_ideal_position, vec3_origin))) + { + ent->monsterinfo.fly_pinned = false; + ent->monsterinfo.fly_position_time = level.time + fly_frand_range(FLY_POSITION_UPDATE_MIN, FLY_POSITION_UPDATE_MAX); + G_IdealHoverPosition(ent, ent->monsterinfo.fly_ideal_position); + } + + VectorCopy(ent->velocity, dir); + current_speed = VectorNormalize(dir); + if (current_speed < 0.1f) + VectorClear(dir); + + VectorClear(towards_origin); + VectorClear(towards_velocity); + visible_combat_enemy = fly_has_visible_combat_enemy(ent); + recent_combat_memory = visible_combat_enemy || fly_has_recent_combat_memory(ent); + following_paths = fly_following_path(ent) && !visible_combat_enemy; + have_target = false; + los_preserve_drive = false; + los_recover_drive = false; + if (dest) { - idealZ = dest[2] + 16; - VectorCopy(dest, goalpos); + VectorCopy(dest, towards_origin); + following_paths = true; + have_target = true; } - else if (ent->goalentity) + else if (following_paths) { - if (ent->goalentity == world) - return true; - idealZ = ent->goalentity->s.origin[2] + 16; - VectorCopy(ent->goalentity->s.origin, goalpos); + if (ent->goalentity && ent->goalentity->inuse) + { + VectorCopy(ent->goalentity->s.origin, towards_origin); + have_target = true; + } + else if (fly_has_last_sighting(ent)) + { + VectorCopy(ent->monsterinfo.last_sighting, towards_origin); + have_target = true; + } } - else - return true; // no goal, no destination - probably idle - // if we can see our enemy, stay above him - if (ent->enemy && visible(ent, ent->enemy) && entdist(ent, ent->enemy) <= 256) - idealZ = ent->enemy->absmax[2] + 48; - // goal position is obstructed, so vertical movement should fail if we are obstructed - //else if (!G_IsClearPath(ent, MASK_SHOT, ent->s.origin, goalpos)) - // stopOnCollision = true; - //gi.dprintf("idealZ @ %.0f ", idealZ); - // check floor height - VectorCopy(ent->s.origin, end); - end[2] -= 8192; - trace = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_MONSTERSOLID); - // always stay above the floor, regardless of goal/destination - if (idealZ < trace.endpos[2] + 16) - idealZ = trace.endpos[2] + 16; - //gi.dprintf("floor @ %.0f position @ %.0f\n", trace.endpos[2], ent->s.origin[2]); - dz = ent->s.origin[2] - idealZ; - VectorCopy(ent->s.origin, end); - if (dz > 0) // we are above our target + else if (!visible_combat_enemy && recent_combat_memory && fly_has_last_sighting(ent)) { - if (dz > zSpeed) - dz = zSpeed; - end[2] -= dz; + VectorCopy(ent->monsterinfo.last_sighting, towards_origin); + following_paths = true; + have_target = true; } - else if (dz < 0) // we are below our target + else if (ent->enemy && ent->enemy->inuse) { - dz = fabs(dz); - if (dz > zSpeed) - dz = zSpeed; - end[2] += dz; + VectorCopy(ent->enemy->s.origin, towards_origin); + VectorCopy(ent->enemy->velocity, towards_velocity); + have_target = true; } - else // we at the correct Z height, so we're done + else if (ent->goalentity && ent->goalentity->inuse && ent->goalentity != world) + { + VectorCopy(ent->goalentity->s.origin, towards_origin); + have_target = true; + } + if (!have_target) + { + accel = ent->monsterinfo.fly_acceleration; + if (current_speed > 0.0f) + current_speed = max(0.0f, current_speed - accel); + + if (current_speed > 0.0f) + VectorScale(dir, current_speed, ent->velocity); + else + VectorClear(ent->velocity); return true; - trace = gi.trace(ent->s.origin, ent->mins, ent->maxs, end, ent, MASK_MONSTERSOLID); - if (trace.fraction == 1) + } + + if (ent->monsterinfo.fly_pinned) + VectorCopy(ent->monsterinfo.fly_ideal_position, wanted_pos); + else if (following_paths) + VectorCopy(towards_origin, wanted_pos); + else { - VectorCopy(trace.endpos, neworg); - /*if (relink) + VectorMA(towards_origin, FLY_TARGET_LEAD_TIME, towards_velocity, wanted_pos); + VectorAdd(wanted_pos, ent->monsterinfo.fly_ideal_position, wanted_pos); + } + if (following_paths) + catchup_goal = false; + else + catchup_goal = fly_adjust_visible_combat_goal(ent, towards_origin, towards_velocity, wanted_pos, visible_combat_enemy); + if (visible_combat_enemy && !following_paths) + fly_apply_neighbor_separation(ent, wanted_pos); + + VectorSet(box_mins, -8.0f, -8.0f, -8.0f); + VectorSet(box_maxs, 8.0f, 8.0f, 8.0f); + tr = gi.trace(towards_origin, box_mins, box_maxs, wanted_pos, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (!tr.allsolid) + VectorCopy(tr.endpos, wanted_pos); + fly_clamp_to_ceiling(ent, wanted_pos); + VectorSubtract(wanted_pos, ent->s.origin, dest_diff); + if (dest_diff[2] > ent->mins[2] && dest_diff[2] < ent->maxs[2]) + dest_diff[2] = 0.0f; + + VectorCopy(dest_diff, wanted_dir); + dist_to_wanted = VectorNormalize(wanted_dir); + if (dist_to_wanted > 0.1f && + fly_should_descend_for_lower_path(ent, following_paths, towards_origin, wanted_pos)) + fly_force_descent(ent, wanted_dir, FLY_BLOCKED_DESCENT_XY_SCALE); + + if (dist_to_wanted > 0.1f && (visible_combat_enemy || following_paths || recent_combat_memory)) + { + vec3_t climb_target; + + if (visible_combat_enemy && ent->enemy && ent->enemy->inuse) + VectorCopy(ent->enemy->s.origin, climb_target); + else + VectorCopy(towards_origin, climb_target); + + if (fly_try_climb_for_higher_target(ent, climb_target, wanted_dir)) + catchup_goal = true; + } + + if (visible_combat_enemy && dist_to_wanted > 0.1f && + ent->enemy && ent->enemy->inuse && + fly_try_prevent_combat_sight_loss(ent, ent->enemy->s.origin, ent->enemy->velocity, wanted_dir, wanted_dir)) + { + los_preserve_drive = true; + ent->monsterinfo.fly_position_time = 0.0f; + ent->monsterinfo.fly_pinned = false; + } + else if (!visible_combat_enemy && recent_combat_memory && dist_to_wanted > 0.1f && + ent->enemy && ent->enemy->inuse && + fly_try_recover_combat_sight(ent, ent->enemy->s.origin, ent->enemy->velocity, wanted_dir, wanted_dir)) + { + los_recover_drive = true; + ent->monsterinfo.fly_position_time = 0.0f; + ent->monsterinfo.fly_pinned = false; + } + + if (visible_combat_enemy) + { + fly_face_target(ent, ent->enemy->s.origin); + M_ChangeYaw(ent); + } + else if (los_recover_drive && ent->enemy && ent->enemy->inuse) + { + fly_face_target(ent, ent->enemy->s.origin); + M_ChangeYaw(ent); + } + else + { + fly_face_target(ent, towards_origin); + M_ChangeYaw(ent); + } + + if (dist_to_wanted > 0.1f) + { + VectorMA(ent->s.origin, ent->monsterinfo.fly_acceleration, wanted_dir, trace_end); + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, trace_end, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + } + else + { + tr.fraction = 1.0f; + } + + VectorSet(yaw_angles, 0.0f, ent->s.angles[YAW], 0.0f); + AngleVectors(yaw_angles, aim_fwd, aim_rgt, aim_up); + + if (tr.fraction < 0.25f && dist_to_wanted > 0.1f) + { + vec3_t bottom_pos, top_pos, startb, floor_check, side_offset, lateral_offset, left_start, right_start, obstacle_normal; + qboolean bottom_visible, top_visible, left_visible, right_visible, force_descent; + + if (ent->monsterinfo.fly_wall_stuck_time == 0.0f) + ent->monsterinfo.fly_wall_stuck_time = level.time; + + VectorCopy(tr.plane.normal, obstacle_normal); + + VectorCopy(ent->s.origin, bottom_pos); + bottom_pos[2] += ent->mins[2]; + VectorCopy(ent->s.origin, top_pos); + top_pos[2] += ent->maxs[2]; + + VectorCopy(ent->s.origin, startb); + startb[2] += ent->mins[2] - ent->monsterinfo.fly_acceleration; + bottom_visible = SV_flystep_testvisposition(bottom_pos, wanted_pos, ent->s.origin, startb, ent); + + VectorCopy(top_pos, startb); + startb[2] += ent->monsterinfo.fly_acceleration; + top_visible = SV_flystep_testvisposition(top_pos, wanted_pos, ent->s.origin, startb, ent); + + force_descent = false; + if (visible_combat_enemy && + fly_try_preserve_combat_sight(ent, towards_origin, towards_velocity, wanted_dir, obstacle_normal, wanted_dir)) { - gi.linkentity (ent); - G_TouchTriggers (ent); - }*/ - return true; + los_preserve_drive = true; + ent->monsterinfo.fly_position_time = 0.0f; + ent->monsterinfo.fly_pinned = false; + } + else if ((following_paths || visible_combat_enemy) && + ent->s.origin[2] > towards_origin[2] + FLY_BLOCKED_DESCENT_HEIGHT && + fly_force_descent(ent, wanted_dir, FLY_BLOCKED_DESCENT_XY_SCALE)) + { + force_descent = true; + } + else if (level.time > ent->monsterinfo.fly_wall_stuck_time + + (visible_combat_enemy ? FLY_COMBAT_WALL_STUCK_THRESHOLD : FLY_WALL_STUCK_THRESHOLD)) + { + VectorCopy(ent->s.origin, floor_check); + floor_check[2] -= 512.0f; + tr = gi.trace(ent->s.origin, ent->mins, ent->maxs, floor_check, ent, MASK_SOLID | CONTENTS_MONSTERCLIP); + if (tr.fraction < 1.0f && fly_force_descent(ent, wanted_dir, 0.3f)) + { + force_descent = true; + } + } + + if (!los_preserve_drive && !force_descent) + { + if (bottom_visible == top_visible) + { + side_offset[0] = aim_fwd[0] * ent->maxs[0]; + side_offset[1] = aim_fwd[1] * ent->maxs[1]; + side_offset[2] = aim_fwd[2] * ent->maxs[2]; + lateral_offset[0] = aim_rgt[0] * ent->maxs[0]; + lateral_offset[1] = aim_rgt[1] * ent->maxs[1]; + lateral_offset[2] = aim_rgt[2] * ent->maxs[2]; + + VectorAdd(ent->s.origin, side_offset, left_start); + VectorSubtract(left_start, lateral_offset, left_start); + VectorAdd(ent->s.origin, side_offset, right_start); + VectorAdd(right_start, lateral_offset, right_start); + + left_visible = gi.trace(left_start, NULL, NULL, wanted_pos, ent, MASK_SOLID | CONTENTS_MONSTERCLIP).fraction == 1.0f; + right_visible = gi.trace(right_start, NULL, NULL, wanted_pos, ent, MASK_SOLID | CONTENTS_MONSTERCLIP).fraction == 1.0f; + + if (left_visible != right_visible) + { + if (right_visible) + VectorAdd(wanted_dir, aim_rgt, wanted_dir); + else + VectorSubtract(wanted_dir, aim_rgt, wanted_dir); + } + else + { + VectorCopy(obstacle_normal, wanted_dir); + wanted_dir[2] -= 0.2f; + } + } + else + { + if (top_visible) + VectorAdd(wanted_dir, aim_up, wanted_dir); + else + VectorSubtract(wanted_dir, aim_up, wanted_dir); + } + VectorNormalize(wanted_dir); + } } - return false; + else + { + ent->monsterinfo.fly_wall_stuck_time = 0.0f; + } + + bad_movement_direction = false; + if (dist_to_wanted > 0.1f) + { + vec3_t contents_test; + VectorMA(ent->s.origin, max(current_speed, ent->monsterinfo.fly_speed) * FRAMETIME, wanted_dir, contents_test); + if ((ent->flags & FL_FLY) && ent->waterlevel < 3) + bad_movement_direction = (gi.pointcontents(contents_test) & MASK_WATER) != 0; + else if (ent->flags & FL_SWIM) + bad_movement_direction = (gi.pointcontents(contents_test) & MASK_WATER) == 0; + } + + if (bad_movement_direction) + { + if (ent->monsterinfo.fly_recovery_time < level.time) + { + VectorSet(ent->monsterinfo.fly_recovery_dir, crandom(), crandom(), crandom()); + if (VectorNormalize(ent->monsterinfo.fly_recovery_dir) < 0.1f) + VectorSet(ent->monsterinfo.fly_recovery_dir, 0.0f, 0.0f, 1.0f); + ent->monsterinfo.fly_recovery_time = level.time + 1.0f; + } + VectorCopy(ent->monsterinfo.fly_recovery_dir, wanted_dir); + } + + los_pressure_drive = los_preserve_drive || los_recover_drive; + if (dir[0] || dir[1] || dir[2]) + { + float dir_dot = DotProduct(dir, wanted_dir); + + if (los_pressure_drive && dir_dot < FLY_LOS_PRESSURE_SNAP_DOT) + { + VectorCopy(wanted_dir, final_dir); + } + else if (los_pressure_drive) + { + turn_factor = FLY_LOS_PRESERVE_TURN_FACTOR; + fly_scale_add(final_dir, dir, turn_factor, wanted_dir, 1.0f - turn_factor); + if (VectorNormalize(final_dir) < 0.1f) + VectorCopy(wanted_dir, final_dir); + } + else if (catchup_goal && dir_dot < 0.0f) + { + VectorCopy(wanted_dir, final_dir); + } + else if (catchup_goal && dir_dot < 0.7f) + { + turn_factor = 0.2f; + fly_scale_add(final_dir, dir, turn_factor, wanted_dir, 1.0f - turn_factor); + if (VectorNormalize(final_dir) < 0.1f) + VectorCopy(wanted_dir, final_dir); + } + else if (((ent->monsterinfo.fly_thrusters && !ent->monsterinfo.fly_pinned) || following_paths) && + DotProduct(dir, wanted_dir) > 0.0f) + { + turn_factor = FLY_TURN_FACTOR_FAST; + fly_scale_add(final_dir, dir, turn_factor, wanted_dir, 1.0f - turn_factor); + if (VectorNormalize(final_dir) < 0.1f) + VectorCopy(wanted_dir, final_dir); + } + else + { + turn_factor = min(1.0f, FLY_TURN_FACTOR_BASE + + (FLY_TURN_FACTOR_SPEED_SCALE * (current_speed / ent->monsterinfo.fly_speed))); + fly_scale_add(final_dir, dir, turn_factor, wanted_dir, 1.0f - turn_factor); + if (VectorNormalize(final_dir) < 0.1f) + VectorCopy(wanted_dir, final_dir); + } + } + else + { + VectorCopy(wanted_dir, final_dir); + } + + base_fly_speed = ent->monsterinfo.fly_speed; + accel = ent->monsterinfo.fly_acceleration; + combat_attack_drive = visible_combat_enemy && + (ent->monsterinfo.attack_state == AS_STRAIGHT || ent->monsterinfo.attack_state == AS_SLIDING); + if (combat_attack_drive) + { + base_fly_speed *= FLY_ATTACK_SPEED_SCALE; + accel *= FLY_ATTACK_ACCEL_SCALE; + } + if (los_pressure_drive) + { + base_fly_speed *= FLY_LOS_PRESERVE_SPEED_SCALE; + accel *= FLY_LOS_PRESERVE_ACCEL_SCALE; + } + + if (!ent->enemy || (ent->monsterinfo.fly_thrusters && !ent->monsterinfo.fly_pinned) || following_paths || catchup_goal) + { + if (following_paths && (dir[0] || dir[1] || dir[2]) && DotProduct(wanted_dir, dir) < -0.25f) + speed_factor = 0.0f; + else + speed_factor = 1.0f; + } + else + { + speed_factor = min(1.0f, dist_to_wanted / base_fly_speed); + } + + if (bad_movement_direction) + speed_factor = -speed_factor; + + if (DotProduct(final_dir, wanted_dir) < 0.25f) + accel *= 2.0f; + + wanted_speed = base_fly_speed * speed_factor; + if (current_speed > wanted_speed) + current_speed = max(wanted_speed, current_speed - accel); + else if (current_speed < wanted_speed) + current_speed = min(wanted_speed, current_speed + accel); + + if (!fly_vector_valid(final_dir) || !isfinite(current_speed)) + return false; + + VectorScale(final_dir, current_speed, ent->velocity); + + if (ent->enemy && (ent->monsterinfo.fly_buzzard || (ent->monsterinfo.aiflags & AI_MEDIC))) + { + vec3_t pitch_dir, pitch_angles, pitch_target; + if (visible_combat_enemy) + VectorCopy(ent->enemy->s.origin, pitch_target); + else + VectorCopy(towards_origin, pitch_target); + + VectorSubtract(ent->s.origin, pitch_target, pitch_dir); + if (VectorNormalize(pitch_dir) > 0.1f) + { + vectoangles(pitch_dir, pitch_angles); + ent->s.angles[PITCH] = LerpAngle(ent->s.angles[PITCH], -pitch_angles[PITCH], FRAMETIME * FLY_PITCH_LERP_SPEED); + } + } + else + { + ent->s.angles[PITCH] = 0.0f; + } + + return true; } + qboolean M_FlyMove(edict_t* ent, vec3_t dest, vec3_t move, qboolean relink) { //float dz; // delta Z @@ -413,6 +1938,22 @@ qboolean M_FlyMove(edict_t* ent, vec3_t dest, vec3_t move, qboolean relink) float zSpeed = 8; // Z movement speed trace_t trace; vec3_t neworg1, neworg2; + + if (ent->monsterinfo.aiflags & AI_ALTERNATE_FLY) + { + if (fly_use_alternate_step(ent, dest)) + { + qboolean use_path_dest = dest && !fly_has_visible_combat_enemy(ent); + + // Once a flyer has visible combat contact, let the alternate + // fly logic steer by enemy/hover offset instead of parking on path nodes. + if (SV_alternate_flystep(ent, use_path_dest ? dest : NULL, move, relink)) + return true; + } + + fly_reset_alternate_step(ent); + } + if (!M_MoveVertical(ent, dest, neworg1)) { //gi.dprintf("movevertical failed\n"); @@ -485,6 +2026,19 @@ qboolean SV_movestep(edict_t* ent, vec3_t dest, vec3_t move, qboolean relink) { if (ent->flags & FL_FLY) return M_FlyMove(ent, dest, move, relink); + + if (fly_use_alternate_step(ent, dest)) + { + if (SV_alternate_flystep(ent, NULL, move, relink)) + return true; + + fly_reset_alternate_step(ent); + } + else if (ent->monsterinfo.aiflags & AI_ALTERNATE_FLY) + { + fly_reset_alternate_step(ent); + } + // gi.dprintf("trying to swim\n"); // try one move with vertical motion, then one without for (i = 0; i < 2; i++) @@ -1103,6 +2657,41 @@ void SV_MoveRandom(edict_t* actor, vec3_t dest, float dist) } } +static qboolean SV_FlyIdleWallDodge(edict_t *actor, float dist) +{ + vec3_t start, end, forward, wall_angles; + trace_t tr; + float yaw1, yaw2; + + if (!(actor->flags & FL_FLY)) + return false; + + AngleVectors(actor->s.angles, forward, NULL, NULL); + G_EntMidPoint(actor, start); + VectorMA(start, max(64.0f, fabsf(dist) * 4.0f), forward, end); + tr = gi.trace(start, NULL, NULL, end, actor, MASK_MONSTERSOLID); + if (tr.fraction == 1.0f || tr.startsolid || tr.allsolid) + return false; + + vectoangles(tr.plane.normal, wall_angles); + yaw1 = wall_angles[YAW] + 90.0f; + yaw2 = wall_angles[YAW] - 90.0f; + AngleCheck(&yaw1); + AngleCheck(&yaw2); + + actor->monsterinfo.lefty = 1 - actor->monsterinfo.lefty; + if (actor->monsterinfo.lefty) + { + if (SV_StepDirection(actor, NULL, yaw1, dist, true)) + return true; + return SV_StepDirection(actor, NULL, yaw2, dist, true); + } + + if (SV_StepDirection(actor, NULL, yaw2, dist, true)) + return true; + return SV_StepDirection(actor, NULL, yaw1, dist, true); +} + void SV_NewChaseDir2 (edict_t *self, vec3_t dest, float dist) { float minyaw, maxyaw, bestyaw, temp; @@ -1433,7 +3022,8 @@ void M_MoveToGoal (edict_t *ent, float dist) // if the next step hits the enemy, return immediately if (ent->enemy && (ent->enemy->solid == SOLID_BBOX) - && SV_CloseEnough (ent, ent->enemy, dist) ) + && SV_CloseEnough (ent, ent->enemy, dist) + && !((ent->monsterinfo.aiflags & AI_ALTERNATE_FLY) && fly_use_alternate_step(ent, NULL)) ) //GHz START { vec3_t v; @@ -1473,7 +3063,15 @@ void M_MoveToGoal (edict_t *ent, float dist) if (ent->inuse && (level.time > ent->monsterinfo.bump_delay)) { //gi.dprintf("tried course correction %s\n", ent->goalentity?"true":"false"); - SV_NewChaseDir (ent, goal, dist); + if ((ent->flags & FL_FLY) && (!goal || goal == world)) + { + if (!SV_FlyIdleWallDodge(ent, dist)) + SV_MoveRandom(ent, NULL, dist); + } + else + { + SV_NewChaseDir(ent, goal, dist); + } ent->monsterinfo.bump_delay = level.time + FRAMETIME*GetRandom(2, 5); return; } @@ -1587,4 +3185,3 @@ qboolean M_walkmove (edict_t *ent, float yaw, float dist) else return false; } - diff --git a/src/entities/drone/drone_mutant.c b/src/entities/drone/drone_mutant.c index d7fb023f..dbece6d2 100644 --- a/src/entities/drone/drone_mutant.c +++ b/src/entities/drone/drone_mutant.c @@ -23,7 +23,12 @@ static int sound_step2; static int sound_step3; static int sound_thud; +#define MUTANT_SEARCH_SOUND_MIN_DELAY 15.0f +#define MUTANT_SEARCH_SOUND_RANDOM_DELAY 15.0f + void mutant_jump (edict_t *self); +extern mmove_t mutant_move_pain_short1; +extern mmove_t mutant_move_pain_short2; // // SOUNDS // @@ -45,6 +50,15 @@ void mutant_sight (edict_t *self, edict_t *other) gi.sound (self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); } +void mutant_search (edict_t *self) +{ + if (level.time < self->wait) + return; + + gi.sound (self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); + self->wait = level.time + MUTANT_SEARCH_SOUND_MIN_DELAY + (float)random() * MUTANT_SEARCH_SOUND_RANDOM_DELAY; +} + void mutant_swing (edict_t *self) { gi.sound (self, CHAN_VOICE, sound_swing, 1, ATTN_NORM, 0); @@ -366,7 +380,7 @@ void mutant_check_landing (edict_t *self) || (level.time > self->monsterinfo.pausetime)) { self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; - //VectorClear(self->velocity); + self->monsterinfo.nextattack = 0; } else { @@ -376,13 +390,19 @@ void mutant_check_landing (edict_t *self) } +static void mutant_check_landing_ai(edict_t *self, float dist) +{ + ai_charge(self, dist); + mutant_check_landing(self); +} + mframe_t mutant_frames_jump [] = { ai_charge, 0, NULL, ai_charge, 0, NULL, ai_charge, 0, mutant_jump_takeoff, ai_charge, 0, NULL, - ai_charge, 0, mutant_check_landing, + mutant_check_landing_ai, 0, NULL, ai_charge, 0, NULL, // ai_charge, 0, NULL, // ai_charge, 0, NULL @@ -421,7 +441,7 @@ void mutant_attack (edict_t *self) void mutant_dead (edict_t *self) { VectorSet (self->mins, -16, -16, -24); - VectorSet (self->maxs, 16, 16, -8); + VectorSet (self->maxs, 16, 16, 0); self->movetype = MOVETYPE_TOSS; self->svflags |= SVF_DEADMONSTER; //self->nextthink = 0; @@ -431,6 +451,13 @@ void mutant_dead (edict_t *self) // M_FlyCheck (self); } +static void mutant_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t mutant_frames_death1 [] = { ai_move, 0, NULL, @@ -438,7 +465,7 @@ mframe_t mutant_frames_death1 [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, mutant_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL @@ -451,7 +478,7 @@ mframe_t mutant_frames_death2 [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, mutant_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -462,8 +489,6 @@ mmove_t mutant_move_death2 = {FRAME_death201, FRAME_death210, mutant_frames_deat void mutant_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; - M_Notify(self); #ifdef OLD_NOLAG_STYLE @@ -479,14 +504,7 @@ void mutant_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damag if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); @@ -506,7 +524,7 @@ void mutant_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damag gi.sound (self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; - self->s.skinnum = 1; + vrx_update_drone_death_skin(self); if (random() < 0.5) self->monsterinfo.currentmove = &mutant_move_death1; @@ -565,6 +583,18 @@ mframe_t mutant_frames_pain_short2[] = }; mmove_t mutant_move_pain_short2 = { FRAME_pain101, FRAME_pain105, mutant_frames_pain_short2, mutant_run }; +static float mutant_air_pain_velocity_scale(edict_t *self, int damage) +{ + float heavy_damage = self->max_health * 0.12f; + float medium_damage = self->max_health * 0.06f; + + if (damage >= heavy_damage) + return 0.70f; + if (damage >= medium_damage) + return 0.85f; + return 0.90f; +} + void mutant_pain(edict_t* self, edict_t* other, float kick, int damage) { const double rng = random(); @@ -585,6 +615,25 @@ void mutant_pain(edict_t* self, edict_t* other, float kick, int damage) if (invasion->value == 2) return; + if (self->monsterinfo.currentmove == &mutant_move_jump && !self->groundentity) + { + float air_scale = mutant_air_pain_velocity_scale(self, damage); + + if (level.time >= self->pain_debounce_time) + { + if (random() < 0.5) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + self->pain_debounce_time = level.time + 0.5; + } + + self->velocity[0] *= air_scale; + self->velocity[1] *= air_scale; + self->velocity[2] *= air_scale; + return; + } + // if we're fidgeting, always go into pain state. if (rng <= (1.0f - self->monsterinfo.pain_chance) && self->monsterinfo.currentmove != &mutant_move_idle && @@ -592,18 +641,13 @@ void mutant_pain(edict_t* self, edict_t* other, float kick, int damage) self->monsterinfo.currentmove != &mutant_move_walk) return; - if (self->monsterinfo.currentmove == &mutant_move_jump) - gi.sound(self, CHAN_VOICE, sound_thud, 1, ATTN_NORM, 0); + if (random() < 0.5) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); else - { - if (random() < 0.5) - gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); - else - gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); - } + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + self->monsterinfo.nextattack = 0; if (self->monsterinfo.currentmove == &mutant_move_idle || - self->monsterinfo.currentmove == &mutant_move_jump || self->monsterinfo.currentmove == &mutant_move_walk) { self->monsterinfo.currentmove = &mutant_move_pain_long1; } @@ -631,7 +675,7 @@ void init_drone_mutant (edict_t *self) sound_pain1 = gi.soundindex ("mutant/mutpain1.wav"); sound_pain2 = gi.soundindex ("mutant/mutpain2.wav"); sound_sight = gi.soundindex ("mutant/mutsght1.wav"); - //sound_search = gi.soundindex ("mutant/mutsrch1.wav"); + sound_search = gi.soundindex ("mutant/mutsrch1.wav"); sound_step1 = gi.soundindex ("mutant/step1.wav"); sound_step2 = gi.soundindex ("mutant/step2.wav"); sound_step3 = gi.soundindex ("mutant/step3.wav"); @@ -665,8 +709,8 @@ void init_drone_mutant (edict_t *self) self->monsterinfo.attack = mutant_attack; self->monsterinfo.melee = mutant_melee; self->monsterinfo.sight = mutant_sight; -// self->monsterinfo.search = mutant_search; - //self->monsterinfo.idle = mutant_idle; + self->monsterinfo.idle = mutant_search; + self->wait = level.time + MUTANT_SEARCH_SOUND_MIN_DELAY + (float)random() * MUTANT_SEARCH_SOUND_RANDOM_DELAY; // self->monsterinfo.checkattack = mutant_checkattack; self->monsterinfo.jumpup = 64; self->monsterinfo.jumpdn = 512; diff --git a/src/entities/drone/drone_parasite.c b/src/entities/drone/drone_parasite.c index 36c879a6..d1608319 100644 --- a/src/entities/drone/drone_parasite.c +++ b/src/entities/drone/drone_parasite.c @@ -33,6 +33,14 @@ void myparasite_checkattack (edict_t *self); void myparasite_attack1 (edict_t *self); void myparasite_continue (edict_t *self); +static void ParasiteProjectDrainSource(edict_t *self, vec3_t start) +{ + vec3_t forward, right, offset; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorSet(offset, 24, 0, 6); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} void myparasite_launch (edict_t *self) { @@ -245,8 +253,6 @@ void myparasite_run (edict_t *self) static qboolean ParasiteCanAttack (edict_t *self, vec3_t start, vec3_t end) { - vec3_t f, r, offset; - if (!self->enemy) return false; if (entdist(self, self->enemy) > 128) @@ -265,16 +271,11 @@ static qboolean ParasiteCanAttack (edict_t *self, vec3_t start, vec3_t end) return false; // get starting point - AngleVectors (self->s.angles, f, r, NULL); - VectorSet (offset, 24, 0, 6); - G_ProjectSource (self->s.origin, offset, f, r, start); - - // target the midpoint of enemy - G_EntMidPoint(self->enemy, end); + ParasiteProjectDrainSource(self, start); // make sure there is a clear shot //if (!G_IsClearPath(self->enemy, MASK_SHOT, start, end)) - if (!G_ClearShot(self, start, self->enemy)) + if (!M_MonsterFindClearShot(self, start, end)) { //gi.dprintf("no clear path\n"); return false; @@ -377,7 +378,9 @@ mmove_t myparasite_move_runandattack = {FRAME_run03, FRAME_run09, myparasite_fra void myparasite_continue (edict_t *self) { - if (G_ValidTarget(self, self->enemy, true, true) && (entdist(self, self->enemy) <= 128)) + vec3_t start, end; + + if (G_ValidTarget(self, self->enemy, true, true) && ParasiteCanAttack(self, start, end)) self->monsterinfo.currentmove = &myparasite_move_runandattack; } @@ -466,6 +469,12 @@ void myparasite_dead (edict_t *self) M_PrepBodyRemoval(self); } +static void myparasite_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} mframe_t myparasite_frames_death [] = { ai_move, 0, NULL, @@ -480,8 +489,6 @@ mmove_t myparasite_move_death = {FRAME_death101, FRAME_death107, myparasite_fram void myparasite_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; - M_Notify(self); #ifdef OLD_NOLAG_STYLE @@ -500,14 +507,7 @@ void myparasite_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int d if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); diff --git a/src/entities/drone/drone_redmutant.c b/src/entities/drone/drone_redmutant.c new file mode 100644 index 00000000..02944dc3 --- /dev/null +++ b/src/entities/drone/drone_redmutant.c @@ -0,0 +1,686 @@ +/* +============================================================================== + +RED MUTANT + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_redmutant.h" + +static int sound_swing; +static int sound_hit; +static int sound_hit2; +static int sound_death; +static int sound_idle; +static int sound_pain1; +static int sound_pain2; +static int sound_sight; +static int sound_search; +static int sound_step1; +static int sound_step2; +static int sound_step3; +static int sound_thud; + +#define REDMUTANT_STAND_MAX_Z 36 +#define REDMUTANT_IDLE_MAX_Z 56 + +static void redmutant_set_bbox_height(edict_t *self, float max_z) +{ + if (self->maxs[2] == max_z) + return; + + self->maxs[2] = max_z; + gi.linkentity(self); +} + +static void redmutant_restore_bbox(edict_t *self) +{ + redmutant_set_bbox_height(self, REDMUTANT_STAND_MAX_Z); +} + +static void redmutant_stand(edict_t *self); +static void redmutant_walk(edict_t *self); +static void redmutant_run(edict_t *self); +static void redmutant_attack(edict_t *self); +static void redmutant_melee_unused(edict_t *self); +static void redmutant_post_jump(edict_t *self); +extern mmove_t redmutant_move_jump_finish; + +static void redmutant_step(edict_t *self) +{ + switch (GetRandom(0, 2)) + { + case 0: gi.sound(self, CHAN_BODY, sound_step1, 1, ATTN_NORM, 0); break; + case 1: gi.sound(self, CHAN_BODY, sound_step2, 1, ATTN_NORM, 0); break; + default: gi.sound(self, CHAN_BODY, sound_step3, 1, ATTN_NORM, 0); break; + } +} + +static void redmutant_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); +} + +static void redmutant_search(edict_t *self) +{ + gi.sound(self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); +} + +static void redmutant_swing(edict_t *self) +{ + gi.sound(self, CHAN_VOICE, sound_swing, 1, ATTN_NORM, 0); +} + +mframe_t redmutant_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t redmutant_move_stand = { FRAME_stand101, FRAME_stand112, redmutant_frames_stand, NULL }; + +static void redmutant_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &redmutant_move_stand; +} + +static void redmutant_idle_loop(edict_t *self) +{ + if (random() < 0.75) + self->monsterinfo.nextframe = FRAME_stand202; +} + +mframe_t redmutant_frames_idle[] = +{ + drone_ai_stand, 0, redmutant_idle_loop, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t redmutant_move_idle = { FRAME_stand202, FRAME_stand228, redmutant_frames_idle, redmutant_stand }; + +static void redmutant_idle(edict_t *self) +{ + self->monsterinfo.currentmove = &redmutant_move_idle; + gi.sound(self, CHAN_VOICE, sound_idle, 1, ATTN_IDLE, 0); +} + +mframe_t redmutant_frames_walk[] = +{ + drone_ai_walk, 3, NULL, + drone_ai_walk, 1, NULL, + drone_ai_walk, 5, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 13, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 5, NULL, + drone_ai_walk, 6, NULL, + drone_ai_walk, 16, NULL, + drone_ai_walk, 15, NULL, + drone_ai_walk, 6, NULL +}; +mmove_t redmutant_move_walk = { FRAME_walk05, FRAME_walk16, redmutant_frames_walk, NULL }; + +static void redmutant_walk_loop(edict_t *self) +{ + self->monsterinfo.currentmove = &redmutant_move_walk; +} + +mframe_t redmutant_frames_start_walk[] = +{ + drone_ai_walk, 5, NULL, + drone_ai_walk, 5, NULL, + drone_ai_walk, -2, NULL, + drone_ai_walk, 1, NULL +}; +mmove_t redmutant_move_start_walk = { FRAME_walk01, FRAME_walk04, redmutant_frames_start_walk, redmutant_walk_loop }; + +static void redmutant_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &redmutant_move_start_walk; +} + +mframe_t redmutant_frames_run[] = +{ + drone_ai_run, 44, NULL, + drone_ai_run, 44, redmutant_step, + drone_ai_run, 28, NULL, + drone_ai_run, 8, redmutant_step, + drone_ai_run, 22, NULL, + drone_ai_run, 15, NULL +}; +mmove_t redmutant_move_run = { FRAME_run03, FRAME_run08, redmutant_frames_run, NULL }; + +static void redmutant_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &redmutant_move_stand; + else + self->monsterinfo.currentmove = &redmutant_move_run; +} + +static void redmutant_hit_left(edict_t *self) +{ + int damage; + vec3_t aim; + + damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + if (M_MELEE_DMG_MAX && damage > M_MELEE_DMG_MAX) + damage = M_MELEE_DMG_MAX; + + VectorSet(aim, 100, self->mins[0], 8); + if (fire_hit(self, aim, damage, 100)) + gi.sound(self, CHAN_WEAPON, sound_hit, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_WEAPON, sound_swing, 1, ATTN_NORM, 0); +} + +static void redmutant_hit_right(edict_t *self) +{ + int damage; + vec3_t aim; + + damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + if (M_MELEE_DMG_MAX && damage > M_MELEE_DMG_MAX) + damage = M_MELEE_DMG_MAX; + + VectorSet(aim, 100, self->maxs[0], 8); + if (fire_hit(self, aim, damage, 100)) + gi.sound(self, CHAN_WEAPON, sound_hit2, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_WEAPON, sound_swing, 1, ATTN_NORM, 0); +} + +static void redmutant_check_refire(edict_t *self) +{ + if (G_ValidTarget(self, self->enemy, true, true) + && ((random() < 0.5) || (entdist(self, self->enemy) <= 96))) + self->monsterinfo.nextframe = FRAME_attack109; +} + +mframe_t redmutant_frames_attack[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, redmutant_hit_left, + ai_charge, 0, redmutant_hit_right, + ai_charge, 0, NULL, + ai_charge, 0, redmutant_hit_right, + ai_charge, 0, NULL, + ai_charge, 0, redmutant_check_refire +}; +mmove_t redmutant_move_attack = { FRAME_attack109, FRAME_attack115, redmutant_frames_attack, redmutant_run }; + +static void redmutant_melee_unused(edict_t *self) +{ + (void)self; +} + +static void redmutant_jump_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf) +{ + int damage, knockback; + vec3_t point, normal; + + if (self->health <= 0) + { + self->touch = NULL; + return; + } + + if (G_EntExists(other)) + { + VectorCopy(self->velocity, normal); + VectorNormalize(normal); + VectorMA(self->s.origin, self->maxs[0], normal, point); + + damage = 100 + 20 * drone_damagelevel(self); + damage = vrx_increase_monster_damage_by_talent(self->activator, damage); + knockback = min(damage, 500); + + T_Damage(other, self, self, self->velocity, point, normal, damage, knockback, 0, MOD_UNKNOWN); + self->style = 1; + } + + if (!M_CheckBottom(self)) + { + if (self->groundentity) + { + self->monsterinfo.nextframe = FRAME_attack102; + self->touch = NULL; + } + return; + } + + self->touch = NULL; +} + +extern mmove_t redmutant_move_jump_air; + +static void redmutant_jump_takeoff(edict_t *self) +{ + vec3_t forward; + qboolean high_jump = false; + + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + self->lastsound = level.framenum; + AngleVectors(self->s.angles, forward, NULL, NULL); + self->s.origin[2] += 1; + + if (random() < 0.28) + high_jump = true; + + VectorScale(forward, 1125, self->velocity); + self->velocity[2] = high_jump ? 240 : 160; + self->groundentity = NULL; + self->monsterinfo.aiflags |= AI_DUCKED; + self->monsterinfo.attack_finished = level.time + (high_jump ? 1.55 : 1.3); + self->style = 0; + self->touch = redmutant_jump_touch; + + self->monsterinfo.currentmove = &redmutant_move_jump_air; +} + +static void redmutant_check_landing(edict_t *self) +{ + if (self->groundentity) + { + gi.sound(self, CHAN_WEAPON, sound_thud, 1, ATTN_NORM, 0); + self->monsterinfo.attack_finished = level.time + GetRandom(5, 15) * FRAMETIME; + self->monsterinfo.aiflags &= ~(AI_HOLD_FRAME | AI_DUCKED); + + self->monsterinfo.currentmove = &redmutant_move_jump_finish; + self->style = 0; + self->touch = NULL; + return; + } + + self->monsterinfo.aiflags |= AI_HOLD_FRAME; +} + +static void redmutant_check_landing_ai(edict_t *self, float dist) +{ + ai_charge(self, dist); + redmutant_check_landing(self); +} + +mframe_t redmutant_frames_jump_air[] = { + redmutant_check_landing_ai, 0, NULL +}; +mmove_t redmutant_move_jump_air = { FRAME_attack103, FRAME_attack103, redmutant_frames_jump_air, NULL }; + +mframe_t redmutant_frames_jump_start[] = { + ai_charge, 0, NULL, + ai_charge, 17, NULL, + ai_charge, 15, redmutant_jump_takeoff +}; +mmove_t redmutant_move_jump_start = { FRAME_attack101, FRAME_attack103, redmutant_frames_jump_start, NULL }; + +mframe_t redmutant_frames_jump_finish[] = { + ai_charge, 15, NULL, + ai_charge, 0, NULL, + ai_charge, 3, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t redmutant_move_jump_finish = { FRAME_attack104, FRAME_attack108, redmutant_frames_jump_finish, redmutant_post_jump }; + +static void redmutant_jump(edict_t *self) +{ + self->monsterinfo.currentmove = &redmutant_move_jump_start; +} + +static void redmutant_post_jump(edict_t *self) +{ + if (G_ValidTarget(self, self->enemy, true, true) && (entdist(self, self->enemy) <= MELEE_DISTANCE * 2)) + self->monsterinfo.currentmove = &redmutant_move_attack; + else + redmutant_run(self); +} + +static void redmutant_flip_takeoff(edict_t *self) +{ + vec3_t forward; + + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + self->lastsound = level.framenum; + AngleVectors(self->s.angles, forward, NULL, NULL); + self->s.origin[2] += 1; + + VectorScale(forward, 800, self->velocity); + self->velocity[2] = 200; + self->groundentity = NULL; + self->monsterinfo.aiflags |= AI_DUCKED; +} + +static void redmutant_flip_check_landing(edict_t *self) +{ + if (self->groundentity) + { + gi.sound(self, CHAN_WEAPON, sound_thud, 1, ATTN_NORM, 0); + self->monsterinfo.aiflags &= ~(AI_HOLD_FRAME | AI_DUCKED); + return; + } + + self->monsterinfo.nextframe = FRAME_attack108; + self->monsterinfo.aiflags |= AI_HOLD_FRAME; +} + +static void redmutant_flip_check_landing_ai(edict_t *self, float dist) +{ + ai_charge(self, dist); + redmutant_flip_check_landing(self); +} + +mframe_t redmutant_frames_flip[] = { + ai_charge, 0, NULL, + ai_charge, 17, NULL, + ai_charge, 15, redmutant_flip_takeoff, + ai_charge, 15, NULL, + ai_charge, 15, NULL, + ai_charge, 0, NULL, + ai_charge, 3, NULL, + redmutant_flip_check_landing_ai, 0, NULL +}; +mmove_t redmutant_move_flip = { FRAME_attack101, FRAME_attack108, redmutant_frames_flip, redmutant_post_jump }; + +static void redmutant_flip(edict_t *self) +{ + self->monsterinfo.currentmove = &redmutant_move_flip; +} + +static void redmutant_attack(edict_t *self) +{ + float dist; + float height_diff; + + if (!self->groundentity && !self->waterlevel) + return; + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + dist = entdist(self, self->enemy); + height_diff = self->enemy->absmin[2] - self->absmin[2]; + + if (dist <= MELEE_DISTANCE) + { + redmutant_restore_bbox(self); + self->monsterinfo.currentmove = &redmutant_move_attack; + } + else if ((dist <= 384) && (height_diff > -64) && (height_diff < 96) && (random() < 0.8)) + { + if (random() < 0.5) + redmutant_jump(self); + else + redmutant_flip(self); + } +} + +static void redmutant_dead(edict_t *self) +{ + VectorSet(self->mins, -16, -16, -24); + VectorSet(self->maxs, 16, 16, 0); + self->movetype = MOVETYPE_TOSS; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); + M_PrepBodyRemoval(self); +} + +static void redmutant_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + +static void ai_move_slide_right(edict_t *self, float dist) +{ + M_walkmove(self, self->s.angles[YAW] + 90, dist); +} + +static void ai_move_slide_left(edict_t *self, float dist) +{ + M_walkmove(self, self->s.angles[YAW] - 90, dist); +} + +mframe_t redmutant_frames_death1[] = +{ + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 2, NULL, + ai_move_slide_right, 5, NULL, + ai_move_slide_right, 7, redmutant_shrink, + ai_move_slide_right, 6, NULL, + ai_move_slide_right, 2, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL, + ai_move_slide_right, 0, NULL +}; +mmove_t redmutant_move_death1 = { FRAME_death101, FRAME_death120, redmutant_frames_death1, redmutant_dead }; + +mframe_t redmutant_frames_death2[] = +{ + ai_move_slide_left, 0, NULL, + ai_move_slide_left, 1, NULL, + ai_move_slide_left, 6, NULL, + ai_move_slide_left, 8, NULL, + ai_move_slide_left, 3, redmutant_shrink, + ai_move_slide_left, 2, NULL, + ai_move_slide_left, 0, NULL +}; +mmove_t redmutant_move_death2 = { FRAME_death201, FRAME_death207, redmutant_frames_death2, redmutant_dead }; + +static void redmutant_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + M_Notify(self); + +#ifdef OLD_NOLAG_STYLE + if (nolag->value) + { + M_Remove(self, false, true); + return; + } +#endif + + if (self->health <= self->gib_health) + { + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); + vrx_throw_drone_gibs(self, damage); +#ifdef OLD_NOLAG_STYLE + M_Remove(self, false, false); +#else + if (nolag->value) + M_Remove(self, false, true); + else + M_Remove(self, false, false); +#endif + return; + } + + if (self->deadflag == DEAD_DEAD) + return; + + gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + self->s.skinnum = 1; + self->monsterinfo.currentmove = (random() < 0.5) ? &redmutant_move_death1 : &redmutant_move_death2; + + DroneList_Remove(self); + + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; +} + +mframe_t redmutant_frames_pain1[] = +{ + ai_move, 4, NULL, + ai_move, -3, NULL, + ai_move, -8, NULL, + ai_move, 2, NULL, + ai_move, 5, NULL +}; +mmove_t redmutant_move_pain1 = { FRAME_pain101, FRAME_pain105, redmutant_frames_pain1, redmutant_run }; + +mframe_t redmutant_frames_pain2[] = +{ + ai_move, -24, NULL, + ai_move, 11, NULL, + ai_move, 5, NULL, + ai_move, -2, NULL, + ai_move, 6, NULL, + ai_move, 4, NULL +}; +mmove_t redmutant_move_pain2 = { FRAME_pain201, FRAME_pain206, redmutant_frames_pain2, redmutant_run }; + +mframe_t redmutant_frames_pain3[] = +{ + ai_move, -22, NULL, + ai_move, 3, NULL, + ai_move, 3, NULL, + ai_move, 2, NULL, + ai_move, 1, NULL, + ai_move, 1, NULL, + ai_move, 6, NULL, + ai_move, 3, NULL, + ai_move, 2, NULL, + ai_move, 0, NULL, + ai_move, 1, NULL +}; +mmove_t redmutant_move_pain3 = { FRAME_pain301, FRAME_pain311, redmutant_frames_pain3, redmutant_run }; + +static void redmutant_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + float r; + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0; + r = random(); + + if (r < 0.33) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + else if (r < 0.66) + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + + redmutant_restore_bbox(self); + + if (r < 0.33) + self->monsterinfo.currentmove = &redmutant_move_pain1; + else if (r < 0.66) + self->monsterinfo.currentmove = &redmutant_move_pain2; + else + self->monsterinfo.currentmove = &redmutant_move_pain3; +} + +void init_drone_redmutant(edict_t *self) +{ + sound_swing = gi.soundindex("mutant/mutatck1.wav"); + sound_hit = gi.soundindex("mutant/mutatck2.wav"); + sound_hit2 = gi.soundindex("mutant/mutatck3.wav"); + sound_death = gi.soundindex("mutant/mutdeth1.wav"); + sound_idle = gi.soundindex("mutant/mutidle1.wav"); + sound_pain1 = gi.soundindex("mutant/mutpain1.wav"); + sound_pain2 = gi.soundindex("mutant/mutpain2.wav"); + sound_sight = gi.soundindex("mutant/mutsght1.wav"); + sound_search = gi.soundindex("mutant/mutsrch1.wav"); + sound_step1 = gi.soundindex("mutant/step1.wav"); + sound_step2 = gi.soundindex("mutant/step2.wav"); + sound_step3 = gi.soundindex("mutant/step3.wav"); + sound_thud = gi.soundindex("mutant/thud1.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/vault/monsters/mutant/tris.md2"); + gi.modelindex("models/monsters/mutant/gibs/head.md2"); + gi.modelindex("models/monsters/mutant/gibs/chest.md2"); + gi.modelindex("models/monsters/mutant/gibs/hand.md2"); + gi.modelindex("models/monsters/mutant/gibs/foot.md2"); + + VectorSet(self->mins, -18, -18, -24); + VectorSet(self->maxs, 18, 18, REDMUTANT_STAND_MAX_Z); + + self->health = M_REDMUTANT_INITIAL_HEALTH + M_REDMUTANT_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -BASE_GIB_HEALTH; + self->mass = 350; + + self->monsterinfo.pain_chance = 0.15f; + self->pain = redmutant_pain; + self->die = redmutant_die; + + self->monsterinfo.stand = redmutant_stand; + self->monsterinfo.walk = redmutant_walk; + self->monsterinfo.run = redmutant_run; + self->monsterinfo.attack = redmutant_attack; + self->monsterinfo.melee = redmutant_melee_unused; + self->monsterinfo.sight = redmutant_sight; + self->monsterinfo.idle = redmutant_idle; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.power_armor_power = M_REDMUTANT_INITIAL_ARMOR + M_REDMUTANT_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_MUTANT_CONTROL_COST; + self->monsterinfo.cost = M_MUTANT_COST; + self->mtype = M_REDMUTANT; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &redmutant_move_stand; + self->monsterinfo.scale = MODEL_SCALE; + self->nextthink = level.time + FRAMETIME; +} diff --git a/src/entities/drone/drone_rogue_turret.c b/src/entities/drone/drone_rogue_turret.c new file mode 100644 index 00000000..28000ba2 --- /dev/null +++ b/src/entities/drone/drone_rogue_turret.c @@ -0,0 +1,357 @@ +/* +============================================================================== + +rogue rocket turret + +============================================================================== +*/ + +#include "g_local.h" + +#define TURRET_FRAME_stand01 0 +#define TURRET_FRAME_stand02 1 +#define TURRET_FRAME_active01 2 +#define TURRET_FRAME_run01 8 +#define TURRET_FRAME_run02 9 +#define TURRET_FRAME_pow01 10 +#define TURRET_FRAME_pow04 13 + +#define TURRET_ROCKET_DAMAGE 40 +#define TURRET_ROCKET_SPEED 650 + +static int sound_moved; +static int sound_moving; + +void drone_ai_stand(edict_t *self, float dist); + +static void rogue_turret_stand(edict_t *self); +static void rogue_turret_run(edict_t *self); +static void rogue_turret_active(edict_t *self); + +static void rogue_turret_laser_off(edict_t *self) +{ + if (self->target_ent && self->target_ent->inuse) + G_FreeEdict(self->target_ent); + self->target_ent = NULL; +} + +static void rogue_turret_update_laser(edict_t *self) +{ + vec3_t forward; + vec3_t end; + trace_t tr; + edict_t *laser; + float scan_range; + float phase; + + if (!G_ValidTarget(self, self->enemy, false, true)) + { + rogue_turret_laser_off(self); + return; + } + + laser = self->target_ent; + if (!laser || !laser->inuse) + { + laser = G_Spawn(); + if (!laser) + return; + self->target_ent = laser; + laser->movetype = MOVETYPE_NONE; + laser->solid = SOLID_NOT; + laser->s.renderfx = RF_BEAM | RF_TRANSLUCENT; + laser->s.modelindex = 1; + laser->s.frame = 1; + laser->s.skinnum = 0xf2f2f0f0; + laser->classname = "turret_lasersight"; + laser->owner = self; + } + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorMA(self->s.origin, 8192, forward, end); + tr = gi.trace(self->s.origin, NULL, NULL, end, self, MASK_SOLID); + + scan_range = visible(self, self->enemy) ? 12.0f : 64.0f; + phase = level.time + (float)(self - g_edicts); + tr.endpos[0] += sinf(phase) * scan_range; + tr.endpos[1] += cosf(phase * 3.0f) * scan_range; + tr.endpos[2] += sinf(phase * 2.5f) * scan_range; + + VectorSubtract(tr.endpos, self->s.origin, forward); + if (VectorNormalize(forward)) + { + VectorMA(self->s.origin, 8192, forward, end); + tr = gi.trace(self->s.origin, NULL, NULL, end, self, MASK_SOLID); + } + + VectorCopy(self->s.origin, laser->s.origin); + VectorCopy(tr.endpos, laser->s.old_origin); + VectorCopy(self->s.origin, laser->pos1); + VectorCopy(tr.endpos, laser->pos2); + laser->think = G_FreeEdict; + laser->nextthink = level.time + 0.3f; + gi.linkentity(laser); +} + +static void rogue_turret_aim(edict_t *self) +{ + vec3_t dir; + vec3_t angles; + + if (!G_ValidTarget(self, self->enemy, false, true)) + { + rogue_turret_laser_off(self); + return; + } + + VectorSubtract(self->enemy->s.origin, self->s.origin, dir); + if (VectorLength(dir) < 1) + { + rogue_turret_laser_off(self); + return; + } + + vectoangles(dir, angles); + self->ideal_yaw = angles[YAW]; + M_ChangeYaw(self); + self->s.angles[PITCH] = angles[PITCH]; + rogue_turret_update_laser(self); +} + +static void rogue_turret_aim_stand_ai(edict_t *self, float dist) +{ + drone_ai_stand(self, dist); + rogue_turret_aim(self); +} + +static void rogue_turret_aim_move_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + rogue_turret_aim(self); +} + +static void rogue_turret_aim_charge_ai(edict_t *self, float dist) +{ + ai_charge(self, dist); + rogue_turret_aim(self); +} + +static void rogue_turret_fire(edict_t *self) +{ + vec3_t forward; + vec3_t aim; + vec3_t right; + vec3_t start; + vec3_t end; + trace_t tr; + + if (!G_ValidTarget(self, self->enemy, false, true)) + { + rogue_turret_laser_off(self); + return; + } + + rogue_turret_aim(self); + + if (entdist(self, self->enemy) <= 72) + return; + + G_EntMidPoint(self->enemy, end); + AngleVectors(self->s.angles, forward, right, NULL); + G_ProjectSource(self->s.origin, monster_flash_offset[MZ2_TURRET_ROCKET], forward, right, start); + VectorSubtract(end, start, aim); + if (!VectorNormalize(aim)) + return; + + if (DotProduct(aim, forward) < 0.90f) + return; + + tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT); + if (tr.fraction < 1.0 && tr.ent != self->enemy && !G_ValidTarget(self, tr.ent, false, true)) + return; + + monster_fire_rocket(self, start, aim, TURRET_ROCKET_DAMAGE, TURRET_ROCKET_SPEED, MZ2_TURRET_ROCKET); +} + +static mframe_t rogue_turret_frames_stand[] = +{ + rogue_turret_aim_stand_ai, 0, NULL, + rogue_turret_aim_stand_ai, 0, NULL +}; +static mmove_t rogue_turret_move_stand = { TURRET_FRAME_stand01, TURRET_FRAME_stand02, rogue_turret_frames_stand, NULL }; + +static void rogue_turret_stand(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, false, true)) + rogue_turret_laser_off(self); + self->monsterinfo.currentmove = &rogue_turret_move_stand; +} + +static mframe_t rogue_turret_frames_ready[] = +{ + rogue_turret_aim_move_ai, 0, NULL, + rogue_turret_aim_move_ai, 0, NULL, + rogue_turret_aim_move_ai, 0, NULL, + rogue_turret_aim_move_ai, 0, NULL, + rogue_turret_aim_move_ai, 0, NULL, + rogue_turret_aim_move_ai, 0, NULL, + rogue_turret_aim_move_ai, 0, NULL +}; +static mmove_t rogue_turret_move_ready = { TURRET_FRAME_active01, TURRET_FRAME_run01, rogue_turret_frames_ready, rogue_turret_run }; + +static void rogue_turret_ready(edict_t *self) +{ + if (self->monsterinfo.currentmove != &rogue_turret_move_ready) + { + self->monsterinfo.currentmove = &rogue_turret_move_ready; + self->random = 1; + gi.sound(self, CHAN_WEAPON, sound_moving, 1, ATTN_NORM, 0); + } +} + +static mframe_t rogue_turret_frames_run[] = +{ + rogue_turret_aim_stand_ai, 0, rogue_turret_active, + rogue_turret_aim_stand_ai, 0, rogue_turret_active +}; +static mmove_t rogue_turret_move_run = { TURRET_FRAME_run01, TURRET_FRAME_run02, rogue_turret_frames_run, rogue_turret_run }; + +static void rogue_turret_run(edict_t *self) +{ + if (self->s.frame < TURRET_FRAME_run01) + { + rogue_turret_ready(self); + return; + } + + if (self->random) + { + self->random = 0; + gi.sound(self, CHAN_WEAPON, sound_moved, 1, ATTN_NORM, 0); + } + + self->monsterinfo.currentmove = &rogue_turret_move_run; +} + +static mframe_t rogue_turret_frames_fire[] = +{ + rogue_turret_aim_charge_ai, 0, NULL, + rogue_turret_aim_charge_ai, 0, rogue_turret_fire, + rogue_turret_aim_charge_ai, 0, NULL, + rogue_turret_aim_charge_ai, 0, NULL +}; +static mmove_t rogue_turret_move_fire = { TURRET_FRAME_pow01, TURRET_FRAME_pow04, rogue_turret_frames_fire, rogue_turret_run }; + +static void rogue_turret_active(edict_t *self) +{ + rogue_turret_aim(self); + + if (!G_ValidTarget(self, self->enemy, false, true)) + return; + if (level.time < self->monsterinfo.attack_finished) + return; + if (entdist(self, self->enemy) <= 72) + { + M_DelayNextAttack(self, 0.3f, false); + return; + } + + self->monsterinfo.currentmove = &rogue_turret_move_fire; + M_DelayNextAttack(self, 0.8f + random() * 0.4f, true); +} + +static void rogue_turret_attack(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, false, true)) + { + rogue_turret_laser_off(self); + return; + } + + rogue_turret_aim(self); + if (self->s.frame < TURRET_FRAME_run01) + rogue_turret_ready(self); + else + self->monsterinfo.currentmove = &rogue_turret_move_fire; + + M_DelayNextAttack(self, 1.0f + random() * 0.5f, true); +} + +static void rogue_turret_pain(edict_t *self, edict_t *other, float kick, int damage) +{ +} + +void rogue_turret_force_ready(edict_t *self) +{ + self->s.frame = TURRET_FRAME_run01; + self->monsterinfo.currentmove = &rogue_turret_move_run; + self->monsterinfo.attack_finished = level.time; + self->monsterinfo.pausetime = 0; + self->nextthink = level.time + FRAMETIME; + + if (G_ValidTarget(self, self->enemy, false, true)) + rogue_turret_aim(self); + else + rogue_turret_laser_off(self); +} + +static void rogue_turret_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + rogue_turret_laser_off(self); + + vrx_throw_drone_gibs(self, damage ? damage : 150); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_EXPLOSION1); + gi.WritePosition(self->s.origin); + gi.multicast(self->s.origin, MULTICAST_PVS); + + M_Remove(self, false, false); +} + +void init_drone_rogue_turret(edict_t *self) +{ + sound_moved = gi.soundindex("turret/moved.wav"); + sound_moving = gi.soundindex("turret/moving.wav"); + gi.soundindex("weapons/rockfly.wav"); + gi.soundindex("chick/chkatck2.wav"); + gi.modelindex("models/objects/rocket/tris.md2"); + gi.modelindex("models/objects/debris1/tris.md2"); + + self->movetype = MOVETYPE_NONE; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/turret/tris.md2"); + self->s.skinnum = 2; + VectorSet(self->mins, -12, -12, -12); + VectorSet(self->maxs, 12, 12, 12); + + self->health = M_ROGUE_TURRET_INITIAL_HEALTH + M_ROGUE_TURRET_ADDON_HEALTH * self->monsterinfo.level; + self->gib_health = -100; + self->mass = 250; + self->mtype = M_ROGUE_TURRET; + self->flags |= FL_NO_KNOCKBACK; + self->max_health = self->health; + self->monsterinfo.power_armor_power = M_ROGUE_TURRET_INITIAL_ARMOR + M_ROGUE_TURRET_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_DEFAULT_CONTROL_COST; + self->monsterinfo.cost = M_DEFAULT_COST; + self->monsterinfo.sight_range = 1024; + self->monsterinfo.aiflags |= AI_STAND_GROUND; + self->monsterinfo.pain_chance = 0.0f; + self->gravity = 0; + self->yaw_speed = 40; + + self->pain = rogue_turret_pain; + self->die = rogue_turret_die; + + self->monsterinfo.stand = rogue_turret_stand; + self->monsterinfo.walk = rogue_turret_run; + self->monsterinfo.run = rogue_turret_run; + self->monsterinfo.attack = rogue_turret_attack; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &rogue_turret_move_stand; + self->monsterinfo.scale = 3.5f; +} diff --git a/src/entities/drone/drone_runnertank.c b/src/entities/drone/drone_runnertank.c new file mode 100644 index 00000000..73b0a6c5 --- /dev/null +++ b/src/entities/drone/drone_runnertank.c @@ -0,0 +1,1068 @@ +#include "g_local.h" +#include "../../quake2/monsterframes/m_runnertank.h" + +static int sound_thud; +static int sound_pain; +static int sound_idle; +static int sound_die; +static int sound_step; +static int sound_sight; +static int sound_windup; +static int sound_strike; +static int sound_plasma; +static int skin_normal; +static int skin_pain; + +#define RUNNERTANK_JUMP_ATTACK_DELAY 12.0f +#define RUNNERTANK_JUMP_ATTACK_FOV 35 +#define RUNNERTANK_JUMP_ATTACK_DROP_RADIUS 90.0f +#define RUNNERTANK_JUMP_ATTACK_DROP_SPEED 900.0f +#define RUNNERTANK_JUMP_ATTACK_DROP_GRAVITY 3.0f +#define RUNNERTANK_MELEE_RANGE 144.0f +#define RUNNERTANK_INVASION_RUN_SCALE 1.15f +#define RUNNERTANK_NORMAL_SKIN "models/vault/monsters/tank/skin.pcx" +#define RUNNERTANK_PAIN_SKIN "models/monsters/tank/pain.pcx" + +static void runnertank_stand(edict_t *self); +static void runnertank_walk(edict_t *self); +static void runnertank_walk_loop(edict_t *self); +static void runnertank_run(edict_t *self); +static void runnertank_attack(edict_t *self); +static void runnertank_melee(edict_t *self); +static void runnertank_restrike(edict_t *self); +static void runnertank_reattack_blast(edict_t *self); +static void runnertank_refire_rocket(edict_t *self); +static void runnertank_doattack_rocket(edict_t *self); +static void runnertank_jump_attack_takeoff(edict_t *self); +static void runnertank_jump_attack_hold(edict_t *self); + +static void runnertank_update_skin(edict_t *self) +{ + int desired_skin; + + if (self->max_health <= 0) + return; + + if (!skin_normal) + skin_normal = gi.imageindex(RUNNERTANK_NORMAL_SKIN); + if (!skin_pain) + skin_pain = gi.imageindex(RUNNERTANK_PAIN_SKIN); + + self->s.renderfx |= RF_CUSTOMSKIN; + desired_skin = self->health < (self->max_health / 2) ? skin_pain : skin_normal; + + if (desired_skin) + self->s.skinnum = desired_skin; +} + +static void runnertank_ai_run(edict_t *self, float dist) +{ + runnertank_update_skin(self); + drone_ai_run(self, invasion->value ? dist * RUNNERTANK_INVASION_RUN_SCALE : dist); +} + +static void runnertank_footstep(edict_t *self) +{ + gi.sound(self, CHAN_BODY, sound_step, 1, ATTN_NORM, 0); +} + +static void runnertank_thud(edict_t *self) +{ + gi.sound(self, CHAN_BODY, sound_thud, 1, ATTN_NORM, 0); +} + +static void runnertank_windup(edict_t *self) +{ + gi.sound(self, CHAN_WEAPON, sound_windup, 1, ATTN_NORM, 0); +} + +static void runnertank_slam_origin(edict_t *self, vec3_t origin) +{ + vec3_t forward, right, offset; + trace_t tr; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorSet(offset, 20, -14.3f, -21); + G_ProjectSource(self->s.origin, offset, forward, right, origin); + tr = gi.trace(self->s.origin, NULL, NULL, origin, self, MASK_SOLID); + VectorCopy(tr.endpos, origin); +} + +static void runnertank_slam_effect(vec3_t origin) +{ + vec3_t up; + + VectorSet(up, 0, 0, 1); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BERSERK_SLAM); + gi.WritePosition(origin); + gi.WriteDir(up); + gi.multicast(origin, MULTICAST_PHS); +} + +static qboolean runnertank_can_rail(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_TANK_BLASTER_1); +} + +static qboolean runnertank_can_rocket(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_TANK_ROCKET_1); +} + +static qboolean runnertank_can_chain(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_TANK_MACHINEGUN_1); +} + +static void runnertank_idle(edict_t *self) +{ + gi.sound(self, CHAN_VOICE, sound_idle, 1, ATTN_IDLE, 0); + self->superspeed = false; +} + +static void runnertank_sight(edict_t *self, edict_t *other) +{ + (void)other; + + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); +} + +mframe_t runnertank_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t runnertank_move_stand = { FRAME_stand01, FRAME_stand30, runnertank_frames_stand, NULL }; + +static void runnertank_stand(edict_t *self) +{ + runnertank_update_skin(self); + self->monsterinfo.currentmove = &runnertank_move_stand; +} + +mframe_t runnertank_frames_walk_start[] = +{ + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL +}; +mmove_t runnertank_move_walk_start = { FRAME_walk01, FRAME_walk15, runnertank_frames_walk_start, runnertank_walk_loop }; + +mframe_t runnertank_frames_walk[] = +{ + drone_ai_walk, 4, runnertank_footstep, + drone_ai_walk, 4, NULL, + drone_ai_walk, 3, NULL, + drone_ai_walk, 5, NULL, + drone_ai_walk, 4, NULL, + drone_ai_walk, 5, NULL, + drone_ai_walk, 7, NULL, + drone_ai_walk, 7, NULL, + drone_ai_walk, 6, runnertank_footstep, + drone_ai_walk, 6, NULL, + drone_ai_walk, 4, NULL, + drone_ai_walk, 5, NULL, + drone_ai_walk, 3, NULL, + drone_ai_walk, 4, NULL, + drone_ai_walk, 6, NULL, + drone_ai_walk, 7, NULL, + drone_ai_walk, 5, NULL +}; +mmove_t runnertank_move_walk = { FRAME_walk22, FRAME_walk38, runnertank_frames_walk, NULL }; + +static void runnertank_walk_loop(edict_t *self) +{ + runnertank_update_skin(self); + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &runnertank_move_walk; +} + +static void runnertank_walk(edict_t *self) +{ + runnertank_update_skin(self); + if (!self->goalentity) + self->goalentity = world; + + if (self->s.frame >= FRAME_walk22 && self->s.frame <= FRAME_walk38) + self->monsterinfo.currentmove = &runnertank_move_walk; + else + self->monsterinfo.currentmove = &runnertank_move_walk_start; +} + +mframe_t runnertank_frames_run[] = +{ + runnertank_ai_run, 14, runnertank_footstep, + runnertank_ai_run, 18, NULL, + runnertank_ai_run, 15, NULL, + runnertank_ai_run, 15, NULL, + runnertank_ai_run, 15, NULL, + runnertank_ai_run, 19, runnertank_footstep, + runnertank_ai_run, 15, NULL, + runnertank_ai_run, 13, NULL, + runnertank_ai_run, 18, NULL, + runnertank_ai_run, 17, NULL +}; +mmove_t runnertank_move_run = { FRAME_run01, FRAME_run10, runnertank_frames_run, NULL }; + +static void runnertank_run(edict_t *self) +{ + if (self->deadflag == DEAD_DEAD) + return; + + runnertank_update_skin(self); + + if (self->enemy && self->enemy->client) + self->monsterinfo.aiflags |= AI_BRUTAL; + else + self->monsterinfo.aiflags &= ~AI_BRUTAL; + + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + { + self->monsterinfo.currentmove = &runnertank_move_stand; + return; + } + + self->monsterinfo.currentmove = &runnertank_move_run; +} + +static void runnertank_rail(edict_t *self) +{ + int flash_number, damage; + vec3_t forward, start; + + if (self->s.frame == FRAME_attak110) + flash_number = MZ2_TANK_BLASTER_1; + else if (self->s.frame == FRAME_attak113) + flash_number = MZ2_TANK_BLASTER_2; + else + flash_number = MZ2_TANK_BLASTER_3; + + damage = M_RAILGUN_DMG_BASE + M_RAILGUN_DMG_ADDON * drone_damagelevel(self); + if (M_RAILGUN_DMG_MAX && damage > M_RAILGUN_DMG_MAX) + damage = M_RAILGUN_DMG_MAX; + + MonsterAim(self, M_HITSCAN_INSTANT_ACC, 0, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_railgun(self, start, forward, damage, damage, flash_number); +} + +static void runnertank_rocket(edict_t *self) +{ + int flash_number, damage, speed; + vec3_t forward, start; + + if (!self->enemy || !self->enemy->inuse) + return; + + if (self->s.frame == FRAME_attak313) + flash_number = MZ2_TANK_ROCKET_1; + else if (self->s.frame == FRAME_attak316) + flash_number = MZ2_TANK_ROCKET_2; + else + flash_number = MZ2_TANK_ROCKET_3; + + damage = M_ROCKETLAUNCHER_DMG_BASE + M_ROCKETLAUNCHER_DMG_ADDON * drone_damagelevel(self); + if (M_ROCKETLAUNCHER_DMG_MAX && damage > M_ROCKETLAUNCHER_DMG_MAX) + damage = M_ROCKETLAUNCHER_DMG_MAX; + speed = M_ROCKETLAUNCHER_SPEED_BASE + M_ROCKETLAUNCHER_SPEED_ADDON * drone_damagelevel(self); + if (M_ROCKETLAUNCHER_SPEED_MAX && speed > M_ROCKETLAUNCHER_SPEED_MAX) + speed = M_ROCKETLAUNCHER_SPEED_MAX; + + MonsterAim(self, M_PROJECTILE_ACC, speed, true, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_rocket(self, start, forward, damage, speed, flash_number); +} + +static void runnertank_plasma(edict_t *self) +{ + vec3_t forward, start; + int flash_number, damage, speed, radius_damage; + + if (!self->enemy || !self->enemy->inuse) + return; + + flash_number = MZ2_TANK_MACHINEGUN_1 + (self->s.frame - FRAME_attak406); + damage = M_PLASMA_DMG_BASE + M_PLASMA_DMG_ADDON * drone_damagelevel(self); + if (M_PLASMA_DMG_MAX && damage > M_PLASMA_DMG_MAX) + damage = M_PLASMA_DMG_MAX; + speed = M_PLASMA_SPEED_BASE + M_PLASMA_SPEED_ADDON * drone_damagelevel(self); + if (M_PLASMA_SPEED_MAX && speed > M_PLASMA_SPEED_MAX) + speed = M_PLASMA_SPEED_MAX; + radius_damage = max(1, damage / 2); + + MonsterAim(self, M_PROJECTILE_ACC, speed, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + fire_plasma(self, start, forward, damage, speed, M_PLASMA_DAMAGE_RADIUS, radius_damage); + gi.positioned_sound(start, self, CHAN_WEAPON, sound_plasma, 1, ATTN_NORM, 0); +} + +static void runnertank_strike_sound(edict_t *self) +{ + gi.sound(self, CHAN_WEAPON, sound_strike, 1, ATTN_NORM, 0); +} + +static void runnertank_meleeattack(edict_t *self) +{ + int damage; + trace_t tr; + edict_t *other = NULL; + vec3_t v, damage_origin; + + if (!self->groundentity) + return; + + damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + if (M_MELEE_DMG_MAX && damage > M_MELEE_DMG_MAX) + damage = M_MELEE_DMG_MAX; + + gi.sound(self, CHAN_AUTO, sound_strike, 1, ATTN_NORM, 0); + runnertank_slam_origin(self, damage_origin); + runnertank_slam_effect(damage_origin); + + while ((other = findradius(other, damage_origin, 128)) != NULL) + { + if (!G_ValidTarget(self, other, true, true)) + continue; + if (que_typeexists(self->curses, CURSE) && rand() > 0.2) + continue; + + VectorSubtract(other->s.origin, damage_origin, v); + VectorNormalize(v); + tr = gi.trace(damage_origin, NULL, NULL, other->s.origin, self, (MASK_PLAYERSOLID | MASK_MONSTERSOLID)); + T_Damage(other, self, self, v, tr.endpos, tr.plane.normal, damage, 200, 0, MOD_TANK_PUNCH); + } +} + +static void runnertank_attack_finished(edict_t *self) +{ + runnertank_update_skin(self); + M_DelayNextAttack(self, 0.5, false); + runnertank_run(self); +} + +mframe_t runnertank_frames_attack_blast[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, runnertank_rail, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, runnertank_rail, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, runnertank_rail +}; +mmove_t runnertank_move_attack_blast = { FRAME_attak101, FRAME_attak116, runnertank_frames_attack_blast, runnertank_reattack_blast }; + +mframe_t runnertank_frames_reattack_blast[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, runnertank_rail, + ai_charge, 0, NULL, + ai_charge, 0, runnertank_rail +}; +mmove_t runnertank_move_reattack_blast = { FRAME_attak111, FRAME_attak116, runnertank_frames_reattack_blast, runnertank_reattack_blast }; + +mframe_t runnertank_frames_attack_post_blast[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 2, NULL, + ai_move, 3, NULL, + ai_move, 2, NULL, + ai_move, -2, runnertank_footstep +}; +mmove_t runnertank_move_attack_post_blast = { FRAME_attak117, FRAME_attak122, runnertank_frames_attack_post_blast, runnertank_attack_finished }; + +static void runnertank_reattack_blast(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, true, true) || !visible(self, self->enemy) || !runnertank_can_rail(self)) + { + self->monsterinfo.currentmove = &runnertank_move_attack_post_blast; + M_DelayNextAttack(self, 0, true); + return; + } + + M_ContinueAttack(self, &runnertank_move_reattack_blast, &runnertank_move_attack_post_blast, 0, 1024, 0.5); +} + +mframe_t runnertank_frames_strike[] = +{ + ai_move, 0, runnertank_windup, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, runnertank_meleeattack +}; +mmove_t runnertank_move_strike = { FRAME_attak222, FRAME_attak226, runnertank_frames_strike, runnertank_restrike }; + +mframe_t runnertank_frames_post_strike[] = +{ + ai_move, 0, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, -3, NULL, + ai_move, -10, NULL, + ai_move, -10, NULL, + ai_move, -2, NULL, + ai_move, -3, NULL, + ai_move, -2, runnertank_footstep +}; +mmove_t runnertank_move_post_strike = { FRAME_attak227, FRAME_attak238, runnertank_frames_post_strike, runnertank_attack_finished }; + +static void runnertank_restrike(edict_t *self) +{ + M_ContinueAttack(self, &runnertank_move_strike, &runnertank_move_post_strike, 0, 128, 0.66); +} + +mframe_t runnertank_frames_attack_pre_rocket[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t runnertank_move_attack_pre_rocket = { FRAME_attak303, FRAME_attak312, runnertank_frames_attack_pre_rocket, runnertank_doattack_rocket }; + +mframe_t runnertank_frames_attack_fire_rocket[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, runnertank_rocket, + ai_charge, 1, NULL, + ai_charge, 2, NULL, + ai_charge, 7, runnertank_rocket, + ai_charge, 7, NULL, + ai_charge, 7, runnertank_footstep, + ai_charge, 0, runnertank_rocket, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t runnertank_move_attack_fire_rocket = { FRAME_attak312, FRAME_attak321, runnertank_frames_attack_fire_rocket, runnertank_refire_rocket }; + +mframe_t runnertank_frames_attack_post_rocket[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, -9, NULL, + ai_charge, -8, NULL, + ai_charge, -7, NULL, + ai_charge, -1, NULL, + ai_charge, -1, runnertank_footstep, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t runnertank_move_attack_post_rocket = { FRAME_attak322, FRAME_attak335, runnertank_frames_attack_post_rocket, runnertank_attack_finished }; + +static void runnertank_refire_rocket(edict_t *self) +{ + if (!G_ValidTarget(self, self->enemy, true, true) || !visible(self, self->enemy) || !runnertank_can_rocket(self)) + { + self->monsterinfo.currentmove = &runnertank_move_attack_post_rocket; + M_DelayNextAttack(self, 0, true); + return; + } + + M_ContinueAttack(self, &runnertank_move_attack_fire_rocket, &runnertank_move_attack_post_rocket, 0, 768, 0.35); +} + +static void runnertank_doattack_rocket(edict_t *self) +{ + self->monsterinfo.currentmove = &runnertank_move_attack_fire_rocket; +} + +mframe_t runnertank_frames_attack_chain[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, runnertank_plasma, + ai_charge, 0, NULL +}; +mmove_t runnertank_move_attack_chain = { FRAME_attak404, FRAME_attak415, runnertank_frames_attack_chain, runnertank_attack_finished }; + +static void runnertank_jump_attack_takeoff(edict_t *self) +{ + const int speed = 800; + vec3_t forward; + float height_diff; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); + self->lastsound = level.framenum; + + AngleVectors(self->s.angles, forward, NULL, NULL); + self->s.origin[2] += 1; + self->groundentity = NULL; + + VectorScale(forward, speed, self->velocity); + height_diff = self->enemy->absmin[2] - self->absmin[2]; + self->velocity[2] = 260; + if (height_diff > 32) + self->velocity[2] += min(height_diff, 120); + + self->gravity = 1.35; + self->monsterinfo.pausetime = level.time + 1.6; + self->monsterinfo.melee_finished = level.time + RUNNERTANK_JUMP_ATTACK_DELAY; + self->monsterinfo.aiflags |= AI_DUCKED; +} + +static void runnertank_jump_attack_hold(edict_t *self) +{ + vec3_t v; + qboolean close_enough = false; + + if (G_ValidTarget(self, self->enemy, true, true)) + { + VectorSubtract(self->enemy->s.origin, self->s.origin, v); + self->ideal_yaw = vectoyaw(v); + M_ChangeYaw(self); + close_enough = entdist(self, self->enemy) <= RUNNERTANK_JUMP_ATTACK_DROP_RADIUS; + + if (!self->groundentity && close_enough) + { + self->velocity[0] *= 0.35f; + self->velocity[1] *= 0.35f; + if (self->velocity[2] > -RUNNERTANK_JUMP_ATTACK_DROP_SPEED) + self->velocity[2] = -RUNNERTANK_JUMP_ATTACK_DROP_SPEED; + self->gravity = RUNNERTANK_JUMP_ATTACK_DROP_GRAVITY; + if (self->monsterinfo.pausetime > level.time + 0.35f) + self->monsterinfo.pausetime = level.time + 0.35f; + } + } + + if (self->groundentity || (self->waterlevel > 1) || (level.time > self->monsterinfo.pausetime)) + { + self->monsterinfo.aiflags &= ~(AI_HOLD_FRAME | AI_DUCKED); + self->gravity = 1.0; + + if (self->groundentity && G_ValidTarget(self, self->enemy, true, true) && entdist(self, self->enemy) <= RUNNERTANK_JUMP_ATTACK_DROP_RADIUS) + self->monsterinfo.currentmove = &runnertank_move_strike; + } + else + { + self->monsterinfo.aiflags |= AI_HOLD_FRAME; + } +} + +static void runnertank_jump_attack_hold_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + runnertank_jump_attack_hold(self); +} + +mframe_t runnertank_frames_jump_attack[] = +{ + ai_charge, 15, NULL, + ai_move, 0, runnertank_jump_attack_takeoff, + runnertank_jump_attack_hold_ai, 0, NULL, + runnertank_jump_attack_hold_ai, 0, NULL, + runnertank_jump_attack_hold_ai, 0, NULL +}; +mmove_t runnertank_move_jump_attack = { FRAME_run01, FRAME_run05, runnertank_frames_jump_attack, runnertank_attack_finished }; + +static void runnertank_start_jump_attack(edict_t *self) +{ + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + self->monsterinfo.currentmove = &runnertank_move_jump_attack; + self->monsterinfo.nextframe = FRAME_run01; +} + +static qboolean runnertank_can_jump_attack(edict_t *self, float range) +{ + float height_diff; + + if (level.time < self->monsterinfo.melee_finished) + return false; + if (!self->groundentity || (self->waterlevel > 1)) + return false; + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + return false; + if ((range <= RUNNERTANK_MELEE_RANGE) || (range > 512)) + return false; + if (!nearfov(self, self->enemy, 0, RUNNERTANK_JUMP_ATTACK_FOV)) + return false; + + height_diff = self->enemy->absmin[2] - self->absmin[2]; + return (height_diff > -64) && (height_diff < 128); +} + +static qboolean runnertank_should_melee(edict_t *self, float range) +{ + return self->groundentity && range <= RUNNERTANK_MELEE_RANGE; +} + +static void runnertank_melee(edict_t *self) +{ + float range; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + range = entdist(self, self->enemy); + if (runnertank_should_melee(self, range)) + self->monsterinfo.currentmove = &runnertank_move_strike; + else if (runnertank_can_jump_attack(self, range)) + runnertank_start_jump_attack(self); +} + +static void runnertank_attack(edict_t *self) +{ + float r, range; + qboolean attack_started = false; + qboolean can_rail; + qboolean can_rocket; + qboolean can_chain; + + if (!G_ValidTarget(self, self->enemy, true, true)) + return; + + r = random(); + range = entdist(self, self->enemy); + + if (runnertank_should_melee(self, range)) + { + self->monsterinfo.currentmove = &runnertank_move_strike; + M_DelayNextAttack(self, 0, true); + return; + } + + if (runnertank_can_jump_attack(self, range)) + { + runnertank_start_jump_attack(self); + M_DelayNextAttack(self, 0, true); + return; + } + + can_rail = runnertank_can_rail(self); + can_rocket = runnertank_can_rocket(self); + can_chain = runnertank_can_chain(self); + + if (range <= 256) + { + if (r <= 0.35) + { + if (can_chain) + { + self->monsterinfo.currentmove = &runnertank_move_attack_chain; + attack_started = true; + } + else if (can_rocket) + { + self->monsterinfo.currentmove = &runnertank_move_attack_pre_rocket; + attack_started = true; + } + else if (can_rail) + { + self->monsterinfo.currentmove = &runnertank_move_attack_blast; + attack_started = true; + } + } + else if (r <= 0.5) + { + if (can_rocket) + { + self->monsterinfo.currentmove = &runnertank_move_attack_pre_rocket; + attack_started = true; + } + else if (can_chain) + { + self->monsterinfo.currentmove = &runnertank_move_attack_chain; + attack_started = true; + } + else if (can_rail) + { + self->monsterinfo.currentmove = &runnertank_move_attack_blast; + attack_started = true; + } + } + } + else if (range <= 640) + { + if (r <= 0.25) + { + if (can_chain) + { + self->monsterinfo.currentmove = &runnertank_move_attack_chain; + attack_started = true; + } + else if (can_rail) + { + self->monsterinfo.currentmove = &runnertank_move_attack_blast; + attack_started = true; + } + else if (can_rocket) + { + self->monsterinfo.currentmove = &runnertank_move_attack_pre_rocket; + attack_started = true; + } + } + else if (r <= 0.55) + { + if (can_rail) + { + self->monsterinfo.currentmove = &runnertank_move_attack_blast; + attack_started = true; + } + else if (can_rocket) + { + self->monsterinfo.currentmove = &runnertank_move_attack_pre_rocket; + attack_started = true; + } + else if (can_chain) + { + self->monsterinfo.currentmove = &runnertank_move_attack_chain; + attack_started = true; + } + } + else if (r <= 0.7) + { + if (can_rocket) + { + self->monsterinfo.currentmove = &runnertank_move_attack_pre_rocket; + attack_started = true; + } + else if (can_rail) + { + self->monsterinfo.currentmove = &runnertank_move_attack_blast; + attack_started = true; + } + else if (can_chain) + { + self->monsterinfo.currentmove = &runnertank_move_attack_chain; + attack_started = true; + } + } + } + else + { + if (r <= 0.35) + { + if (can_rail) + { + self->monsterinfo.currentmove = &runnertank_move_attack_blast; + attack_started = true; + } + else if (can_rocket) + { + self->monsterinfo.currentmove = &runnertank_move_attack_pre_rocket; + attack_started = true; + } + else if (can_chain) + { + self->monsterinfo.currentmove = &runnertank_move_attack_chain; + attack_started = true; + } + } + else if (r <= 0.55) + { + if (can_rocket) + { + self->monsterinfo.currentmove = &runnertank_move_attack_pre_rocket; + attack_started = true; + } + else if (can_rail) + { + self->monsterinfo.currentmove = &runnertank_move_attack_blast; + attack_started = true; + } + else if (can_chain) + { + self->monsterinfo.currentmove = &runnertank_move_attack_chain; + attack_started = true; + } + } + } + + if (attack_started) + M_DelayNextAttack(self, 0, true); +} + +mframe_t runnertank_frames_pain1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t runnertank_move_pain1 = { FRAME_pain201, FRAME_pain204, runnertank_frames_pain1, runnertank_run }; + +mframe_t runnertank_frames_pain3[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, runnertank_footstep +}; +mmove_t runnertank_move_pain3 = { FRAME_pain301, FRAME_pain316, runnertank_frames_pain3, runnertank_run }; + +static void runnertank_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + runnertank_update_skin(self); + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0; + gi.sound(self, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + + if (damage <= 30 || random() < 0.5) + self->monsterinfo.currentmove = &runnertank_move_pain1; + else + self->monsterinfo.currentmove = &runnertank_move_pain3; +} + +static void runnertank_dead(edict_t *self) +{ + VectorSet(self->mins, -16, -16, -16); + VectorSet(self->maxs, 16, 16, 0); + self->movetype = MOVETYPE_TOSS; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); + M_PrepBodyRemoval(self); +} + +static void runnertank_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + +mframe_t runnertank_frames_death[] = +{ + ai_move, -7, NULL, + ai_move, -2, NULL, + ai_move, -2, NULL, + ai_move, 1, NULL, + ai_move, 3, NULL, + ai_move, 6, NULL, + ai_move, 1, NULL, + ai_move, 1, NULL, + ai_move, 2, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, -2, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, -3, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, -4, NULL, + ai_move, -6, NULL, + ai_move, -4, NULL, + ai_move, -5, NULL, + ai_move, -7, runnertank_shrink, + ai_move, -15, runnertank_thud, + ai_move, -5, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t runnertank_move_death = { FRAME_death01, FRAME_death32, runnertank_frames_death, runnertank_dead }; + +static void runnertank_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + M_Notify(self); + +#ifdef OLD_NOLAG_STYLE + if (nolag->value) + { + M_Remove(self, false, true); + return; + } +#endif + + if (self->health <= self->gib_health) + { + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); + vrx_throw_drone_gibs(self, damage); +#ifdef OLD_NOLAG_STYLE + M_Remove(self, false, false); +#else + if (nolag->value) + M_Remove(self, false, true); + else + M_Remove(self, false, false); +#endif + return; + } + + if (self->deadflag == DEAD_DEAD) + return; + + vrx_drop_tank_death_arm(self, damage); + + DroneList_Remove(self); + + gi.sound(self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + runnertank_update_skin(self); + self->monsterinfo.currentmove = &runnertank_move_death; + + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; +} + +void init_drone_runnertank(edict_t *self) +{ + self->s.modelindex = gi.modelindex("models/vault/monsters/tank/tris.md2"); + VectorSet(self->mins, -28, -28, -14); + VectorSet(self->maxs, 28, 28, 56); + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + + sound_pain = gi.soundindex("tank/tnkpain2.wav"); + sound_idle = gi.soundindex("tank/tnkidle1.wav"); + sound_die = gi.soundindex("tank/death.wav"); + sound_step = gi.soundindex("tank/step.wav"); + sound_windup = gi.soundindex("tank/tnkatck4.wav"); + sound_strike = gi.soundindex("tank/tnkatck5.wav"); + sound_sight = gi.soundindex("tank/sight1.wav"); + sound_thud = gi.soundindex("tank/tnkdeth2.wav"); + sound_plasma = gi.soundindex("weapons/plasshot.wav"); + + gi.soundindex("tank/tnkatck1.wav"); + gi.soundindex("tank/tnkatk2a.wav"); + gi.soundindex("tank/tnkatk2b.wav"); + gi.soundindex("tank/tnkatk2c.wav"); + gi.soundindex("tank/tnkatk2d.wav"); + gi.soundindex("tank/tnkatk2e.wav"); + gi.soundindex("tank/tnkatck3.wav"); + + self->health = M_RUNNERTANK_INITIAL_HEALTH + M_RUNNERTANK_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -200; + self->mass = 500; + + self->pain = runnertank_pain; + self->die = runnertank_die; + + self->monsterinfo.stand = runnertank_stand; + self->monsterinfo.walk = runnertank_walk; + self->monsterinfo.run = runnertank_run; + self->monsterinfo.attack = runnertank_attack; + self->monsterinfo.melee = runnertank_melee; + self->monsterinfo.sight = runnertank_sight; + self->monsterinfo.idle = runnertank_idle; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.power_armor_power = M_RUNNERTANK_INITIAL_ARMOR + M_RUNNERTANK_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_TANK_CONTROL_COST; + self->monsterinfo.cost = M_TANK_COST; + self->mtype = M_RUNNERTANK; + skin_normal = gi.imageindex(RUNNERTANK_NORMAL_SKIN); + skin_pain = gi.imageindex(RUNNERTANK_PAIN_SKIN); + runnertank_update_skin(self); + + gi.linkentity(self); + + self->monsterinfo.currentmove = &runnertank_move_stand; + self->monsterinfo.scale = MODEL_SCALE; + self->nextthink = level.time + FRAMETIME; +} diff --git a/src/entities/drone/drone_shambler.c b/src/entities/drone/drone_shambler.c index c8399220..0cf5d4f9 100644 --- a/src/entities/drone/drone_shambler.c +++ b/src/entities/drone/drone_shambler.c @@ -425,19 +425,9 @@ void ShamblerCastLightning(edict_t* self) gi.WritePosition(tr.endpos); gi.multicast(start, MULTICAST_PVS); - - //apply dmg! - - - - if (tr.fraction < 1.0 && tr.ent) + if (tr.fraction < 1.0f && tr.ent) { - //int damage = M_SHAMBLER_LIGHTNING_BASE_DMG + M_SHAMBLER_ADDON_LIGHTNING_DMG; //* self->monsterinfo.level; ? const int damage = 4 + 3 * drone_damagelevel(self); - - //if (M_SHAMBLER_LIGHTNING_MAX_DMG && damage > M_SHAMBLER_LIGHTNING_MAX_DMG) - // damage = M_SHAMBLER_LIGHTNING_MAX_DMG; - T_Damage(tr.ent, self, self, dir, tr.endpos, tr.plane.normal, damage, 0, DAMAGE_ENERGY, MOD_LIGHTNING); } } @@ -458,44 +448,6 @@ mframe_t shambler_frames_magic[] = { }; mmove_t shambler_move_attack = { FRAME_magic1, FRAME_magic12, shambler_frames_magic, shambler_run }; - -//fiery skull attack - -static void shambler_fieryskull_update(edict_t* self) -{ - const int frame_offset = self->s.frame - FRAME_magic1; - if (frame_offset >= MAX_LIGHTNING_FRAMES) - { - return; - } - - vec3_t f, r; - AngleVectors(self->s.angles, f, r, NULL); - - vec3_t left_pos, right_pos; - VectorMA(self->s.origin, lightning_left_hand[frame_offset][0], f, left_pos); - VectorMA(left_pos, lightning_left_hand[frame_offset][1], r, left_pos); - left_pos[2] += lightning_left_hand[frame_offset][2]; - - VectorMA(self->s.origin, lightning_right_hand[frame_offset][0], f, right_pos); - VectorMA(right_pos, lightning_right_hand[frame_offset][1], r, right_pos); - right_pos[2] += lightning_right_hand[frame_offset][2]; - - gi.WriteByte(svc_temp_entity); -#ifndef VRX_REPRO - gi.WriteByte(TE_MONSTER_HEATBEAM); - gi.WriteShort(self - g_edicts); -#else - gi.WriteByte(TE_LIGHTNING); - gi.WriteShort(self - g_edicts); - gi.WriteShort(0); -#endif - - gi.WritePosition(left_pos); - gi.WritePosition(right_pos); - gi.multicast(left_pos, MULTICAST_PVS); -} - // New function to calculate aim direction void CalculateAimDirection(edict_t* self, vec3_t start, vec3_t aim) { @@ -583,32 +535,29 @@ static void shambler_ice_update(edict_t* self) VectorMA(right_pos, lightning_right_hand[frame_offset][1], r, right_pos); right_pos[2] += lightning_right_hand[frame_offset][2]; - // create fire models on both hands - edict_t* left_fire = G_Spawn(); - edict_t* right_fire = G_Spawn(); + // create cyan ice glow on both hands + edict_t* left_glow = G_Spawn(); + edict_t* right_glow = G_Spawn(); - VectorCopy(left_pos, left_fire->s.origin); - VectorCopy(right_pos, right_fire->s.origin); + VectorCopy(left_pos, left_glow->s.origin); + VectorCopy(right_pos, right_glow->s.origin); - left_fire->s.modelindex = gi.modelindex("models/fire/tris.md2"); - right_fire->s.modelindex = gi.modelindex("models/fire/tris.md2"); - - //left_fire->s.modelindex = gi.modelindex("models/objects/flball/tris.md2"); // ugly - //right_fire->s.modelindex = gi.modelindex("models/objects/flball/tris.md2"); // ugly - left_fire->s.effects |= EF_QUAD | RF_SHELL_CYAN; - right_fire->s.effects |= EF_QUAD | RF_SHELL_CYAN; + left_glow->s.modelindex = gi.modelindex("models/fire/tris.md2"); + right_glow->s.modelindex = gi.modelindex("models/fire/tris.md2"); + left_glow->s.effects |= EF_QUAD | RF_SHELL_CYAN; + right_glow->s.effects |= EF_QUAD | RF_SHELL_CYAN; - left_fire->s.renderfx |= RF_FULLBRIGHT; - right_fire->s.renderfx |= RF_FULLBRIGHT; + left_glow->s.renderfx |= RF_FULLBRIGHT; + right_glow->s.renderfx |= RF_FULLBRIGHT; - left_fire->think = G_FreeEdict; - right_fire->think = G_FreeEdict; + left_glow->think = G_FreeEdict; + right_glow->think = G_FreeEdict; - left_fire->nextthink = level.time + 0.1; - right_fire->nextthink = level.time + 0.1; + left_glow->nextthink = level.time + 0.1; + right_glow->nextthink = level.time + 0.1; - gi.linkentity(left_fire); - gi.linkentity(right_fire); + gi.linkentity(left_glow); + gi.linkentity(right_glow); } void shambler_windupIce(edict_t* self) // lightning preparing @@ -637,159 +586,6 @@ mframe_t shambler_frames_icebolt[] = { }; mmove_t shambler_move_icebolt = { FRAME_magic1, FRAME_magic12, shambler_frames_icebolt, shambler_run }; - -// FIERY ROCKET SKULLS - -void shambler_windupFire(edict_t* self) // lightning preparing -{ - shambler_fieryskull_update(self); - - gi.sound(self, CHAN_WEAPON, gi.soundindex("sound_attack"), 1, ATTN_NORM, 0); - - self->nextthink = level.time + FRAMETIME; -} - - -//pre fire attack stuff -static void shambler_fire_update(edict_t* self) -{ - const int frame_offset = self->s.frame - FRAME_magic1; - if (frame_offset >= MAX_LIGHTNING_FRAMES) - { - return; - } - vec3_t f, r; - AngleVectors(self->s.angles, f, r, NULL); - - // Proyectar las posiciones de las manos - vec3_t left_pos, right_pos; - VectorMA(self->s.origin, lightning_left_hand[frame_offset][0], f, left_pos); - VectorMA(left_pos, lightning_left_hand[frame_offset][1], r, left_pos); - left_pos[2] += lightning_left_hand[frame_offset][2]; - - VectorMA(self->s.origin, lightning_right_hand[frame_offset][0], f, right_pos); - VectorMA(right_pos, lightning_right_hand[frame_offset][1], r, right_pos); - right_pos[2] += lightning_right_hand[frame_offset][2]; - - // Crear efectos de cr�neo en ambas manos - edict_t* left_fire = G_Spawn(); - edict_t* right_fire = G_Spawn(); - - VectorCopy(left_pos, left_fire->s.origin); - VectorCopy(right_pos, right_fire->s.origin); - - left_fire->s.modelindex = gi.modelindex("models/fire/tris.md2"); - right_fire->s.modelindex = gi.modelindex("models/fire/tris.md2"); - - left_fire->s.effects |= EF_GIB | EF_ROCKET; - right_fire->s.effects |= EF_GIB | EF_ROCKET; - - left_fire->s.renderfx |= RF_FULLBRIGHT; - right_fire->s.renderfx |= RF_FULLBRIGHT; - - left_fire->think = G_FreeEdict; - right_fire->think = G_FreeEdict; - - left_fire->nextthink = level.time + 0.1; - right_fire->nextthink = level.time + 0.1; - - gi.linkentity(left_fire); - gi.linkentity(right_fire); -} - -void bskull_touch(edict_t* self, edict_t* other, cplane_t* plane, csurface_t* surf); -//void magicbolt_touch(edict_t* self, edict_t* other, cplane_t* plane, csurface_t* surf); -void fire_shambler_skull(edict_t* self, vec3_t start, vec3_t dir, int damage, int speed, float damage_radius) -{ - edict_t* skull; - skull = G_Spawn(); - VectorCopy(start, skull->s.origin); - VectorCopy(dir, skull->movedir); - vectoangles(dir, skull->s.angles); - VectorScale(dir, speed, skull->velocity); - skull->movetype = MOVETYPE_FLYMISSILE; - skull->clipmask = MASK_SHOT; - skull->solid = SOLID_BBOX; - VectorClear(skull->mins); - VectorClear(skull->maxs); - skull->s.modelindex = gi.modelindex("models/objects/gibs/skull/tris.md2"); - skull->owner = self; - skull->touch = bskull_touch; - skull->dmg = damage; - skull->radius_dmg = 120; - skull->dmg_radius = damage_radius; - skull->s.effects = EF_GIB | EF_ROCKET; - skull->s.sound = gi.soundindex("weapons/rockfly.wav"); - skull->classname = "shambler_skull"; - skull->nextthink = level.time + 8000 / speed; - skull->think = G_FreeEdict; - gi.linkentity(skull); -} - -void fire_skull(edict_t* self, vec3_t start, vec3_t dir, int damage, int speed, float damage_radius); - -void ShamblerCastSkull(edict_t* self) -{ - vec3_t forward, right; - vec3_t start_left, start_right; - const float accuracy = M_PROJECTILE_ACC; - - if (!G_EntIsAlive(self->enemy)) - return; - - // Get the current frame offset - int frame_offset = self->s.frame - FRAME_magic1; - if (frame_offset >= MAX_LIGHTNING_FRAMES) - { - frame_offset = MAX_LIGHTNING_FRAMES - 1; - } - - // Calculate hand positions using the same method as for lightning - AngleVectors(self->s.angles, forward, right, NULL); - - // Left hand - VectorMA(self->s.origin, lightning_left_hand[frame_offset][0], forward, start_left); - VectorMA(start_left, lightning_left_hand[frame_offset][1], right, start_left); - start_left[2] = self->s.origin[2] + lightning_left_hand[frame_offset][2]; - - // Right hand - VectorMA(self->s.origin, lightning_right_hand[frame_offset][0], forward, start_right); - VectorMA(start_right, lightning_right_hand[frame_offset][1], right, start_right); - start_right[2] = self->s.origin[2] + lightning_right_hand[frame_offset][2]; - - // Calculate damage - const int damage = 30 + 15 * self->monsterinfo.level; - - // Fire skull from left hand - MonsterAim(self, accuracy, 1200, false, -1, forward, start_left); - fire_shambler_skull(self, start_left, forward, damage, 1600, 50); - - // Fire skull from right hand - MonsterAim(self, accuracy, 1600, false, -1, forward, start_right); - fire_shambler_skull(self, start_right, forward, damage, 1300, 50); - - // Play sound effect - gi.sound(self, CHAN_WEAPON, gi.soundindex("spells/circle1.wav"), 1, ATTN_NORM, 0); -} - - -mframe_t shambler_frames_skull[] = { - {ai_charge, 0, shambler_windupFire}, - {ai_charge, 0, shambler_fire_update}, - {ai_charge, 0, shambler_fire_update}, - {ai_move, 0, shambler_fire_update}, - {ai_move, 0, shambler_fire_update}, - {ai_move, 0, ShamblerSaveLoc}, - {ai_move, 0, NULL}, - {ai_move, 0, NULL}, - {ai_move, 0, ShamblerCastSkull}, - {ai_move, 0, ShamblerSaveLoc}, - {ai_move, 0, ShamblerCastSkull}, - {ai_charge, 0, NULL}, -}; -mmove_t shambler_move_skull = { FRAME_magic1, FRAME_magic12, shambler_frames_skull, shambler_run }; - - void shambler_meleehit(edict_t* self); mframe_t shambler_frames_melee[] = @@ -823,12 +619,19 @@ mmove_t shambler_move_pain = { FRAME_pain1, FRAME_pain6, shambler_frames_pain, s void shambler_dead(edict_t* self); +static void shambler_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t shambler_frames_death[] = { {ai_move, 0, NULL}, {ai_move, 0, NULL}, - {ai_move, 0, NULL}, + {ai_move, 0, shambler_shrink}, {ai_move, 0, NULL}, {ai_move, 0, NULL}, {ai_move, 0, NULL}, @@ -894,7 +697,6 @@ void shambler_attack(edict_t* self) } else if (r < 0.3 && infront(self, self->enemy)) // 30% to use icebolt attack { - // self->monsterinfo.currentmove = &shambler_move_skull; self->monsterinfo.currentmove = &shambler_move_icebolt; } else @@ -928,7 +730,7 @@ void shambler_pain(edict_t* self, edict_t* other, float kick, int damage) void shambler_dead(edict_t* self) { VectorSet(self->mins, -16, -16, -24); - VectorSet(self->maxs, 16, 16, -8); + VectorSet(self->maxs, 16, 16, 0); self->movetype = MOVETYPE_TOSS; self->svflags |= SVF_DEADMONSTER; self->nextthink = 0; @@ -938,8 +740,6 @@ void shambler_dead(edict_t* self) void shambler_die(edict_t* self, edict_t* inflictor, edict_t* attacker, int damage, vec3_t point) { - int n; - // notify the owner that the monster is dead M_Notify(self); @@ -955,14 +755,7 @@ void shambler_die(edict_t* self, edict_t* inflictor, edict_t* attacker, int dama if (self->health <= self->gib_health) { gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - ThrowHead(self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; //return;//FIXME: this will cause DroneList_Next to enter an infinite loop @@ -986,6 +779,7 @@ void shambler_die(edict_t* self, edict_t* inflictor, edict_t* attacker, int dama gi.sound(self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); self->monsterinfo.currentmove = &shambler_move_death; if (self->activator && !self->activator->client) @@ -1012,13 +806,6 @@ void init_drone_shambler(edict_t* self) //icebolt shambler gi.soundindex("spells/coldcast.wav"); - //fire shambler ( unused ) - // - //gi.modelindex("models/objects/gibs/skull/tris.md2"); - //gi.modelindex("models/fire/tris.md2"); - //gi.soundindex("weapons/rockfly.wav"); - //gi.soundindex("spells/circle1.wav"); - //shambler sounds sound_pain = gi.soundindex("shambler/shurt2.wav"); sound_idle = gi.soundindex("shambler/sidle.wav"); diff --git a/src/entities/drone/drone_soldier.c b/src/entities/drone/drone_soldier.c index 8bb9f1ac..6cfbc8a8 100644 --- a/src/entities/drone/drone_soldier.c +++ b/src/entities/drone/drone_soldier.c @@ -20,8 +20,109 @@ static int sound_death; static int sound_death_ss; static int sound_cock; +static mmove_t m_soldier_move_attack3; +static mmove_t m_soldier_move_attack2; +static mmove_t m_soldier_move_attack5; +static mmove_t m_soldier_move_trip; +static mmove_t m_soldier_move_duck; + +void m_soldier_duck_down(edict_t *self); +void m_soldier_duck_up(edict_t *self); + +static const int soldier_blaster_flash[] = +{ + MZ2_SOLDIER_BLASTER_1, + MZ2_SOLDIER_BLASTER_2, + MZ2_SOLDIER_BLASTER_3, + MZ2_SOLDIER_BLASTER_4, + MZ2_SOLDIER_BLASTER_5, + MZ2_SOLDIER_BLASTER_6, + MZ2_SOLDIER_BLASTER_7, + MZ2_SOLDIER_BLASTER_8, + MZ2_SOLDIER_BLASTER_9 +}; + +static const int soldier_shotgun_flash[] = +{ + MZ2_SOLDIER_SHOTGUN_1, + MZ2_SOLDIER_SHOTGUN_2, + MZ2_SOLDIER_SHOTGUN_3, + MZ2_SOLDIER_SHOTGUN_4, + MZ2_SOLDIER_SHOTGUN_5, + MZ2_SOLDIER_SHOTGUN_6, + MZ2_SOLDIER_SHOTGUN_7, + MZ2_SOLDIER_SHOTGUN_8, + MZ2_SOLDIER_SHOTGUN_9 +}; + +static const int soldier_machinegun_flash[] = +{ + MZ2_SOLDIER_MACHINEGUN_1, + MZ2_SOLDIER_MACHINEGUN_2, + MZ2_SOLDIER_MACHINEGUN_3, + MZ2_SOLDIER_MACHINEGUN_4, + MZ2_SOLDIER_MACHINEGUN_5, + MZ2_SOLDIER_MACHINEGUN_6, + MZ2_SOLDIER_MACHINEGUN_7, + MZ2_SOLDIER_MACHINEGUN_8, + MZ2_SOLDIER_MACHINEGUN_9 +}; + +static const int soldier_ripper_flash[] = +{ + MZ2_SOLDIER_RIPPER_1, + MZ2_SOLDIER_RIPPER_2, + MZ2_SOLDIER_RIPPER_3, + MZ2_SOLDIER_RIPPER_4, + MZ2_SOLDIER_RIPPER_5, + MZ2_SOLDIER_RIPPER_6, + MZ2_SOLDIER_RIPPER_7, + MZ2_SOLDIER_RIPPER_8, + MZ2_SOLDIER_RIPPER_9 +}; + +static const int soldier_hyper_flash[] = +{ + MZ2_SOLDIER_HYPERGUN_1, + MZ2_SOLDIER_HYPERGUN_2, + MZ2_SOLDIER_HYPERGUN_3, + MZ2_SOLDIER_HYPERGUN_4, + MZ2_SOLDIER_HYPERGUN_5, + MZ2_SOLDIER_HYPERGUN_6, + MZ2_SOLDIER_HYPERGUN_7, + MZ2_SOLDIER_HYPERGUN_8, + MZ2_SOLDIER_HYPERGUN_9 +}; + void m_soldier_stand (edict_t *self); +void m_soldier_walk (edict_t *self); +void m_soldier_run (edict_t *self); void m_soldier_runandshoot_continue (edict_t *self); +void drone_ai_run_slide(edict_t *self, float dist); + +static qboolean soldier_uses_light_voice(edict_t *self) +{ + return self->mtype == M_SOLDIER || + self->mtype == M_SOLDIER_RIPPER; +} + +static int soldier_pain_sound(edict_t *self) +{ + if (soldier_uses_light_voice(self)) + return sound_pain_light; + if (self->mtype == M_SOLDIERSS) + return sound_pain; + return sound_pain_ss; +} + +static int soldier_death_sound(edict_t *self) +{ + if (soldier_uses_light_voice(self)) + return sound_death_light; + if (self->mtype == M_SOLDIERSS) + return sound_death; + return sound_death_ss; +} void m_soldier_idle (edict_t *self) { @@ -37,6 +138,14 @@ void m_soldier_cock (edict_t *self) gi.sound (self, CHAN_WEAPON, sound_cock, 1, ATTN_NORM, 0); } +void m_soldier_sight (edict_t *self, edict_t *other) +{ + if (random() < 0.5) + gi.sound (self, CHAN_VOICE, sound_sight1, 1, ATTN_NORM, 0); + else + gi.sound (self, CHAN_VOICE, sound_sight2, 1, ATTN_NORM, 0); +} + mframe_t m_soldier_frames_stand1 [] = { drone_ai_stand, 0, m_soldier_idle, @@ -74,6 +183,54 @@ mframe_t m_soldier_frames_stand1 [] = }; mmove_t m_soldier_move_stand1 = {FRAME_stand101, FRAME_stand130, m_soldier_frames_stand1, m_soldier_stand}; +mframe_t m_soldier_frames_stand2 [] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t m_soldier_move_stand2 = {FRAME_stand201, FRAME_stand240, m_soldier_frames_stand2, m_soldier_stand}; + mframe_t m_soldier_frames_stand3 [] = { drone_ai_stand, 0, NULL, @@ -123,12 +280,90 @@ mmove_t m_soldier_move_stand3 = {FRAME_stand301, FRAME_stand339, m_soldier_frame void m_soldier_stand (edict_t *self) { - if (random() > 0.5) + float r = random(); + + if (self->monsterinfo.currentmove != &m_soldier_move_stand1 || r < 0.6f) self->monsterinfo.currentmove = &m_soldier_move_stand1; + else if (r < 0.8f) + self->monsterinfo.currentmove = &m_soldier_move_stand2; else self->monsterinfo.currentmove = &m_soldier_move_stand3; } +mframe_t m_soldier_frames_walk1 [] = +{ + drone_ai_walk, 3, NULL, + drone_ai_walk, 6, NULL, + drone_ai_walk, 2, NULL, + drone_ai_walk, 2, NULL, + drone_ai_walk, 2, NULL, + drone_ai_walk, 1, NULL, + drone_ai_walk, 6, NULL, + drone_ai_walk, 5, NULL, + drone_ai_walk, 3, NULL, + drone_ai_walk, -1, NULL, + + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL, + drone_ai_walk, 0, NULL +}; +mmove_t m_soldier_move_walk1 = {FRAME_walk101, FRAME_walk133, m_soldier_frames_walk1, NULL}; + +mframe_t m_soldier_frames_walk2 [] = +{ + drone_ai_walk, 4, NULL, + drone_ai_walk, 4, NULL, + drone_ai_walk, 9, NULL, + drone_ai_walk, 8, NULL, + drone_ai_walk, 5, NULL, + drone_ai_walk, 1, NULL, + drone_ai_walk, 3, NULL, + drone_ai_walk, 7, NULL, + drone_ai_walk, 6, NULL, + drone_ai_walk, 7, NULL +}; +mmove_t m_soldier_move_walk2 = {FRAME_walk209, FRAME_walk218, m_soldier_frames_walk2, NULL}; + +void m_soldier_walk (edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + + if (random() < 0.5f) + self->monsterinfo.currentmove = &m_soldier_move_walk1; + else + self->monsterinfo.currentmove = &m_soldier_move_walk2; +} + +mframe_t m_soldier_frames_start_run [] = +{ + drone_ai_run, 7, NULL, + drone_ai_run, 5, NULL +}; +mmove_t m_soldier_move_start_run = {FRAME_run01, FRAME_run02, m_soldier_frames_start_run, m_soldier_run}; + mframe_t m_soldier_frames_run [] = { drone_ai_run, 25, NULL, @@ -140,15 +375,110 @@ mframe_t m_soldier_frames_run [] = }; mmove_t m_soldier_move_run = {FRAME_run03, FRAME_run08, m_soldier_frames_run, NULL}; +static void m_soldier_ai_dodge_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(self->monsterinfo.attacker)) + self->enemy = self->monsterinfo.attacker; + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_run_slide(self, dist); +} + +mframe_t m_soldier_frames_dodge_slide[] = +{ + m_soldier_ai_dodge_slide, 10, NULL, + m_soldier_ai_dodge_slide, 11, NULL, + m_soldier_ai_dodge_slide, 11, NULL, + m_soldier_ai_dodge_slide, 16, NULL, + m_soldier_ai_dodge_slide, 10, NULL, + m_soldier_ai_dodge_slide, 15, NULL +}; +mmove_t m_soldier_move_dodge_slide = {FRAME_run03, FRAME_run08, m_soldier_frames_dodge_slide, m_soldier_run}; + void m_soldier_run (edict_t *self) { if (self->monsterinfo.aiflags & AI_STAND_GROUND) m_soldier_stand(self); - else + else if (self->monsterinfo.currentmove == &m_soldier_move_walk1 || + self->monsterinfo.currentmove == &m_soldier_move_walk2 || + self->monsterinfo.currentmove == &m_soldier_move_start_run || + self->monsterinfo.currentmove == &m_soldier_move_run) self->monsterinfo.currentmove = &m_soldier_move_run; + else + self->monsterinfo.currentmove = &m_soldier_move_start_run; } -void soldier_fireblaster(edict_t* self) +static int soldier_flash_from_table(const int *flashes, size_t count, int flash_number) +{ + if (flash_number < 0 || flash_number >= (int)count) + flash_number = 7; + return flashes[flash_number]; +} + +static void soldier_project_raw_flash_origin(edict_t *self, int flash, vec3_t forward, vec3_t start) +{ + vec3_t right; + + AngleVectors(self->s.angles, forward, right, NULL); + G_ProjectSource(self->s.origin, monster_flash_offset[flash], forward, right, start); +} + +static qboolean soldier_has_raw_flash_shot(edict_t *self, int flash) +{ + vec3_t forward, start; + + soldier_project_raw_flash_origin(self, flash, forward, start); + return M_MonsterHasClearShotFrom(self, start); +} + +static qboolean soldier_has_flash_shot(edict_t *self, int flash, qboolean raw_origin) +{ + if (raw_origin) + return soldier_has_raw_flash_shot(self, flash); + + return M_MonsterHasClearShotFromFlash(self, flash); +} + +static void soldier_project_laser_origin(edict_t *self, int flash, vec3_t start) +{ + vec3_t forward, right, up, offset; + + AngleVectors(self->s.angles, forward, right, up); + VectorCopy(self->s.origin, start); + VectorCopy(monster_flash_offset[flash], offset); + VectorMA(start, offset[0], forward, start); + VectorMA(start, offset[1], right, start); + VectorMA(start, offset[2] + 6, up, start); +} + +static qboolean soldier_has_laser_shot(edict_t *self, int flash) +{ + vec3_t start; + + soldier_project_laser_origin(self, flash, start); + return M_MonsterHasClearShotFrom(self, start); +} + +static qboolean m_soldier_can_primary_shot(edict_t *self) +{ + if (self->mtype == M_SOLDIER) + return M_MonsterHasClearShotFromFlash(self, MZ2_SOLDIER_BLASTER_8); + if (self->mtype == M_SOLDIERLT) + return M_MonsterHasClearShotFromFlash(self, MZ2_SOLDIER_BLASTER_8); + if (self->mtype == M_SOLDIERSS) + return M_MonsterHasClearShotFromFlash(self, MZ2_SOLDIER_SHOTGUN_8); + if (self->mtype == M_SOLDIER_RIPPER) + return M_MonsterHasClearShotFromFlash(self, MZ2_SOLDIER_RIPPER_8); + if (self->mtype == M_SOLDIER_BLUEBLASTER) + return M_MonsterHasClearShotFromFlash(self, MZ2_SOLDIER_HYPERGUN_8); + if (self->mtype == M_SOLDIER_LASER) + return soldier_has_laser_shot(self, MZ2_SOLDIER_MACHINEGUN_4); + + return false; +} + +static void soldier_fireblaster_flash_ex(edict_t* self, int flash, qboolean raw_origin) { int damage, speed; vec3_t forward, start; @@ -164,11 +494,30 @@ void soldier_fireblaster(edict_t* self) if (M_BLASTER_SPEED_MAX && speed > M_BLASTER_SPEED_MAX) speed = M_BLASTER_SPEED_MAX; - MonsterAim(self, M_PROJECTILE_ACC, speed, false, MZ2_SOLDIER_BLASTER_8, forward, start); - monster_fire_blaster(self, start, forward, damage, speed, EF_BLASTER, BLASTER_PROJ_BOLT, 2.0, true, MZ2_SOLDIER_BLASTER_8); + if (raw_origin) + { + soldier_project_raw_flash_origin(self, flash, forward, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, -1, forward, start); + } + else + MonsterAim(self, M_PROJECTILE_ACC, speed, false, flash, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_blaster(self, start, forward, damage, speed, EF_BLASTER, BLASTER_PROJ_BOLT, 2.0, true, flash); } -void soldier_firerocket(edict_t* self) +static void soldier_fireblaster_flash(edict_t* self, int flash) +{ + soldier_fireblaster_flash_ex(self, flash, false); +} + +void soldier_fireblaster(edict_t* self) +{ + soldier_fireblaster_flash(self, MZ2_SOLDIER_BLASTER_8); +} + +static void soldier_firerocket_flash_ex(edict_t* self, int flash, qboolean raw_origin) { int damage, speed; vec3_t forward, start; @@ -183,11 +532,30 @@ void soldier_firerocket(edict_t* self) if (M_ROCKETLAUNCHER_SPEED_MAX && speed > M_ROCKETLAUNCHER_SPEED_MAX) speed = M_ROCKETLAUNCHER_SPEED_MAX; - MonsterAim(self, M_PROJECTILE_ACC, speed, true, MZ2_SOLDIER_BLASTER_8, forward, start); - monster_fire_rocket(self, start, forward, damage, speed, MZ2_SOLDIER_BLASTER_8); + if (raw_origin) + { + soldier_project_raw_flash_origin(self, flash, forward, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, true, -1, forward, start); + } + else + MonsterAim(self, M_PROJECTILE_ACC, speed, true, flash, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_rocket(self, start, forward, damage, speed, flash); } -void soldier_fireshotgun(edict_t* self) +static void soldier_firerocket_flash(edict_t* self, int flash) +{ + soldier_firerocket_flash_ex(self, flash, false); +} + +void soldier_firerocket(edict_t* self) +{ + soldier_firerocket_flash(self, MZ2_SOLDIER_BLASTER_8); +} + +static void soldier_fireshotgun_flash(edict_t* self, int flash) { int damage; vec3_t forward, start; @@ -198,8 +566,136 @@ void soldier_fireshotgun(edict_t* self) damage = M_SHOTGUN_DMG_BASE + M_SHOTGUN_DMG_ADDON * drone_damagelevel(self); if (M_SHOTGUN_DMG_MAX && damage > M_SHOTGUN_DMG_MAX) damage = M_SHOTGUN_DMG_MAX; - MonsterAim(self, M_HITSCAN_INSTANT_ACC, 0, false, MZ2_SOLDIER_SHOTGUN_8, forward, start); - monster_fire_shotgun(self, start, forward, damage, 15, 375, 375, 10, MZ2_SOLDIER_SHOTGUN_8); + MonsterAim(self, M_HITSCAN_INSTANT_ACC, 0, false, flash, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_shotgun(self, start, forward, damage, 15, 375, 375, 10, flash); +} + +void soldier_fireshotgun(edict_t* self) +{ + soldier_fireshotgun_flash(self, MZ2_SOLDIER_SHOTGUN_8); +} + +static void soldier_fireionripper_ex(edict_t* self, int flash_number, qboolean raw_origin) +{ + int damage, speed; + int flash; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + flash = soldier_flash_from_table(soldier_ripper_flash, + sizeof(soldier_ripper_flash) / sizeof(soldier_ripper_flash[0]), flash_number); + damage = IONRIPPER_INITIAL_DAMAGE + IONRIPPER_ADDON_DAMAGE * drone_damagelevel(self); + speed = IONRIPPER_INITIAL_SPEED + IONRIPPER_ADDON_SPEED * drone_damagelevel(self); + + if (raw_origin) + { + soldier_project_raw_flash_origin(self, flash, forward, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, -1, forward, start); + } + else + MonsterAim(self, M_PROJECTILE_ACC, speed, false, flash, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_ionripper(self, start, forward, damage, speed, EF_IONRIPPER, flash); +} + +void soldier_fireionripper(edict_t* self, int flash_number) +{ + soldier_fireionripper_ex(self, flash_number, false); +} + +static void soldier_fireblueblaster_ex(edict_t* self, int flash_number, qboolean raw_origin) +{ + int damage, speed; + int flash; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + flash = soldier_flash_from_table(soldier_hyper_flash, + sizeof(soldier_hyper_flash) / sizeof(soldier_hyper_flash[0]), flash_number); + damage = M_HYPERBLASTER_DMG_BASE + M_HYPERBLASTER_DMG_ADDON * drone_damagelevel(self); + if (M_HYPERBLASTER_DMG_MAX && damage > M_HYPERBLASTER_DMG_MAX) + damage = M_HYPERBLASTER_DMG_MAX; + + speed = HYPERBLASTER_INITIAL_SPEED + HYPERBLASTER_ADDON_SPEED * drone_damagelevel(self); + if (speed < 400) + speed = 400; + + if (raw_origin) + { + soldier_project_raw_flash_origin(self, flash, forward, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, -1, forward, start); + } + else + MonsterAim(self, M_PROJECTILE_ACC, speed, false, flash, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_blueblaster(self, start, forward, damage, speed, EF_BLUEHYPERBLASTER, flash); +} + +void soldier_fireblueblaster(edict_t* self, int flash_number) +{ + soldier_fireblueblaster_ex(self, flash_number, false); +} + +static void soldier_laser_update(edict_t *laser) +{ + edict_t *self; + vec3_t forward, right, up, start, offset, target; + qboolean do_damage; + + self = laser->owner; + if (!self || !self->inuse || !G_EntExists(self->enemy)) + { + laser->spawnflags |= DABEAM_SPAWNED; + return; + } + + if (self->radius_dmg <= 0 || self->radius_dmg > MZ2_LAST) + self->radius_dmg = MZ2_SOLDIER_MACHINEGUN_4; + + AngleVectors(self->s.angles, forward, right, up); + VectorCopy(self->s.origin, start); + VectorCopy(monster_flash_offset[self->radius_dmg], offset); + VectorMA(start, offset[0], forward, start); + VectorMA(start, offset[1], right, start); + VectorMA(start, offset[2] + 6, up, start); + + G_EntMidPoint(self->enemy, target); + VectorSubtract(target, start, forward); + VectorNormalize(forward); + + VectorCopy(start, laser->s.origin); + VectorCopy(forward, laser->movedir); + + do_damage = !(laser->spawnflags & DABEAM_SPAWNED); + dabeam_update(laser, do_damage); + laser->spawnflags |= DABEAM_SPAWNED; +} + +void soldier_firelaser(edict_t* self, int flash_number) +{ + int damage; + + if (!G_EntExists(self->enemy)) + return; + if (!soldier_has_laser_shot(self, flash_number)) + return; + + self->radius_dmg = flash_number; + damage = M_DABEAM_DMG_BASE + M_DABEAM_DMG_ADDON * drone_damagelevel(self); + if (M_DABEAM_DMG_MAX && damage > M_DABEAM_DMG_MAX) + damage = M_DABEAM_DMG_MAX; + monster_fire_dabeam(self, damage, false, soldier_laser_update); } void m_soldier_fire (edict_t *self) @@ -213,16 +709,148 @@ void m_soldier_fire (edict_t *self) soldier_firerocket(self); else if (self->mtype == M_SOLDIERSS) soldier_fireshotgun(self); + else if (self->mtype == M_SOLDIER_RIPPER) + soldier_fireionripper(self, 7); + else if (self->mtype == M_SOLDIER_BLUEBLASTER) + soldier_fireblueblaster(self, 7); + else if (self->mtype == M_SOLDIER_LASER) + soldier_firelaser(self, MZ2_SOLDIER_MACHINEGUN_4); } +static qboolean m_soldier_can_flash_slot_shot(edict_t *self, int flash_slot) +{ + if (self->mtype == M_SOLDIER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), flash_slot), false); + if (self->mtype == M_SOLDIERLT) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), flash_slot), false); + if (self->mtype == M_SOLDIERSS) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_shotgun_flash, + sizeof(soldier_shotgun_flash) / sizeof(soldier_shotgun_flash[0]), flash_slot), false); + if (self->mtype == M_SOLDIER_RIPPER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_ripper_flash, + sizeof(soldier_ripper_flash) / sizeof(soldier_ripper_flash[0]), flash_slot), false); + if (self->mtype == M_SOLDIER_BLUEBLASTER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_hyper_flash, + sizeof(soldier_hyper_flash) / sizeof(soldier_hyper_flash[0]), flash_slot), false); + if (self->mtype == M_SOLDIER_LASER) + return soldier_has_laser_shot(self, soldier_flash_from_table(soldier_machinegun_flash, + sizeof(soldier_machinegun_flash) / sizeof(soldier_machinegun_flash[0]), flash_slot)); + + return false; +} + +static void m_soldier_fire_flash_slot(edict_t *self, int flash_slot) +{ + if (!G_EntExists(self->enemy)) + return; + + if (self->mtype == M_SOLDIER) + soldier_fireblaster_flash(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), flash_slot)); + else if (self->mtype == M_SOLDIERLT) + soldier_firerocket_flash(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), flash_slot)); + else if (self->mtype == M_SOLDIERSS) + soldier_fireshotgun_flash(self, soldier_flash_from_table(soldier_shotgun_flash, + sizeof(soldier_shotgun_flash) / sizeof(soldier_shotgun_flash[0]), flash_slot)); + else if (self->mtype == M_SOLDIER_RIPPER) + soldier_fireionripper(self, flash_slot); + else if (self->mtype == M_SOLDIER_BLUEBLASTER) + soldier_fireblueblaster(self, flash_slot); + else if (self->mtype == M_SOLDIER_LASER) + soldier_firelaser(self, soldier_flash_from_table(soldier_machinegun_flash, + sizeof(soldier_machinegun_flash) / sizeof(soldier_machinegun_flash[0]), flash_slot)); +} + +void m_soldier_hyperripper_run_fire(edict_t *self) +{ + if (self->mtype == M_SOLDIER_RIPPER) + soldier_fireionripper(self, 7); + else if (self->mtype == M_SOLDIER_BLUEBLASTER) + soldier_fireblueblaster(self, 7); +} + +static qboolean m_soldier_can_duck_shot(edict_t *self) +{ + const int duck_flash = 2; + + if (self->mtype == M_SOLDIER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), duck_flash), false); + if (self->mtype == M_SOLDIERLT) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), duck_flash), false); + if (self->mtype == M_SOLDIERSS) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_shotgun_flash, + sizeof(soldier_shotgun_flash) / sizeof(soldier_shotgun_flash[0]), duck_flash), false); + if (self->mtype == M_SOLDIER_RIPPER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_ripper_flash, + sizeof(soldier_ripper_flash) / sizeof(soldier_ripper_flash[0]), duck_flash), false); + if (self->mtype == M_SOLDIER_BLUEBLASTER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_hyper_flash, + sizeof(soldier_hyper_flash) / sizeof(soldier_hyper_flash[0]), duck_flash), false); + if (self->mtype == M_SOLDIER_LASER) + return soldier_has_laser_shot(self, soldier_flash_from_table(soldier_machinegun_flash, + sizeof(soldier_machinegun_flash) / sizeof(soldier_machinegun_flash[0]), duck_flash)); + + return false; +} + +static void m_soldier_fire_duck(edict_t *self) +{ + const int duck_flash = 2; + + if (!G_EntExists(self->enemy)) + return; + + if (self->mtype == M_SOLDIER) + soldier_fireblaster_flash(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), duck_flash)); + else if (self->mtype == M_SOLDIERLT) + soldier_firerocket_flash(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), duck_flash)); + else if (self->mtype == M_SOLDIERSS) + soldier_fireshotgun_flash(self, soldier_flash_from_table(soldier_shotgun_flash, + sizeof(soldier_shotgun_flash) / sizeof(soldier_shotgun_flash[0]), duck_flash)); + else if (self->mtype == M_SOLDIER_RIPPER) + soldier_fireionripper(self, duck_flash); + else if (self->mtype == M_SOLDIER_BLUEBLASTER) + soldier_fireblueblaster(self, duck_flash); + else if (self->mtype == M_SOLDIER_LASER) + soldier_firelaser(self, soldier_flash_from_table(soldier_machinegun_flash, + sizeof(soldier_machinegun_flash) / sizeof(soldier_machinegun_flash[0]), duck_flash)); +} + +static void m_soldier_attack3_refire(edict_t *self) +{ + if (level.time + 0.4f < self->monsterinfo.pausetime && m_soldier_can_duck_shot(self)) + self->monsterinfo.nextframe = FRAME_attak303; +} + +mframe_t m_soldier_frames_attack3 [] = +{ + ai_charge, 0, m_soldier_duck_down, + ai_charge, 0, NULL, + ai_charge, 0, m_soldier_fire_duck, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, m_soldier_attack3_refire, + ai_charge, 0, m_soldier_duck_up, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +static mmove_t m_soldier_move_attack3 = {FRAME_attak301, FRAME_attak309, m_soldier_frames_attack3, m_soldier_run}; + mframe_t m_soldier_frames_runandshoot [] = { drone_ai_run, 25, NULL, //109 drone_ai_run, 25, NULL, drone_ai_run, 25, NULL, drone_ai_run, 25, m_soldier_fire, //112 - drone_ai_run, 25, NULL, - drone_ai_run, 25, NULL, + drone_ai_run, 25, m_soldier_hyperripper_run_fire, + drone_ai_run, 25, m_soldier_hyperripper_run_fire, drone_ai_run, 25, NULL, drone_ai_run, 25, NULL, drone_ai_run, 25, m_soldier_fire, //117 @@ -236,7 +864,7 @@ mmove_t m_soldier_move_runandshoot = {FRAME_runs01, FRAME_runs14, m_soldier_fram void m_soldier_runandshoot_continue (edict_t* self) { - if (M_ContinueAttack(self, &m_soldier_move_runandshoot, NULL, 0, 512, 0.9)) + if (m_soldier_can_primary_shot(self) && M_ContinueAttack(self, &m_soldier_move_runandshoot, NULL, 0, 512, 0.9)) return; // end attack @@ -252,7 +880,8 @@ void m_soldier_attack1_refire1 (edict_t* self) { // continue firing if the enemy is still close, or we are standing ground if (G_ValidTarget(self, self->enemy, true, true) && (random() <= 0.9) - && ((entdist(self, self->enemy) <= 512) || (self->monsterinfo.aiflags & AI_STAND_GROUND))) + && ((entdist(self, self->enemy) <= 512) || (self->monsterinfo.aiflags & AI_STAND_GROUND)) + && m_soldier_can_primary_shot(self)) self->s.frame = FRAME_attak102; M_DelayNextAttack(self, 0, true); @@ -282,16 +911,144 @@ mframe_t m_soldier_frames_attack1 [] = }; mmove_t m_soldier_move_attack1 = {FRAME_attak101, FRAME_attak112, m_soldier_frames_attack1, m_soldier_endattack1}; +static qboolean m_soldier_can_attack2_shot(edict_t *self) +{ + return m_soldier_can_flash_slot_shot(self, 1); +} + +static void m_soldier_fire_attack2(edict_t *self) +{ + m_soldier_fire_flash_slot(self, 1); +} + +static void m_soldier_attack2_refire(edict_t *self) +{ + if (G_ValidTarget(self, self->enemy, true, true) && + entdist(self, self->enemy) <= 512 && + random() < 0.65f && + m_soldier_can_attack2_shot(self)) + self->monsterinfo.nextframe = FRAME_attak204; + + M_DelayNextAttack(self, 0, true); +} + +mframe_t m_soldier_frames_attack2 [] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, m_soldier_fire_attack2, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, m_soldier_cock, + ai_charge, 0, NULL, + ai_charge, 0, m_soldier_attack2_refire, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +static mmove_t m_soldier_move_attack2 = {FRAME_attak201, FRAME_attak218, m_soldier_frames_attack2, m_soldier_run}; + +static void m_soldier_select_stationary_attack(edict_t *self) +{ + if (m_soldier_can_attack2_shot(self) && random() < 0.5f) + self->monsterinfo.currentmove = &m_soldier_move_attack2; + else + self->monsterinfo.currentmove = &m_soldier_move_attack1; +} + +void m_soldier_endattack_laser(edict_t* self) +{ + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + self->monsterinfo.pausetime = 0; + if (self->beam && self->beam->inuse) + { + self->beam->prethink = NULL; + self->beam->nextthink = level.time + FRAMETIME; + } + m_soldier_run(self); +} + +void m_soldier_firelaser_frame(edict_t* self) +{ + if (!G_EntExists(self->enemy) || !visible(self, self->enemy) + || !soldier_has_laser_shot(self, MZ2_SOLDIER_MACHINEGUN_4)) + { + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + self->monsterinfo.pausetime = 0; + if (self->beam && self->beam->inuse) + { + self->beam->prethink = NULL; + self->beam->nextthink = level.time + FRAMETIME; + } + return; + } + + if (!(self->monsterinfo.aiflags & AI_HOLD_FRAME)) + self->monsterinfo.pausetime = level.time + 0.3 + random() * 0.8; + + soldier_firelaser(self, MZ2_SOLDIER_MACHINEGUN_4); + + if (level.time < self->monsterinfo.pausetime) + self->monsterinfo.aiflags |= AI_HOLD_FRAME; + else + { + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + self->monsterinfo.pausetime = 0; + if (self->beam && self->beam->inuse) + { + self->beam->prethink = NULL; + self->beam->nextthink = level.time + FRAMETIME; + } + } +} + +mframe_t m_soldier_frames_attack_laser[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, m_soldier_firelaser_frame, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +mmove_t m_soldier_move_attack_laser = {FRAME_attak401, FRAME_attak406, m_soldier_frames_attack_laser, m_soldier_endattack_laser}; + void m_soldier_attack(edict_t* self) { + if (!m_soldier_can_primary_shot(self) && !m_soldier_can_attack2_shot(self)) + return; + + if (self->mtype == M_SOLDIER_LASER) + { + self->monsterinfo.currentmove = &m_soldier_move_attack_laser; + M_DelayNextAttack(self, 0, true); + return; + } + + if ((self->mtype == M_SOLDIER_RIPPER || self->mtype == M_SOLDIER_BLUEBLASTER) + && !(self->monsterinfo.aiflags & AI_STAND_GROUND)) + { + self->monsterinfo.currentmove = &m_soldier_move_runandshoot; + M_DelayNextAttack(self, 0, true); + return; + } + if (self->monsterinfo.aiflags & AI_STAND_GROUND) { - self->monsterinfo.currentmove = &m_soldier_move_attack1; + m_soldier_select_stationary_attack(self); return; } if ((entdist(self, self->enemy) < 128) && (random() <= 0.8)) - self->monsterinfo.currentmove = &m_soldier_move_attack1; + m_soldier_select_stationary_attack(self); else self->monsterinfo.currentmove = &m_soldier_move_runandshoot; @@ -320,14 +1077,30 @@ void m_soldier_duck_up (edict_t *self) gi.linkentity (self); } +void m_soldier_duck_hold (edict_t *self) +{ + if (self->monsterinfo.pausetime > level.time) + self->monsterinfo.nextframe = self->s.frame; +} + +static void m_soldier_start_duck_dodge(edict_t *self, float hold_time) +{ + self->monsterinfo.nextattack = 0; + self->monsterinfo.pausetime = level.time + hold_time; + self->monsterinfo.currentmove = &m_soldier_move_duck; + m_soldier_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.25f; +} + mframe_t m_soldier_frames_duck [] = { - ai_move, 0, m_soldier_duck_down, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, m_soldier_duck_up + ai_move, 5, m_soldier_duck_down, + ai_move, -1, m_soldier_duck_hold, + ai_move, 1, NULL, + ai_move, 0, m_soldier_duck_up, + ai_move, 5, NULL }; -mmove_t m_soldier_move_duck = {FRAME_duck01, FRAME_duck04, m_soldier_frames_duck, m_soldier_run}; +static mmove_t m_soldier_move_duck = {FRAME_duck01, FRAME_duck05, m_soldier_frames_duck, m_soldier_run}; void m_soldier_jump_takeoff (edict_t *self) { @@ -342,6 +1115,28 @@ void m_soldier_jump_takeoff (edict_t *self) self->monsterinfo.pausetime = level.time + 2.0; // maximum duration of jump } +void m_soldier_jump_forward_takeoff(edict_t *self, float forward_velocity, float up_velocity) +{ + vec3_t forward; + + gi.sound(self, CHAN_VOICE, sound_sight1, 1, ATTN_NORM, 0); + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorScale(forward, forward_velocity, self->velocity); + self->velocity[2] = up_velocity; + self->groundentity = NULL; + self->monsterinfo.pausetime = level.time + 2.0; +} + +void m_soldier_jump_attack_takeoff(edict_t *self) +{ + m_soldier_jump_forward_takeoff(self, 250, 250); +} + +void m_soldier_jump2_takeoff(edict_t *self) +{ + m_soldier_jump_forward_takeoff(self, 200, 350); +} + void m_soldier_jump_hold (edict_t *self) { vec3_t v; @@ -366,20 +1161,191 @@ void m_soldier_jump_hold (edict_t *self) } } +static void m_soldier_jump_hold_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + m_soldier_jump_hold(self); +} + mframe_t m_soldier_frames_jump [] = { - ai_move, 0, m_soldier_jump_takeoff, - ai_move, 0, m_soldier_jump_hold, + ai_move, 0, m_soldier_jump2_takeoff, + m_soldier_jump_hold_ai, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL }; mmove_t m_soldier_move_jump = {FRAME_duck01, FRAME_duck05, m_soldier_frames_jump, m_soldier_run}; +static qboolean m_soldier_prone_shoot_ok(edict_t *self) +{ + vec3_t forward, diff; + const int prone_flash = 8; + + if (!G_EntIsAlive(self->enemy)) + return false; + + AngleVectors(self->s.angles, forward, NULL, NULL); + VectorSubtract(self->enemy->s.origin, self->s.origin, diff); + diff[2] = 0; + if (VectorNormalize(diff) == 0) + return false; + if (DotProduct(forward, diff) < 0.80f) + return false; + + if (self->mtype == M_SOLDIER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), prone_flash), true); + if (self->mtype == M_SOLDIERLT) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), prone_flash), true); + if (self->mtype == M_SOLDIERSS) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_shotgun_flash, + sizeof(soldier_shotgun_flash) / sizeof(soldier_shotgun_flash[0]), prone_flash), false); + if (self->mtype == M_SOLDIER_RIPPER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_ripper_flash, + sizeof(soldier_ripper_flash) / sizeof(soldier_ripper_flash[0]), prone_flash), true); + if (self->mtype == M_SOLDIER_BLUEBLASTER) + return soldier_has_flash_shot(self, soldier_flash_from_table(soldier_hyper_flash, + sizeof(soldier_hyper_flash) / sizeof(soldier_hyper_flash[0]), prone_flash), true); + if (self->mtype == M_SOLDIER_LASER) + return soldier_has_laser_shot(self, soldier_flash_from_table(soldier_machinegun_flash, + sizeof(soldier_machinegun_flash) / sizeof(soldier_machinegun_flash[0]), prone_flash)); + + return false; +} + +static void m_soldier_stand_up(edict_t *self) +{ + if (self->monsterinfo.nextattack > 0 && m_soldier_prone_shoot_ok(self)) + { + self->monsterinfo.nextattack--; + self->monsterinfo.currentmove = &m_soldier_move_attack5; + self->monsterinfo.nextframe = FRAME_attak504; + return; + } + + self->monsterinfo.nextattack = 0; + self->monsterinfo.currentmove = &m_soldier_move_trip; + self->monsterinfo.nextframe = FRAME_runt08; +} + +static void m_soldier_ai_prone_move(edict_t *self, float dist) +{ + ai_move(self, dist); + + if (!m_soldier_prone_shoot_ok(self)) + m_soldier_stand_up(self); +} + +static void m_soldier_fire_prone(edict_t *self) +{ + const int prone_flash = 8; + + if (self->mtype == M_SOLDIER) + soldier_fireblaster_flash_ex(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), prone_flash), true); + else if (self->mtype == M_SOLDIERLT) + soldier_firerocket_flash_ex(self, soldier_flash_from_table(soldier_blaster_flash, + sizeof(soldier_blaster_flash) / sizeof(soldier_blaster_flash[0]), prone_flash), true); + else if (self->mtype == M_SOLDIERSS) + soldier_fireshotgun_flash(self, soldier_flash_from_table(soldier_shotgun_flash, + sizeof(soldier_shotgun_flash) / sizeof(soldier_shotgun_flash[0]), prone_flash)); + else if (self->mtype == M_SOLDIER_RIPPER) + soldier_fireionripper_ex(self, prone_flash, true); + else if (self->mtype == M_SOLDIER_BLUEBLASTER) + soldier_fireblueblaster_ex(self, prone_flash, true); + else if (self->mtype == M_SOLDIER_LASER) + soldier_firelaser(self, soldier_flash_from_table(soldier_machinegun_flash, + sizeof(soldier_machinegun_flash) / sizeof(soldier_machinegun_flash[0]), prone_flash)); +} + +static void m_soldier_start_prone_dodge(edict_t *self) +{ + self->monsterinfo.nextattack = GetRandom(1, 2); + self->monsterinfo.currentmove = &m_soldier_move_attack5; + m_soldier_duck_down(self); + self->monsterinfo.dodge_time = level.time + 2.25f; +} + +mframe_t m_soldier_frames_attack5 [] = +{ + ai_move, 18, m_soldier_duck_down, + ai_move, 11, NULL, + ai_move, 0, NULL, + m_soldier_ai_prone_move, 0, NULL, + m_soldier_ai_prone_move, 0, NULL, + m_soldier_ai_prone_move, 0, m_soldier_fire_prone, + m_soldier_ai_prone_move, 0, m_soldier_fire_prone, + m_soldier_ai_prone_move, 0, m_soldier_fire_prone +}; +static mmove_t m_soldier_move_attack5 = {FRAME_attak501, FRAME_attak508, m_soldier_frames_attack5, m_soldier_stand_up}; + +static void m_soldier_check_prone(edict_t *self) +{ + if (m_soldier_prone_shoot_ok(self)) + self->monsterinfo.currentmove = &m_soldier_move_attack5; +} + +mframe_t m_soldier_frames_trip [] = +{ + ai_move, 10, NULL, + ai_move, 2, m_soldier_check_prone, + ai_move, 18, m_soldier_duck_down, + ai_move, 11, NULL, + ai_move, 9, NULL, + ai_move, -11, NULL, + ai_move, -2, NULL, + ai_move, 0, NULL, + ai_move, 6, NULL, + ai_move, -5, NULL, + ai_move, 0, NULL, + ai_move, 1, NULL, + ai_move, 0, NULL, + ai_move, 0, m_soldier_duck_up, + ai_move, 3, NULL, + ai_move, 2, NULL, + ai_move, -1, NULL, + ai_move, 2, NULL, + ai_move, 0, NULL +}; + +static void m_soldier_end_trip(edict_t *self) +{ + self->monsterinfo.nextattack = 0; + m_soldier_run(self); +} + +static mmove_t m_soldier_move_trip = {FRAME_runt01, FRAME_runt19, m_soldier_frames_trip, m_soldier_end_trip}; + void m_soldier_jump (edict_t *self) { if (self->groundentity) - self->monsterinfo.currentmove = &m_soldier_move_jump; + { + if (random() < 0.5 && m_soldier_prone_shoot_ok(self)) + m_soldier_start_prone_dodge(self); + else + { + self->monsterinfo.nextattack = random() < 0.5f ? 1 : 0; + self->monsterinfo.currentmove = &m_soldier_move_trip; + } + } +} + +static qboolean m_soldier_is_dodge_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &m_soldier_move_duck || + self->monsterinfo.currentmove == &m_soldier_move_jump || + self->monsterinfo.currentmove == &m_soldier_move_attack5 || + self->monsterinfo.currentmove == &m_soldier_move_trip || + self->monsterinfo.currentmove == &m_soldier_move_dodge_slide; +} + +static qboolean m_soldier_dodge_hit_low(edict_t *self, vec3_t dir) +{ + const float duck_height = self->absmax[2] - 33; + + return dir[2] > self->absmin[2] && dir[2] <= duck_height; } void m_soldier_dodge (edict_t *self, edict_t *attacker, vec3_t dir, int radius) @@ -388,20 +1354,57 @@ void m_soldier_dodge (edict_t *self, edict_t *attacker, vec3_t dir, int radius) return; if (level.time < self->monsterinfo.dodge_time) return; + if (!attacker) + return; if (OnSameTeam(self, attacker)) return; + if (m_soldier_is_dodge_move(self)) + return; - if (!self->enemy && G_EntIsAlive(attacker)) + self->monsterinfo.attacker = attacker; + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) self->enemy = attacker; if (!radius) { - self->monsterinfo.currentmove = &m_soldier_move_duck; - self->monsterinfo.dodge_time = level.time + 2.0; + if (!m_soldier_dodge_hit_low(self, dir)) + { + if (self->groundentity && m_soldier_prone_shoot_ok(self) && + (self->monsterinfo.currentmove == &m_soldier_move_runandshoot || random() < 0.45f)) + { + m_soldier_start_prone_dodge(self); + } + else + { + if (m_soldier_can_duck_shot(self) && random() < 0.5f) + { + self->monsterinfo.nextattack = 0; + self->monsterinfo.pausetime = level.time + 1.10f; + self->monsterinfo.currentmove = &m_soldier_move_attack3; + m_soldier_duck_down(self); + self->monsterinfo.dodge_time = level.time + 1.25f; + } + else + m_soldier_start_duck_dodge(self, 1.10f); + } + return; + } + + if (!(self->monsterinfo.aiflags & AI_STAND_GROUND)) + { + self->monsterinfo.nextattack = 0; + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + self->monsterinfo.currentmove = &m_soldier_move_dodge_slide; + self->monsterinfo.dodge_time = level.time + 1.35f; + } + else + { + m_soldier_start_duck_dodge(self, 1.10f); + } } else { m_soldier_jump(self); - self->monsterinfo.dodge_time = level.time + 3.0; + self->monsterinfo.dodge_time = level.time + 2.5; } } @@ -415,54 +1418,60 @@ void m_soldier_dead (edict_t *self) M_PrepBodyRemoval(self); } +static void m_soldier_death_shrink(edict_t *self) +{ + self->svflags |= SVF_DEADMONSTER; + self->maxs[2] = 0; + gi.linkentity(self); +} mframe_t soldier_frames_pain_short1[] = { - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, -3, NULL, + ai_move, 4, NULL, + ai_move, 1, NULL, + ai_move, 1, NULL, ai_move, 0, NULL, }; mmove_t soldier_move_pain_short1 = { FRAME_pain101, FRAME_pain105, soldier_frames_pain_short1, m_soldier_run }; mframe_t soldier_frames_pain_short2[] = { - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, -13, NULL, + ai_move, -1, NULL, + ai_move, 2, NULL, + ai_move, 4, NULL, + + ai_move, 2, NULL, + ai_move, 3, NULL, + ai_move, 2, NULL, }; mmove_t soldier_move_pain_short2 = { FRAME_pain201, FRAME_pain207, soldier_frames_pain_short2, m_soldier_run }; mframe_t soldier_frames_pain_long1[] = { - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, -8, NULL, + ai_move, 10, NULL, + ai_move, -4, NULL, + ai_move, -1, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, -3, NULL, ai_move, 0, NULL, + ai_move, 3, NULL, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 1, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 1, NULL, + ai_move, 2, NULL, + ai_move, 4, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 3, NULL, + ai_move, 2, NULL, }; mmove_t soldier_move_pain_long1 = { FRAME_pain301, FRAME_pain318, soldier_frames_pain_long1, m_soldier_run }; @@ -471,22 +1480,22 @@ mframe_t soldier_frames_pain_long2[] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, -10, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, -6, NULL, + ai_move, 8, NULL, + ai_move, 4, NULL, + ai_move, 1, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 2, NULL, + ai_move, 5, NULL, + ai_move, 2, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL, + ai_move, 3, NULL, + ai_move, 2, NULL, ai_move, 0, NULL, }; @@ -495,13 +1504,16 @@ mmove_t soldier_move_pain_long2 = { FRAME_pain401, FRAME_pain417, soldier_frames void soldier_pain(edict_t* self, edict_t* other, float kick, int damage) { if (self->health < (self->max_health / 2)) - self->s.skinnum = 1; + self->s.skinnum |= 1; + else + self->s.skinnum &= ~1; // we're already in a pain state if (self->monsterinfo.currentmove == &soldier_move_pain_long1 || self->monsterinfo.currentmove == &soldier_move_pain_long2 || self->monsterinfo.currentmove == &soldier_move_pain_short1 || - self->monsterinfo.currentmove == &soldier_move_pain_short2) + self->monsterinfo.currentmove == &soldier_move_pain_short2 || + m_soldier_is_dodge_move(self)) return; // monster players don't get pain state induced @@ -512,15 +1524,21 @@ void soldier_pain(edict_t* self, edict_t* other, float kick, int damage) if (invasion->value == 2) return; + if (level.time >= self->pain_debounce_time) + { + gi.sound(self, CHAN_VOICE, soldier_pain_sound(self), 1, ATTN_NORM, 0); + self->pain_debounce_time = level.time + 0.5; + } + // if we're fidgeting, always go into pain state. if (random() <= (1.0f - self->monsterinfo.pain_chance) && self->monsterinfo.currentmove != &m_soldier_move_stand1 && + self->monsterinfo.currentmove != &m_soldier_move_stand2 && self->monsterinfo.currentmove != &m_soldier_move_stand3) return; - gi.sound(self, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0); - if (self->monsterinfo.currentmove == &m_soldier_move_stand1 || + self->monsterinfo.currentmove == &m_soldier_move_stand2 || self->monsterinfo.currentmove == &m_soldier_move_stand3) { if (random() < 0.5) self->monsterinfo.currentmove = &soldier_move_pain_long1; @@ -540,7 +1558,7 @@ mframe_t m_soldier_frames_death1 [] = ai_move, 0, NULL, ai_move, -10, NULL, ai_move, -10, NULL, - ai_move, -10, NULL, + ai_move, -10, m_soldier_death_shrink, ai_move, -5, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -584,7 +1602,7 @@ mframe_t m_soldier_frames_death2 [] = ai_move, -5, NULL, ai_move, -5, NULL, ai_move, -5, NULL, - ai_move, 0, NULL, + ai_move, 0, m_soldier_death_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -627,7 +1645,7 @@ mframe_t m_soldier_frames_death3 [] = ai_move, -5, NULL, ai_move, -5, NULL, ai_move, -5, NULL, - ai_move, 0, NULL, + ai_move, 0, m_soldier_death_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -692,7 +1710,7 @@ mframe_t m_soldier_frames_death4 [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, m_soldier_death_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -746,7 +1764,7 @@ mframe_t m_soldier_frames_death5 [] = ai_move, -5, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, m_soldier_death_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -777,7 +1795,7 @@ mframe_t m_soldier_frames_death6 [] = ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, - ai_move, 0, NULL, + ai_move, 0, m_soldier_death_shrink, ai_move, 0, NULL, ai_move, 0, NULL, ai_move, 0, NULL, @@ -805,14 +1823,7 @@ void m_soldier_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int da if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 2; n++) - ThrowGib(self, "models/objects/gibs/bone/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/head2/tris.md2", damage, GIB_ORGANIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); @@ -829,19 +1840,30 @@ void m_soldier_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int da return; // regular death - gi.sound (self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + gi.sound (self, CHAN_VOICE, soldier_death_sound(self), 1, ATTN_NORM, 0); self->takedamage = DAMAGE_YES; self->deadflag = DEAD_DEAD; + vrx_update_drone_death_skin(self); - n = GetRandom(1, 6); - switch (n) + if (self->monsterinfo.currentmove == &m_soldier_move_trip || + self->monsterinfo.currentmove == &m_soldier_move_attack5) { - case 1: self->monsterinfo.currentmove = &m_soldier_move_death1; break; - case 2: self->monsterinfo.currentmove = &m_soldier_move_death2; break; - case 3: self->monsterinfo.currentmove = &m_soldier_move_death3; break; - case 4: self->monsterinfo.currentmove = &m_soldier_move_death4; break; - case 5: self->monsterinfo.currentmove = &m_soldier_move_death5; break; - case 6: self->monsterinfo.currentmove = &m_soldier_move_death6; break; + self->monsterinfo.currentmove = &m_soldier_move_death4; + self->monsterinfo.nextframe = FRAME_death413; + m_soldier_death_shrink(self); + } + else + { + n = GetRandom(1, 6); + switch (n) + { + case 1: self->monsterinfo.currentmove = &m_soldier_move_death1; break; + case 2: self->monsterinfo.currentmove = &m_soldier_move_death2; break; + case 3: self->monsterinfo.currentmove = &m_soldier_move_death3; break; + case 4: self->monsterinfo.currentmove = &m_soldier_move_death4; break; + case 5: self->monsterinfo.currentmove = &m_soldier_move_death5; break; + case 6: self->monsterinfo.currentmove = &m_soldier_move_death6; break; + } } DroneList_Remove(self); @@ -868,6 +1890,12 @@ void init_drone_soldier (edict_t *self) sound_sight1 = gi.soundindex ("soldier/solsght1.wav"); sound_sight2 = gi.soundindex ("soldier/solsrch1.wav"); sound_cock = gi.soundindex ("infantry/infatck3.wav"); + sound_pain_light = gi.soundindex ("soldier/solpain2.wav"); + sound_pain = gi.soundindex ("soldier/solpain1.wav"); + sound_pain_ss = gi.soundindex ("soldier/solpain3.wav"); + sound_death_light = gi.soundindex ("soldier/soldeth2.wav"); + sound_death = gi.soundindex ("soldier/soldeth1.wav"); + sound_death_ss = gi.soundindex ("soldier/soldeth3.wav"); self->mass = 100; @@ -879,15 +1907,45 @@ void init_drone_soldier (edict_t *self) self->monsterinfo.cost = 50; // set health - self->health = M_SOLDIER_INITIAL_HEALTH+M_SOLDIER_ADDON_HEALTH*self->monsterinfo.level; // hlt: soldier + if (self->mtype == M_SOLDIER_RIPPER) + self->health = M_SOLDIER_RIPPER_INITIAL_HEALTH + M_SOLDIER_RIPPER_ADDON_HEALTH * self->monsterinfo.level; + else if (self->mtype == M_SOLDIER_BLUEBLASTER) + self->health = M_SOLDIER_BLUEBLASTER_INITIAL_HEALTH + M_SOLDIER_BLUEBLASTER_ADDON_HEALTH * self->monsterinfo.level; + else if (self->mtype == M_SOLDIER_LASER) + self->health = M_SOLDIER_LASER_INITIAL_HEALTH + M_SOLDIER_LASER_ADDON_HEALTH * self->monsterinfo.level; + else + self->health = M_SOLDIER_INITIAL_HEALTH+M_SOLDIER_ADDON_HEALTH*self->monsterinfo.level; // hlt: soldier self->max_health = self->health; self->gib_health = -1.5 * BASE_GIB_HEALTH; // set armor self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; - self->monsterinfo.power_armor_power = M_SOLDIER_INITIAL_ARMOR+M_SOLDIER_ADDON_ARMOR*self->monsterinfo.level; // pow: soldier + if (self->mtype == M_SOLDIER_RIPPER) + self->monsterinfo.power_armor_power = M_SOLDIER_RIPPER_INITIAL_ARMOR + M_SOLDIER_RIPPER_ADDON_ARMOR * self->monsterinfo.level; + else if (self->mtype == M_SOLDIER_BLUEBLASTER) + self->monsterinfo.power_armor_power = M_SOLDIER_BLUEBLASTER_INITIAL_ARMOR + M_SOLDIER_BLUEBLASTER_ADDON_ARMOR * self->monsterinfo.level; + else if (self->mtype == M_SOLDIER_LASER) + self->monsterinfo.power_armor_power = M_SOLDIER_LASER_INITIAL_ARMOR + M_SOLDIER_LASER_ADDON_ARMOR * self->monsterinfo.level; + else + self->monsterinfo.power_armor_power = M_SOLDIER_INITIAL_ARMOR+M_SOLDIER_ADDON_ARMOR*self->monsterinfo.level; // pow: soldier self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + if (self->mtype == M_SOLDIER_RIPPER) + { + self->s.skinnum = 6; + self->count = self->s.skinnum - 6; + } + else if (self->mtype == M_SOLDIER_BLUEBLASTER) + { + self->s.skinnum = 8; + self->count = self->s.skinnum - 6; + } + else if (self->mtype == M_SOLDIER_LASER) + { + self->s.skinnum = 10; + self->count = self->s.skinnum - 6; + } + // set AI jump parameters self->monsterinfo.jumpdn = 512; self->monsterinfo.jumpup = 64; @@ -898,11 +1956,13 @@ void init_drone_soldier (edict_t *self) self->die = m_soldier_die; self->monsterinfo.stand = m_soldier_stand; + self->monsterinfo.walk = m_soldier_walk; self->monsterinfo.run = m_soldier_run; self->monsterinfo.dodge = m_soldier_dodge; self->monsterinfo.attack = m_soldier_attack; + self->monsterinfo.sight = m_soldier_sight; gi.linkentity (self); self->nextthink = level.time + FRAMETIME; self->monsterinfo.currentmove = &m_soldier_move_stand1; -} \ No newline at end of file +} diff --git a/src/entities/drone/drone_stalker.c b/src/entities/drone/drone_stalker.c new file mode 100644 index 00000000..738c2e6b --- /dev/null +++ b/src/entities/drone/drone_stalker.c @@ -0,0 +1,830 @@ +/* +============================================================================== + +STALKER + +============================================================================== +*/ + +#include "g_local.h" +#include "../../quake2/monsterframes/m_rogue_stalker.h" + +static int sound_pain; +static int sound_die; +static int sound_sight; +static int sound_punch_hit1; +static int sound_punch_hit2; +static int sound_idle; + +#define STALKER_CEILING_NONE 0 +#define STALKER_CEILING_ON 1 +#define STALKER_CEILING_JUMPING 2 +#define STALKER_CEILING_TRACE_DIST 256 +#define STALKER_CEILING_JUMP_SPEED 550 +#define STALKER_CEILING_MIN_SPEED 360 + +void drone_ai_stand(edict_t *self, float dist); +void drone_ai_run(edict_t *self, float dist); +void drone_ai_run_slide(edict_t *self, float dist); +void drone_ai_walk(edict_t *self, float dist); + +static void stalker_stand(edict_t *self); +static void stalker_run(edict_t *self); +static void stalker_set_floor(edict_t *self); +static void stalker_jump_wait_land(edict_t *self); +extern mmove_t stalker_move_jump_straightup; +static mmove_t stalker_move_dodge_run; + +static void stalker_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_sight, 1, ATTN_NORM, 0); +} + +static void stalker_idle_noise(edict_t *self) +{ + gi.sound(self, CHAN_VOICE, sound_idle, 0.5, ATTN_IDLE, 0); +} + +mframe_t stalker_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, stalker_idle_noise, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t stalker_move_stand = { FRAME_idle01, FRAME_idle21, stalker_frames_stand, stalker_stand }; + +mframe_t stalker_frames_idle2[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +mmove_t stalker_move_idle2 = { FRAME_idle201, FRAME_idle213, stalker_frames_idle2, stalker_stand }; + +static void stalker_idle(edict_t *self) +{ + if (random() < 0.35f) + self->monsterinfo.currentmove = &stalker_move_stand; + else + self->monsterinfo.currentmove = &stalker_move_idle2; +} + +static void stalker_stand(edict_t *self) +{ + if (random() < 0.25f) + self->monsterinfo.currentmove = &stalker_move_stand; + else + self->monsterinfo.currentmove = &stalker_move_idle2; +} + +mframe_t stalker_frames_walk[] = +{ + drone_ai_walk, 10, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 10, NULL +}; +mmove_t stalker_move_walk = { FRAME_walk01, FRAME_walk08, stalker_frames_walk, stalker_run }; + +static void stalker_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &stalker_move_walk; +} + +static void stalker_reactivate(edict_t *self); +static void stalker_false_death(edict_t *self); + +static mframe_t stalker_frames_reactivate[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +static mmove_t stalker_move_false_death_end = { FRAME_reactive01, FRAME_reactive04, stalker_frames_reactivate, stalker_run }; + +static void stalker_reactivate(edict_t *self) +{ + self->monsterinfo.aiflags &= ~AI_STAND_GROUND; + self->monsterinfo.currentmove = &stalker_move_false_death_end; +} + +static void stalker_heal(edict_t *self) +{ + if (skill->value >= 3) + self->health += 3; + else if (skill->value >= 2) + self->health += 2; + else + self->health++; + + self->s.skinnum = self->health < (self->max_health / 2); + + if (self->health >= self->max_health) + { + self->health = self->max_health; + stalker_reactivate(self); + } +} + +static mframe_t stalker_frames_false_death[] = +{ + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal, + ai_move, 0, stalker_heal +}; +static mmove_t stalker_move_false_death = { FRAME_twitch01, FRAME_twitch10, stalker_frames_false_death, stalker_false_death }; + +static void stalker_false_death(edict_t *self) +{ + self->monsterinfo.currentmove = &stalker_move_false_death; +} + +static mframe_t stalker_frames_false_death_start[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +static mmove_t stalker_move_false_death_start = { FRAME_death01, FRAME_death09, stalker_frames_false_death_start, stalker_false_death }; + +static void stalker_false_death_start(edict_t *self) +{ + stalker_set_floor(self); + self->monsterinfo.aiflags |= AI_STAND_GROUND; + self->monsterinfo.currentmove = &stalker_move_false_death_start; +} + +mframe_t stalker_frames_run[] = +{ + drone_ai_run, 24, NULL, + drone_ai_run, 28, NULL, + drone_ai_run, 32, NULL, + drone_ai_run, 28, NULL +}; +mmove_t stalker_move_run = { FRAME_run01, FRAME_run04, stalker_frames_run, NULL }; + +static void stalker_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &stalker_move_stand; + else + self->monsterinfo.currentmove = &stalker_move_run; +} + +static qboolean stalker_dodge_allowed(edict_t *self) +{ + return self && self->health > 0 && self->deadflag == DEAD_NO; +} + +static qboolean stalker_ceiling_allowed(edict_t *self) +{ + return stalker_dodge_allowed(self) && !invasion->value; +} + +static qboolean stalker_on_ceiling(edict_t *self) +{ + return self->style == STALKER_CEILING_ON; +} + +static qboolean stalker_find_ceiling(edict_t *self, float max_dist, float *ceiling_z) +{ + trace_t tr; + vec3_t end; + + VectorCopy(self->s.origin, end); + end[2] += max_dist; + tr = gi.trace(self->s.origin, self->mins, self->maxs, end, self, MASK_MONSTERSOLID); + + if (tr.fraction == 1.0 || !(tr.contents & CONTENTS_SOLID) || tr.ent != world) + return false; + if (tr.plane.normal[2] > -0.7) + return false; + + if (ceiling_z) + *ceiling_z = tr.endpos[2] + self->maxs[2]; + return true; +} + +static void stalker_attach_ceiling(edict_t *self, float ceiling_z) +{ + self->style = STALKER_CEILING_ON; + self->flags |= FL_FLY; + self->gravity = 0; + self->groundentity = NULL; + self->s.angles[ROLL] = 180; + self->s.origin[2] = ceiling_z - self->maxs[2] - 1; + VectorClear(self->velocity); + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; + gi.linkentity(self); +} + +static void stalker_set_floor(edict_t *self) +{ + self->style = STALKER_CEILING_NONE; + self->flags &= ~FL_FLY; + self->gravity = 1.0; + self->s.angles[ROLL] = 0; + self->monsterinfo.aiflags &= ~AI_HOLD_FRAME; +} + +static void stalker_drop_from_ceiling(edict_t *self) +{ + stalker_set_floor(self); + self->groundentity = NULL; + self->velocity[2] = -300; + gi.linkentity(self); +} + +static void stalker_abort_ceiling_jump(edict_t *self) +{ + stalker_set_floor(self); + self->gravity = 1.0; + self->flags &= ~FL_FLY; + self->monsterinfo.dodge_time = level.time + 2.0f; + if (self->groundentity) + VectorClear(self->velocity); + else if (self->velocity[2] > 0) + self->velocity[2] = 0; + stalker_run(self); +} + +static qboolean stalker_is_jump_move(edict_t *self) +{ + return self->monsterinfo.currentmove == &stalker_move_jump_straightup || + self->style == STALKER_CEILING_JUMPING; +} + +static qboolean stalker_is_dodge_move(edict_t *self) +{ + return stalker_is_jump_move(self) || + self->monsterinfo.currentmove == &stalker_move_dodge_run; +} + +static void stalker_ceiling_prethink(edict_t *self) +{ + float ceiling_z; + + if (self->style == STALKER_CEILING_JUMPING) + { + stalker_jump_wait_land(self); + return; + } + + if (!stalker_on_ceiling(self)) + return; + + if (!stalker_ceiling_allowed(self)) + { + stalker_set_floor(self); + return; + } + + if (stalker_find_ceiling(self, 96, &ceiling_z)) + stalker_attach_ceiling(self, ceiling_z); + else + stalker_drop_from_ceiling(self); +} + +static void stalker_jump_straightup(edict_t *self) +{ + float ceiling_z; + + if (!stalker_ceiling_allowed(self)) + return; + + if (stalker_on_ceiling(self)) + { + stalker_set_floor(self); + self->velocity[2] = -300; + return; + } + + if (!self->groundentity) + return; + + if (stalker_find_ceiling(self, STALKER_CEILING_TRACE_DIST, &ceiling_z)) + { + self->pos1[2] = ceiling_z; + self->flags |= FL_FLY; + self->gravity = 0; + self->velocity[2] = STALKER_CEILING_JUMP_SPEED; + } + else + { + stalker_abort_ceiling_jump(self); + return; + } + + self->style = STALKER_CEILING_JUMPING; + self->s.origin[2] += 1; + self->groundentity = NULL; + self->velocity[0] += crandom() * 5; + self->velocity[1] += crandom() * 5; + self->monsterinfo.pausetime = level.time + 1.3; + self->monsterinfo.aiflags |= AI_HOLD_FRAME; +} + +static void stalker_jump_wait_land(edict_t *self) +{ + float ceiling_z; + + if (self->style != STALKER_CEILING_JUMPING) + return; + + if (!stalker_ceiling_allowed(self)) + { + stalker_set_floor(self); + return; + } + + if (stalker_find_ceiling(self, STALKER_CEILING_TRACE_DIST, &ceiling_z)) + self->pos1[2] = ceiling_z; + + if (self->pos1[2] && self->s.origin[2] + self->maxs[2] >= self->pos1[2] - 12) + { + stalker_attach_ceiling(self, self->pos1[2]); + return; + } + + if (self->pos1[2]) + { + self->flags |= FL_FLY; + self->gravity = 0; + if (self->velocity[2] < STALKER_CEILING_MIN_SPEED) + self->velocity[2] = STALKER_CEILING_MIN_SPEED; + } + + if (self->groundentity || level.time > self->monsterinfo.pausetime) + { + stalker_abort_ceiling_jump(self); + return; + } + + self->monsterinfo.aiflags |= AI_HOLD_FRAME; +} + +static void stalker_jump_wait_land_ai(edict_t *self, float dist) +{ + ai_move(self, dist); + stalker_jump_wait_land(self); +} + +mframe_t stalker_frames_jump_straightup[] = +{ + ai_move, 1, stalker_jump_straightup, + stalker_jump_wait_land_ai, 1, NULL, + ai_move, -1, NULL, + ai_move, -1, NULL +}; +mmove_t stalker_move_jump_straightup = { FRAME_jump04, FRAME_jump07, stalker_frames_jump_straightup, stalker_run }; + +static qboolean stalker_start_ceiling_jump(edict_t *self, float cooldown) +{ + float ceiling_z; + + if (!stalker_ceiling_allowed(self) || stalker_on_ceiling(self) || !self->groundentity) + return false; + if (stalker_is_dodge_move(self) || level.time < self->monsterinfo.dodge_time) + return false; + if (!stalker_find_ceiling(self, STALKER_CEILING_TRACE_DIST, &ceiling_z)) + { + self->monsterinfo.dodge_time = level.time + cooldown; + return false; + } + + self->monsterinfo.dodge_time = level.time + cooldown; + self->pos1[2] = ceiling_z; + self->monsterinfo.currentmove = &stalker_move_jump_straightup; + stalker_jump_straightup(self); + return true; +} + +static void stalker_ai_dodge_slide(edict_t *self, float dist) +{ + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(self->monsterinfo.attacker)) + self->enemy = self->monsterinfo.attacker; + if (!G_EntIsAlive(self->enemy)) + return; + + drone_ai_run_slide(self, dist); +} + +mframe_t stalker_frames_dodge_run[] = +{ + stalker_ai_dodge_slide, 13, NULL, + stalker_ai_dodge_slide, 17, NULL, + stalker_ai_dodge_slide, 21, NULL, + stalker_ai_dodge_slide, 18, NULL +}; +static mmove_t stalker_move_dodge_run = { FRAME_run01, FRAME_run04, stalker_frames_dodge_run, stalker_run }; + +static qboolean stalker_start_dodge_slide(edict_t *self, edict_t *attacker, vec3_t dir) +{ + vec3_t right, diff; + + if (!self->groundentity || stalker_is_dodge_move(self)) + return false; + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) + self->enemy = attacker; + if (!G_EntIsAlive(self->enemy)) + return false; + + AngleVectors(self->s.angles, NULL, right, NULL); + VectorSubtract(dir, self->s.origin, diff); + if (VectorLength(diff) > 1) + self->monsterinfo.lefty = DotProduct(right, diff) >= 0; + else + self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; + + self->monsterinfo.currentmove = &stalker_move_dodge_run; + self->monsterinfo.dodge_time = level.time + 0.4f + random() * 1.2f; + return true; +} + +static void stalker_fire(edict_t *self) +{ + int damage, speed; + vec3_t forward, right, start, target, dir, offset; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_BLASTER2_DMG_BASE + M_BLASTER2_DMG_ADDON * drone_damagelevel(self); + if (M_BLASTER2_DMG_MAX && damage > M_BLASTER2_DMG_MAX) + damage = M_BLASTER2_DMG_MAX; + speed = M_BLASTER2_SPEED_BASE + M_BLASTER2_SPEED_ADDON * drone_damagelevel(self); + if (M_BLASTER2_SPEED_MAX && speed > M_BLASTER2_SPEED_MAX) + speed = M_BLASTER2_SPEED_MAX; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[MZ2_STALKER_BLASTER], offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + VectorCopy(self->enemy->s.origin, target); + target[2] += self->enemy->viewheight; + VectorSubtract(target, start, dir); + VectorNormalize(dir); + + monster_fire_blaster2(self, start, dir, damage, speed, EF_BLASTER, MZ2_STALKER_BLASTER); +} + +mframe_t stalker_frames_shoot[] = +{ + drone_ai_run, 10, NULL, + drone_ai_run, 10, stalker_fire, + drone_ai_run, 12, stalker_fire, + drone_ai_run, 12, stalker_fire +}; +mmove_t stalker_move_shoot = { FRAME_run01, FRAME_run04, stalker_frames_shoot, stalker_run }; + +static void stalker_attack(edict_t *self) +{ + if (stalker_on_ceiling(self)) + { + // sometimes come back down during combat + if (entdist(self, self->enemy) < 96 || random() < 0.25f) + { + stalker_set_floor(self); + self->velocity[2] = -300; + stalker_run(self); + M_DelayNextAttack(self, 0.6, true); + return; + } + + // otherwise keep fighting from the ceiling + self->monsterinfo.currentmove = &stalker_move_shoot; + M_DelayNextAttack(self, 0.4, true); + return; + } + + if (stalker_ceiling_allowed(self) && !stalker_on_ceiling(self) && self->groundentity && + level.time > self->monsterinfo.melee_finished && random() < 0.33f) + { + if (stalker_start_ceiling_jump(self, 3.0)) + { + self->monsterinfo.melee_finished = level.time + 3.0; + M_DelayNextAttack(self, 1.0, true); + return; + } + } + + self->monsterinfo.currentmove = &stalker_move_shoot; + M_DelayNextAttack(self, 0.4, true); +} + +static void stalker_dodge(edict_t *self, edict_t *attacker, vec3_t dir, int radius) +{ + float ceiling_z; + float eta; + + if (!stalker_dodge_allowed(self) || !self->groundentity) + return; + if (!attacker || OnSameTeam(self, attacker)) + return; + if (level.time < self->monsterinfo.dodge_time || stalker_is_dodge_move(self)) + return; + + if (!G_EntIsAlive(self->enemy) && G_EntIsAlive(attacker)) + self->enemy = attacker; + self->monsterinfo.attacker = attacker; + + eta = self->monsterinfo.eta - level.time; + if ((eta < FRAMETIME) || (eta > 5.0f)) + return; + + if (stalker_ceiling_allowed(self) && + stalker_find_ceiling(self, STALKER_CEILING_TRACE_DIST, &ceiling_z) && + stalker_start_ceiling_jump(self, 1.0f + random() * 1.5f)) + return; + + if (radius || random() < 0.35f) + { + if (stalker_start_ceiling_jump(self, 1.0f + random() * 1.5f)) + return; + } + + stalker_start_dodge_slide(self, attacker, dir); +} + +static void stalker_swing_attack(edict_t *self) +{ + int damage; + + damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + if (M_MELEE_DMG_MAX && damage > M_MELEE_DMG_MAX) + damage = M_MELEE_DMG_MAX; + + if (M_MeleeAttack(self, self->enemy, 80, damage, 160)) + gi.sound(self, CHAN_WEAPON, (random() < 0.5) ? sound_punch_hit1 : sound_punch_hit2, 1, ATTN_NORM, 0); +} + +mframe_t stalker_frames_swing_l[] = +{ + ai_charge, 0, NULL, + ai_charge, 4, NULL, + ai_charge, 5, stalker_swing_attack, + ai_charge, 5, NULL, + ai_charge, 5, stalker_swing_attack, + ai_charge, 4, NULL, + ai_charge, 2, NULL, + ai_charge, 0, NULL +}; +mmove_t stalker_move_swing_l = { FRAME_attack01, FRAME_attack08, stalker_frames_swing_l, stalker_run }; + +mframe_t stalker_frames_swing_r[] = +{ + ai_charge, 0, NULL, + ai_charge, 6, stalker_swing_attack, + ai_charge, 5, NULL, + ai_charge, 5, stalker_swing_attack, + ai_charge, 0, NULL +}; +mmove_t stalker_move_swing_r = { FRAME_attack11, FRAME_attack15, stalker_frames_swing_r, stalker_run }; + +static void stalker_melee(edict_t *self) +{ + if (stalker_on_ceiling(self)) + { + if (!G_ValidTarget(self, self->enemy, true, true) || entdist(self, self->enemy) > 96) + { + self->monsterinfo.melee_finished = level.time + 0.5; + stalker_run(self); + return; + } + + if (random() < 0.5) + self->monsterinfo.currentmove = &stalker_move_swing_l; + else + self->monsterinfo.currentmove = &stalker_move_swing_r; + return; + } + + if (!G_ValidTarget(self, self->enemy, true, true) || entdist(self, self->enemy) > 96) + { + self->monsterinfo.melee_finished = level.time + 0.5; + stalker_run(self); + return; + } + + if (random() < 0.5) + self->monsterinfo.currentmove = &stalker_move_swing_l; + else + self->monsterinfo.currentmove = &stalker_move_swing_r; +} + +mframe_t stalker_frames_pain[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t stalker_move_pain = { FRAME_pain01, FRAME_pain04, stalker_frames_pain, stalker_run }; + +static void stalker_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + if (self->health < (self->max_health / 2)) + self->s.skinnum |= 1; + + if (self->monsterinfo.currentmove == &stalker_move_false_death_end || + self->monsterinfo.currentmove == &stalker_move_false_death_start) + return; + + if (self->monsterinfo.currentmove == &stalker_move_false_death) + { + stalker_reactivate(self); + return; + } + + if (self->health > 0 && self->health < (self->max_health / 4) && + self->groundentity && !stalker_on_ceiling(self) && random() < 0.30f) + { + stalker_false_death_start(self); + return; + } + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0; + gi.sound(self, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0); + + if (invasion->value == 2) + return; + + if (self->style == STALKER_CEILING_JUMPING && damage > 10) + { + stalker_abort_ceiling_jump(self); + self->monsterinfo.currentmove = &stalker_move_pain; + return; + } + + if (stalker_is_dodge_move(self)) + return; + + if (damage > 10 && random() < 0.5 && stalker_start_ceiling_jump(self, 3.0)) + return; + + self->monsterinfo.currentmove = &stalker_move_pain; +} + +static void stalker_dead(edict_t *self) +{ + VectorSet(self->mins, -28, -28, -18); + VectorSet(self->maxs, 28, 28, -4); + self->movetype = MOVETYPE_TOSS; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); + M_PrepBodyRemoval(self); +} + +mframe_t stalker_frames_death[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t stalker_move_death = { FRAME_death01, FRAME_death09, stalker_frames_death, stalker_dead }; + +static void stalker_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + M_Notify(self); + stalker_set_floor(self); + self->prethink = NULL; + self->movetype = MOVETYPE_TOSS; + + if (self->health <= self->gib_health) + { + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/udeath.wav"), 1, ATTN_NORM, 0); + vrx_throw_drone_gibs(self, damage); + M_Remove(self, false, false); + return; + } + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + gi.sound(self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); + self->monsterinfo.currentmove = &stalker_move_death; + + if (self->activator && !self->activator->client) + self->activator->num_monsters_real--; +} + +void init_drone_stalker(edict_t *self) +{ + sound_pain = gi.soundindex("stalker/pain.wav"); + sound_die = gi.soundindex("stalker/death.wav"); + sound_sight = gi.soundindex("stalker/sight.wav"); + sound_punch_hit1 = gi.soundindex("stalker/melee1.wav"); + sound_punch_hit2 = gi.soundindex("stalker/melee2.wav"); + sound_idle = gi.soundindex("stalker/idle.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/stalker/tris.md2"); + gi.modelindex("models/monsters/stalker/gibs/bodya.md2"); + gi.modelindex("models/monsters/stalker/gibs/bodyb.md2"); + gi.modelindex("models/monsters/stalker/gibs/claw.md2"); + gi.modelindex("models/monsters/stalker/gibs/foot.md2"); + gi.modelindex("models/monsters/stalker/gibs/head.md2"); + gi.modelindex("models/monsters/stalker/gibs/leg.md2"); + + VectorSet(self->mins, -28, -28, -18); + VectorSet(self->maxs, 28, 28, 18); + + self->health = M_STALKER_INITIAL_HEALTH + M_STALKER_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -125; + self->mass = 250; + self->mtype = M_STALKER; + + self->monsterinfo.power_armor_type = POWER_ARMOR_SCREEN; + self->monsterinfo.power_armor_power = M_STALKER_INITIAL_ARMOR + M_STALKER_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_BERSERKER_CONTROL_COST; + self->monsterinfo.cost = M_DEFAULT_COST; + + self->item = FindItemByClassname("ammo_cells"); + + self->pain = stalker_pain; + self->die = stalker_die; + self->monsterinfo.stand = stalker_stand; + self->monsterinfo.walk = stalker_walk; + self->monsterinfo.run = stalker_run; + self->monsterinfo.dodge = stalker_dodge; + self->monsterinfo.attack = stalker_attack; + self->monsterinfo.melee = stalker_melee; + self->monsterinfo.sight = stalker_sight; + self->monsterinfo.idle = stalker_idle; + self->prethink = stalker_ceiling_prethink; + self->monsterinfo.pain_chance = 0.3f; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &stalker_move_stand; + self->monsterinfo.scale = MODEL_SCALE; + self->nextthink = level.time + FRAMETIME; +} diff --git a/src/entities/drone/drone_supertank.c b/src/entities/drone/drone_supertank.c index 9d990e38..8c182ec4 100644 --- a/src/entities/drone/drone_supertank.c +++ b/src/entities/drone/drone_supertank.c @@ -10,6 +10,13 @@ SUPERTANK #include "../../quake2/monsterframes/m_supertank.h" qboolean visible (const edict_t *self, const edict_t *other); +#define SUPERTANK_INVASION_SCALE 0.65f +#define SUPERTANK_INVASION_BASE_HEALTH 5000 +#define SUPERTANK_INVASION_ADDON_HEALTH 1000 + +static int sound_pain1; +static int sound_pain2; +static int sound_pain3; static int sound_death; static int sound_search1; static int sound_search2; @@ -18,13 +25,38 @@ static int tread_sound; void BossExplode (edict_t *self); +static qboolean supertank_is_boss5(const edict_t *self) +{ + return self->mtype == M_BOSS5; +} + +static void supertank_project_flash(edict_t *self, int flash_number, vec3_t forward, vec3_t start) +{ + vec3_t right, offset; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[flash_number], offset); + if (self->s.scale && self->s.scale != 1.0f) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + void TreadSound (edict_t *self) { gi.sound (self, CHAN_VOICE, tread_sound, 1, ATTN_NORM, 0); } +void supertank_search (edict_t *self) +{ + if (random() > 0.5) + gi.sound (self, CHAN_VOICE, sound_search1, 1, ATTN_NORM, 0); + else + gi.sound (self, CHAN_VOICE, sound_search2, 1, ATTN_NORM, 0); +} + void supertankRocket (edict_t *self); void supertankMachineGun (edict_t *self); +void supertankGrenade (edict_t *self); void supertank_reattack1(edict_t *self); // @@ -125,6 +157,29 @@ mframe_t supertank_frames_run [] = }; mmove_t supertank_move_run = {FRAME_forwrd_1, FRAME_forwrd_18, supertank_frames_run, NULL}; +mframe_t supertank_frames_run_janitor [] = +{ + drone_ai_run, 18, TreadSound, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 18, NULL +}; +mmove_t supertank_move_run_janitor = {FRAME_forwrd_1, FRAME_forwrd_18, supertank_frames_run_janitor, NULL}; + // // walk // @@ -170,10 +225,73 @@ void supertank_run (edict_t *self) { if (self->monsterinfo.aiflags & AI_STAND_GROUND) self->monsterinfo.currentmove = &supertank_move_stand; + else if (self->mtype == M_JANITOR) + self->monsterinfo.currentmove = &supertank_move_run_janitor; else self->monsterinfo.currentmove = &supertank_move_run; } +mframe_t supertank_frames_pain3[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t supertank_move_pain3 = { FRAME_pain3_9, FRAME_pain3_12, supertank_frames_pain3, supertank_run }; + +mframe_t supertank_frames_pain2[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t supertank_move_pain2 = { FRAME_pain2_5, FRAME_pain2_8, supertank_frames_pain2, supertank_run }; + +mframe_t supertank_frames_pain1[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +mmove_t supertank_move_pain1 = { FRAME_pain1_1, FRAME_pain1_4, supertank_frames_pain1, supertank_run }; + +void supertank_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + (void)other; + (void)kick; + + if (level.time < self->pain_debounce_time) + return; + + if (damage <= 25 && random() < 0.2f) + return; + + if (self->s.frame >= FRAME_attak2_1 && self->s.frame <= FRAME_attak2_14) + return; + + if (damage <= 10) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + else if (damage <= 25) + gi.sound(self, CHAN_VOICE, sound_pain3, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + + self->pain_debounce_time = level.time + 3.0f; + + if (invasion->value == 2) + return; + + if (damage <= 10) + self->monsterinfo.currentmove = &supertank_move_pain1; + else if (damage <= 25) + self->monsterinfo.currentmove = &supertank_move_pain2; + else + self->monsterinfo.currentmove = &supertank_move_pain3; +} + mframe_t supertank_frames_turn_right [] = { ai_move, 0, TreadSound, @@ -374,42 +492,49 @@ mframe_t supertank_frames_end_attack1[]= ai_move, 0, NULL }; mmove_t supertank_move_end_attack1 = {FRAME_attak1_7, FRAME_attak1_20, supertank_frames_end_attack1, supertank_run}; -/* -void fire_mirv_grenade (edict_t *self, vec3_t start, vec3_t aimdir, int damage, float radius, int speed, float timer); - -void supertankMirv (edict_t *self) +void supertankGrenade (edict_t *self) { vec3_t forward, start; - int damage; + int damage, speed, flash_number; + + if (!G_EntExists(self->enemy)) + return; - damage = 50 + 10*self->monsterinfo.level; + if (self->s.frame == FRAME_attak4_1) + flash_number = MZ2_SUPERTANK_GRENADE_1; + else + flash_number = MZ2_SUPERTANK_GRENADE_2; - MonsterAim(self, 0.5, 400, true, 0, forward, start); + damage = M_GRENADELAUNCHER_DMG_BASE + M_GRENADELAUNCHER_DMG_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_DMG_MAX && damage > M_GRENADELAUNCHER_DMG_MAX) + damage = M_GRENADELAUNCHER_DMG_MAX; - fire_mirv_grenade(self, start, forward, damage, 150, 400, 3.0); -} + speed = M_GRENADELAUNCHER_SPEED_BASE + M_GRENADELAUNCHER_SPEED_ADDON * drone_damagelevel(self); + if (M_GRENADELAUNCHER_SPEED_MAX && speed > M_GRENADELAUNCHER_SPEED_MAX) + speed = M_GRENADELAUNCHER_SPEED_MAX; + + if (self->s.scale && self->s.scale != 1.0f) + { + supertank_project_flash(self, flash_number, forward, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, true, -1, forward, start); + } + else + MonsterAim(self, M_PROJECTILE_ACC, speed, true, flash_number, forward, start); -void supertank_reattack4(edict_t *self); + monster_fire_grenade(self, start, forward, damage, speed, flash_number); +} mframe_t supertank_frames_attack4[]= { - ai_charge, 0, supertankMirv,//74 + ai_charge, 0, supertankGrenade,//74 ai_charge, 0, NULL, ai_charge, 0, NULL, - ai_charge, 0, supertankMirv,//79 + ai_charge, 0, supertankGrenade,//77 ai_charge, 0, NULL, ai_charge, 0, NULL, }; -mmove_t supertank_move_attack4 = {FRAME_attak4_1, FRAME_attak4_6, supertank_frames_attack4, supertank_reattack4}; - -void supertank_reattack4(edict_t *self) -{ - if (G_ValidTarget(self, self->enemy, true) && random() <= 0.5) - self->monsterinfo.currentmove = &supertank_move_attack4; - self->monsterinfo.attack_finished = level.time + 1.0; -} -*/ +mmove_t supertank_move_attack4 = {FRAME_attak4_1, FRAME_attak4_6, supertank_frames_attack4, supertank_run}; void supertank_reattack1(edict_t *self) { if (G_ValidTarget(self, self->enemy, true, true) && random() <= 0.9) @@ -421,7 +546,7 @@ void supertank_reattack1(edict_t *self) void supertankRocket (edict_t *self) { - vec3_t forward, start; + vec3_t forward, right, start, offset; int damage, speed, flash_number; if (self->s.frame == FRAME_attak2_8) @@ -434,7 +559,43 @@ void supertankRocket (edict_t *self) damage = 50 + 10 * drone_damagelevel(self); speed = 650 + 30 * drone_damagelevel(self); - MonsterAim(self, 0.5, speed, true, flash_number, forward, start); + if (supertank_is_boss5(self)) + { + if (self->s.scale && self->s.scale != 1.0f) + { + supertank_project_flash(self, flash_number, forward, start); + MonsterAim(self, 0.5, speed, true, -1, forward, start); + } + else + MonsterAim(self, 0.5, speed, true, flash_number, forward, start); + monster_fire_heat(self, start, forward, damage, speed, flash_number, 0.075f); + return; + } + + if (self->mtype == M_JANITOR) + { + AngleVectors(self->s.angles, forward, right, NULL); + if (flash_number == MZ2_SUPERTANK_ROCKET_1) + VectorSet(offset, 16.0, -22.5, 108.7); + else if (flash_number == MZ2_SUPERTANK_ROCKET_2) + VectorSet(offset, 16.0, -33.4, 106.7); + else + VectorSet(offset, 16.0, -42.8, 104.7); + if (self->s.scale) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); + MonsterAim(self, 0.5, speed, true, -1, forward, start); + } + else + { + if (self->s.scale && self->s.scale != 1.0f) + { + supertank_project_flash(self, flash_number, forward, start); + MonsterAim(self, 0.5, speed, true, -1, forward, start); + } + else + MonsterAim(self, 0.5, speed, true, flash_number, forward, start); + } monster_fire_rocket (self, start, forward, damage, speed, flash_number); } @@ -448,7 +609,13 @@ void supertankMachineGun (edict_t *self) damage = 20 + 2* drone_damagelevel(self); - MonsterAim(self, 0.8, 0, false, flash_number, forward, start); + if (self->s.scale && self->s.scale != 1.0f) + { + supertank_project_flash(self, flash_number, forward, start); + MonsterAim(self, 0.8, 0, false, -1, forward, start); + } + else + MonsterAim(self, 0.8, 0, false, flash_number, forward, start); monster_fire_bullet (self, start, forward, damage, damage, DEFAULT_BULLET_HSPREAD, DEFAULT_BULLET_VSPREAD, flash_number); @@ -465,8 +632,8 @@ void supertank_attack(edict_t *self) { if (r <= 0.2) self->monsterinfo.currentmove = &supertank_move_attack1; - //else if (r <= 0.4) - // self->monsterinfo.currentmove = &supertank_move_attack4; + else if (r <= 0.4) + self->monsterinfo.currentmove = &supertank_move_attack4; else self->monsterinfo.currentmove = &supertank_move_attack2; } @@ -475,6 +642,8 @@ void supertank_attack(edict_t *self) { if (r <= 0.2) self->monsterinfo.currentmove = &supertank_move_attack2; + else if (r <= 0.4) + self->monsterinfo.currentmove = &supertank_move_attack4; else self->monsterinfo.currentmove = &supertank_move_attack1; } @@ -490,7 +659,6 @@ void supertank_attack(edict_t *self) void BossExplode (edict_t *self) { vec3_t org; - int n; self->think = BossExplode; VectorCopy (self->s.origin, org); @@ -531,12 +699,7 @@ void BossExplode (edict_t *self) break; case 8: self->s.sound = 0; - for (n= 0; n < 4; n++) - ThrowGib (self, "models/objects/gibs/sm_meat/tris.md2", 500, GIB_ORGANIC); - for (n= 0; n < 8; n++) - ThrowGib (self, "models/objects/gibs/sm_metal/tris.md2", 500, GIB_METALLIC); - ThrowGib (self, "models/objects/gibs/chest/tris.md2", 500, GIB_ORGANIC); - ThrowHead (self, "models/objects/gibs/gear/tris.md2", 500, GIB_METALLIC); + vrx_throw_drone_gibs(self, 500); self->deadflag = DEAD_DEAD; M_Remove(self, false, false); return; @@ -572,33 +735,77 @@ void supertank_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int da void supertank_sight (edict_t *self, edict_t *other) { - if (random() > 0.5) - gi.sound (self, CHAN_VOICE, sound_search1, 1, ATTN_NORM, 0); - else - gi.sound (self, CHAN_VOICE, sound_search2, 1, ATTN_NORM, 0); + supertank_search(self); } void init_drone_supertank (edict_t *self) { + qboolean janitor = (self->mtype == M_JANITOR); + qboolean boss5 = supertank_is_boss5(self); + sound_death = gi.soundindex ("bosstank/btkdeth1.wav"); + sound_pain1 = gi.soundindex ("bosstank/btkpain1.wav"); + sound_pain2 = gi.soundindex ("bosstank/btkpain2.wav"); + sound_pain3 = gi.soundindex ("bosstank/btkpain3.wav"); sound_search1 = gi.soundindex ("bosstank/btkunqv1.wav"); sound_search2 = gi.soundindex ("bosstank/btkunqv2.wav"); tread_sound = gi.soundindex ("bosstank/btkengn1.wav"); + if (boss5) + gi.soundindex("weapons/railgr1a.wav"); self->movetype = MOVETYPE_STEP; self->solid = SOLID_BBOX; - self->mtype = M_SUPERTANK; - self->monsterinfo.control_cost = M_SUPERTANK_CONTROL_COST; - self->monsterinfo.cost = 300; + if (!janitor && !boss5) + self->mtype = M_SUPERTANK; + self->monsterinfo.control_cost = janitor ? M_TANK_CONTROL_COST : M_SUPERTANK_CONTROL_COST; + self->monsterinfo.cost = janitor ? 150 : 300; self->s.modelindex = gi.modelindex ("models/monsters/boss1/tris.md2"); - VectorSet (self->mins, -64, -64, 0); - VectorSet (self->maxs, 64, 64, 112); - - self->health = self->max_health = 20000*self->monsterinfo.level; + if (janitor) + { + self->s.skinnum = 2; + self->s.scale = 0.6f; + self->monsterinfo.scale = MODEL_SCALE * 0.6f; + VectorSet (self->mins, -38, -38, 0); + VectorSet (self->maxs, 38, 38, 67); + self->health = self->max_health = M_JANITOR_INITIAL_HEALTH + M_JANITOR_ADDON_HEALTH * self->monsterinfo.level; + } + else + { + if (boss5) + self->s.skinnum = 2; + if (invasion->value) + { + self->s.scale = SUPERTANK_INVASION_SCALE; + self->monsterinfo.scale = MODEL_SCALE * SUPERTANK_INVASION_SCALE; + VectorSet (self->mins, -40, -40, 0); + VectorSet (self->maxs, 40, 40, 72); + self->health = self->max_health = SUPERTANK_INVASION_BASE_HEALTH + SUPERTANK_INVASION_ADDON_HEALTH * self->monsterinfo.level; + } + else + { + VectorSet (self->mins, -64, -64, 0); + VectorSet (self->maxs, 64, 64, 112); + self->health = self->max_health = 20000*self->monsterinfo.level; + self->monsterinfo.scale = MODEL_SCALE; + } + } self->gib_health = -5 * BASE_GIB_HEALTH; - self->mass = 800; + self->mass = janitor ? 480 : 800; + + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + if (janitor) + self->monsterinfo.power_armor_power = M_JANITOR_INITIAL_ARMOR + M_JANITOR_ADDON_ARMOR * self->monsterinfo.level; + else if (boss5) + self->monsterinfo.power_armor_power = 400 * self->monsterinfo.level; + else + self->monsterinfo.power_armor_power = 0; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; self->die = supertank_die; + // vortex bosses have pain animations disabled. + if (janitor) + self->pain = supertank_pain; + self->monsterinfo.stand = supertank_stand; self->monsterinfo.walk = supertank_walk; self->monsterinfo.run = supertank_run; @@ -607,11 +814,18 @@ void init_drone_supertank (edict_t *self) self->monsterinfo.jumpdn = 512; self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; self->monsterinfo.currentmove = &supertank_move_stand; - self->monsterinfo.scale = MODEL_SCALE; self->monsterinfo.sight = supertank_sight; + self->monsterinfo.idle = supertank_search; self->nextthink = level.time + FRAMETIME; gi.linkentity (self); - G_PrintGreenText(va("A level %d super tank has spawned!", self->monsterinfo.level)); + if (!janitor && !invasion->value && (!self->activator || !self->activator->client)) + G_PrintGreenText(va("A level %d %s has spawned!", self->monsterinfo.level, boss5 ? "super tank heat" : "super tank")); +} + +void init_drone_boss5(edict_t *self) +{ + self->mtype = M_BOSS5; + init_drone_supertank(self); } diff --git a/src/entities/drone/drone_tank.c b/src/entities/drone/drone_tank.c index a01c9018..380908a6 100644 --- a/src/entities/drone/drone_tank.c +++ b/src/entities/drone/drone_tank.c @@ -19,6 +19,7 @@ void mytank_attack_chain (edict_t *self); static int sound_thud; static int sound_pain; +static int sound_pain2; static int sound_idle; static int sound_die; static int sound_step; @@ -45,6 +46,46 @@ void mytank_windup (edict_t *self) gi.sound (self, CHAN_WEAPON, sound_windup, 1, ATTN_NORM, 0); } +static void mytank_slam_origin(edict_t *self, vec3_t origin) +{ + vec3_t forward, right, offset; + trace_t tr; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorSet(offset, 20, -14.3f, -21); + G_ProjectSource(self->s.origin, offset, forward, right, origin); + tr = gi.trace(self->s.origin, NULL, NULL, origin, self, MASK_SOLID); + VectorCopy(tr.endpos, origin); +} + +static void mytank_slam_effect(vec3_t origin) +{ + vec3_t up; + + VectorSet(up, 0, 0, 1); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BERSERK_SLAM); + gi.WritePosition(origin); + gi.WriteDir(up); + gi.multicast(origin, MULTICAST_PHS); +} + +static qboolean mytank_can_blaster(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_TANK_BLASTER_1); +} + +static qboolean mytank_can_rocket(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_TANK_ROCKET_1); +} + +static qboolean mytank_can_chain(edict_t *self) +{ + return M_MonsterHasClearShotFromFlash(self, MZ2_TANK_MACHINEGUN_5); +} + void mytank_idle (edict_t *self) { int range; @@ -253,7 +294,10 @@ void myTankRail (edict_t *self) damage = M_RAILGUN_DMG_MAX; MonsterAim(self, M_HITSCAN_INSTANT_ACC, 0, false, flash_number, forward, start); - monster_fire_railgun(self, start, forward, damage, damage, MZ2_GLADIATOR_RAILGUN_1); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + + monster_fire_railgun(self, start, forward, damage, damage, flash_number); } void myTankBlaster(edict_t* self) @@ -284,6 +328,9 @@ void myTankBlaster(edict_t* self) speed = M_BLASTER_SPEED_MAX; MonsterAim(self, M_PROJECTILE_ACC, speed, false, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; + monster_fire_blaster(self, start, forward, damage, speed, EF_BLASTER, BLASTER_PROJ_BOLT, 2.0, true, flash_number); } @@ -316,13 +363,15 @@ void myTankRocket(edict_t* self) speed = M_ROCKETLAUNCHER_SPEED_MAX; MonsterAim(self, M_PROJECTILE_ACC, speed, true, flash_number, forward, start); + if (!M_MonsterHasClearShotFrom(self, start)) + return; monster_fire_rocket(self, start, forward, damage, speed, flash_number); } void myTankMachineGun(edict_t* self) { - vec3_t forward, start; + vec3_t forward, right, start, dir, vec; int flash_number, damage; // sanity check @@ -336,7 +385,28 @@ void myTankMachineGun(edict_t* self) if (M_MACHINEGUN_DMG_MAX && damage > 2 * M_MACHINEGUN_DMG_MAX) damage = 2 * M_MACHINEGUN_DMG_MAX; - MonsterAim(self, M_HITSCAN_CONT_ACC, 0, false, flash_number, forward, start); + AngleVectors(self->s.angles, forward, right, NULL); + G_ProjectSource(self->s.origin, monster_flash_offset[flash_number], forward, right, start); + + if (self->enemy) + { + VectorCopy(self->enemy->s.origin, vec); + vec[2] += self->enemy->viewheight; + VectorSubtract(vec, start, vec); + vectoangles(vec, dir); + } + else + dir[0] = 0; + if (self->s.frame <= FRAME_attak415) + dir[1] = self->s.angles[1] - 8 * (self->s.frame - FRAME_attak411); + else + dir[1] = self->s.angles[1] + 8 * (self->s.frame - FRAME_attak419); + dir[2] = 0; + + AngleVectors(dir, forward, NULL, NULL); + + if (!M_MonsterHasClearShotFrom(self, start)) + return; monster_fire_bullet(self, start, forward, damage, 40, DEFAULT_BULLET_HSPREAD, DEFAULT_BULLET_VSPREAD, flash_number); @@ -631,6 +701,11 @@ void mytank_doattack_rocket (edict_t *self) self->monsterinfo.currentmove = &mytank_move_attack_fire_rocket; } +static qboolean mytank_should_melee(edict_t *self, float range) +{ + return self->groundentity && range <= 144.0f; +} + void mytank_melee (edict_t *self) { @@ -641,7 +716,7 @@ void mytank_meleeattack (edict_t *self) int damage; trace_t tr; edict_t *other=NULL; - vec3_t v; + vec3_t v, damage_origin; // tank must be on the ground to punch if (!self->groundentity) @@ -654,8 +729,10 @@ void mytank_meleeattack (edict_t *self) damage = M_MELEE_DMG_MAX; gi.sound (self, CHAN_AUTO, gi.soundindex ("tank/tnkatck5.wav"), 1, ATTN_NORM, 0); + mytank_slam_origin(self, damage_origin); + mytank_slam_effect(damage_origin); - while ((other = findradius(other, self->s.origin, 128)) != NULL) + while ((other = findradius(other, damage_origin, 128)) != NULL) { if (!G_ValidTarget(self, other, true, true)) continue; @@ -666,9 +743,9 @@ void mytank_meleeattack (edict_t *self) //if ((self->monsterinfo.control_cost < 3) && !nearfov(self, other, 0, 60))//!infront(self, other)) // continue; - VectorSubtract(other->s.origin, self->s.origin, v); + VectorSubtract(other->s.origin, damage_origin, v); VectorNormalize(v); - tr = gi.trace(self->s.origin, NULL, NULL, other->s.origin, self, (MASK_PLAYERSOLID | MASK_MONSTERSOLID)); + tr = gi.trace(damage_origin, NULL, NULL, other->s.origin, self, (MASK_PLAYERSOLID | MASK_MONSTERSOLID)); T_Damage (other, self, self, v, tr.endpos, tr.plane.normal, damage, 200, 0, MOD_TANK_PUNCH); //other->velocity[2] += 200;//damage / 2; } @@ -776,6 +853,15 @@ void commander_attack (edict_t *self) { const float r = random(); float range = entdist(self, self->enemy); + qboolean can_blast; + qboolean can_rocket; + + if (mytank_should_melee(self, range)) + { + self->monsterinfo.currentmove = &mytank_move_strike; + self->monsterinfo.attack_finished = level.time + 2.0; + return; + } // short range attack if (range <= 128 && r <= 0.6) @@ -789,30 +875,40 @@ void commander_attack (edict_t *self) { if (TeleportNearTarget(self, self->enemy, 16.0, true)) { - if (r <= 0.5) + range = entdist(self, self->enemy); + if (mytank_should_melee(self, range)) { self->monsterinfo.currentmove = &mytank_move_strike; self->monsterinfo.attack_finished = level.time + 0.5; return; } - - // recalculate enemy distance - range = entdist(self, self->enemy); } } + can_blast = mytank_can_blaster(self); + can_rocket = mytank_can_rocket(self); + // medium range attack if (range <= 512) { - if (r <= 0.2) + if (r <= 0.2 && can_blast) self->monsterinfo.currentmove = &mytank_move_attack_blast; - else + else if (can_rocket) self->monsterinfo.currentmove = &mytank_move_attack_fire_rocket; + else if (can_blast) + self->monsterinfo.currentmove = &mytank_move_attack_blast; + else + return; } // long range attack else { - self->monsterinfo.currentmove = &mytank_move_attack_blast; + if (can_blast) + self->monsterinfo.currentmove = &mytank_move_attack_blast; + else if (can_rocket) + self->monsterinfo.currentmove = &mytank_move_attack_fire_rocket; + else + return; } } @@ -824,9 +920,23 @@ void tank_attack(edict_t* self) { const float r = random(); const float range = entdist(self, self->enemy); + qboolean can_blast; + qboolean can_rocket; + qboolean can_chain; //gi.dprintf("%d tank_attack()\n", level.framenum); + if (mytank_should_melee(self, range)) + { + self->monsterinfo.currentmove = &mytank_move_strike; + M_DelayNextAttack(self, 0, true); + return; + } + + can_blast = mytank_can_blaster(self); + can_rocket = mytank_can_rocket(self); + can_chain = mytank_can_chain(self); + // short range attack (60% strike, then 20% blaster, 80% rocket) if (range <= 128) { @@ -836,29 +946,45 @@ void tank_attack(edict_t* self) } else { - if (r <= 0.2) + if (r <= 0.2 && can_blast) self->monsterinfo.currentmove = &mytank_move_attack_blast; - else + else if (can_rocket) self->monsterinfo.currentmove = &mytank_move_attack_fire_rocket; + else if (can_blast) + self->monsterinfo.currentmove = &mytank_move_attack_blast; + else if (can_chain) + self->monsterinfo.currentmove = &mytank_move_attack_chain; + else + return; } } // medium range attack (20% chain, 40% blaster, 40% rocket) else if (range <= 512) { - if (r <= 0.2) + if (r <= 0.2 && can_chain) self->monsterinfo.currentmove = &mytank_move_attack_chain; - else if (r <= 0.6) + else if (r <= 0.6 && can_blast) self->monsterinfo.currentmove = &mytank_move_attack_blast; - else + else if (can_rocket) self->monsterinfo.currentmove = &mytank_move_attack_fire_rocket; + else if (can_blast) + self->monsterinfo.currentmove = &mytank_move_attack_blast; + else if (can_chain) + self->monsterinfo.currentmove = &mytank_move_attack_chain; + else + return; } // long range attack (20% blaster, 80% chain) else { - if (r <= 0.2) + if (r <= 0.2 && can_blast) self->monsterinfo.currentmove = &mytank_move_attack_blast; - else + else if (can_chain) self->monsterinfo.currentmove = &mytank_move_attack_chain; + else if (can_blast) + self->monsterinfo.currentmove = &mytank_move_attack_blast; + else + return; } M_DelayNextAttack(self, 0, true); @@ -961,7 +1087,7 @@ void tank_pain(edict_t* self, edict_t* other, float kick, int damage) !(is_idling || moving_without_enemy)) return; - gi.sound(self, CHAN_VOICE, sound_pain, 1, ATTN_NORM, 0); + gi.sound(self, CHAN_VOICE, self->mtype == M_COMMANDER ? sound_pain2 : sound_pain, 1, ATTN_NORM, 0); if (is_idling || moving_without_enemy) self->monsterinfo.currentmove = &tank_move_pain_long; @@ -988,6 +1114,13 @@ void mytank_dead(edict_t* self) M_PrepBodyRemoval(self); } +static void mytank_shrink(edict_t *self) +{ + self->maxs[2] = 0; + self->svflags |= SVF_DEADMONSTER; + gi.linkentity(self); +} + mframe_t mytank_frames_death1[] = { ai_move, -7, NULL, @@ -1016,7 +1149,7 @@ mframe_t mytank_frames_death1[] = ai_move, -6, NULL, ai_move, -4, NULL, ai_move, -5, NULL, - ai_move, -7, NULL, + ai_move, -7, mytank_shrink, ai_move, -15, mytank_thud, ai_move, -5, NULL, ai_move, 0, NULL, @@ -1027,8 +1160,6 @@ mmove_t mytank_move_death = { FRAME_death101, FRAME_death132, mytank_frames_deat void mytank_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) { - int n; - //gi.dprintf("mytank_die called at %.1f\n", level.time);//DEBUG M_Notify(self); @@ -1045,15 +1176,7 @@ void mytank_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damag if (self->health <= self->gib_health) { gi.sound (self, CHAN_VOICE, gi.soundindex ("misc/udeath.wav"), 1, ATTN_NORM, 0); - if (vrx_spawn_nonessential_ent(self->s.origin)) - { - for (n = 0; n < 1; n++) - ThrowGib(self, "models/objects/gibs/sm_meat/tris.md2", damage, GIB_ORGANIC); - for (n = 0; n < 4; n++) - ThrowGib(self, "models/objects/gibs/sm_metal/tris.md2", damage, GIB_METALLIC); - ThrowGib(self, "models/objects/gibs/chest/tris.md2", damage, GIB_ORGANIC); - //ThrowHead (self, "models/objects/gibs/gear/tris.md2", damage, GIB_METALLIC); - } + vrx_throw_drone_gibs(self, damage); //self->deadflag = DEAD_DEAD; #ifdef OLD_NOLAG_STYLE M_Remove(self, false, false); @@ -1069,12 +1192,15 @@ void mytank_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damag if (self->deadflag == DEAD_DEAD) return; + vrx_drop_tank_death_arm(self, damage); + DroneList_Remove(self); // begin death sequence gi.sound (self, CHAN_VOICE, sound_die, 1, ATTN_NORM, 0); self->deadflag = DEAD_DEAD; self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); self->monsterinfo.currentmove = &mytank_move_death; if (self->activator && !self->activator->client) @@ -1107,6 +1233,7 @@ void init_drone_tank (edict_t *self) self->solid = SOLID_BBOX; sound_pain = gi.soundindex ("tank/tnkpain2.wav"); + sound_pain2 = gi.soundindex ("tank/pain.wav"); sound_thud = gi.soundindex ("tank/tnkdeth2.wav"); sound_idle = gi.soundindex ("tank/tnkidle1.wav"); sound_die = gi.soundindex ("tank/death.wav"); diff --git a/src/entities/drone/drone_widow.c b/src/entities/drone/drone_widow.c new file mode 100644 index 00000000..f094873b --- /dev/null +++ b/src/entities/drone/drone_widow.c @@ -0,0 +1,915 @@ +/* +============================================================================== + +black widow + +============================================================================== +*/ + +#include "g_local.h" + +#define WIDOW_FRAME_idle01 0 +#define WIDOW_FRAME_idle11 10 +#define WIDOW_FRAME_walk01 11 +#define WIDOW_FRAME_walk13 23 +#define WIDOW_FRAME_run01 24 +#define WIDOW_FRAME_run08 31 +#define WIDOW_FRAME_firea01 32 +#define WIDOW_FRAME_firea09 40 +#define WIDOW_FRAME_fireb01 41 +#define WIDOW_FRAME_fireb09 49 +#define WIDOW_FRAME_firec01 50 +#define WIDOW_FRAME_firec09 58 +#define WIDOW_FRAME_fired02a 61 +#define WIDOW_FRAME_fired03 62 +#define WIDOW_FRAME_fired20 79 +#define WIDOW_FRAME_spawn01 82 +#define WIDOW_FRAME_spawn18 99 +#define WIDOW_FRAME_pain01 100 +#define WIDOW_FRAME_pain05 104 +#define WIDOW_FRAME_pain13 112 +#define WIDOW_FRAME_pain201 113 +#define WIDOW_FRAME_pain203 115 +#define WIDOW_FRAME_death01 130 +#define WIDOW_FRAME_death31 160 +#define WIDOW_FRAME_kick01 161 +#define WIDOW_FRAME_kick08 168 + +#define WIDOW_SUMMON_COUNT 2 +#define WIDOW_SUMMON_COOLDOWN 10.0f +#define WIDOW_MELEE_RANGE 176.0f +#define WIDOW_INVASION_SCALE 0.75f +#define WIDOW_INVASION_HALF_WIDTH 30.0f +#define WIDOW_INVASION_HEIGHT 108.0f +#define WIDOW_LEGS_MAX_FRAME 23 +#define WIDOW_LEGS_FRAME_TIME 0.1f +#define WIDOW_LEGS_WAIT_TIME 1.0f + +static int sound_pain1; +static int sound_pain2; +static int sound_pain3; +static int sound_death; +static int sound_laugh; +static int sound_rail; +static int sound_step1; +static int sound_step2; +static int sound_hit; +static int sound_spawn; +static int shotsfired; + +void drone_ai_stand(edict_t *self, float dist); +void drone_ai_run(edict_t *self, float dist); +void drone_ai_walk(edict_t *self, float dist); +qboolean drone_findtarget(edict_t *self, qboolean force); + +static void widow_stand(edict_t *self); +static void widow_walk(edict_t *self); +static void widow_run(edict_t *self); +static void widow_attack(edict_t *self); +static void widow_fire_blaster(edict_t *self); +static void widow_fire_rail(edict_t *self); +static void widow_melee_hit(edict_t *self); +static void widow_spawn_effects(edict_t *self); +static void widow_finish_spawn(edict_t *self); +static void widow_explode(edict_t *self); +static void widow_spawn_out_start(edict_t *self); +static void widow_spawn_out_do(edict_t *self); +static void widow_step1(edict_t *self); +static void widow_step2(edict_t *self); + +static vec3_t widow_beam_effects[] = { + { 12.58f, -43.71f, 68.88f }, + { 3.43f, 58.72f, 68.41f } +}; + +static mframe_t widow_frames_stand[] = +{ + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL, + drone_ai_stand, 0, NULL +}; +static mmove_t widow_move_stand = { WIDOW_FRAME_idle01, WIDOW_FRAME_idle11, widow_frames_stand, widow_stand }; + +static mframe_t widow_frames_walk[] = +{ + drone_ai_walk, 7, widow_step1, + drone_ai_walk, 8, NULL, + drone_ai_walk, 9, NULL, + drone_ai_walk, 10, NULL, + drone_ai_walk, 9, widow_step2, + drone_ai_walk, 8, NULL, + drone_ai_walk, 7, NULL, + drone_ai_walk, 6, NULL, + drone_ai_walk, 7, widow_step1, + drone_ai_walk, 8, NULL, + drone_ai_walk, 9, NULL, + drone_ai_walk, 8, NULL, + drone_ai_walk, 7, widow_step2 +}; +static mmove_t widow_move_walk = { WIDOW_FRAME_walk01, WIDOW_FRAME_walk13, widow_frames_walk, widow_walk }; + +static mframe_t widow_frames_run[] = +{ + drone_ai_run, 12, widow_step1, + drone_ai_run, 16, NULL, + drone_ai_run, 18, NULL, + drone_ai_run, 20, widow_step2, + drone_ai_run, 18, NULL, + drone_ai_run, 16, NULL, + drone_ai_run, 14, NULL, + drone_ai_run, 12, widow_step1 +}; +static mmove_t widow_move_run = { WIDOW_FRAME_run01, WIDOW_FRAME_run08, widow_frames_run, NULL }; + +static mframe_t widow_frames_blaster[] = +{ + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster, + ai_charge, 0, widow_fire_blaster +}; +static mmove_t widow_move_blaster = { WIDOW_FRAME_fired02a, WIDOW_FRAME_fired20, widow_frames_blaster, widow_run }; + +static mframe_t widow_frames_rail[] = +{ + ai_charge, 0, NULL, + ai_charge, -6, NULL, + ai_charge, -10, widow_fire_rail, + ai_charge, -4, NULL, + ai_charge, 0, NULL, + ai_charge, -6, widow_fire_rail, + ai_charge, -4, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +static mmove_t widow_move_rail = { WIDOW_FRAME_firea01, WIDOW_FRAME_firea09, widow_frames_rail, widow_run }; +static mmove_t widow_move_rail_right = { WIDOW_FRAME_fireb01, WIDOW_FRAME_fireb09, widow_frames_rail, widow_run }; +static mmove_t widow_move_rail_left = { WIDOW_FRAME_firec01, WIDOW_FRAME_firec09, widow_frames_rail, widow_run }; + +static mframe_t widow_frames_spawn[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, widow_spawn_effects, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, -4, widow_finish_spawn, + ai_charge, -8, NULL, + ai_charge, -10, NULL, + ai_charge, -8, NULL, + ai_charge, -4, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +static mmove_t widow_move_spawn = { WIDOW_FRAME_spawn01, WIDOW_FRAME_spawn18, widow_frames_spawn, widow_run }; + +static mframe_t widow_frames_pain_heavy[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +static mmove_t widow_move_pain_heavy = { WIDOW_FRAME_pain01, WIDOW_FRAME_pain13, widow_frames_pain_heavy, widow_run }; + +static mframe_t widow_frames_pain_light[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +static mmove_t widow_move_pain_light = { WIDOW_FRAME_pain201, WIDOW_FRAME_pain203, widow_frames_pain_light, widow_run }; + +static mframe_t widow_frames_death[] = +{ + ai_move, 0, widow_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow_explode, + ai_move, 0, widow_spawn_out_start, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow_explode, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow_explode, + ai_move, 0, widow_spawn_out_do +}; +static mmove_t widow_move_death = { WIDOW_FRAME_death01, WIDOW_FRAME_death31, widow_frames_death, NULL }; + +static mframe_t widow_frames_kick[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, widow_melee_hit, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +static mmove_t widow_move_kick = { WIDOW_FRAME_kick01, WIDOW_FRAME_kick08, widow_frames_kick, widow_run }; + +static void widow_step1(edict_t *self) +{ + gi.sound(self, CHAN_BODY, sound_step1, 1, ATTN_NORM, 0); +} + +static void widow_step2(edict_t *self) +{ + gi.sound(self, CHAN_BODY, sound_step2, 1, ATTN_NORM, 0); +} + +static void widow_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_laugh, 1, ATTN_NORM, 0); +} + +static void widow_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &widow_move_stand; +} + +static void widow_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &widow_move_walk; +} + +static void widow_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &widow_move_stand; + else + self->monsterinfo.currentmove = &widow_move_run; +} + +static qboolean widow_can_melee(edict_t *self) +{ + return G_EntExists(self->enemy) && entdist(self, self->enemy) <= WIDOW_MELEE_RANGE; +} + +static void widow_project_flash(edict_t *self, int flash, vec3_t forward, vec3_t start) +{ + vec3_t right, offset; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[flash], offset); + if (self->s.scale && self->s.scale != 1.0f) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static int widow_blaster_flash(edict_t *self) +{ + if (self->s.frame >= WIDOW_FRAME_spawn01 + 4 && self->s.frame <= WIDOW_FRAME_spawn01 + 12) + return MZ2_WIDOW_BLASTER_SWEEP1 + self->s.frame - (WIDOW_FRAME_spawn01 + 4); + if (self->s.frame == WIDOW_FRAME_fired02a) + return MZ2_WIDOW_BLASTER_0; + if (self->s.frame >= WIDOW_FRAME_fired03 && self->s.frame <= WIDOW_FRAME_fired20) + return MZ2_WIDOW_BLASTER_100 + self->s.frame - WIDOW_FRAME_fired03; + if (self->s.frame >= WIDOW_FRAME_run01 && self->s.frame <= WIDOW_FRAME_run08) + return MZ2_WIDOW_RUN_1 + self->s.frame - WIDOW_FRAME_run01; + return MZ2_WIDOW_BLASTER; +} + +static void widow_fire_blaster(edict_t *self) +{ + int damage; + int speed; + int flash; + int effect; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_BLASTER2_DMG_BASE + M_BLASTER2_DMG_ADDON * drone_damagelevel(self); + if (M_BLASTER2_DMG_MAX && damage > M_BLASTER2_DMG_MAX) + damage = M_BLASTER2_DMG_MAX; + speed = M_BLASTER2_SPEED_BASE + M_BLASTER2_SPEED_ADDON * drone_damagelevel(self); + if (M_BLASTER2_SPEED_MAX && speed > M_BLASTER2_SPEED_MAX) + speed = M_BLASTER2_SPEED_MAX; + + flash = widow_blaster_flash(self); + shotsfired++; + effect = (shotsfired % 4) ? 0 : EF_BLASTER; + widow_project_flash(self, flash, forward, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, false, -1, forward, start); + monster_fire_blaster2(self, start, forward, damage, speed, effect, flash); +} + +static void widow_fire_rail(edict_t *self) +{ + int damage; + int flash; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_RAILGUN_DMG_BASE + M_RAILGUN_DMG_ADDON * drone_damagelevel(self); + if (M_RAILGUN_DMG_MAX && damage > M_RAILGUN_DMG_MAX) + damage = M_RAILGUN_DMG_MAX; + + if (self->monsterinfo.currentmove == &widow_move_rail_left) + flash = MZ2_WIDOW_RAIL_LEFT; + else if (self->monsterinfo.currentmove == &widow_move_rail_right) + flash = MZ2_WIDOW_RAIL_RIGHT; + else + flash = MZ2_WIDOW_RAIL; + widow_project_flash(self, flash, forward, start); + MonsterAim(self, M_HITSCAN_INSTANT_ACC, 0, false, -1, forward, start); + gi.sound(self, CHAN_WEAPON, sound_rail, 1, ATTN_NORM, 0); + monster_fire_railgun(self, start, forward, damage, damage, flash); +} + +static void widow_melee_hit(edict_t *self) +{ + int damage; + + damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + if (M_MeleeAttack(self, self->enemy, 160, damage, 350)) + gi.sound(self, CHAN_WEAPON, sound_hit, 1, ATTN_NORM, 0); +} + +static qboolean widow_valid_spawn_spot(edict_t *self, vec3_t mins, vec3_t maxs, vec3_t spot) +{ + vec3_t start, end; + trace_t tr; + + VectorCopy(spot, start); + start[2] += 72; + VectorCopy(spot, end); + end[2] -= 128; + + tr = gi.trace(start, mins, maxs, end, self, MASK_MONSTERSOLID); + if (tr.fraction == 1.0f || tr.startsolid || tr.allsolid) + return false; + + VectorCopy(tr.endpos, spot); + return G_IsValidLocation(self, spot, mins, maxs); +} + +static qboolean widow_find_spawn_spot(edict_t *self, int index, vec3_t spot) +{ + vec3_t mins, maxs; + vec3_t forward, right; + float side; + + VectorSet(mins, -28, -28, -18); + VectorSet(maxs, 28, 28, 18); + AngleVectors(self->s.angles, forward, right, NULL); + + side = (index & 1) ? 88.0f : -88.0f; + VectorCopy(self->s.origin, spot); + VectorMA(spot, 112.0f, forward, spot); + VectorMA(spot, side, right, spot); + if (widow_valid_spawn_spot(self, mins, maxs, spot)) + return true; + + VectorCopy(self->s.origin, spot); + VectorMA(spot, -96.0f, forward, spot); + VectorMA(spot, side, right, spot); + return widow_valid_spawn_spot(self, mins, maxs, spot); +} + +static void widow_cleanup_failed_spawn(edict_t *owner, edict_t *spawned) +{ + if (owner && owner->client) + layout_remove_tracked_entity(&owner->client->layout, spawned); + + DroneList_Remove(spawned); + AI_EnemyRemoved(spawned); + G_FreeEdict(spawned); +} + +static void widow_setup_invasion_spawn(edict_t *spawned) +{ + if (!invasion->value) + return; + + spawned->monsterinfo.aiflags &= ~AI_STAND_GROUND; + spawned->monsterinfo.aiflags |= AI_FIND_NAVI; + spawned->prev_navi = NULL; + spawned->goalentity = NULL; +} + +static void widow_start_spawned_monster(edict_t *self, edict_t *spawned) +{ + const qboolean force_start = invasion->value || pvm->value; + + if (G_ValidTarget(spawned, self->enemy, !force_start, true)) + { + spawned->enemy = self->enemy; + VectorCopy(self->enemy->s.origin, spawned->monsterinfo.last_sighting); + } + else if (force_start) + drone_findtarget(spawned, true); + + if ((spawned->enemy || spawned->goalentity) && spawned->monsterinfo.run) + spawned->monsterinfo.run(spawned); + else if (spawned->monsterinfo.stand) + spawned->monsterinfo.stand(spawned); +} + +static qboolean widow_spawn_stalker(edict_t *self, int index) +{ + edict_t *owner; + edict_t *spawned; + vec3_t spot; + + owner = (self->activator && self->activator->inuse) ? self->activator : self; + spawned = G_Spawn(); + spawned->mtype = M_STALKER; + spawned->activator = owner; + spawned->monsterinfo.level = self->monsterinfo.level; + + if (!M_Initialize(owner, spawned, 0.0f)) + { + G_FreeEdict(spawned); + return false; + } + + if (!widow_find_spawn_spot(self, index, spot)) + { + widow_cleanup_failed_spawn(owner, spawned); + return false; + } + + spawned->monsterinfo.cost = 0; + spawned->s.effects |= EF_PLASMA; + VectorCopy(spot, spawned->s.origin); + VectorCopy(spot, spawned->s.old_origin); + VectorCopy(self->s.angles, spawned->s.angles); + spawned->nextthink = level.time + FRAMETIME; + spawned->monsterinfo.attack_finished = level.time + 1.0f; + widow_setup_invasion_spawn(spawned); + + gi.linkentity(spawned); + owner->num_monsters += spawned->monsterinfo.control_cost; + owner->num_monsters_real++; + + widow_start_spawned_monster(self, spawned); + + return true; +} + +static void widow_spawn_effects(edict_t *self) +{ + vec3_t mins, maxs, size, spot, effect_origin; + float radius; + + VectorSet(mins, -28, -28, -18); + VectorSet(maxs, 28, 28, 18); + VectorSubtract(maxs, mins, size); + radius = VectorLength(size) * 0.5f; + + for (int i = 0; i < WIDOW_SUMMON_COUNT; i++) + { + if (!widow_find_spawn_spot(self, i, spot)) + continue; + + VectorAdd(mins, maxs, effect_origin); + VectorAdd(spot, effect_origin, effect_origin); + SpawnGrow_Spawn(effect_origin, radius, radius * 2.0f); + } +} + +static void widow_finish_spawn(edict_t *self) +{ + int spawned = 0; + + for (int i = 0; i < WIDOW_SUMMON_COUNT; i++) + { + if (widow_spawn_stalker(self, i)) + spawned++; + } + + if (spawned) + self->monsterinfo.melee_finished = level.time + WIDOW_SUMMON_COOLDOWN; +} + +static qboolean widow_can_spawn_stalker(edict_t *self) +{ + vec3_t spot; + + for (int i = 0; i < WIDOW_SUMMON_COUNT; i++) + { + if (widow_find_spawn_spot(self, i, spot)) + return true; + } + + return false; +} + +static void widow_start_spawn(edict_t *self) +{ + gi.sound(self, CHAN_WEAPON, sound_spawn, 1, ATTN_NORM, 0); + self->monsterinfo.currentmove = &widow_move_spawn; +} + +static void widow_melee(edict_t *self) +{ + if (!widow_can_melee(self)) + { + self->monsterinfo.melee_finished = level.time + 0.5f; + return; + } + + self->monsterinfo.currentmove = &widow_move_kick; + self->monsterinfo.melee_finished = level.time + 1.2f; + M_DelayNextAttack(self, 1.0f, true); +} + +static void widow_attack(edict_t *self) +{ + float r; + + if (!G_EntExists(self->enemy)) + return; + + r = random(); + if (level.time >= self->monsterinfo.melee_finished && widow_can_spawn_stalker(self)) + widow_start_spawn(self); + else if (widow_can_melee(self) && r < 0.40f) + widow_melee(self); + else if (r < 0.82f) + { + if (random() < 0.33f) + self->monsterinfo.currentmove = &widow_move_rail_left; + else if (random() < 0.5f) + self->monsterinfo.currentmove = &widow_move_rail_right; + else + self->monsterinfo.currentmove = &widow_move_rail; + } + else + self->monsterinfo.currentmove = &widow_move_blaster; + + M_DelayNextAttack(self, 1.0f + random(), true); +} + +static void widow_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + if (self->health < (self->max_health / 2)) + self->s.skinnum = 1; + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 5.0f; + if (damage < 15) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + else if (damage < 75) + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_pain3, 1, ATTN_NORM, 0); + + if (skill->value == 3) + return; + + if (damage >= 40 && damage < 200) + { + if (random() < (0.6f - (0.2f * skill->value))) + self->monsterinfo.currentmove = &widow_move_pain_light; + } + else if (damage >= 200) + { + if (random() < (0.75f - (0.1f * skill->value))) + self->monsterinfo.currentmove = &widow_move_pain_heavy; + } +} + +static void widow_explode(edict_t *self) +{ + vec3_t org; + + VectorCopy(self->s.origin, org); + org[0] += crandom() * self->maxs[0]; + org[1] += crandom() * self->maxs[1]; + org[2] += random() * self->maxs[2]; + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_EXPLOSION1_BIG); + gi.WritePosition(org); + gi.multicast(self->s.origin, MULTICAST_PVS); +} + +static void widow_project_source2(vec3_t origin, vec3_t offset, vec3_t forward, vec3_t right, vec3_t up, vec3_t result) +{ + result[0] = origin[0] + forward[0] * offset[0] + right[0] * offset[1] + up[0] * offset[2]; + result[1] = origin[1] + forward[1] * offset[0] + right[1] * offset[1] + up[1] * offset[2]; + result[2] = origin[2] + forward[2] * offset[0] + right[2] * offset[1] + up[2] * offset[2]; +} + +static void widow_temp_explosion(vec3_t point) +{ + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_EXPLOSION1); + gi.WritePosition(point); + gi.multicast(point, MULTICAST_ALL); +} + +static void widowlegs_throw_gib_at(edict_t *self, char *gibname, int damage, int type, vec3_t point) +{ + edict_t *gib; + float scale; + + if (nolag->value) + return; + + scale = self->s.scale ? self->s.scale : 1.0f; + gib = ThrowGibEx(self, gibname, damage, type, scale); + if (!gib) + return; + + VectorCopy(point, gib->s.origin); + VectorCopy(point, gib->s.old_origin); + gi.linkentity(gib); +} + +static void widowlegs_project(edict_t *self, vec3_t offset, vec3_t point) +{ + vec3_t forward, right, up; + + AngleVectors(self->s.angles, forward, right, up); + widow_project_source2(self->s.origin, offset, forward, right, up, point); +} + +static void widowlegs_think(edict_t *self) +{ + vec3_t offset; + vec3_t point; + + if (self->s.frame == 17) + { + VectorSet(offset, 11.77f, -7.24f, 23.31f); + widowlegs_project(self, offset, point); + widow_temp_explosion(point); + } + + if (self->s.frame < WIDOW_LEGS_MAX_FRAME) + { + self->s.frame++; + self->nextthink = level.time + WIDOW_LEGS_FRAME_TIME; + return; + } + + if (!self->wait) + self->wait = level.time + WIDOW_LEGS_WAIT_TIME; + + if ((level.time > self->wait - 0.5f) && !self->count) + { + self->count = 1; + + VectorSet(offset, 31.0f, -88.7f, 10.96f); + widowlegs_project(self, offset, point); + widow_temp_explosion(point); + + VectorSet(offset, -12.67f, -4.39f, 15.68f); + widowlegs_project(self, offset, point); + widow_temp_explosion(point); + } + + if (level.time > self->wait) + { + VectorSet(offset, -65.6f, -8.44f, 28.59f); + widowlegs_project(self, offset, point); + widow_temp_explosion(point); + widowlegs_throw_gib_at(self, "models/monsters/blackwidow/gib1/tris.md2", 90, GIB_METALLIC | GIB_UPRIGHT, point); + widowlegs_throw_gib_at(self, "models/monsters/blackwidow/gib2/tris.md2", 90, GIB_METALLIC | GIB_UPRIGHT, point); + + VectorSet(offset, -1.04f, -51.18f, 7.04f); + widowlegs_project(self, offset, point); + widow_temp_explosion(point); + widowlegs_throw_gib_at(self, "models/monsters/blackwidow/gib1/tris.md2", 90, GIB_METALLIC | GIB_UPRIGHT, point); + widowlegs_throw_gib_at(self, "models/monsters/blackwidow/gib2/tris.md2", 90, GIB_METALLIC | GIB_UPRIGHT, point); + widowlegs_throw_gib_at(self, "models/monsters/blackwidow/gib3/tris.md2", 90, GIB_METALLIC | GIB_UPRIGHT, point); + + G_FreeEdict(self); + return; + } + + self->nextthink = level.time + WIDOW_LEGS_FRAME_TIME; +} + +static void widowlegs_spawn(vec3_t startpos, vec3_t angles, float scale) +{ + edict_t *ent; + + if (nolag->value) + return; + + ent = G_Spawn(); + VectorCopy(startpos, ent->s.origin); + VectorCopy(angles, ent->s.angles); + ent->solid = SOLID_NOT; + ent->s.renderfx = RF_IR_VISIBLE; + ent->movetype = MOVETYPE_NONE; + ent->classname = "widowlegs"; + ent->s.modelindex = gi.modelindex("models/monsters/legs/tris.md2"); + if (scale) + ent->s.scale = scale; + ent->think = widowlegs_think; + ent->nextthink = level.time + WIDOW_LEGS_FRAME_TIME; + gi.linkentity(ent); +} + +static void widow_spawn_out_start(edict_t *self) +{ + vec3_t startpoint; + vec3_t forward, right, up; + + AngleVectors(self->s.angles, forward, right, up); + + widow_project_source2(self->s.origin, widow_beam_effects[0], forward, right, up, startpoint); + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_WIDOWBEAMOUT); + gi.WriteShort(20001); + gi.WritePosition(startpoint); + gi.multicast(startpoint, MULTICAST_ALL); + + widow_project_source2(self->s.origin, widow_beam_effects[1], forward, right, up, startpoint); + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_WIDOWBEAMOUT); + gi.WriteShort(20002); + gi.WritePosition(startpoint); + gi.multicast(startpoint, MULTICAST_ALL); + + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/bwidowbeamout.wav"), 1, ATTN_NORM, 0); +} + +static void widow_spawn_out_do(edict_t *self) +{ + vec3_t startpoint; + vec3_t forward, right, up; + + AngleVectors(self->s.angles, forward, right, up); + + widow_project_source2(self->s.origin, widow_beam_effects[0], forward, right, up, startpoint); + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_WIDOWSPLASH); + gi.WritePosition(startpoint); + gi.multicast(startpoint, MULTICAST_ALL); + + widow_project_source2(self->s.origin, widow_beam_effects[1], forward, right, up, startpoint); + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_WIDOWSPLASH); + gi.WritePosition(startpoint); + gi.multicast(startpoint, MULTICAST_ALL); + + VectorCopy(self->s.origin, startpoint); + startpoint[2] += 36; + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_BOSSTPORT); + gi.WritePosition(startpoint); + gi.multicast(startpoint, MULTICAST_PHS); + + widowlegs_spawn(self->s.origin, self->s.angles, self->s.scale); + vrx_throw_drone_gibs(self, 500); + + M_Remove(self, false, false); +} + +static void widow_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + M_Notify(self); + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); + self->monsterinfo.currentmove = &widow_move_death; +} + +void init_drone_widow(edict_t *self) +{ + sound_pain1 = gi.soundindex("widow/bw1pain1.wav"); + sound_pain2 = gi.soundindex("widow/bw1pain2.wav"); + sound_pain3 = gi.soundindex("widow/bw1pain3.wav"); + sound_death = gi.soundindex("widow/death.wav"); + sound_laugh = gi.soundindex("widow/laugh.wav"); + sound_rail = gi.soundindex("gladiator/railgun.wav"); + sound_step1 = gi.soundindex("widow/bwstep1.wav"); + sound_step2 = gi.soundindex("widow/bwstep2.wav"); + sound_hit = gi.soundindex("tank/tnkatck3.wav"); + sound_spawn = gi.soundindex("medic_commander/monsterspawn1.wav"); + gi.soundindex("misc/bwidowbeamout.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/blackwidow/tris.md2"); + VectorSet(self->mins, -40, -40, 0); + VectorSet(self->maxs, 40, 40, 144); + if (invasion->value) + { + self->s.scale = WIDOW_INVASION_SCALE; + VectorSet(self->mins, -WIDOW_INVASION_HALF_WIDTH, -WIDOW_INVASION_HALF_WIDTH, 0); + VectorSet(self->maxs, WIDOW_INVASION_HALF_WIDTH, WIDOW_INVASION_HALF_WIDTH, WIDOW_INVASION_HEIGHT); + } + + gi.modelindex("models/items/spawngro3/tris.md2"); + gi.modelindex("models/monsters/stalker/tris.md2"); + gi.modelindex("models/monsters/legs/tris.md2"); + gi.modelindex("models/monsters/blackwidow/gib1/tris.md2"); + gi.modelindex("models/monsters/blackwidow/gib2/tris.md2"); + gi.modelindex("models/monsters/blackwidow/gib3/tris.md2"); + gi.modelindex("models/monsters/blackwidow/gib4/tris.md2"); + + self->health = M_WIDOW_INITIAL_HEALTH + M_WIDOW_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -5000; + self->mass = 1500; + self->mtype = M_WIDOW; + self->yaw_speed = 30; + self->flags |= FL_IMMUNE_LASER; + + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.power_armor_power = M_WIDOW_INITIAL_ARMOR + M_WIDOW_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_JORG_CONTROL_COST; + self->monsterinfo.cost = M_COMMANDER_COST; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + + self->pain = widow_pain; + self->die = widow_die; + self->monsterinfo.stand = widow_stand; + self->monsterinfo.walk = widow_walk; + self->monsterinfo.run = widow_run; + self->monsterinfo.attack = widow_attack; + self->monsterinfo.melee = widow_melee; + self->monsterinfo.sight = widow_sight; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &widow_move_stand; + self->monsterinfo.scale = 2.0f; + self->nextthink = level.time + FRAMETIME; + + if (!invasion->value) + G_PrintGreenText(va("A level %d widow has spawned!", self->monsterinfo.level)); +} diff --git a/src/entities/drone/drone_widow2.c b/src/entities/drone/drone_widow2.c new file mode 100644 index 00000000..c21ee31d --- /dev/null +++ b/src/entities/drone/drone_widow2.c @@ -0,0 +1,1022 @@ +/* +============================================================================== + +black widow 2 + +============================================================================== +*/ + +#include "g_local.h" + +#define WIDOW2_FRAME_blackwidow3 0 +#define WIDOW2_FRAME_walk01 1 +#define WIDOW2_FRAME_walk09 9 +#define WIDOW2_FRAME_spawn01 10 +#define WIDOW2_FRAME_spawn04 13 +#define WIDOW2_FRAME_spawn14 23 +#define WIDOW2_FRAME_spawn18 27 +#define WIDOW2_FRAME_firea01 28 +#define WIDOW2_FRAME_firea07 34 +#define WIDOW2_FRAME_fireb01 35 +#define WIDOW2_FRAME_fireb04 38 +#define WIDOW2_FRAME_fireb05 39 +#define WIDOW2_FRAME_fireb09 43 +#define WIDOW2_FRAME_tongs01 47 +#define WIDOW2_FRAME_tongs08 54 +#define WIDOW2_FRAME_pain01 55 +#define WIDOW2_FRAME_pain05 59 +#define WIDOW2_FRAME_death01 60 +#define WIDOW2_FRAME_death44 103 +#define WIDOW2_FRAME_dthsrh01 104 +#define WIDOW2_FRAME_dthsrh15 118 +#define WIDOW2_FRAME_dthsrh16 119 +#define WIDOW2_FRAME_dthsrh22 125 + +#define WIDOW2_SUMMON_COUNT 2 +#define WIDOW2_SUMMON_COOLDOWN 10.0f +#define WIDOW2_MELEE_RANGE 256.0f +#define WIDOW2_INVASION_SCALE 0.60f +#define WIDOW2_INVASION_MOVE_SCALE 1.75f + +static int sound_pain1; +static int sound_pain2; +static int sound_pain3; +static int sound_death; +static int sound_search; +static int sound_tongue; +static int sound_step; +static int sound_beam; +static int sound_hit; +static int sound_spawn; + +static vec3_t widow2_tongue_offsets[] = +{ + { 17.48f, 0.10f, 68.92f }, + { 17.47f, 0.29f, 68.91f }, + { 17.45f, 0.53f, 68.87f }, + { 17.42f, 0.78f, 68.81f }, + { 17.39f, 1.02f, 68.75f }, + { 17.37f, 1.20f, 68.70f }, + { 17.36f, 1.24f, 68.71f }, + { 17.37f, 1.21f, 68.72f } +}; + +void drone_ai_stand(edict_t *self, float dist); +void drone_ai_run(edict_t *self, float dist); +void drone_ai_walk(edict_t *self, float dist); +qboolean drone_findtarget(edict_t *self, qboolean force); + +static void widow2_ai_walk(edict_t *self, float dist) +{ + drone_ai_walk(self, invasion->value ? dist * WIDOW2_INVASION_MOVE_SCALE : dist); +} + +static void widow2_ai_run(edict_t *self, float dist) +{ + drone_ai_run(self, invasion->value ? dist * WIDOW2_INVASION_MOVE_SCALE : dist); +} + +static void widow2_stand(edict_t *self); +static void widow2_walk(edict_t *self); +static void widow2_run(edict_t *self); +static void widow2_attack(edict_t *self); +static void widow2_fire_beam(edict_t *self); +static void widow2_fire_disruptor(edict_t *self); +static void widow2_attack_beam(edict_t *self); +static void widow2_show_proboscis(edict_t *self); +static void widow2_pull_proboscis(edict_t *self); +static void widow2_melee_hit(edict_t *self); +static void widow2_spawn_effects(edict_t *self); +static void widow2_finish_spawn(edict_t *self); +static void widow2_explosion1(edict_t *self); +static void widow2_explosion2(edict_t *self); +static void widow2_explosion3(edict_t *self); +static void widow2_explosion4(edict_t *self); +static void widow2_explosion5(edict_t *self); +static void widow2_explosion6(edict_t *self); +static void widow2_explosion7(edict_t *self); +static void widow2_explosion_leg(edict_t *self); +static void widow2_dead(edict_t *self); +static void widow2_start_searching(edict_t *self); +static void widow2_keep_searching(edict_t *self); +static void widow2_finaldeath(edict_t *self); +static void widow2_step(edict_t *self); + +static mframe_t widow2_frames_stand[] = +{ + drone_ai_stand, 0, NULL +}; +static mmove_t widow2_move_stand = { WIDOW2_FRAME_blackwidow3, WIDOW2_FRAME_blackwidow3, widow2_frames_stand, widow2_stand }; + +static mframe_t widow2_frames_walk[] = +{ + widow2_ai_walk, 9, widow2_step, + widow2_ai_walk, 8, NULL, + widow2_ai_walk, 7, NULL, + widow2_ai_walk, 7, NULL, + widow2_ai_walk, 6, NULL, + widow2_ai_walk, 6, widow2_step, + widow2_ai_walk, 7, NULL, + widow2_ai_walk, 8, NULL, + widow2_ai_walk, 10, NULL +}; +static mmove_t widow2_move_walk = { WIDOW2_FRAME_walk01, WIDOW2_FRAME_walk09, widow2_frames_walk, widow2_walk }; + +static mframe_t widow2_frames_run[] = +{ + widow2_ai_run, 9, widow2_step, + widow2_ai_run, 8, NULL, + widow2_ai_run, 7, NULL, + widow2_ai_run, 7, NULL, + widow2_ai_run, 6, NULL, + widow2_ai_run, 6, widow2_step, + widow2_ai_run, 7, NULL, + widow2_ai_run, 8, NULL, + widow2_ai_run, 10, NULL +}; +static mmove_t widow2_move_run = { WIDOW2_FRAME_walk01, WIDOW2_FRAME_walk09, widow2_frames_run, NULL }; + +static mframe_t widow2_frames_pre_beam[] = +{ + ai_charge, 4, NULL, + ai_charge, 4, widow2_step, + ai_charge, 4, NULL, + ai_charge, 4, widow2_attack_beam +}; +static mmove_t widow2_move_pre_beam = { WIDOW2_FRAME_fireb01, WIDOW2_FRAME_fireb04, widow2_frames_pre_beam, NULL }; + +static mframe_t widow2_frames_beam[] = +{ + ai_charge, 0, widow2_fire_beam, + ai_charge, 0, widow2_fire_beam, + ai_charge, 0, widow2_fire_beam, + ai_charge, 0, widow2_fire_beam, + ai_charge, 0, widow2_fire_beam +}; +static mmove_t widow2_move_beam = { WIDOW2_FRAME_fireb05, WIDOW2_FRAME_fireb09, widow2_frames_beam, widow2_run }; + +static mframe_t widow2_frames_disruptor[] = +{ + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, -20, widow2_fire_disruptor, + ai_charge, 2, NULL, + ai_charge, 2, NULL, + ai_charge, 2, NULL +}; +static mmove_t widow2_move_disruptor = { WIDOW2_FRAME_firea01, WIDOW2_FRAME_firea07, widow2_frames_disruptor, widow2_run }; + +static mframe_t widow2_frames_spawn[] = +{ + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, widow2_step, + ai_charge, 0, widow2_fire_beam, + ai_charge, 0, NULL, + ai_charge, 0, widow2_fire_beam, + ai_charge, 0, NULL, + ai_charge, 0, widow2_fire_beam, + ai_charge, 0, NULL, + ai_charge, 0, widow2_spawn_effects, + ai_charge, 0, NULL, + ai_charge, 0, widow2_fire_beam, + ai_charge, 0, NULL, + ai_charge, 0, widow2_finish_spawn, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL, + ai_charge, 0, NULL +}; +static mmove_t widow2_move_spawn = { WIDOW2_FRAME_spawn01, WIDOW2_FRAME_spawn18, widow2_frames_spawn, widow2_run }; + +static mframe_t widow2_frames_tongs[] = +{ + ai_charge, 0, widow2_show_proboscis, + ai_charge, 0, widow2_show_proboscis, + ai_charge, 0, widow2_show_proboscis, + ai_charge, 0, widow2_pull_proboscis, + ai_charge, 0, widow2_pull_proboscis, + ai_charge, 0, widow2_pull_proboscis, + ai_charge, 0, widow2_melee_hit, + ai_charge, 0, NULL +}; +static mmove_t widow2_move_tongs = { WIDOW2_FRAME_tongs01, WIDOW2_FRAME_tongs08, widow2_frames_tongs, widow2_run }; + +static mframe_t widow2_frames_pain[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL +}; +static mmove_t widow2_move_pain = { WIDOW2_FRAME_pain01, WIDOW2_FRAME_pain05, widow2_frames_pain, widow2_run }; + +static mframe_t widow2_frames_death[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_explosion1, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_explosion2, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_explosion3, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_explosion4, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_explosion5, + ai_move, 0, widow2_explosion_leg, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_explosion6, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_explosion7, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_dead +}; +static mmove_t widow2_move_death = { WIDOW2_FRAME_death01, WIDOW2_FRAME_death44, widow2_frames_death, NULL }; + +static mframe_t widow2_frames_dead[] = +{ + ai_move, 0, widow2_start_searching, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_keep_searching +}; +static mmove_t widow2_move_dead = { WIDOW2_FRAME_dthsrh01, WIDOW2_FRAME_dthsrh15, widow2_frames_dead, NULL }; + +static mframe_t widow2_frames_really_dead[] = +{ + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, NULL, + ai_move, 0, widow2_finaldeath +}; +static mmove_t widow2_move_really_dead = { WIDOW2_FRAME_dthsrh16, WIDOW2_FRAME_dthsrh22, widow2_frames_really_dead, NULL }; + +static void widow2_start_searching(edict_t *self) +{ + self->count = 0; +} + +static void widow2_keep_searching(edict_t *self) +{ + if (self->count <= 2) + { + self->monsterinfo.currentmove = &widow2_move_dead; + self->s.frame = WIDOW2_FRAME_dthsrh01; + self->count++; + return; + } + + self->monsterinfo.currentmove = &widow2_move_really_dead; +} + +static void widow2_finaldeath(edict_t *self) +{ + // M_Remove ignores SOLID_NOT entities; the corpse is kept nonsolid during + // the final death animation so it does not block map paths. + self->solid = SOLID_BBOX; + M_Remove(self, false, false); +} + +static void widow2_step(edict_t *self) +{ + gi.sound(self, CHAN_BODY, sound_step, 1, ATTN_NORM, 0); +} + +static void widow2_search(edict_t *self) +{ + if (random() < 0.5f) + gi.sound(self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); +} + +static void widow2_sight(edict_t *self, edict_t *other) +{ + gi.sound(self, CHAN_VOICE, sound_search, 1, ATTN_NORM, 0); +} + +static void widow2_stand(edict_t *self) +{ + self->monsterinfo.currentmove = &widow2_move_stand; +} + +static void widow2_walk(edict_t *self) +{ + if (!self->goalentity) + self->goalentity = world; + self->monsterinfo.currentmove = &widow2_move_walk; +} + +static void widow2_run(edict_t *self) +{ + if (self->monsterinfo.aiflags & AI_STAND_GROUND) + self->monsterinfo.currentmove = &widow2_move_stand; + else + self->monsterinfo.currentmove = &widow2_move_run; +} + +static qboolean widow2_can_melee(edict_t *self) +{ + return G_EntExists(self->enemy) && entdist(self, self->enemy) <= WIDOW2_MELEE_RANGE; +} + +static void widow2_project_flash(edict_t *self, int flash, vec3_t forward, vec3_t start) +{ + vec3_t right, offset; + + AngleVectors(self->s.angles, forward, right, NULL); + VectorCopy(monster_flash_offset[flash], offset); + if (self->s.scale && self->s.scale != 1.0f) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static void widow2_fire_beam(edict_t *self) +{ + int damage; + int flash; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + damage = M_RAILGUN_DMG_BASE + M_RAILGUN_DMG_ADDON * drone_damagelevel(self); + if (M_RAILGUN_DMG_MAX && damage > M_RAILGUN_DMG_MAX) + damage = M_RAILGUN_DMG_MAX; + + if (self->s.frame >= WIDOW2_FRAME_fireb05 && self->s.frame <= WIDOW2_FRAME_fireb09) + flash = MZ2_WIDOW2_BEAMER_1 + self->s.frame - WIDOW2_FRAME_fireb05; + else if (self->s.frame >= WIDOW2_FRAME_spawn04 && self->s.frame <= WIDOW2_FRAME_spawn14) + flash = MZ2_WIDOW2_BEAM_SWEEP_1 + self->s.frame - WIDOW2_FRAME_spawn04; + else + flash = MZ2_WIDOW2_BEAM_SWEEP_1; + + widow2_project_flash(self, flash, forward, start); + MonsterAim(self, M_HITSCAN_INSTANT_ACC, 0, false, -1, forward, start); + gi.sound(self, CHAN_WEAPON, sound_beam, 1, ATTN_NORM, 0); + monster_fire_railgun(self, start, forward, damage, damage, flash); +} + +static void widow2_fire_disruptor(edict_t *self) +{ + int damage; + int speed; + vec3_t forward, start; + + if (!G_EntExists(self->enemy)) + return; + + damage = DISRUPTOR_INITIAL_DAMAGE + DISRUPTOR_ADDON_DAMAGE * drone_damagelevel(self); + if (M_DISRUPTOR_DMG_MAX && damage > M_DISRUPTOR_DMG_MAX) + damage = M_DISRUPTOR_DMG_MAX; + speed = DISRUPTOR_INITIAL_SPEED + DISRUPTOR_ADDON_SPEED * drone_damagelevel(self); + if (M_DISRUPTOR_SPEED_MAX && speed > M_DISRUPTOR_SPEED_MAX) + speed = M_DISRUPTOR_SPEED_MAX; + + widow2_project_flash(self, MZ2_WIDOW_DISRUPTOR, forward, start); + MonsterAim(self, M_PROJECTILE_ACC, speed, true, -1, forward, start); + fire_disruptor(self, start, forward, damage, speed, visible(self, self->enemy) ? self->enemy : NULL); + gi.WriteByte(svc_muzzleflash2); + gi.WriteShort(self - g_edicts); + gi.WriteByte(MZ2_WIDOW_DISRUPTOR); + gi.multicast(start, MULTICAST_PVS); + widow2_step(self); +} + +static void widow2_attack_beam(edict_t *self) +{ + self->monsterinfo.currentmove = &widow2_move_beam; + widow2_step(self); +} + +static void widow2_proboscis_start(edict_t *self, vec3_t start) +{ + int index = self->s.frame - WIDOW2_FRAME_tongs01; + vec3_t forward, right, offset; + + if (index < 0) + index = 0; + else if (index > 7) + index = 7; + + VectorCopy(widow2_tongue_offsets[index], offset); + if (self->s.scale && self->s.scale != 1.0f) + VectorScale(offset, self->s.scale, offset); + AngleVectors(self->s.angles, forward, right, NULL); + G_ProjectSource(self->s.origin, offset, forward, right, start); +} + +static qboolean widow2_proboscis_ok(edict_t *self, vec3_t start, vec3_t end) +{ + vec3_t dir, angles; + trace_t tr; + + VectorSubtract(start, end, dir); + if (VectorLength(dir) > WIDOW2_MELEE_RANGE) + return false; + + vectoangles(dir, angles); + if (angles[PITCH] < -180) + angles[PITCH] += 360; + if (fabsf(angles[PITCH]) > 30) + return false; + + tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT); + return tr.ent == self->enemy; +} + +static qboolean widow2_proboscis_target(edict_t *self, vec3_t start, vec3_t end) +{ + if (!G_EntExists(self->enemy)) + return false; + + G_EntMidPoint(self->enemy, end); + if (widow2_proboscis_ok(self, start, end)) + return true; + + VectorCopy(self->enemy->s.origin, end); + end[2] = self->enemy->absmax[2] - 8; + if (widow2_proboscis_ok(self, start, end)) + return true; + + VectorCopy(self->enemy->s.origin, end); + end[2] = self->enemy->absmin[2] + 8; + return widow2_proboscis_ok(self, start, end); +} + +static qboolean widow2_draw_proboscis(edict_t *self, vec3_t start, vec3_t end) +{ + widow2_proboscis_start(self, start); + if (!widow2_proboscis_target(self, start, end)) + return false; + + gi.sound(self, CHAN_WEAPON, sound_tongue, 1, ATTN_NORM, 0); + gi.WriteByte(svc_temp_entity); + gi.WriteByte(TE_PARASITE_ATTACK); + gi.WriteShort(self - g_edicts); + gi.WritePosition(start); + gi.WritePosition(end); + gi.multicast(self->s.origin, MULTICAST_PVS); + return true; +} + +static void widow2_prepare_pulled_enemy(edict_t *self) +{ + if (!G_EntExists(self->enemy)) + return; + + if (self->enemy->groundentity) + { + self->enemy->s.origin[2] += 1; + self->enemy->groundentity = NULL; + } +} + +static int widow2_proboscis_damage(edict_t *self) +{ + int damage; + + damage = M_MELEE_DMG_BASE + M_MELEE_DMG_ADDON * drone_damagelevel(self); + if (M_MELEE_DMG_MAX && damage > M_MELEE_DMG_MAX) + damage = M_MELEE_DMG_MAX; + + damage = max(1, damage / 4); + return vrx_increase_monster_damage_by_talent(self->activator, damage); +} + +static int widow2_proboscis_pull(edict_t *self) +{ + int pull; + + pull = PARASITE_INITIAL_KNOCKBACK + PARASITE_ADDON_KNOCKBACK * drone_damagelevel(self); + if (PARASITE_MAX_KNOCKBACK && pull < PARASITE_MAX_KNOCKBACK) + pull = PARASITE_MAX_KNOCKBACK; + if (G_EntExists(self->enemy) && self->enemy->groundentity) + pull *= 2; + + return pull; +} + +static void widow2_heal_from_proboscis(edict_t *self, int damage) +{ + if (self->health >= self->max_health) + return; + + self->health += damage; + if (self->health > self->max_health) + self->health = self->max_health; +} + +static void widow2_drain_proboscis(edict_t *self) +{ + int damage; + int pull; + vec3_t start, end, dir; + + if (!widow2_draw_proboscis(self, start, end)) + return; + + damage = widow2_proboscis_damage(self); + pull = widow2_proboscis_pull(self); + widow2_prepare_pulled_enemy(self); + widow2_heal_from_proboscis(self, damage); + + if (self->s.frame == WIDOW2_FRAME_tongs01 + 3) + gi.sound(self->enemy, CHAN_AUTO, sound_hit, 1, ATTN_NORM, 0); + + VectorSubtract(end, start, dir); + T_Damage(self->enemy, self, self, dir, self->enemy->s.origin, end, damage, pull, DAMAGE_NO_ABILITIES, MOD_UNKNOWN); +} + +static void widow2_show_proboscis(edict_t *self) +{ + vec3_t start, end; + + widow2_draw_proboscis(self, start, end); +} + +static void widow2_pull_proboscis(edict_t *self) +{ + widow2_drain_proboscis(self); +} + +static void widow2_melee_hit(edict_t *self) +{ + widow2_drain_proboscis(self); +} + +static qboolean widow2_valid_spawn_spot(edict_t *self, vec3_t mins, vec3_t maxs, vec3_t spot) +{ + vec3_t start, end; + trace_t tr; + + VectorCopy(spot, start); + start[2] += 96; + VectorCopy(spot, end); + end[2] -= 160; + + tr = gi.trace(start, mins, maxs, end, self, MASK_MONSTERSOLID); + if (tr.fraction == 1.0f || tr.startsolid || tr.allsolid) + return false; + + VectorCopy(tr.endpos, spot); + return G_IsValidLocation(self, spot, mins, maxs); +} + +static qboolean widow2_find_spawn_spot(edict_t *self, int index, vec3_t spot) +{ + vec3_t mins, maxs; + vec3_t forward, right; + float side; + + VectorSet(mins, -28, -28, -18); + VectorSet(maxs, 28, 28, 18); + AngleVectors(self->s.angles, forward, right, NULL); + + side = (index & 1) ? 112.0f : -112.0f; + VectorCopy(self->s.origin, spot); + VectorMA(spot, 144.0f, forward, spot); + VectorMA(spot, side, right, spot); + if (widow2_valid_spawn_spot(self, mins, maxs, spot)) + return true; + + VectorCopy(self->s.origin, spot); + VectorMA(spot, -120.0f, forward, spot); + VectorMA(spot, side, right, spot); + return widow2_valid_spawn_spot(self, mins, maxs, spot); +} + +static void widow2_cleanup_failed_spawn(edict_t *owner, edict_t *spawned) +{ + if (owner && owner->client) + layout_remove_tracked_entity(&owner->client->layout, spawned); + + DroneList_Remove(spawned); + AI_EnemyRemoved(spawned); + G_FreeEdict(spawned); +} + +static void widow2_setup_invasion_spawn(edict_t *spawned) +{ + if (!invasion->value) + return; + + spawned->monsterinfo.aiflags &= ~AI_STAND_GROUND; + spawned->monsterinfo.aiflags |= AI_FIND_NAVI; + spawned->prev_navi = NULL; + spawned->goalentity = NULL; +} + +static void widow2_start_spawned_monster(edict_t *self, edict_t *spawned) +{ + const qboolean force_start = invasion->value || pvm->value; + + if (G_ValidTarget(spawned, self->enemy, !force_start, true)) + { + spawned->enemy = self->enemy; + VectorCopy(self->enemy->s.origin, spawned->monsterinfo.last_sighting); + } + else if (force_start) + drone_findtarget(spawned, true); + + if ((spawned->enemy || spawned->goalentity) && spawned->monsterinfo.run) + spawned->monsterinfo.run(spawned); + else if (spawned->monsterinfo.stand) + spawned->monsterinfo.stand(spawned); +} + +static qboolean widow2_spawn_stalker(edict_t *self, int index) +{ + edict_t *owner; + edict_t *spawned; + vec3_t spot; + + owner = (self->activator && self->activator->inuse) ? self->activator : self; + spawned = G_Spawn(); + spawned->mtype = M_STALKER; + spawned->activator = owner; + spawned->monsterinfo.level = self->monsterinfo.level; + + if (!M_Initialize(owner, spawned, 0.0f)) + { + G_FreeEdict(spawned); + return false; + } + + if (!widow2_find_spawn_spot(self, index, spot)) + { + widow2_cleanup_failed_spawn(owner, spawned); + return false; + } + + spawned->monsterinfo.cost = 0; + spawned->s.effects |= EF_PLASMA; + VectorCopy(spot, spawned->s.origin); + VectorCopy(spot, spawned->s.old_origin); + VectorCopy(self->s.angles, spawned->s.angles); + spawned->nextthink = level.time + FRAMETIME; + spawned->monsterinfo.attack_finished = level.time + 1.0f; + widow2_setup_invasion_spawn(spawned); + + gi.linkentity(spawned); + owner->num_monsters += spawned->monsterinfo.control_cost; + owner->num_monsters_real++; + + widow2_start_spawned_monster(self, spawned); + + return true; +} + +static void widow2_spawn_effects(edict_t *self) +{ + vec3_t mins, maxs, size, spot, effect_origin; + float radius; + + VectorSet(mins, -28, -28, -18); + VectorSet(maxs, 28, 28, 18); + VectorSubtract(maxs, mins, size); + radius = VectorLength(size) * 0.5f; + + for (int i = 0; i < WIDOW2_SUMMON_COUNT; i++) + { + if (!widow2_find_spawn_spot(self, i, spot)) + continue; + + VectorAdd(mins, maxs, effect_origin); + VectorAdd(spot, effect_origin, effect_origin); + SpawnGrow_Spawn(effect_origin, radius, radius * 2.0f); + } +} + +static void widow2_finish_spawn(edict_t *self) +{ + int spawned = 0; + + for (int i = 0; i < WIDOW2_SUMMON_COUNT; i++) + { + if (widow2_spawn_stalker(self, i)) + spawned++; + } + + if (spawned) + self->monsterinfo.melee_finished = level.time + WIDOW2_SUMMON_COOLDOWN; +} + +static qboolean widow2_can_spawn_stalker(edict_t *self) +{ + vec3_t spot; + + for (int i = 0; i < WIDOW2_SUMMON_COUNT; i++) + { + if (widow2_find_spawn_spot(self, i, spot)) + return true; + } + + return false; +} + +static void widow2_start_spawn(edict_t *self) +{ + gi.sound(self, CHAN_WEAPON, sound_spawn, 1, ATTN_NORM, 0); + self->monsterinfo.currentmove = &widow2_move_spawn; +} + +static void widow2_melee(edict_t *self) +{ + if (!widow2_can_melee(self)) + { + self->monsterinfo.melee_finished = level.time + 0.5f; + return; + } + + self->monsterinfo.currentmove = &widow2_move_tongs; + self->monsterinfo.melee_finished = level.time + 1.5f; + M_DelayNextAttack(self, 1.0f, true); +} + +static void widow2_attack(edict_t *self) +{ + float r; + + if (!G_EntExists(self->enemy)) + return; + + r = random(); + if (level.time >= self->monsterinfo.melee_finished && widow2_can_spawn_stalker(self)) + widow2_start_spawn(self); + else if (widow2_can_melee(self) && r < 0.35f) + widow2_melee(self); + else if (r < 0.80f) + self->monsterinfo.currentmove = &widow2_move_disruptor; + else + self->monsterinfo.currentmove = &widow2_move_pre_beam; + + M_DelayNextAttack(self, 1.0f + random(), true); +} + +static void widow2_pain(edict_t *self, edict_t *other, float kick, int damage) +{ + if (self->health < (self->max_health / 2)) + self->s.skinnum = 1; + + if (level.time < self->pain_debounce_time) + return; + + self->pain_debounce_time = level.time + 3.0f; + if (random() < 0.33f) + gi.sound(self, CHAN_VOICE, sound_pain1, 1, ATTN_NORM, 0); + else if (random() < 0.5f) + gi.sound(self, CHAN_VOICE, sound_pain2, 1, ATTN_NORM, 0); + else + gi.sound(self, CHAN_VOICE, sound_pain3, 1, ATTN_NORM, 0); + + if (invasion->value != 2) + self->monsterinfo.currentmove = &widow2_move_pain; +} + +static void widow2_project_source2(vec3_t origin, vec3_t offset, vec3_t forward, vec3_t right, vec3_t up, vec3_t result) +{ + result[0] = origin[0] + forward[0] * offset[0] + right[0] * offset[1] + up[0] * offset[2]; + result[1] = origin[1] + forward[1] * offset[0] + right[1] * offset[1] + up[1] * offset[2]; + result[2] = origin[2] + forward[2] * offset[0] + right[2] * offset[1] + up[2] * offset[2]; +} + +static void widow2_throw_gib_at(edict_t *self, char *gibname, int damage, int type, vec3_t point) +{ + edict_t *gib; + float scale; + + if (nolag->value || !vrx_spawn_nonessential_ent(point)) + return; + + scale = self->s.scale ? self->s.scale : 1.0f; + gib = ThrowGibEx(self, gibname, damage, type, scale); + if (!gib) + return; + + VectorCopy(point, gib->s.origin); + VectorCopy(point, gib->s.old_origin); + gi.linkentity(gib); +} + +static void widow2_explode_at(edict_t *self, vec3_t offset, int effect, vec3_t point) +{ + vec3_t forward, right, up; + + AngleVectors(self->s.angles, forward, right, up); + widow2_project_source2(self->s.origin, offset, forward, right, up, point); + + gi.WriteByte(svc_temp_entity); + gi.WriteByte(effect); + gi.WritePosition(point); + gi.multicast(self->s.origin, MULTICAST_ALL); +} + +static void widow2_throw_loose_gibs(edict_t *self, vec3_t point) +{ + widow2_throw_gib_at(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_ORGANIC, point); + widow2_throw_gib_at(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, point); + widow2_throw_gib_at(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, point); + widow2_throw_gib_at(self, "models/objects/gibs/sm_metal/tris.md2", 300, GIB_METALLIC, point); +} + +static void widow2_explosion(edict_t *self, float x, float y, float z) +{ + vec3_t offset; + vec3_t point; + + VectorSet(offset, x, y, z); + widow2_explode_at(self, offset, TE_EXPLOSION1, point); + widow2_throw_loose_gibs(self, point); +} + +static void widow2_explosion1(edict_t *self) +{ + widow2_explosion(self, 23.74f, -37.67f, 76.96f); +} + +static void widow2_explosion2(edict_t *self) +{ + widow2_explosion(self, -20.49f, 36.92f, 73.52f); +} + +static void widow2_explosion3(edict_t *self) +{ + widow2_explosion(self, 2.11f, 0.05f, 92.20f); +} + +static void widow2_explosion4(edict_t *self) +{ + widow2_explosion(self, -28.04f, -35.57f, -77.56f); +} + +static void widow2_explosion5(edict_t *self) +{ + widow2_explosion(self, -20.11f, -1.11f, 40.76f); +} + +static void widow2_explosion6(edict_t *self) +{ + widow2_explosion(self, -20.11f, -1.11f, 40.76f); +} + +static void widow2_explosion7(edict_t *self) +{ + widow2_explosion(self, -20.11f, -1.11f, 40.76f); +} + +static void widow2_explosion_leg(edict_t *self) +{ + vec3_t offset; + vec3_t point; + + VectorSet(offset, -31.89f, -47.86f, 67.02f); + widow2_explode_at(self, offset, TE_EXPLOSION1_BIG, point); + widow2_throw_gib_at(self, "models/monsters/blackwidow2/gib2/tris.md2", 200, GIB_METALLIC | GIB_UPRIGHT, point); + widow2_throw_gib_at(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_ORGANIC, point); + widow2_throw_gib_at(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, point); + + VectorSet(offset, -44.9f, -82.14f, 54.72f); + widow2_explode_at(self, offset, TE_EXPLOSION1, point); + widow2_throw_gib_at(self, "models/monsters/blackwidow2/gib1/tris.md2", 300, GIB_METALLIC | GIB_UPRIGHT, point); + widow2_throw_gib_at(self, "models/objects/gibs/sm_meat/tris.md2", 300, GIB_ORGANIC, point); + widow2_throw_gib_at(self, "models/objects/gibs/sm_metal/tris.md2", 100, GIB_METALLIC, point); +} + +static void widow2_dead(edict_t *self) +{ + vrx_throw_drone_gibs(self, 500); + self->s.sound = 0; + self->count = 0; + self->solid = SOLID_NOT; + self->takedamage = DAMAGE_NO; + gi.linkentity(self); + self->monsterinfo.currentmove = &widow2_move_dead; +} + +static void widow2_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point) +{ + M_Notify(self); + + if (self->deadflag == DEAD_DEAD) + return; + + DroneList_Remove(self); + gi.sound(self, CHAN_VOICE, sound_death, 1, ATTN_NORM, 0); + self->deadflag = DEAD_DEAD; + self->takedamage = DAMAGE_YES; + vrx_update_drone_death_skin(self); + self->monsterinfo.currentmove = &widow2_move_death; +} + +void init_drone_widow2(edict_t *self) +{ + sound_pain1 = gi.soundindex("widow/bw2pain1.wav"); + sound_pain2 = gi.soundindex("widow/bw2pain2.wav"); + sound_pain3 = gi.soundindex("widow/bw2pain3.wav"); + sound_death = gi.soundindex("widow/death.wav"); + sound_search = gi.soundindex("bosshovr/bhvunqv1.wav"); + sound_tongue = gi.soundindex("brain/brnatck3.wav"); + sound_step = gi.soundindex("widow/bwstep1.wav"); + gi.soundindex("weapons/disrupt.wav"); + sound_beam = gi.soundindex("weapons/disint2.wav"); + sound_hit = gi.soundindex("infantry/melee2.wav"); + sound_spawn = gi.soundindex("medic_commander/monsterspawn1.wav"); + + self->movetype = MOVETYPE_STEP; + self->solid = SOLID_BBOX; + self->s.modelindex = gi.modelindex("models/monsters/blackwidow2/tris.md2"); + VectorSet(self->mins, -70, -70, 0); + VectorSet(self->maxs, 70, 70, 144); + if (invasion->value) + { + self->s.scale = WIDOW2_INVASION_SCALE; + VectorSet(self->mins, -40, -40, 0); + VectorSet(self->maxs, 40, 40, 82); + } + + gi.modelindex("models/items/spawngro3/tris.md2"); + gi.modelindex("models/monsters/stalker/tris.md2"); + gi.modelindex("models/objects/gibs/sm_metal/tris.md2"); + gi.modelindex("models/monsters/blackwidow2/gib1/tris.md2"); + gi.modelindex("models/monsters/blackwidow2/gib2/tris.md2"); + gi.modelindex("models/monsters/blackwidow2/gib3/tris.md2"); + gi.modelindex("models/monsters/blackwidow2/gib4/tris.md2"); + + self->health = M_WIDOW2_INITIAL_HEALTH + M_WIDOW2_ADDON_HEALTH * self->monsterinfo.level; + self->max_health = self->health; + self->gib_health = -5000; + self->mass = 2500; + self->mtype = M_WIDOW2; + self->yaw_speed = 30; + self->flags |= FL_IMMUNE_LASER; + + self->monsterinfo.power_armor_type = POWER_ARMOR_SHIELD; + self->monsterinfo.power_armor_power = M_WIDOW2_INITIAL_ARMOR + M_WIDOW2_ADDON_ARMOR * self->monsterinfo.level; + self->monsterinfo.max_armor = self->monsterinfo.power_armor_power; + self->monsterinfo.control_cost = M_JORG_CONTROL_COST; + self->monsterinfo.cost = M_COMMANDER_COST; + self->monsterinfo.jumpup = 64; + self->monsterinfo.jumpdn = 512; + self->monsterinfo.aiflags |= AI_NO_CIRCLE_STRAFE; + + self->pain = widow2_pain; + self->die = widow2_die; + self->monsterinfo.stand = widow2_stand; + self->monsterinfo.walk = widow2_walk; + self->monsterinfo.run = widow2_run; + self->monsterinfo.attack = widow2_attack; + self->monsterinfo.melee = widow2_melee; + self->monsterinfo.sight = widow2_sight; + self->monsterinfo.idle = widow2_search; + + gi.linkentity(self); + + self->monsterinfo.currentmove = &widow2_move_stand; + self->monsterinfo.scale = 2.0f; + self->nextthink = level.time + FRAMETIME; + + if (!invasion->value) + G_PrintGreenText(va("A level %d widow2 has spawned!", self->monsterinfo.level)); +} diff --git a/src/entities/drone/g_monster.c b/src/entities/drone/g_monster.c index 840c41fd..10d5077e 100644 --- a/src/entities/drone/g_monster.c +++ b/src/entities/drone/g_monster.c @@ -19,6 +19,27 @@ float vrx_increase_monster_damage_by_talent(edict_t *owner, float damage) // monster weapons // +static void monster_muzzleflash(edict_t *self, vec3_t start, int flashtype) +{ + if (flashtype < 0) + return; + + if (flashtype <= 255) + { + gi.WriteByte(svc_muzzleflash2); + gi.WriteShort(self - g_edicts); + gi.WriteByte(flashtype); + } + else + { + gi.WriteByte(svc_muzzleflash3); + gi.WriteShort(self - g_edicts); + gi.WriteShort(flashtype); + } + + gi.multicast(start, MULTICAST_PVS); +} + //FIXME mosnters should call these with a totally accurate direction // and we can mess it up based on skill. Spread should be for normal // and we can tighten or loosen based on skill. We could muck with @@ -45,11 +66,8 @@ void monster_fire_bullet (edict_t *self, vec3_t start, vec3_t dir, int damage, i damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_bullet (self, start, dir, damage, kick, hspread, vspread, MOD_UNKNOWN); - gi.WriteByte (svc_muzzleflash2); - gi.WriteShort (self - g_edicts); - gi.WriteByte (flashtype); - gi.multicast (start, MULTICAST_PVS); -} + monster_muzzleflash(self, start, flashtype); +} static void debris_die(edict_t* self, edict_t* inflictor, edict_t* attacker, int damage, vec3_t point) { @@ -95,6 +113,8 @@ void monster_fire_20mm(edict_t* self, vec3_t start, vec3_t dir, int damage, int { float chance; + (void)flashtype; + // holy freeze reduces firing rate by 50% if (que_typeexists(self->curses, AURA_HOLYFREEZE)) { @@ -113,12 +133,7 @@ void monster_fire_20mm(edict_t* self, vec3_t start, vec3_t dir, int damage, int damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_20mm(self, start, dir, damage, kick, range); - gi.WriteByte(svc_muzzleflash); - gi.WriteShort(self - g_edicts); - gi.WriteByte(flashtype); - gi.multicast(start, MULTICAST_PVS); - - gi.sound(self, CHAN_WEAPON, gi.soundindex("weapons/sgun1.wav"), 1, ATTN_NORM, 0); + gi.positioned_sound(start, self, CHAN_WEAPON, gi.soundindex("weapons/sgun1.wav"), 1, ATTN_NORM, 0); if (vrx_spawn_nonessential_ent(self->s.origin)) ThrowShell(self, "models/objects/shell1/tris.md2", start); @@ -146,11 +161,8 @@ void monster_fire_shotgun (edict_t *self, vec3_t start, vec3_t aimdir, float dam damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_shotgun (self, start, aimdir, damage, kick, hspread, vspread, count, MOD_UNKNOWN); - gi.WriteByte (svc_muzzleflash2); - gi.WriteShort (self - g_edicts); - gi.WriteByte (flashtype); - gi.multicast (start, MULTICAST_PVS); -} + monster_muzzleflash(self, start, flashtype); +} void monster_fire_blaster (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int proj_type, float duration, qboolean bounce, int flashtype) { @@ -181,12 +193,393 @@ void monster_fire_blaster (edict_t *self, vec3_t start, vec3_t dir, int damage, damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_blaster(self, start, dir, damage, speed, effect, proj_type, mod, duration, bounce); - gi.WriteByte (svc_muzzleflash2); - gi.WriteShort (self - g_edicts); - gi.WriteByte (flashtype); - gi.multicast (start, MULTICAST_PVS); + monster_muzzleflash(self, start, flashtype); } +void monster_fire_blaster2(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int flashtype) +{ + float chance; + + if (que_typeexists(self->curses, AURA_HOLYFREEZE) && random() <= 0.5) + return; + + if (self->chill_time > level.time) + { + chance = 1 / (1 + CHILL_DEFAULT_BASE + CHILL_DEFAULT_ADDON * self->chill_level); + if (random() > chance) + return; + } + + damage = vrx_increase_monster_damage_by_talent(self->activator, damage); + fire_blaster2(self, start, dir, damage, speed, effect, false); + + monster_muzzleflash(self, start, flashtype); +} + +void monster_fire_blueblaster(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int flashtype) +{ + float chance; + + if (que_typeexists(self->curses, AURA_HOLYFREEZE) && random() <= 0.5) + return; + + if (self->chill_time > level.time) + { + chance = 1 / (1 + CHILL_DEFAULT_BASE + CHILL_DEFAULT_ADDON * self->chill_level); + if (random() > chance) + return; + } + + damage = vrx_increase_monster_damage_by_talent(self->activator, damage); + fire_blueblaster(self, start, dir, damage, speed, effect); + + monster_muzzleflash(self, start, flashtype); +} + +void monster_fire_ionripper(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int flashtype) +{ + float chance; + + if (que_typeexists(self->curses, AURA_HOLYFREEZE) && random() <= 0.5) + return; + + if (self->chill_time > level.time) + { + chance = 1 / (1 + CHILL_DEFAULT_BASE + CHILL_DEFAULT_ADDON * self->chill_level); + if (random() > chance) + return; + } + + damage = vrx_increase_monster_damage_by_talent(self->activator, damage); + fire_ionripper(self, start, dir, damage, speed, effect); + + monster_muzzleflash(self, start, flashtype); +} + +static void dabeam_think(edict_t *self) +{ + if (self->owner && self->owner->inuse) + { + if ((self->spawnflags & DABEAM_SECONDARY) && self->owner->beam2 == self) + self->owner->beam2 = NULL; + else if (!(self->spawnflags & DABEAM_SECONDARY) && self->owner->beam == self) + self->owner->beam = NULL; + } + + G_FreeEdict(self); +} + +void dabeam_update(edict_t *self, qboolean damage) +{ + vec3_t start, end; + trace_t tr; + + if (!self->owner || !self->owner->inuse || !G_EntExists(self->owner->enemy)) + return; + + VectorCopy(self->s.origin, start); + VectorMA(start, 8192, self->movedir, end); + tr = gi.trace(start, NULL, NULL, end, self->owner, MASK_SHOT); + + if (damage && tr.ent && tr.ent->takedamage && !OnSameTeam(self->owner, tr.ent)) + T_Damage(tr.ent, self->owner, self->owner, self->movedir, tr.endpos, tr.plane.normal, self->dmg, self->dmg, + DAMAGE_ENERGY, MOD_BFG_LASER); + + VectorCopy(tr.endpos, self->s.old_origin); + VectorCopy(start, self->pos1); + VectorCopy(tr.endpos, self->pos2); + gi.linkentity(self); +} + +void monster_fire_dabeam(edict_t *self, int damage, qboolean secondary, void (*update_func)(edict_t *self)) +{ + vec3_t forward, right, start, end, offset; + trace_t tr; + edict_t **beam_slot; + edict_t *beam; + int flashtype; + float chance; + + if (!G_EntExists(self->enemy)) + return; + + if (que_typeexists(self->curses, AURA_HOLYFREEZE) && random() <= 0.5) + return; + + if (self->chill_time > level.time) + { + chance = 1 / (1 + CHILL_DEFAULT_BASE + CHILL_DEFAULT_ADDON * self->chill_level); + if (random() > chance) + return; + } + + if (self->mtype == M_GUARDIAN) + flashtype = -1; + else if (self->mtype == M_MINIGUARDIAN) + flashtype = MZ2_SOLDIER_RIPPER_8; + else + flashtype = MZ2_SOLDIER_HYPERGUN_8; + + damage = vrx_increase_monster_damage_by_talent(self->activator, damage); + + if (update_func) + { + beam_slot = secondary ? &self->beam2 : &self->beam; + beam = *beam_slot; + if (!beam || !beam->inuse) + { + beam = G_Spawn(); + if (!beam) + return; + *beam_slot = beam; + beam->movetype = MOVETYPE_NONE; + beam->solid = SOLID_NOT; + beam->s.renderfx = RF_BEAM | RF_TRANSLUCENT; + beam->s.modelindex = 1; + beam->s.frame = 2; + beam->s.skinnum = 0xf2f2f0f0; // red + beam->owner = self; + beam->classname = "monster_dabeam"; + beam->think = dabeam_think; + beam->s.sound = gi.soundindex("misc/lasfly.wav"); + beam->spawnflags = secondary ? DABEAM_SECONDARY : 0; + } + + beam->dmg = damage; + beam->prethink = update_func; + beam->nextthink = level.time + 0.2; + beam->spawnflags &= ~DABEAM_SPAWNED; + update_func(beam); + + beam = *beam_slot; + if (!beam || !beam->inuse) + return; + + if (!(beam->spawnflags & DABEAM_SPAWNED)) + { + dabeam_update(beam, true); + beam->spawnflags |= DABEAM_SPAWNED; + } + + return; + } + + if (self->mtype == M_GUARDIAN) + { + AngleVectors(self->s.angles, forward, right, NULL); + if (secondary) + VectorSet(offset, 112, -62, 60); + else + VectorSet(offset, 125, -70, 60); + if (self->s.scale && self->s.scale != 1.0f) + VectorScale(offset, self->s.scale, offset); + G_ProjectSource(self->s.origin, offset, forward, right, start); + MonsterAim(self, M_HITSCAN_CONT_ACC, 0, false, -1, forward, start); + } + else + { + MonsterAim(self, M_HITSCAN_CONT_ACC, 0, false, flashtype, forward, start); + } + + VectorMA(start, 8192, forward, end); + tr = gi.trace(start, NULL, NULL, end, self, MASK_SHOT); + + if (tr.ent && tr.ent->takedamage && !OnSameTeam(self, tr.ent)) + T_Damage(tr.ent, self, self, forward, tr.endpos, tr.plane.normal, damage, damage, DAMAGE_ENERGY, MOD_BFG_LASER); + + beam_slot = secondary ? &self->beam2 : &self->beam; + beam = *beam_slot; + if (!beam || !beam->inuse) + { + beam = G_Spawn(); + if (!beam) + return; + *beam_slot = beam; + beam->movetype = MOVETYPE_NONE; + beam->solid = SOLID_NOT; + beam->s.renderfx = RF_BEAM | RF_TRANSLUCENT; + beam->s.modelindex = 1; + beam->s.frame = 2; + beam->s.skinnum = 0xf2f2f0f0; // red + beam->owner = self; + beam->classname = "monster_dabeam"; + beam->think = dabeam_think; + beam->prethink = NULL; + beam->s.sound = gi.soundindex("misc/lasfly.wav"); + beam->spawnflags = secondary ? DABEAM_SECONDARY : 0; + } + + beam->dmg = damage; + beam->prethink = NULL; + beam->nextthink = level.time + 0.2; + VectorCopy(start, beam->s.origin); + VectorCopy(tr.endpos, beam->s.old_origin); + VectorCopy(start, beam->pos1); + VectorCopy(tr.endpos, beam->pos2); + gi.linkentity(beam); + + monster_muzzleflash(self, start, flashtype); +} + +void rocket_touch(edict_t *ent, edict_t *other, cplane_t *plane, csurface_t *surf); + +static qboolean heat_valid_target(edict_t *self, edict_t *target) +{ + if (target == self->owner) + return false; + if (!G_ValidTargetEnt(self->owner, target, true)) + return false; + if (self->owner && OnSameTeam(self->owner, target)) + return false; + if (!visible(self, target)) + return false; + return true; +} + +static void heat_think(edict_t *self) +{ + edict_t *target = NULL; + edict_t *acquire = NULL; + float best_dot = -1.0f; + float best_dist = 0.0f; + float turn_fraction; + float dot, dist; + vec3_t forward, dir; + + if (!G_EntExists(self->owner) || level.time >= self->delay) + { + BecomeExplosion1(self); + return; + } + + AngleVectors(self->s.angles, forward, NULL, NULL); + + if (heat_valid_target(self, self->enemy)) + acquire = self->enemy; + else + { + self->enemy = NULL; + } + + if (!acquire) + { + while ((target = findradius(target, self->s.origin, 1024)) != NULL) + { + if (!heat_valid_target(self, target)) + continue; + + VectorSubtract(target->s.origin, self->s.origin, dir); + dist = VectorNormalize(dir); + dot = DotProduct(dir, forward); + + if (dot <= best_dot) + continue; + if (!acquire || dot > best_dot || dist < best_dist) + { + acquire = target; + best_dot = dot; + best_dist = dist; + } + } + } + + if (acquire) + { + VectorSubtract(acquire->s.origin, self->s.origin, dir); + if (!VectorNormalize(dir)) + { + self->nextthink = level.time + FRAMETIME; + return; + } + + turn_fraction = self->accel; + if (turn_fraction < 0) + turn_fraction = 0; + else if (turn_fraction > 1) + turn_fraction = 1; + + VectorScale(self->movedir, 1.0f - turn_fraction, self->movedir); + VectorMA(self->movedir, turn_fraction, dir, self->movedir); + VectorNormalize(self->movedir); + vectoangles(self->movedir, self->s.angles); + self->enemy = acquire; + } + else + { + self->enemy = NULL; + } + + VectorScale(self->movedir, self->speed, self->velocity); + self->nextthink = level.time + FRAMETIME; +} + +qboolean monster_fire_heat(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int flashtype, float turn_fraction) +{ + float chance; + float radius; + edict_t *heat; + + if (que_typeexists(self->curses, AURA_HOLYFREEZE) && random() <= 0.5) + return false; + + if (self->chill_time > level.time) + { + chance = 1 / (1 + CHILL_DEFAULT_BASE + CHILL_DEFAULT_ADDON * self->chill_level); + if (random() > chance) + return false; + } + + radius = damage; + if (damage > 125) + radius = 125; + if (speed > 1000) + speed = 1000; + + damage = vrx_increase_monster_damage_by_talent(self->activator, damage); + if (speed < 1) + speed = 1; + self->lastsound = level.framenum; + VectorNormalize(dir); + + heat = G_Spawn(); + VectorCopy(start, heat->s.origin); + VectorCopy(start, heat->s.old_origin); + VectorCopy(dir, heat->movedir); + vectoangles(dir, heat->s.angles); + VectorScale(dir, speed, heat->velocity); + heat->movetype = MOVETYPE_FLYMISSILE; + heat->clipmask = MASK_SHOT; + heat->solid = SOLID_BBOX; + heat->s.effects |= EF_ROCKET; + VectorClear(heat->mins); + VectorClear(heat->maxs); + heat->s.modelindex = gi.modelindex("models/objects/rocket/tris.md2"); + heat->owner = self; + heat->touch = rocket_touch; + heat->speed = speed; + heat->accel = turn_fraction; + heat->delay = level.time + 8000 * (sv_fps->value) / speed; + heat->nextthink = level.time + FRAMETIME; + heat->think = heat_think; + heat->dmg = damage; + heat->radius_dmg = damage; + heat->dmg_radius = radius; + heat->s.sound = gi.soundindex("weapons/rockfly.wav"); + heat->classname = "heat rocket"; + + if (G_ValidTarget(self, self->enemy, true, true)) + heat->enemy = self->enemy; + + gi.linkentity(heat); + + if (self->client) + check_dodge(self, heat->s.origin, dir, speed, damage); + + monster_muzzleflash(self, start, flashtype); + + return true; +} + void monster_fire_grenade (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, int flashtype) { float radius, chance; @@ -219,10 +612,7 @@ void monster_fire_grenade (edict_t *self, vec3_t start, vec3_t aimdir, int damag damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_grenade (self, start, aimdir, damage, speed, 2.5, radius, damage); - gi.WriteByte (svc_muzzleflash2); - gi.WriteShort (self - g_edicts); - gi.WriteByte (flashtype); - gi.multicast (start, MULTICAST_PVS); + monster_muzzleflash(self, start, flashtype); } void monster_fire_rocket (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int flashtype) @@ -257,10 +647,7 @@ void monster_fire_rocket (edict_t *self, vec3_t start, vec3_t dir, int damage, i damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_rocket (self, start, dir, damage, speed, radius, damage); - gi.WriteByte (svc_muzzleflash2); - gi.WriteShort (self - g_edicts); - gi.WriteByte (flashtype); - gi.multicast (start, MULTICAST_PVS); + monster_muzzleflash(self, start, flashtype); } void monster_fire_railgun (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int kick, int flashtype) @@ -285,10 +672,7 @@ void monster_fire_railgun (edict_t *self, vec3_t start, vec3_t aimdir, int damag damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_rail (self, start, aimdir, damage, kick); - gi.WriteByte (svc_muzzleflash2); - gi.WriteShort (self - g_edicts); - gi.WriteByte (flashtype); - gi.multicast (start, MULTICAST_PVS); + monster_muzzleflash(self, start, flashtype); } void monster_fire_bfg (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, int kick, float damage_radius, int flashtype) @@ -313,10 +697,7 @@ void monster_fire_bfg (edict_t *self, vec3_t start, vec3_t aimdir, int damage, i damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_bfg (self, start, aimdir, damage, speed, damage_radius); - gi.WriteByte (svc_muzzleflash2); - gi.WriteShort (self - g_edicts); - gi.WriteByte (flashtype); - gi.multicast (start, MULTICAST_PVS); + monster_muzzleflash(self, start, flashtype); } void fire_sword ( edict_t *self, vec3_t start, vec3_t aimdir, int damage, int length, int color); @@ -325,10 +706,7 @@ void monster_fire_sword (edict_t *self, vec3_t start, vec3_t aimdir, int damage, damage = vrx_increase_monster_damage_by_talent(self->activator, damage); fire_sword (self, start, aimdir, damage, kick, 0xd2d3d2d3); - gi.WriteByte (svc_muzzleflash2); - gi.WriteShort (self - g_edicts); - gi.WriteByte (flashtype); - gi.multicast (start, MULTICAST_PVS); + monster_muzzleflash(self, start, flashtype); } void monster_fire_fireball(edict_t* self) @@ -841,4 +1219,4 @@ void M_FlyCheck (edict_t *self) void AttackFinished (edict_t *self, float time) { self->monsterinfo.attack_finished = level.time + time; -} \ No newline at end of file +} diff --git a/src/entities/g_misc.c b/src/entities/g_misc.c index 924152fe..30982373 100644 --- a/src/entities/g_misc.c +++ b/src/entities/g_misc.c @@ -70,6 +70,8 @@ void ClipGibVelocity(edict_t *ent) gibs ================= */ +#define GIB_SPAWNFLAG_MONSTER 1 + void gib_think(edict_t *self) { if ( ( level.framenum % qf2sf(1) ) == 0 ) @@ -85,20 +87,12 @@ void gib_think(edict_t *self) void gib_touch(edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf) { - vec3_t normal_angles, right; - - if (!self->groundentity) - return; - - self->touch = NULL; - - if (plane) + if (plane && plane->normal[2] > 0.7f) { - gi.sound(self, CHAN_VOICE, gi.soundindex("misc/fhit3.wav"), 1, ATTN_NORM, 0); - - vectoangles(plane->normal, normal_angles); - AngleVectors(normal_angles, NULL, right, NULL); - vectoangles(right, self->s.angles); + if (!(self->spawnflags & GIB_SPAWNFLAG_MONSTER) && !(self->svflags & SVF_MONSTER)) + gi.sound(self, CHAN_VOICE, gi.soundindex("misc/fhit3.wav"), 1, ATTN_NORM, 0); + self->s.angles[0] = max(-5.0f, min(self->s.angles[0], 5.0f)); + self->s.angles[2] = max(-5.0f, min(self->s.angles[2], 5.0f)); if (self->s.modelindex == sm_meat_index) { @@ -114,7 +108,7 @@ void gib_die(edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, v G_FreeEdict(self); } -void ThrowGib(edict_t *self, char *gibname, int damage, int type) +edict_t *ThrowGibEx(edict_t *self, char *gibname, int damage, int type, float scale) { edict_t *gib; vec3_t vd = { 0, 0, 0 }; @@ -122,10 +116,8 @@ void ThrowGib(edict_t *self, char *gibname, int damage, int type) vec3_t size; float vscale; -#ifndef OLD_NOLAG_STYLE if (nolag->value) - return; -#endif + return NULL; gib = G_Spawn(); @@ -136,35 +128,74 @@ void ThrowGib(edict_t *self, char *gibname, int damage, int type) gib->s.origin[2] = origin[2] + crandom() * size[2]; gi.setmodel(gib, gibname); + gib->s.modelindex2 = 0; + if (scale != 1.0f) + gib->s.scale = scale; + VectorClear(gib->mins); + VectorClear(gib->maxs); gib->solid = SOLID_NOT; - gib->s.effects |= EF_GIB; + if (type & GIB_ACID) + gib->s.effects |= EF_GREENGIB; + else if (!(type & GIB_DEBRIS)) + gib->s.effects |= EF_GIB; gib->flags |= FL_NO_KNOCKBACK; + gib->classname = "gib"; + if (self->svflags & SVF_MONSTER) + gib->spawnflags |= GIB_SPAWNFLAG_MONSTER; gib->takedamage = DAMAGE_YES; gib->die = gib_die; + gib->s.frame = 0; + gib->s.sound = 0; + if (type & GIB_SKINNED) + gib->s.skinnum = self->s.skinnum; - if (type == GIB_ORGANIC) + if (!(type & GIB_METALLIC)) { gib->movetype = MOVETYPE_TOSS; - gib->touch = gib_touch; - vscale = 0.5; + vscale = (type & GIB_ACID) ? 3.0 : 0.5; } else { gib->movetype = MOVETYPE_BOUNCE; vscale = 1.0; } + if (type & GIB_UPRIGHT) + { + gib->touch = gib_touch; + gib->flags |= FL_ALWAYS_TOUCH; + } - VelocityForDamage(damage, vd); - VectorMA(self->velocity, vscale, vd, gib->velocity); - ClipGibVelocity(gib); + if (type & GIB_DEBRIS) + { + vd[0] = 100 * crandom(); + vd[1] = 100 * crandom(); + vd[2] = 100 + 100 * crandom(); + VectorMA(self->velocity, damage, vd, gib->velocity); + } + else + { + VelocityForDamage(damage, vd); + VectorMA(self->velocity, vscale, vd, gib->velocity); + ClipGibVelocity(gib); + } gib->avelocity[0] = random() * 600; gib->avelocity[1] = random() * 600; gib->avelocity[2] = random() * 600; + gib->s.angles[0] = random() * 359; + gib->s.angles[1] = random() * 359; + gib->s.angles[2] = random() * 359; gib->think = G_FreeEdict; gib->nextthink = level.time + GetRandom(2, 10); gi.linkentity(gib); + + return gib; +} + +void ThrowGib(edict_t *self, char *gibname, int damage, int type) +{ + ThrowGibEx(self, gibname, damage, type, 1.0f); } void ThrowHead(edict_t *self, char *gibname, int damage, int type) @@ -194,11 +225,13 @@ void ThrowHead2(edict_t *self, char *gibname, int damage, int type) self->s.effects &= ~EF_FLIES; self->s.sound = 0; self->flags |= FL_NO_KNOCKBACK; + if (self->svflags & SVF_MONSTER) + self->spawnflags |= GIB_SPAWNFLAG_MONSTER; // self->svflags &= ~SVF_MONSTER; self->takedamage = DAMAGE_YES; self->die = gib_die; - if (type == GIB_ORGANIC) + if (!(type & GIB_METALLIC)) { self->movetype = MOVETYPE_TOSS; self->touch = gib_touch; diff --git a/src/entities/g_spawn.c b/src/entities/g_spawn.c index e28e3c86..7b30d409 100644 --- a/src/entities/g_spawn.c +++ b/src/entities/g_spawn.c @@ -104,6 +104,8 @@ void SP_monster_infantry (edict_t *self); void SP_monster_soldier_light (edict_t *self); void SP_monster_soldier (edict_t *self); void SP_monster_soldier_ss (edict_t *self); +void SP_monster_soldier_blueblaster (edict_t *self); +void SP_monster_soldier_laser (edict_t *self); void SP_monster_tank (edict_t *self); void SP_monster_medic (edict_t *self); void SP_monster_flipper (edict_t *self); @@ -131,6 +133,9 @@ void SP_turret_driver (edict_t *self); void SP_monster_soldier_hypergun (edict_t *self); void SP_monster_soldier_lasergun (edict_t *self); void SP_monster_soldier_ripper (edict_t *self); +void SP_monster_guardian (edict_t *self); +void SP_monster_janitor (edict_t *self); +void SP_monster_miniguardian (edict_t *self); void SP_monster_fixbot (edict_t *self); void SP_monster_gekk (edict_t *self); void SP_monster_chick_heat (edict_t *self); @@ -272,10 +277,19 @@ spawn_t spawns[] = { {"monster_gladiator", SP_monster_gladiator}, {"monster_gunner", SP_monster_gunner}, {"monster_soldier", SP_monster_soldier}, + {"monster_soldier_ripper", SP_monster_soldier_ripper}, + {"monster_soldier_blueblaster", SP_monster_soldier_blueblaster}, + {"monster_soldier_hypergun", SP_monster_soldier_blueblaster}, + {"monster_soldier_laser", SP_monster_soldier_laser}, + {"monster_soldier_lasergun", SP_monster_soldier_laser}, + {"monster_janitor", SP_monster_janitor}, + {"monster_miniguardian", SP_monster_miniguardian}, + {"monster_boss5", SP_monster_boss5}, {"monster_tank", SP_monster_tank}, {"monster_tank_commander", SP_monster_tank_commander}, {"monster_medic", SP_monster_medic}, {"monster_chick", SP_monster_chick}, + {"monster_chick_heat", SP_monster_chick_heat}, {"monster_parasite", SP_monster_parasite}, {"monster_brain", SP_monster_brain}, {"monster_mutant", SP_monster_mutant}, @@ -1079,7 +1093,58 @@ void SP_monster_gunner(edict_t *ent) void SP_monster_soldier(edict_t *ent) { if (coop->value) - vrx_create_drone_from_ent(ent, g_edicts, 10, true, true, 0); + vrx_create_drone_from_ent(ent, g_edicts, DS_SOLDIER, true, true, 0); +} + +void SP_monster_soldier_ripper(edict_t *ent) +{ + if (coop->value) + vrx_create_drone_from_ent(ent, g_edicts, DS_SOLDIER_RIPPER, true, true, 0); +} + +void SP_monster_soldier_blueblaster(edict_t *ent) +{ + if (coop->value) + vrx_create_drone_from_ent(ent, g_edicts, DS_SOLDIER_BLUEBLASTER, true, true, 0); +} + +void SP_monster_soldier_hypergun(edict_t *ent) +{ + SP_monster_soldier_blueblaster(ent); +} + +void SP_monster_soldier_laser(edict_t *ent) +{ + if (coop->value) + vrx_create_drone_from_ent(ent, g_edicts, DS_SOLDIER_LASER, true, true, 0); +} + +void SP_monster_soldier_lasergun(edict_t *ent) +{ + SP_monster_soldier_laser(ent); +} + +void SP_monster_guardian(edict_t *ent) +{ + G_FreeEdict(ent); +} + +void SP_monster_janitor(edict_t *ent) +{ + if (coop->value) + vrx_create_drone_from_ent(ent, g_edicts, DS_JANITOR, true, true, 0); +} + +void SP_monster_miniguardian(edict_t *ent) +{ + if (coop->value) + vrx_create_drone_from_ent(ent, g_edicts, DS_MINIGUARDIAN, true, true, 0); +} + +void SP_monster_boss5(edict_t *ent) +{ + if (coop->value) + vrx_create_drone_from_ent(ent, g_edicts, DS_BOSS5, true, true, 0); } void SP_monster_tank(edict_t *ent) @@ -1115,7 +1180,13 @@ void SP_monster_mutant(edict_t *ent) void SP_monster_chick(edict_t *ent) { if (coop->value) - vrx_create_drone_from_ent(ent, g_edicts, 3, true, true, 0); + vrx_create_drone_from_ent(ent, g_edicts, DS_BITCH, true, true, 0); +} + +void SP_monster_chick_heat(edict_t *ent) +{ + if (coop->value) + vrx_create_drone_from_ent(ent, g_edicts, DS_BITCH_HEAT, true, true, 0); } void SP_monster_parasite(edict_t *ent) @@ -1339,6 +1410,7 @@ void SP_worldspawn (edict_t *ent) gi.modelindex ("models/objects/gibs/chest/tris.md2"); skullindex = gi.modelindex ("models/objects/gibs/skull/tris.md2"); headindex = gi.modelindex ("models/objects/gibs/head2/tris.md2"); + vrx_precache_drone_gibs(); gi.modelindex("models/proj/beam/tris.md2"); // 3.7 heatbeam model //gi.modelindex("models/proj/lightning/tris.md2"); //3.9 lightning model used by hellspawn/holyshock diff --git a/src/g_local.h b/src/g_local.h index 586e4bcd..3473cec8 100644 --- a/src/g_local.h +++ b/src/g_local.h @@ -165,7 +165,8 @@ enum flags_t { FL_UNDEAD = 1 << 23, // entity cannot die (temporary death until resurrection) FL_PACKANIMAL = 1 << 24, // pack animal: use num_packanimals to track active quantity FL_RESPAWN = 1 << 25, // used for item respawning - FL_FLASHLIGHT = 1 << 26 + FL_FLASHLIGHT = 1 << 26, + FL_ALWAYS_TOUCH = 1 << 27 }; @@ -218,6 +219,11 @@ typedef enum { //gib types #define GIB_ORGANIC 0 #define GIB_METALLIC 1 +#define GIB_SKINNED 2 +#define GIB_UPRIGHT 4 +#define GIB_HEAD 8 +#define GIB_ACID 16 +#define GIB_DEBRIS 32 //monster ai flags #define AI_STAND_GROUND 0x00000001 @@ -241,6 +247,7 @@ typedef enum { #define AI_PURSUE_PLAT_GOAL 0x00040000 #define AI_DODGE 0x00080000 #define AI_SNAP_TO_NAVI 0x00100000 +#define AI_ALTERNATE_FLY 0x00200000 //monster attack state #define AS_STRAIGHT 1 @@ -629,7 +636,7 @@ typedef struct { vec3_t last_sighting; // last known position of enemy bool last_sighting_is_navi; // is the last known position of enemy a navigation point - // int attack_state; + int attack_state; int lefty; float idle_delay; // how often idle func is called int idle_frames; // number of frames monster has been idle @@ -678,6 +685,22 @@ typedef struct { dmglist_t dmglist[MAX_CLIENTS]; // keep track of damage by players qboolean slots_freed; // true if player slots have been refunded prior to removal + // Remaster-style alternate flying mechanics. + float fly_max_distance; + float fly_min_distance; + float fly_acceleration; + float fly_speed; + vec3_t fly_ideal_position; + float fly_position_time; + qboolean fly_buzzard; + qboolean fly_above; + qboolean fly_pinned; + qboolean fly_thrusters; + float fly_recovery_time; + vec3_t fly_recovery_dir; + float fly_wall_stuck_time; + float fly_separation_time; + // az begin // targeting @@ -1419,6 +1442,18 @@ void monster_fire_shotgun(edict_t *self, vec3_t start, vec3_t aimdir, float dama void monster_fire_blaster(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int proj_type, float duration, qboolean bounce, int flashtype); +void monster_fire_blaster2(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int flashtype); + +void monster_fire_blueblaster(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int flashtype); + +void monster_fire_ionripper(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int flashtype); +#define DABEAM_SECONDARY 1 +#define DABEAM_SPAWNED 2 +void monster_fire_dabeam(edict_t *self, int damage, qboolean secondary, void (*update_func)(edict_t *self)); +void dabeam_update(edict_t *self, qboolean damage); + +qboolean monster_fire_heat(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int flashtype, float turn_fraction); + void monster_fire_grenade(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, int flashtype); void monster_fire_rocket(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int flashtype); @@ -1501,6 +1536,31 @@ enum mtype_t { M_SHAMBLER = 25, M_SKELETON = 26, M_GOLEM = 27, + M_REDMUTANT = 28, + M_RUNNERTANK = 29, + M_GUNCMDR = 30, + M_DAEDALUS = 31, + M_GLADB = 32, + M_GLADC = 33, + M_STALKER = 34, + M_GEKK = 35, + M_CHICK_HEAT = 36, + M_ARACHNID = 37, + M_MEDIC_COMMANDER = 38, + M_CARRIER = 39, + M_GUARDIAN = 40, + M_JANITOR = 41, + M_MINIGUARDIAN = 42, + M_SOLDIER_RIPPER = 43, + M_SOLDIER_BLUEBLASTER = 44, + M_SOLDIER_LASER = 45, + M_WIDOW = 46, + M_WIDOW2 = 47, + M_FIXBOT = 48, + M_FIXBOT_BOSS = 49, + M_ROGUE_TURRET = 50, + M_BOSS2_SMALL = 51, + M_BOSS5 = 52, M_MINISENTRY = 100, M_SENTRY = 101, M_BFG_SENTRY = 102, @@ -1596,18 +1656,46 @@ enum dronespawn_t { DS_FLOATER = 13, DS_HOVER = 14, DS_SHAMBLER = 15, + DS_REDMUTANT = 16, + DS_RUNNERTANK = 17, + DS_GUNCMDR = 18, + DS_DAEDALUS = 19, DS_DECOY = 20, DS_SKELETON = 21, DS_GOLEM = 22, + DS_GLADB = 23, + DS_GLADC = 24, + DS_STALKER = 25, + DS_GEKK = 26, + DS_BITCH_HEAT = 27, + DS_ARACHNID = 28, + DS_MEDIC_COMMANDER = 29, DS_COMMANDER = 30, DS_MAKRON = 31, DS_BARON_FIRE = 32, DS_SUPERTANK = 33, DS_JORG = 34, + DS_CARRIER = 35, + DS_GUARDIAN = 36, + DS_JANITOR = 37, + DS_MINIGUARDIAN = 38, + DS_SOLDIER_RIPPER = 39, + DS_SOLDIER_BLUEBLASTER = 40, + DS_SOLDIER_LASER = 41, + DS_WIDOW = 42, + DS_WIDOW2 = 43, + DS_FIXBOT = 44, + DS_FIXBOT_BOSS = 45, + DS_ROGUE_TURRET = 46, + DS_BOSS2 = 47, + DS_BOSS2_HYPER = 48, // unused boss, same attacks than small hornet + DS_BOSS2_SMALL = 49, // not a boss, small hornet + DS_BOSS5 = 50, }; edict_t *vrx_create_new_drone(edict_t *ent, enum dronespawn_t drone_type, qboolean worldspawn, qboolean link_now, int bonus_level); +qboolean vrx_drone_spawn_is_boss(enum dronespawn_t drone_type); edict_t * vrx_create_drone_from_ent(edict_t *drone, edict_t *ent, enum dronespawn_t drone_type, qboolean worldspawn, qboolean link_now, @@ -1620,6 +1708,8 @@ void ThrowHead(edict_t *self, char *gibname, int damage, int type); void ThrowClientHead(edict_t *self, int damage); +edict_t *ThrowGibEx(edict_t *self, char *gibname, int damage, int type, float scale); + void ThrowGib(edict_t *self, char *gibname, int damage, int type); void BecomeExplosion1(edict_t *self); @@ -1638,6 +1728,9 @@ int vrx_get_alive_players(void); //Apple int vrx_GetMonsterCost(int mtype); //GHz int vrx_GetMonsterControlCost(int mtype); //GHz void vrx_remove_player_summonables(edict_t *self); //GHz +void vrx_precache_drone_gibs(void); +qboolean vrx_throw_drone_gibs(edict_t *self, int damage); +void vrx_drop_tank_death_arm(edict_t *self, int damage); // // g_ai.c @@ -1677,6 +1770,12 @@ void drone_ai_run(edict_t *self, float dist); void drone_ai_run1(edict_t *self, float dist); +void drone_ai_run_slide(edict_t *self, float dist); + +void drone_ai_dodge_slide(edict_t *self, float dist); + +void drone_set_dodge_side(edict_t *self, vec3_t impact); + void drone_ai_walk(edict_t *self, float dist); // az begin @@ -1701,6 +1800,8 @@ qboolean fire_player_melee(edict_t *self, vec3_t start, vec3_t dir, int range, i extern byte is_silenced; extern qboolean is_quadfire; +void check_dodge(edict_t *self, vec3_t start, vec3_t dir, int speed, int radius); + void fire_bullet(edict_t *self, vec3_t start, vec3_t aimdir, float damage, int kick, int hspread, int vspread, int mod); void fire_shotgun(edict_t *self, vec3_t start, vec3_t aimdir, float damage, int kick, int hspread, int vspread, @@ -1708,6 +1809,7 @@ void fire_shotgun(edict_t *self, vec3_t start, vec3_t aimdir, float damage, int void fire_blaster(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, int proj_type, int mod, float duration, qboolean bounce); +void fire_blaster2(edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int effect, qboolean hyper); void fire_grenade(edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, float timer, float damage_radius, int radius_damage); @@ -1727,6 +1829,7 @@ void fire_disruptor(edict_t *self, vec3_t start, vec3_t dir, int damage, int spe void fire_ionripper (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, int effect); void fire_flechette (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, int kick); void fire_heat (edict_t *self, vec3_t start, vec3_t aimdir, vec3_t offset, int damage, int kick, qboolean monster); +void SpawnGrow_Spawn(vec3_t startpos, float start_size, float end_size); void fire_blueblaster (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, int effect); void fire_plasma (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, float damage_radius, int radius_damage); void fire_prox (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, float damage_radius); @@ -2524,6 +2627,8 @@ struct edict_s { edict_t *selected[4]; // drone selection edict_t *other; // laser effect for hw/ctf + edict_t *beam; // dabeam primary laser + edict_t *beam2; // dabeam secondary laser edict_t *supplystation; //edict_t *magmine; //GHz START @@ -2747,6 +2852,11 @@ void Check_full(edict_t *ent); void MonsterAim(edict_t *self, float accuracy, int projectile_speed, qboolean rocket, int flash_number, vec3_t forward, vec3_t start); +qboolean M_MonsterHasCombatSight(edict_t *self, edict_t *other); +qboolean M_MonsterFindClearShot(edict_t *self, vec3_t start, vec3_t point); +qboolean M_MonsterHasClearShotFrom(edict_t *self, vec3_t start); +qboolean M_MonsterHasClearShotFromFlash(edict_t *self, int flash_number); +void M_MonsterBlockedShot(edict_t *self, float delay); float entdist(const edict_t *ent1, const edict_t *ent2); diff --git a/src/gamemodes/invasion.c b/src/gamemodes/invasion.c index 5da97af0..9484e333 100644 --- a/src/gamemodes/invasion.c +++ b/src/gamemodes/invasion.c @@ -37,25 +37,35 @@ static constexpr int SET_EASY_MODE_MONSTERS[] = { DS_FLYER, DS_FLOATER, DS_HOVER, - // DS_SHAMBLER, + DS_SHAMBLER, + DS_REDMUTANT, + DS_RUNNERTANK, + DS_GUNCMDR, + DS_DAEDALUS, + DS_BOSS2_SMALL, + DS_STALKER, + DS_GEKK, + DS_ARACHNID, + DS_BITCH_HEAT, }; constexpr int SET_EASY_MODE_MONSTERS_COUNT = sizeof(SET_EASY_MODE_MONSTERS) / sizeof(int); // parasite, brain, medic, tank, mutant, gladiator, berserker, infantry, hover static constexpr int SET_HARD_MODE_MONSTERS[] = { - DS_PARASITE, DS_BRAIN, DS_MEDIC, DS_TANK, DS_MUTANT, DS_GLADIATOR, DS_BERSERK, DS_INFANTRY, DS_HOVER + DS_PARASITE, DS_BRAIN, DS_MEDIC, DS_TANK, DS_MUTANT, DS_GLADIATOR, DS_BERSERK, DS_INFANTRY, DS_HOVER, + DS_REDMUTANT, DS_RUNNERTANK, DS_GUNCMDR, DS_DAEDALUS, DS_BOSS2_SMALL, DS_GLADB, DS_GLADC, DS_STALKER, DS_GEKK, DS_ARACHNID, DS_BITCH_HEAT }; constexpr int SET_HARD_MODE_MONSTERS_COUNT = sizeof(SET_HARD_MODE_MONSTERS) / sizeof(int); static constexpr int SET_FLYING_MONSTERS[] = { - DS_FLYER, DS_HOVER, DS_FLOATER + DS_FLYER, DS_HOVER, DS_FLOATER, DS_DAEDALUS, DS_BOSS2_SMALL }; constexpr int SET_FLYING_MONSTERS_COUNT = sizeof(SET_FLYING_MONSTERS) / sizeof(int); // parasite, brain, mutant, berserker static constexpr int SET_MELEE_MONSTERS[] = { - DS_PARASITE, DS_BRAIN, DS_MUTANT, DS_BERSERK, + DS_PARASITE, DS_BRAIN, DS_MUTANT, DS_BERSERK, DS_REDMUTANT, DS_STALKER, DS_GEKK, DS_ARACHNID, }; constexpr int SET_MELEE_MONSTERS_COUNT = sizeof(SET_MELEE_MONSTERS) / sizeof(int); @@ -67,7 +77,7 @@ constexpr int SET_RAGEQUIT_MONSTERS_COUNT = sizeof(SET_RAGEQUIT_MONSTERS) / size // tank, mutant, berserker, shambler! static constexpr int SET_TANKY_MONSTERS[] = { - DS_TANK, DS_MUTANT, DS_BERSERK, DS_SHAMBLER + DS_TANK, DS_MUTANT, DS_BERSERK, DS_SHAMBLER, DS_REDMUTANT, DS_RUNNERTANK, DS_GUNCMDR, DS_GLADB, DS_GLADC, DS_GEKK, DS_ARACHNID }; constexpr int SET_TANKY_MONSTERS_COUNT = sizeof(SET_TANKY_MONSTERS) / sizeof(int); @@ -78,6 +88,20 @@ static constexpr int SET_PARASITE_MONSTERS[] = { constexpr int SET_PARASITE_MONSTERS_COUNT = sizeof(SET_PARASITE_MONSTERS) / sizeof(int); +static constexpr int SET_BOSS_MONSTERS[] = { + DS_GUARDIAN, + DS_FIXBOT_BOSS, + DS_WIDOW2, + DS_WIDOW, + DS_CARRIER, + DS_BOSS2, + DS_BOSS5, + DS_BARON_FIRE, + DS_MAKRON, + DS_COMMANDER, +}; +constexpr int SET_BOSS_MONSTERS_COUNT = sizeof(SET_BOSS_MONSTERS) / sizeof(int); + qboolean vrx_inv_is_boss_wave(int wave) { return wave % 5 == 0 && wave > 0; } @@ -584,7 +608,7 @@ void vrx_inv_spawn_boss(edict_t *self, int index) { if (invasion_data.boss) return; - if (index < 30) + if (!vrx_drone_spawn_is_boss((enum dronespawn_t)index)) return; while ((spawn = vrx_inv_get_monster_spawn(spawn))) { @@ -613,7 +637,7 @@ void vrx_inv_boss_check(edict_t *self) { while ((e = vrx_inv_get_monster_spawn(e)) != NULL) { if (invasion_data.boss) continue; - invasion_data.boss = vrx_inv_spawn_drone(self, e, GetRandom(30, 32)); + invasion_data.boss = vrx_inv_spawn_drone(self, e, SET_BOSS_MONSTERS[GetRandom(0, SET_BOSS_MONSTERS_COUNT - 1)]); if (!invasion_data.boss) { iter++; diff --git a/src/menus/upgradehelp.c b/src/menus/upgradehelp.c index aa55ec78..5286a882 100644 --- a/src/menus/upgradehelp.c +++ b/src/menus/upgradehelp.c @@ -1092,4 +1092,4 @@ int vrx_upgradehelp_add_menu_lines(edict_t *ent, const struct upgradehelp_s *hel } return lines; -} \ No newline at end of file +} diff --git a/src/q_shared.h b/src/q_shared.h index db87f755..91c0938c 100644 --- a/src/q_shared.h +++ b/src/q_shared.h @@ -1338,7 +1338,7 @@ enum monster_muzzleflash_id_t : uint16_t { MZ2_LAST }; -extern vec3_t monster_flash_offset[212]; +extern vec3_t monster_flash_offset[MZ2_LAST + 1]; // temp entity events diff --git a/src/quake2/g_layout.c b/src/quake2/g_layout.c index b5952568..6bfc1014 100644 --- a/src/quake2/g_layout.c +++ b/src/quake2/g_layout.c @@ -302,24 +302,51 @@ sidebar_entry_t layout_add_entity_info(sidebar_t* sidebar, edict_t* ent) case M_SOLDIERLT: case M_SOLDIER: case M_SOLDIERSS: + case M_SOLDIER_RIPPER: + case M_SOLDIER_BLUEBLASTER: + case M_SOLDIER_LASER: case M_FLIPPER: case M_FLYER: case M_INFANTRY: case M_INSANE: case M_GUNNER: case M_CHICK: + case M_CHICK_HEAT: case M_PARASITE: case M_FLOATER: case M_HOVER: case M_BERSERK: case M_MEDIC: + case M_MEDIC_COMMANDER: case M_MUTANT: case M_BRAIN: case M_GLADIATOR: case M_TANK: + case M_SUPERTANK: + case M_BOSS5: case M_FORCEWALL: case M_BARON_FIRE: case M_SHAMBLER: + case M_REDMUTANT: + case M_RUNNERTANK: + case M_GUNCMDR: + case M_DAEDALUS: + case M_GLADB: + case M_GLADC: + case M_STALKER: + case M_GEKK: + case M_ARACHNID: + case M_BOSS2: + case M_BOSS2_SMALL: + case M_CARRIER: + case M_WIDOW: + case M_WIDOW2: + case M_FIXBOT: + case M_FIXBOT_BOSS: + case M_ROGUE_TURRET: + case M_GUARDIAN: + case M_JANITOR: + case M_MINIGUARDIAN: case M_SKELETON: case M_GOLEM: name = lva("%s", V_GetMonsterName(ent)); @@ -769,4 +796,4 @@ void layout_send(edict_t* ent) gi.WriteString(ent->client->layout.layout); gi.unicast(ent, false); #endif -} \ No newline at end of file +} diff --git a/src/quake2/g_main.c b/src/quake2/g_main.c index e6821ae7..34f91558 100644 --- a/src/quake2/g_main.c +++ b/src/quake2/g_main.c @@ -839,7 +839,8 @@ void G_RunFrame(bool main_loop) level.current_entity = ent; - VectorCopy(ent->s.origin, ent->s.old_origin); + if (!(ent->s.renderfx & RF_BEAM)) // this line allows guardian, soldier_lasergun to have visual on their laser attacks :) + VectorCopy(ent->s.origin, ent->s.old_origin); // if the ground entity moved, make sure we are still on it if ((ent->groundentity) && (ent->groundentity->linkcount != ent->groundentity_linkcount)) diff --git a/src/quake2/g_phys.c b/src/quake2/g_phys.c index a5e03534..3e0b18d5 100644 --- a/src/quake2/g_phys.c +++ b/src/quake2/g_phys.c @@ -126,10 +126,10 @@ void SV_Impact (edict_t *e1, trace_t *trace) { edict_t *e2 = trace->ent; - if (e1->touch && e1->solid != SOLID_NOT) + if (e1->touch && (e1->solid != SOLID_NOT || (e1->flags & FL_ALWAYS_TOUCH))) e1->touch (e1, e2, &trace->plane, trace->surface); - if (e2->touch && e2->solid != SOLID_NOT) + if (e2->touch && (e2->solid != SOLID_NOT || (e2->flags & FL_ALWAYS_TOUCH))) e2->touch (e2, e1, NULL, NULL); } @@ -613,11 +613,15 @@ void SV_Physics_Toss (edict_t *ent) qboolean wasinwater; qboolean isinwater; vec3_t old_origin; + qboolean is_gib; + qboolean stop_on_ground; + float impact_speed; const qboolean forcethrough = false; // regular thinking SV_RunThink (ent); + is_gib = ent->classname && !strcmp(ent->classname, "gib"); // if not a team captain, so movement will be handled elsewhere if ( ent->flags & FL_TEAMSLAVE) @@ -675,6 +679,8 @@ void SV_Physics_Toss (edict_t *ent) if (trace.fraction < 1 && !forcethrough) { + impact_speed = fabs(DotProduct(ent->velocity, trace.plane.normal)); + // RAFAEL if (ent->movetype == MOVETYPE_WALLBOUNCE) backoff = 2.0; @@ -703,12 +709,32 @@ void SV_Physics_Toss (edict_t *ent) if (ent->movetype == MOVETYPE_WALLBOUNCE) vectoangles (ent->velocity, ent->s.angles); + if (is_gib && trace.plane.normal[2] <= 0.7f) + { + VectorScale(ent->avelocity, 0.75f, ent->avelocity); + if (impact_speed < 45.0f && VectorLength(ent->velocity) < 120.0f) + VectorCopy(vec3_origin, ent->avelocity); + } + // stop if on ground // RAFAEL //K03 Begin - if (trace.plane.normal[2] > 0.95 && ent->movetype != MOVETYPE_WALLBOUNCE && ent->movetype != MOVETYPE_SLIDE)//was 0.7 + if (trace.plane.normal[2] > (is_gib ? 0.7f : 0.95f) && ent->movetype != MOVETYPE_WALLBOUNCE && ent->movetype != MOVETYPE_SLIDE)//was 0.7 { - if (ent->velocity[2] < 30/*60*/ || ent->movetype != MOVETYPE_BOUNCE ) + if (is_gib) + VectorCopy(vec3_origin, ent->avelocity); + + if (is_gib) + { + if (ent->movetype == MOVETYPE_TOSS) + stop_on_ground = VectorLength(ent->velocity) < 60.0f; + else + stop_on_ground = fabs(DotProduct(ent->velocity, trace.plane.normal)) < 60.0f; + } + else + stop_on_ground = ent->velocity[2] < 30/*60*/ || ent->movetype != MOVETYPE_BOUNCE; + + if (stop_on_ground) //K03 End { // gi.dprintf("SV_Physics_Toss() stopped the entity\n"); @@ -717,6 +743,10 @@ void SV_Physics_Toss (edict_t *ent) VectorCopy (vec3_origin, ent->velocity); VectorCopy (vec3_origin, ent->avelocity); } + else if (is_gib && ent->movetype == MOVETYPE_TOSS) + { + VectorScale(ent->velocity, 0.75f, ent->velocity); + } } // if (ent->touch) @@ -839,7 +869,8 @@ void SV_Physics_Step (edict_t *ent) } // friction for flying monsters that have been given vertical velocity - if ((ent->flags & FL_FLY) && (ent->velocity[2] != 0)) + if ((ent->flags & FL_FLY) && (ent->velocity[2] != 0) + && !(ent->monsterinfo.aiflags & AI_ALTERNATE_FLY)) { //gi.bprintf(PRINT_HIGH,"FLY!\n"); speed = fabs(ent->velocity[2]); @@ -880,7 +911,8 @@ void SV_Physics_Step (edict_t *ent) //gi.dprintf("velocity is nonzero\n"); // apply friction // let dead monsters who aren't completely onground slide - if ((wasonground) || (ent->flags & (FL_SWIM|FL_FLY))) + if (((wasonground) || (ent->flags & (FL_SWIM|FL_FLY))) + && !(ent->monsterinfo.aiflags & AI_ALTERNATE_FLY)) if (!(ent->health <= 0.0 && !M_CheckBottom(ent))) { //K03 Begin diff --git a/src/quake2/g_save.c b/src/quake2/g_save.c index 3bca9700..5adc0768 100644 --- a/src/quake2/g_save.c +++ b/src/quake2/g_save.c @@ -20,6 +20,9 @@ cvar_t *bot_debugmonster; field_t fields[] = { { "classname", FOFS(classname), F_LSTRING }, { "origin", FOFS(s.origin), F_VECTOR }, +#ifdef VRX_REPRO + { "scale", FOFS(s.scale), F_FLOAT }, +#endif { "model", FOFS(model), F_LSTRING }, { "spawnflags", FOFS(spawnflags), F_INT }, { "speed", FOFS(speed), F_FLOAT }, diff --git a/src/quake2/g_svcmds.c b/src/quake2/g_svcmds.c index 420fdcc3..762af2e3 100644 --- a/src/quake2/g_svcmds.c +++ b/src/quake2/g_svcmds.c @@ -637,6 +637,13 @@ void SVCmd_SpawnBoss_f (void) else vrx_create_new_drone(m_worldspawn, 33, true, true, 0); } + else if (!strcmp(gi.argv(2), "boss5") || !strcmp(gi.argv(2), "supertank_heat")) + { + if (invasion->value) + vrx_inv_spawn_boss(m_worldspawn, DS_BOSS5); + else + vrx_create_new_drone(m_worldspawn, DS_BOSS5, true, true, 0); + } else if (!strcmp(gi.argv(2), "jorg")) { if (invasion->value) @@ -644,8 +651,58 @@ void SVCmd_SpawnBoss_f (void) else vrx_create_new_drone(m_worldspawn, 34, true, true, 0); } + else if (!strcmp(gi.argv(2), "boss2") || !strcmp(gi.argv(2), "hornet") || !strcmp(gi.argv(2), "boss2mg")) + { + if (invasion->value) + vrx_inv_spawn_boss(m_worldspawn, DS_BOSS2); + else + vrx_create_new_drone(m_worldspawn, DS_BOSS2, true, true, 0); + } + else if (!strcmp(gi.argv(2), "boss2_hyper") || !strcmp(gi.argv(2), "boss2hyper") || !strcmp(gi.argv(2), "boss2_n64") + || !strcmp(gi.argv(2), "boss2n64") || !strcmp(gi.argv(2), "hornethb") || !strcmp(gi.argv(2), "hornet_hyper")) + { + if (invasion->value) + vrx_inv_spawn_boss(m_worldspawn, DS_BOSS2_HYPER); + else + vrx_create_new_drone(m_worldspawn, DS_BOSS2_HYPER, true, true, 0); + } + else if (!strcmp(gi.argv(2), "carrier")) + { + if (invasion->value) + vrx_inv_spawn_boss(m_worldspawn, DS_CARRIER); + else + vrx_create_new_drone(m_worldspawn, DS_CARRIER, true, true, 0); + } + else if (!strcmp(gi.argv(2), "widow") || !strcmp(gi.argv(2), "blackwidow")) + { + if (invasion->value) + vrx_inv_spawn_boss(m_worldspawn, DS_WIDOW); + else + vrx_create_new_drone(m_worldspawn, DS_WIDOW, true, true, 0); + } + else if (!strcmp(gi.argv(2), "widow2") || !strcmp(gi.argv(2), "blackwidow2")) + { + if (invasion->value) + vrx_inv_spawn_boss(m_worldspawn, DS_WIDOW2); + else + vrx_create_new_drone(m_worldspawn, DS_WIDOW2, true, true, 0); + } + else if (!strcmp(gi.argv(2), "fixbot_boss") || !strcmp(gi.argv(2), "fixbotboss") || !strcmp(gi.argv(2), "fixer")) + { + if (invasion->value) + vrx_inv_spawn_boss(m_worldspawn, DS_FIXBOT_BOSS); + else + vrx_create_new_drone(m_worldspawn, DS_FIXBOT_BOSS, true, true, 0); + } + else if (!strcmp(gi.argv(2), "guardian") || !strcmp(gi.argv(2), "psxguardian")) + { + if (invasion->value) + vrx_inv_spawn_boss(m_worldspawn, DS_GUARDIAN); + else + vrx_create_new_drone(m_worldspawn, DS_GUARDIAN, true, true, 0); + } else - safe_cprintf(NULL, PRINT_HIGH, "Invalid boss type. Usage: sv spawnboss \n"); + safe_cprintf(NULL, PRINT_HIGH, "Invalid boss type. Usage: sv spawnboss \n"); } void SVCmd_MakeBoss_f (void) diff --git a/src/quake2/monsterframes/m_arachnid.h b/src/quake2/monsterframes/m_arachnid.h new file mode 100644 index 00000000..c81f9f89 --- /dev/null +++ b/src/quake2/monsterframes/m_arachnid.h @@ -0,0 +1,138 @@ +// G:\quake2\baseq2\models/monsters/arachnid + +enum +{ + FRAME_rails1, + FRAME_rails2, + FRAME_rails3, + FRAME_rails4, + FRAME_rails5, + FRAME_rails6, + FRAME_rails7, + FRAME_rails8, + FRAME_rails9, + FRAME_rails10, + FRAME_rails11, + FRAME_death1, + FRAME_death2, + FRAME_death3, + FRAME_death4, + FRAME_death5, + FRAME_death6, + FRAME_death7, + FRAME_death8, + FRAME_death9, + FRAME_death10, + FRAME_death11, + FRAME_death12, + FRAME_death13, + FRAME_death14, + FRAME_death15, + FRAME_death16, + FRAME_death17, + FRAME_death18, + FRAME_death19, + FRAME_death20, + FRAME_melee_atk1, + FRAME_melee_atk2, + FRAME_melee_atk3, + FRAME_melee_atk4, + FRAME_melee_atk5, + FRAME_melee_atk6, + FRAME_melee_atk7, + FRAME_melee_atk8, + FRAME_melee_atk9, + FRAME_melee_atk10, + FRAME_melee_atk11, + FRAME_melee_atk12, + FRAME_pain11, + FRAME_pain12, + FRAME_pain13, + FRAME_pain14, + FRAME_pain15, + FRAME_idle1, + FRAME_idle2, + FRAME_idle3, + FRAME_idle4, + FRAME_idle5, + FRAME_idle6, + FRAME_idle7, + FRAME_idle8, + FRAME_idle9, + FRAME_idle10, + FRAME_idle11, + FRAME_idle12, + FRAME_idle13, + FRAME_walk1, + FRAME_walk2, + FRAME_walk3, + FRAME_walk4, + FRAME_walk5, + FRAME_walk6, + FRAME_walk7, + FRAME_walk8, + FRAME_walk9, + FRAME_walk10, + FRAME_turn1, + FRAME_turn2, + FRAME_turn3, + FRAME_melee_out1, + FRAME_melee_out2, + FRAME_melee_out3, + FRAME_pain21, + FRAME_pain22, + FRAME_pain23, + FRAME_pain24, + FRAME_pain25, + FRAME_pain26, + FRAME_melee_pain1, + FRAME_melee_pain2, + FRAME_melee_pain3, + FRAME_melee_pain4, + FRAME_melee_pain5, + FRAME_melee_pain6, + FRAME_melee_pain7, + FRAME_melee_pain8, + FRAME_melee_pain9, + FRAME_melee_pain10, + FRAME_melee_pain11, + FRAME_melee_pain12, + FRAME_melee_pain13, + FRAME_melee_pain14, + FRAME_melee_pain15, + FRAME_melee_pain16, + FRAME_melee_in1, + FRAME_melee_in2, + FRAME_melee_in3, + FRAME_melee_in4, + FRAME_melee_in5, + FRAME_melee_in6, + FRAME_melee_in7, + FRAME_melee_in8, + FRAME_melee_in9, + FRAME_melee_in10, + FRAME_melee_in11, + FRAME_melee_in12, + FRAME_melee_in13, + FRAME_melee_in14, + FRAME_melee_in15, + FRAME_melee_in16, + FRAME_rails_up1, + FRAME_rails_up2, + FRAME_rails_up3, + FRAME_rails_up4, + FRAME_rails_up5, + FRAME_rails_up6, + FRAME_rails_up7, + FRAME_rails_up8, + FRAME_rails_up9, + FRAME_rails_up10, + FRAME_rails_up11, + FRAME_rails_up12, + FRAME_rails_up13, + FRAME_rails_up14, + FRAME_rails_up15, + FRAME_rails_up16 +}; + +#define MODEL_SCALE 1.000000f diff --git a/src/quake2/monsterframes/m_flash.c b/src/quake2/monsterframes/m_flash.c index 65f6d31f..c383169a 100644 --- a/src/quake2/monsterframes/m_flash.c +++ b/src/quake2/monsterframes/m_flash.c @@ -5,7 +5,7 @@ // this file is included in both the game dll and quake2, // the game needs it to source shot locations, the client // needs it to position muzzle flashes -vec3_t monster_flash_offset [212] = +vec3_t monster_flash_offset [MZ2_LAST + 1] = { // flash 0 is not used 0.0, 0.0, 0.0, @@ -464,6 +464,141 @@ vec3_t monster_flash_offset [212] = // MZ2_WIDOW2_BEAM_SWEEP_11 210 58.29, 27.11, 92.00, +// MZ2_SOLDIER_RIPPER_1 211 + 10.6 * 1.2, 7.7 * 1.2, 7.8 * 1.2, +// MZ2_SOLDIER_RIPPER_2 212 + 25.1 * 1.2, 3.6 * 1.2, 19.0 * 1.2, +// MZ2_SOLDIER_RIPPER_3 213 + 20.8 * 1.2, 10.1 * 1.2, -2.7 * 1.2, +// MZ2_SOLDIER_RIPPER_4 214 + 7.6 * 1.2, 9.3 * 1.2, 0.8 * 1.2, +// MZ2_SOLDIER_RIPPER_5 215 + 30.5 * 1.2, 9.9 * 1.2, -18.7 * 1.2, +// MZ2_SOLDIER_RIPPER_6 216 + 27.6 * 1.2, 3.4 * 1.2, -10.4 * 1.2, +// MZ2_SOLDIER_RIPPER_7 217 + 28.9 * 1.2, 4.6 * 1.2, -8.1 * 1.2, +// MZ2_SOLDIER_RIPPER_8 218 + 31.5 * 1.2, 9.6 * 1.2, 10.1 * 1.2, +// MZ2_SOLDIER_HYPERGUN_1 219 + 10.6 * 1.2, 7.7 * 1.2, 7.8 * 1.2, +// MZ2_SOLDIER_HYPERGUN_2 220 + 25.1 * 1.2, 3.6 * 1.2, 19.0 * 1.2, +// MZ2_SOLDIER_HYPERGUN_3 221 + 20.8 * 1.2, 10.1 * 1.2, -2.7 * 1.2, +// MZ2_SOLDIER_HYPERGUN_4 222 + 7.6 * 1.2, 9.3 * 1.2, 0.8 * 1.2, +// MZ2_SOLDIER_HYPERGUN_5 223 + 30.5 * 1.2, 9.9 * 1.2, -18.7 * 1.2, +// MZ2_SOLDIER_HYPERGUN_6 224 + 27.6 * 1.2, 3.4 * 1.2, -10.4 * 1.2, +// MZ2_SOLDIER_HYPERGUN_7 225 + 28.9 * 1.2, 4.6 * 1.2, -8.1 * 1.2, +// MZ2_SOLDIER_HYPERGUN_8 226 + 31.5 * 1.2, 9.6 * 1.2, 10.1 * 1.2, +// MZ2_GUARDIAN_BLASTER 227 + 88, 50, 60, +// MZ2_ARACHNID_RAIL1 228 + 58, 20, 17.2, +// MZ2_ARACHNID_RAIL2 229 + 64, -22, 24, +// MZ2_ARACHNID_RAIL_UP1 230 + 37, 13, 72, +// MZ2_ARACHNID_RAIL_UP2 231 + 58, -25, 72, +// MZ2_INFANTRY_MACHINEGUN_14 232 + 34, 11, 13, +// MZ2_INFANTRY_MACHINEGUN_15 233 + 28, 13, 10.5, +// MZ2_INFANTRY_MACHINEGUN_16 234 + 29, 13, 8.5, +// MZ2_INFANTRY_MACHINEGUN_17 235 + 30, 12.5, 12, +// MZ2_INFANTRY_MACHINEGUN_18 236 + 29, 12.5, 14.7, +// MZ2_INFANTRY_MACHINEGUN_19 237 + 30, 6.5, 12, +// MZ2_INFANTRY_MACHINEGUN_20 238 + 29, 1.5, 8.5, +// MZ2_INFANTRY_MACHINEGUN_21 239 + 29, 6, 10, +// MZ2_GUNCMDR_CHAINGUN_1 240 + 25, 11, 21, +// MZ2_GUNCMDR_CHAINGUN_2 241 + 26.5, 5, 21, +// MZ2_GUNCMDR_GRENADE_MORTAR_1 242 + 27, 6.5, 4, +// MZ2_GUNCMDR_GRENADE_MORTAR_2 243 + 28, 4, 4, +// MZ2_GUNCMDR_GRENADE_MORTAR_3 244 + 27, 1.7, 4, +// MZ2_GUNCMDR_GRENADE_FRONT_1 245 + 21.7, -1.5, 22.5, +// MZ2_GUNCMDR_GRENADE_FRONT_2 246 + 22, 0, 20.5, +// MZ2_GUNCMDR_GRENADE_FRONT_3 247 + 22.5, 3.7, 20.5, +// MZ2_GUNCMDR_GRENADE_CROUCH_1 248 + 8, 40, 18, +// MZ2_GUNCMDR_GRENADE_CROUCH_2 249 + 29, 16, 19, +// MZ2_GUNCMDR_GRENADE_CROUCH_3 250 + 4.7, -30, 20, +// MZ2_SOLDIER_BLASTER_9 251 + 36.33, 12.24, -17.39, +// MZ2_SOLDIER_SHOTGUN_9 252 + 36.33, 12.24, -17.39, +// MZ2_SOLDIER_MACHINEGUN_9 253 + 36.33, 12.24, -17.39, +// MZ2_SOLDIER_RIPPER_9 254 + 36.33, 12.24, -17.39, +// MZ2_SOLDIER_HYPERGUN_9 255 + 36.33, 12.24, -17.39, +// MZ2_GUNNER_GRENADE2_1 256 + 36, -6.2, 19.59, +// MZ2_GUNNER_GRENADE2_2 257 + 36, -6.2, 19.59, +// MZ2_GUNNER_GRENADE2_3 258 + 36, -6.2, 19.59, +// MZ2_GUNNER_GRENADE2_4 259 + 36, -6.2, 19.59, +// MZ2_INFANTRY_MACHINEGUN_22 260 + 14.8, 10.5, 8.82, +// MZ2_SUPERTANK_GRENADE_1 261 + 31.31, -37, 54.32, +// MZ2_SUPERTANK_GRENADE_2 262 + 31.31, 37, 54.32, +// MZ2_HOVER_BLASTER_2 263 + 1.7, -7.0, 11.3, +// MZ2_DAEDALUS_BLASTER_2 264 + 1.7, -7.0, 11.3, +// MZ2_MEDIC_HYPERBLASTER1_1-12 265 + 34, 12.5, 15, + 33.4, 11.2, 15, + 36.6, 7.4, 15, + 35, 4.1, 15, + 37.6, 1, 15, + 35.7, -1.9, 15, + 37.6, -0.5, 15, + 35.2, 2.8, 15, + 37.5, 3.8, 15, + 34.5, 6.9, 15, + 33.7, 9.9, 15, + 35.5, 11, 15, +// MZ2_MEDIC_HYPERBLASTER2_1-12 277 + 34, 12.5, 15, + 33.4, 11.2, 15, + 36.6, 7.4, 15, + 35, 4.1, 15, + 37.6, 1, 15, + 35.7, -1.9, 15, + 37.6, -0.5, 15, + 35.2, 2.8, 15, + 37.5, 3.8, 15, + 34.5, 6.9, 15, + 33.7, 9.9, 15, + 35.5, 11, 15, + // end of table 0.0, 0.0, 0.0 }; diff --git a/src/quake2/monsterframes/m_guardian.h b/src/quake2/monsterframes/m_guardian.h new file mode 100644 index 00000000..e82466de --- /dev/null +++ b/src/quake2/monsterframes/m_guardian.h @@ -0,0 +1,221 @@ +// Frame definitions for models/monsters/guardian/tris.md2. + +enum { + FRAME_sleep1, + FRAME_sleep2, + FRAME_sleep3, + FRAME_sleep4, + FRAME_sleep5, + FRAME_sleep6, + FRAME_sleep7, + FRAME_sleep8, + FRAME_sleep9, + FRAME_sleep10, + FRAME_sleep11, + FRAME_sleep12, + FRAME_sleep13, + FRAME_sleep14, + FRAME_death1, + FRAME_death2, + FRAME_death3, + FRAME_death4, + FRAME_death5, + FRAME_death6, + FRAME_death7, + FRAME_death8, + FRAME_death9, + FRAME_death10, + FRAME_death11, + FRAME_death12, + FRAME_death13, + FRAME_death14, + FRAME_death15, + FRAME_death16, + FRAME_death17, + FRAME_death18, + FRAME_death19, + FRAME_death20, + FRAME_death21, + FRAME_death22, + FRAME_death23, + FRAME_death24, + FRAME_death25, + FRAME_death26, + FRAME_atk1_out1, + FRAME_atk1_out2, + FRAME_atk1_out3, + FRAME_atk2_out1, + FRAME_atk2_out2, + FRAME_atk2_out3, + FRAME_atk2_out4, + FRAME_atk2_out5, + FRAME_atk2_out6, + FRAME_atk2_out7, + FRAME_kick_out1, + FRAME_kick_out2, + FRAME_kick_out3, + FRAME_kick_out4, + FRAME_kick_out5, + FRAME_kick_out6, + FRAME_kick_out7, + FRAME_kick_out8, + FRAME_kick_out9, + FRAME_kick_out10, + FRAME_kick_out11, + FRAME_kick_out12, + FRAME_pain1_1, + FRAME_pain1_2, + FRAME_pain1_3, + FRAME_pain1_4, + FRAME_pain1_5, + FRAME_pain1_6, + FRAME_pain1_7, + FRAME_pain1_8, + FRAME_idle1, + FRAME_idle2, + FRAME_idle3, + FRAME_idle4, + FRAME_idle5, + FRAME_idle6, + FRAME_idle7, + FRAME_idle8, + FRAME_idle9, + FRAME_idle10, + FRAME_idle11, + FRAME_idle12, + FRAME_idle13, + FRAME_idle14, + FRAME_idle15, + FRAME_idle16, + FRAME_idle17, + FRAME_idle18, + FRAME_idle19, + FRAME_idle20, + FRAME_idle21, + FRAME_idle22, + FRAME_idle23, + FRAME_idle24, + FRAME_idle25, + FRAME_idle26, + FRAME_idle27, + FRAME_idle28, + FRAME_idle29, + FRAME_idle30, + FRAME_idle31, + FRAME_idle32, + FRAME_idle33, + FRAME_idle34, + FRAME_idle35, + FRAME_idle36, + FRAME_idle37, + FRAME_idle38, + FRAME_idle39, + FRAME_idle40, + FRAME_idle41, + FRAME_idle42, + FRAME_idle43, + FRAME_idle44, + FRAME_idle45, + FRAME_idle46, + FRAME_idle47, + FRAME_idle48, + FRAME_idle49, + FRAME_idle50, + FRAME_idle51, + FRAME_idle52, + FRAME_atk1_in1, + FRAME_atk1_in2, + FRAME_atk1_in3, + FRAME_kick_in1, + FRAME_kick_in2, + FRAME_kick_in3, + FRAME_kick_in4, + FRAME_kick_in5, + FRAME_kick_in6, + FRAME_kick_in7, + FRAME_kick_in8, + FRAME_kick_in9, + FRAME_kick_in10, + FRAME_kick_in11, + FRAME_kick_in12, + FRAME_kick_in13, + FRAME_walk1, + FRAME_walk2, + FRAME_walk3, + FRAME_walk4, + FRAME_walk5, + FRAME_walk6, + FRAME_walk7, + FRAME_walk8, + FRAME_walk9, + FRAME_walk10, + FRAME_walk11, + FRAME_walk12, + FRAME_walk13, + FRAME_walk14, + FRAME_walk15, + FRAME_walk16, + FRAME_walk17, + FRAME_walk18, + FRAME_walk19, + FRAME_wake1, + FRAME_wake2, + FRAME_wake3, + FRAME_wake4, + FRAME_wake5, + FRAME_atk1_spin1, + FRAME_atk1_spin2, + FRAME_atk1_spin3, + FRAME_atk1_spin4, + FRAME_atk1_spin5, + FRAME_atk1_spin6, + FRAME_atk1_spin7, + FRAME_atk1_spin8, + FRAME_atk1_spin9, + FRAME_atk1_spin10, + FRAME_atk1_spin11, + FRAME_atk1_spin12, + FRAME_atk1_spin13, + FRAME_atk1_spin14, + FRAME_atk1_spin15, + FRAME_atk2_fire1, + FRAME_atk2_fire2, + FRAME_atk2_fire3, + FRAME_atk2_fire4, + FRAME_turnl_1, + FRAME_turnl_2, + FRAME_turnl_3, + FRAME_turnl_4, + FRAME_turnl_5, + FRAME_turnl_6, + FRAME_turnl_7, + FRAME_turnl_8, + FRAME_turnl_9, + FRAME_turnl_10, + FRAME_turnl_11, + FRAME_turnr_1, + FRAME_turnr_2, + FRAME_turnr_3, + FRAME_turnr_4, + FRAME_turnr_5, + FRAME_turnr_6, + FRAME_turnr_7, + FRAME_turnr_8, + FRAME_turnr_9, + FRAME_turnr_10, + FRAME_turnr_11, + FRAME_atk2_in1, + FRAME_atk2_in2, + FRAME_atk2_in3, + FRAME_atk2_in4, + FRAME_atk2_in5, + FRAME_atk2_in6, + FRAME_atk2_in7, + FRAME_atk2_in8, + FRAME_atk2_in9, + FRAME_atk2_in10, + FRAME_atk2_in11, + FRAME_atk2_in12 +}; + +#define MODEL_SCALE 1.000000 diff --git a/src/quake2/monsterframes/m_gunner.h b/src/quake2/monsterframes/m_gunner.h index fc3f5324..cd951dcb 100644 --- a/src/quake2/monsterframes/m_gunner.h +++ b/src/quake2/monsterframes/m_gunner.h @@ -1,215 +1,811 @@ -// G:\quake2\baseq2\models/gunner +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. +// E:\G Drive\md2f\quake2\baseq2\models/monsters/gunner -// This file generated by ModelGen - Do NOT Modify +// This file generated by qdata - Do NOT Modify -#define FRAME_stand01 0 -#define FRAME_stand02 1 -#define FRAME_stand03 2 -#define FRAME_stand04 3 -#define FRAME_stand05 4 -#define FRAME_stand06 5 -#define FRAME_stand07 6 -#define FRAME_stand08 7 -#define FRAME_stand09 8 -#define FRAME_stand10 9 -#define FRAME_stand11 10 -#define FRAME_stand12 11 -#define FRAME_stand13 12 -#define FRAME_stand14 13 -#define FRAME_stand15 14 -#define FRAME_stand16 15 -#define FRAME_stand17 16 -#define FRAME_stand18 17 -#define FRAME_stand19 18 -#define FRAME_stand20 19 -#define FRAME_stand21 20 -#define FRAME_stand22 21 -#define FRAME_stand23 22 -#define FRAME_stand24 23 -#define FRAME_stand25 24 -#define FRAME_stand26 25 -#define FRAME_stand27 26 -#define FRAME_stand28 27 -#define FRAME_stand29 28 -#define FRAME_stand30 29 -#define FRAME_stand31 30 -#define FRAME_stand32 31 -#define FRAME_stand33 32 -#define FRAME_stand34 33 -#define FRAME_stand35 34 -#define FRAME_stand36 35 -#define FRAME_stand37 36 -#define FRAME_stand38 37 -#define FRAME_stand39 38 -#define FRAME_stand40 39 -#define FRAME_stand41 40 -#define FRAME_stand42 41 -#define FRAME_stand43 42 -#define FRAME_stand44 43 -#define FRAME_stand45 44 -#define FRAME_stand46 45 -#define FRAME_stand47 46 -#define FRAME_stand48 47 -#define FRAME_stand49 48 -#define FRAME_stand50 49 -#define FRAME_stand51 50 -#define FRAME_stand52 51 -#define FRAME_stand53 52 -#define FRAME_stand54 53 -#define FRAME_stand55 54 -#define FRAME_stand56 55 -#define FRAME_stand57 56 -#define FRAME_stand58 57 -#define FRAME_stand59 58 -#define FRAME_stand60 59 -#define FRAME_stand61 60 -#define FRAME_stand62 61 -#define FRAME_stand63 62 -#define FRAME_stand64 63 -#define FRAME_stand65 64 -#define FRAME_stand66 65 -#define FRAME_stand67 66 -#define FRAME_stand68 67 -#define FRAME_stand69 68 -#define FRAME_stand70 69 -#define FRAME_walk01 70 -#define FRAME_walk02 71 -#define FRAME_walk03 72 -#define FRAME_walk04 73 -#define FRAME_walk05 74 -#define FRAME_walk06 75 -#define FRAME_walk07 76 -#define FRAME_walk08 77 -#define FRAME_walk09 78 -#define FRAME_walk10 79 -#define FRAME_walk11 80 -#define FRAME_walk12 81 -#define FRAME_walk13 82 -#define FRAME_walk14 83 -#define FRAME_walk15 84 -#define FRAME_walk16 85 -#define FRAME_walk17 86 -#define FRAME_walk18 87 -#define FRAME_walk19 88 -#define FRAME_walk20 89 -#define FRAME_walk21 90 -#define FRAME_walk22 91 -#define FRAME_walk23 92 -#define FRAME_walk24 93 -#define FRAME_run01 94 -#define FRAME_run02 95 -#define FRAME_run03 96 -#define FRAME_run04 97 -#define FRAME_run05 98 -#define FRAME_run06 99 -#define FRAME_run07 100 -#define FRAME_run08 101 -#define FRAME_runs01 102 -#define FRAME_runs02 103 -#define FRAME_runs03 104 -#define FRAME_runs04 105 -#define FRAME_runs05 106 -#define FRAME_runs06 107 -#define FRAME_attak101 108 -#define FRAME_attak102 109 -#define FRAME_attak103 110 -#define FRAME_attak104 111 -#define FRAME_attak105 112 -#define FRAME_attak106 113 -#define FRAME_attak107 114 -#define FRAME_attak108 115 -#define FRAME_attak109 116 -#define FRAME_attak110 117 -#define FRAME_attak111 118 -#define FRAME_attak112 119 -#define FRAME_attak113 120 -#define FRAME_attak114 121 -#define FRAME_attak115 122 -#define FRAME_attak116 123 -#define FRAME_attak117 124 -#define FRAME_attak118 125 -#define FRAME_attak119 126 -#define FRAME_attak120 127 -#define FRAME_attak121 128 -#define FRAME_attak201 129 -#define FRAME_attak202 130 -#define FRAME_attak203 131 -#define FRAME_attak204 132 -#define FRAME_attak205 133 -#define FRAME_attak206 134 -#define FRAME_attak207 135 -#define FRAME_attak208 136 -#define FRAME_attak209 137 -#define FRAME_attak210 138 -#define FRAME_attak211 139 -#define FRAME_attak212 140 -#define FRAME_attak213 141 -#define FRAME_attak214 142 -#define FRAME_attak215 143 -#define FRAME_attak216 144 -#define FRAME_attak217 145 -#define FRAME_attak218 146 -#define FRAME_attak219 147 -#define FRAME_attak220 148 -#define FRAME_attak221 149 -#define FRAME_attak222 150 -#define FRAME_attak223 151 -#define FRAME_attak224 152 -#define FRAME_attak225 153 -#define FRAME_attak226 154 -#define FRAME_attak227 155 -#define FRAME_attak228 156 -#define FRAME_attak229 157 -#define FRAME_attak230 158 -#define FRAME_pain101 159 -#define FRAME_pain102 160 -#define FRAME_pain103 161 -#define FRAME_pain104 162 -#define FRAME_pain105 163 -#define FRAME_pain106 164 -#define FRAME_pain107 165 -#define FRAME_pain108 166 -#define FRAME_pain109 167 -#define FRAME_pain110 168 -#define FRAME_pain111 169 -#define FRAME_pain112 170 -#define FRAME_pain113 171 -#define FRAME_pain114 172 -#define FRAME_pain115 173 -#define FRAME_pain116 174 -#define FRAME_pain117 175 -#define FRAME_pain118 176 -#define FRAME_pain201 177 -#define FRAME_pain202 178 -#define FRAME_pain203 179 -#define FRAME_pain204 180 -#define FRAME_pain205 181 -#define FRAME_pain206 182 -#define FRAME_pain207 183 -#define FRAME_pain208 184 -#define FRAME_pain301 185 -#define FRAME_pain302 186 -#define FRAME_pain303 187 -#define FRAME_pain304 188 -#define FRAME_pain305 189 -#define FRAME_death01 190 -#define FRAME_death02 191 -#define FRAME_death03 192 -#define FRAME_death04 193 -#define FRAME_death05 194 -#define FRAME_death06 195 -#define FRAME_death07 196 -#define FRAME_death08 197 -#define FRAME_death09 198 -#define FRAME_death10 199 -#define FRAME_death11 200 -#define FRAME_duck01 201 -#define FRAME_duck02 202 -#define FRAME_duck03 203 -#define FRAME_duck04 204 -#define FRAME_duck05 205 -#define FRAME_duck06 206 -#define FRAME_duck07 207 -#define FRAME_duck08 208 +#pragma once -#define MODEL_SCALE 1.150000 +enum { + FRAME_stand01, + FRAME_stand02, + FRAME_stand03, + FRAME_stand04, + FRAME_stand05, + FRAME_stand06, + FRAME_stand07, + FRAME_stand08, + FRAME_stand09, + FRAME_stand10, + FRAME_stand11, + FRAME_stand12, + FRAME_stand13, + FRAME_stand14, + FRAME_stand15, + FRAME_stand16, + FRAME_stand17, + FRAME_stand18, + FRAME_stand19, + FRAME_stand20, + FRAME_stand21, + FRAME_stand22, + FRAME_stand23, + FRAME_stand24, + FRAME_stand25, + FRAME_stand26, + FRAME_stand27, + FRAME_stand28, + FRAME_stand29, + FRAME_stand30, + FRAME_stand31, + FRAME_stand32, + FRAME_stand33, + FRAME_stand34, + FRAME_stand35, + FRAME_stand36, + FRAME_stand37, + FRAME_stand38, + FRAME_stand39, + FRAME_stand40, + FRAME_stand41, + FRAME_stand42, + FRAME_stand43, + FRAME_stand44, + FRAME_stand45, + FRAME_stand46, + FRAME_stand47, + FRAME_stand48, + FRAME_stand49, + FRAME_stand50, + FRAME_stand51, + FRAME_stand52, + FRAME_stand53, + FRAME_stand54, + FRAME_stand55, + FRAME_stand56, + FRAME_stand57, + FRAME_stand58, + FRAME_stand59, + FRAME_stand60, + FRAME_stand61, + FRAME_stand62, + FRAME_stand63, + FRAME_stand64, + FRAME_stand65, + FRAME_stand66, + FRAME_stand67, + FRAME_stand68, + FRAME_stand69, + FRAME_stand70, + FRAME_walk01, + FRAME_walk02, + FRAME_walk03, + FRAME_walk04, + FRAME_walk05, + FRAME_walk06, + FRAME_walk07, + FRAME_walk08, + FRAME_walk09, + FRAME_walk10, + FRAME_walk11, + FRAME_walk12, + FRAME_walk13, + FRAME_walk14, + FRAME_walk15, + FRAME_walk16, + FRAME_walk17, + FRAME_walk18, + FRAME_walk19, + FRAME_walk20, + FRAME_walk21, + FRAME_walk22, + FRAME_walk23, + FRAME_walk24, + FRAME_run01, + FRAME_run02, + FRAME_run03, + FRAME_run04, + FRAME_run05, + FRAME_run06, + FRAME_run07, + FRAME_run08, + FRAME_runs01, + FRAME_runs02, + FRAME_runs03, + FRAME_runs04, + FRAME_runs05, + FRAME_runs06, + FRAME_attak101, + FRAME_attak102, + FRAME_attak103, + FRAME_attak104, + FRAME_attak105, + FRAME_attak106, + FRAME_attak107, + FRAME_attak108, + FRAME_attak109, + FRAME_attak110, + FRAME_attak111, + FRAME_attak112, + FRAME_attak113, + FRAME_attak114, + FRAME_attak115, + FRAME_attak116, + FRAME_attak117, + FRAME_attak118, + FRAME_attak119, + FRAME_attak120, + FRAME_attak121, + FRAME_attak201, + FRAME_attak202, + FRAME_attak203, + FRAME_attak204, + FRAME_attak205, + FRAME_attak206, + FRAME_attak207, + FRAME_attak208, + FRAME_attak209, + FRAME_attak210, + FRAME_attak211, + FRAME_attak212, + FRAME_attak213, + FRAME_attak214, + FRAME_attak215, + FRAME_attak216, + FRAME_attak217, + FRAME_attak218, + FRAME_attak219, + FRAME_attak220, + FRAME_attak221, + FRAME_attak222, + FRAME_attak223, + FRAME_attak224, + FRAME_attak225, + FRAME_attak226, + FRAME_attak227, + FRAME_attak228, + FRAME_attak229, + FRAME_attak230, + FRAME_pain101, + FRAME_pain102, + FRAME_pain103, + FRAME_pain104, + FRAME_pain105, + FRAME_pain106, + FRAME_pain107, + FRAME_pain108, + FRAME_pain109, + FRAME_pain110, + FRAME_pain111, + FRAME_pain112, + FRAME_pain113, + FRAME_pain114, + FRAME_pain115, + FRAME_pain116, + FRAME_pain117, + FRAME_pain118, + FRAME_pain201, + FRAME_pain202, + FRAME_pain203, + FRAME_pain204, + FRAME_pain205, + FRAME_pain206, + FRAME_pain207, + FRAME_pain208, + FRAME_pain301, + FRAME_pain302, + FRAME_pain303, + FRAME_pain304, + FRAME_pain305, + FRAME_death01, + FRAME_death02, + FRAME_death03, + FRAME_death04, + FRAME_death05, + FRAME_death06, + FRAME_death07, + FRAME_death08, + FRAME_death09, + FRAME_death10, + FRAME_death11, + FRAME_duck01, + FRAME_duck02, + FRAME_duck03, + FRAME_duck04, + FRAME_duck05, + FRAME_duck06, + FRAME_duck07, + FRAME_duck08, + FRAME_jump01, + FRAME_jump02, + FRAME_jump03, + FRAME_jump04, + FRAME_jump05, + FRAME_jump06, + FRAME_jump07, + FRAME_jump08, + FRAME_jump09, + FRAME_jump10, + FRAME_shield01, + FRAME_shield02, + FRAME_shield03, + FRAME_shield04, + FRAME_shield05, + FRAME_shield06, + FRAME_attak301, + FRAME_attak302, + FRAME_attak303, + FRAME_attak304, + FRAME_attak305, + FRAME_attak306, + FRAME_attak307, + FRAME_attak308, + FRAME_attak309, + FRAME_attak310, + FRAME_attak311, + FRAME_attak312, + FRAME_attak313, + FRAME_attak314, + FRAME_attak315, + FRAME_attak316, + FRAME_attak317, + FRAME_attak318, + FRAME_attak319, + FRAME_attak320, + FRAME_attak321, + FRAME_attak322, + FRAME_attak323, + FRAME_attak324, + FRAME_c_stand101, + FRAME_c_stand102, + FRAME_c_stand103, + FRAME_c_stand104, + FRAME_c_stand105, + FRAME_c_stand106, + FRAME_c_stand107, + FRAME_c_stand108, + FRAME_c_stand109, + FRAME_c_stand110, + FRAME_c_stand111, + FRAME_c_stand112, + FRAME_c_stand113, + FRAME_c_stand114, + FRAME_c_stand115, + FRAME_c_stand116, + FRAME_c_stand117, + FRAME_c_stand118, + FRAME_c_stand119, + FRAME_c_stand120, + FRAME_c_stand121, + FRAME_c_stand122, + FRAME_c_stand123, + FRAME_c_stand124, + FRAME_c_stand125, + FRAME_c_stand126, + FRAME_c_stand127, + FRAME_c_stand128, + FRAME_c_stand129, + FRAME_c_stand130, + FRAME_c_stand131, + FRAME_c_stand132, + FRAME_c_stand133, + FRAME_c_stand134, + FRAME_c_stand135, + FRAME_c_stand136, + FRAME_c_stand137, + FRAME_c_stand138, + FRAME_c_stand139, + FRAME_c_stand140, + FRAME_c_stand201, + FRAME_c_stand202, + FRAME_c_stand203, + FRAME_c_stand204, + FRAME_c_stand205, + FRAME_c_stand206, + FRAME_c_stand207, + FRAME_c_stand208, + FRAME_c_stand209, + FRAME_c_stand210, + FRAME_c_stand211, + FRAME_c_stand212, + FRAME_c_stand213, + FRAME_c_stand214, + FRAME_c_stand215, + FRAME_c_stand216, + FRAME_c_stand217, + FRAME_c_stand218, + FRAME_c_stand219, + FRAME_c_stand220, + FRAME_c_stand221, + FRAME_c_stand222, + FRAME_c_stand223, + FRAME_c_stand224, + FRAME_c_stand225, + FRAME_c_stand226, + FRAME_c_stand227, + FRAME_c_stand228, + FRAME_c_stand229, + FRAME_c_stand230, + FRAME_c_stand231, + FRAME_c_stand232, + FRAME_c_stand233, + FRAME_c_stand234, + FRAME_c_stand235, + FRAME_c_stand236, + FRAME_c_stand237, + FRAME_c_stand238, + FRAME_c_stand239, + FRAME_c_stand240, + FRAME_c_stand241, + FRAME_c_stand242, + FRAME_c_stand243, + FRAME_c_stand244, + FRAME_c_stand245, + FRAME_c_stand246, + FRAME_c_stand247, + FRAME_c_stand248, + FRAME_c_stand249, + FRAME_c_stand250, + FRAME_c_stand251, + FRAME_c_stand252, + FRAME_c_stand253, + FRAME_c_stand254, + FRAME_c_attack101, + FRAME_c_attack102, + FRAME_c_attack103, + FRAME_c_attack104, + FRAME_c_attack105, + FRAME_c_attack106, + FRAME_c_attack107, + FRAME_c_attack108, + FRAME_c_attack109, + FRAME_c_attack110, + FRAME_c_attack111, + FRAME_c_attack112, + FRAME_c_attack113, + FRAME_c_attack114, + FRAME_c_attack115, + FRAME_c_attack116, + FRAME_c_attack117, + FRAME_c_attack118, + FRAME_c_attack119, + FRAME_c_attack120, + FRAME_c_attack121, + FRAME_c_attack122, + FRAME_c_attack123, + FRAME_c_attack124, + FRAME_c_jump01, + FRAME_c_jump02, + FRAME_c_jump03, + FRAME_c_jump04, + FRAME_c_jump05, + FRAME_c_jump06, + FRAME_c_jump07, + FRAME_c_jump08, + FRAME_c_jump09, + FRAME_c_jump10, + FRAME_c_attack201, + FRAME_c_attack202, + FRAME_c_attack203, + FRAME_c_attack204, + FRAME_c_attack205, + FRAME_c_attack206, + FRAME_c_attack207, + FRAME_c_attack208, + FRAME_c_attack209, + FRAME_c_attack210, + FRAME_c_attack211, + FRAME_c_attack212, + FRAME_c_attack213, + FRAME_c_attack214, + FRAME_c_attack215, + FRAME_c_attack216, + FRAME_c_attack217, + FRAME_c_attack218, + FRAME_c_attack219, + FRAME_c_attack220, + FRAME_c_attack221, + FRAME_c_attack301, + FRAME_c_attack302, + FRAME_c_attack303, + FRAME_c_attack304, + FRAME_c_attack305, + FRAME_c_attack306, + FRAME_c_attack307, + FRAME_c_attack308, + FRAME_c_attack309, + FRAME_c_attack310, + FRAME_c_attack311, + FRAME_c_attack312, + FRAME_c_attack313, + FRAME_c_attack314, + FRAME_c_attack315, + FRAME_c_attack316, + FRAME_c_attack317, + FRAME_c_attack318, + FRAME_c_attack319, + FRAME_c_attack320, + FRAME_c_attack321, + FRAME_c_attack401, + FRAME_c_attack402, + FRAME_c_attack403, + FRAME_c_attack404, + FRAME_c_attack405, + FRAME_c_attack501, + FRAME_c_attack502, + FRAME_c_attack503, + FRAME_c_attack504, + FRAME_c_attack505, + FRAME_c_attack601, + FRAME_c_attack602, + FRAME_c_attack603, + FRAME_c_attack604, + FRAME_c_attack605, + FRAME_c_attack701, + FRAME_c_attack702, + FRAME_c_attack703, + FRAME_c_attack704, + FRAME_c_attack705, + FRAME_c_pain101, + FRAME_c_pain102, + FRAME_c_pain103, + FRAME_c_pain104, + FRAME_c_pain201, + FRAME_c_pain202, + FRAME_c_pain203, + FRAME_c_pain204, + FRAME_c_pain301, + FRAME_c_pain302, + FRAME_c_pain303, + FRAME_c_pain304, + FRAME_c_pain401, + FRAME_c_pain402, + FRAME_c_pain403, + FRAME_c_pain404, + FRAME_c_pain405, + FRAME_c_pain406, + FRAME_c_pain407, + FRAME_c_pain408, + FRAME_c_pain409, + FRAME_c_pain410, + FRAME_c_pain411, + FRAME_c_pain412, + FRAME_c_pain413, + FRAME_c_pain414, + FRAME_c_pain415, + FRAME_c_pain501, + FRAME_c_pain502, + FRAME_c_pain503, + FRAME_c_pain504, + FRAME_c_pain505, + FRAME_c_pain506, + FRAME_c_pain507, + FRAME_c_pain508, + FRAME_c_pain509, + FRAME_c_pain510, + FRAME_c_pain511, + FRAME_c_pain512, + FRAME_c_pain513, + FRAME_c_pain514, + FRAME_c_pain515, + FRAME_c_pain516, + FRAME_c_pain517, + FRAME_c_pain518, + FRAME_c_pain519, + FRAME_c_pain520, + FRAME_c_pain521, + FRAME_c_pain522, + FRAME_c_pain523, + FRAME_c_pain524, + FRAME_c_death101, + FRAME_c_death102, + FRAME_c_death103, + FRAME_c_death104, + FRAME_c_death105, + FRAME_c_death106, + FRAME_c_death107, + FRAME_c_death108, + FRAME_c_death109, + FRAME_c_death110, + FRAME_c_death111, + FRAME_c_death112, + FRAME_c_death113, + FRAME_c_death114, + FRAME_c_death115, + FRAME_c_death116, + FRAME_c_death117, + FRAME_c_death118, + FRAME_c_death201, + FRAME_c_death202, + FRAME_c_death203, + FRAME_c_death204, + FRAME_c_death301, + FRAME_c_death302, + FRAME_c_death303, + FRAME_c_death304, + FRAME_c_death305, + FRAME_c_death306, + FRAME_c_death307, + FRAME_c_death308, + FRAME_c_death309, + FRAME_c_death310, + FRAME_c_death311, + FRAME_c_death312, + FRAME_c_death313, + FRAME_c_death314, + FRAME_c_death315, + FRAME_c_death316, + FRAME_c_death317, + FRAME_c_death318, + FRAME_c_death319, + FRAME_c_death320, + FRAME_c_death321, + FRAME_c_death401, + FRAME_c_death402, + FRAME_c_death403, + FRAME_c_death404, + FRAME_c_death405, + FRAME_c_death406, + FRAME_c_death407, + FRAME_c_death408, + FRAME_c_death409, + FRAME_c_death410, + FRAME_c_death411, + FRAME_c_death412, + FRAME_c_death413, + FRAME_c_death414, + FRAME_c_death415, + FRAME_c_death416, + FRAME_c_death417, + FRAME_c_death418, + FRAME_c_death419, + FRAME_c_death420, + FRAME_c_death421, + FRAME_c_death422, + FRAME_c_death423, + FRAME_c_death424, + FRAME_c_death425, + FRAME_c_death426, + FRAME_c_death427, + FRAME_c_death428, + FRAME_c_death429, + FRAME_c_death430, + FRAME_c_death431, + FRAME_c_death432, + FRAME_c_death433, + FRAME_c_death434, + FRAME_c_death435, + FRAME_c_death436, + FRAME_c_death501, + FRAME_c_death502, + FRAME_c_death503, + FRAME_c_death504, + FRAME_c_death505, + FRAME_c_death506, + FRAME_c_death507, + FRAME_c_death508, + FRAME_c_death509, + FRAME_c_death510, + FRAME_c_death511, + FRAME_c_death512, + FRAME_c_death513, + FRAME_c_death514, + FRAME_c_death515, + FRAME_c_death516, + FRAME_c_death517, + FRAME_c_death518, + FRAME_c_death519, + FRAME_c_death520, + FRAME_c_death521, + FRAME_c_death522, + FRAME_c_death523, + FRAME_c_death524, + FRAME_c_death525, + FRAME_c_death526, + FRAME_c_death527, + FRAME_c_death528, + FRAME_c_run101, + FRAME_c_run102, + FRAME_c_run103, + FRAME_c_run104, + FRAME_c_run105, + FRAME_c_run106, + FRAME_c_run201, + FRAME_c_run202, + FRAME_c_run203, + FRAME_c_run204, + FRAME_c_run205, + FRAME_c_run206, + FRAME_c_run301, + FRAME_c_run302, + FRAME_c_run303, + FRAME_c_run304, + FRAME_c_run305, + FRAME_c_run306, + FRAME_c_walk101, + FRAME_c_walk102, + FRAME_c_walk103, + FRAME_c_walk104, + FRAME_c_walk105, + FRAME_c_walk106, + FRAME_c_walk107, + FRAME_c_walk108, + FRAME_c_walk109, + FRAME_c_walk110, + FRAME_c_walk111, + FRAME_c_walk112, + FRAME_c_walk113, + FRAME_c_walk114, + FRAME_c_walk115, + FRAME_c_walk116, + FRAME_c_walk117, + FRAME_c_walk118, + FRAME_c_walk119, + FRAME_c_walk120, + FRAME_c_walk121, + FRAME_c_walk122, + FRAME_c_walk123, + FRAME_c_walk124, + FRAME_c_pain601, + FRAME_c_pain602, + FRAME_c_pain603, + FRAME_c_pain604, + FRAME_c_pain605, + FRAME_c_pain606, + FRAME_c_pain607, + FRAME_c_pain608, + FRAME_c_pain609, + FRAME_c_pain610, + FRAME_c_pain611, + FRAME_c_pain612, + FRAME_c_pain613, + FRAME_c_pain614, + FRAME_c_pain615, + FRAME_c_pain616, + FRAME_c_pain617, + FRAME_c_pain618, + FRAME_c_pain619, + FRAME_c_pain620, + FRAME_c_pain621, + FRAME_c_pain622, + FRAME_c_pain623, + FRAME_c_pain624, + FRAME_c_pain625, + FRAME_c_pain626, + FRAME_c_pain627, + FRAME_c_pain628, + FRAME_c_pain629, + FRAME_c_pain630, + FRAME_c_pain631, + FRAME_c_pain632, + FRAME_c_death601, + FRAME_c_death602, + FRAME_c_death603, + FRAME_c_death604, + FRAME_c_death605, + FRAME_c_death606, + FRAME_c_death607, + FRAME_c_death608, + FRAME_c_death609, + FRAME_c_death610, + FRAME_c_death611, + FRAME_c_death612, + FRAME_c_death613, + FRAME_c_death614, + FRAME_c_death701, + FRAME_c_death702, + FRAME_c_death703, + FRAME_c_death704, + FRAME_c_death705, + FRAME_c_death706, + FRAME_c_death707, + FRAME_c_death708, + FRAME_c_death709, + FRAME_c_death710, + FRAME_c_death711, + FRAME_c_death712, + FRAME_c_death713, + FRAME_c_death714, + FRAME_c_death715, + FRAME_c_death716, + FRAME_c_death717, + FRAME_c_death718, + FRAME_c_death719, + FRAME_c_death720, + FRAME_c_death721, + FRAME_c_death722, + FRAME_c_death723, + FRAME_c_death724, + FRAME_c_death725, + FRAME_c_death726, + FRAME_c_death727, + FRAME_c_death728, + FRAME_c_death729, + FRAME_c_death730, + FRAME_c_pain701, + FRAME_c_pain702, + FRAME_c_pain703, + FRAME_c_pain704, + FRAME_c_pain705, + FRAME_c_pain706, + FRAME_c_pain707, + FRAME_c_pain708, + FRAME_c_pain709, + FRAME_c_pain710, + FRAME_c_pain711, + FRAME_c_pain712, + FRAME_c_pain713, + FRAME_c_pain714, + FRAME_c_attack801, + FRAME_c_attack802, + FRAME_c_attack803, + FRAME_c_attack804, + FRAME_c_attack805, + FRAME_c_attack806, + FRAME_c_attack807, + FRAME_c_attack808, + FRAME_c_attack809, + FRAME_c_attack901, + FRAME_c_attack902, + FRAME_c_attack903, + FRAME_c_attack904, + FRAME_c_attack905, + FRAME_c_attack906, + FRAME_c_attack907, + FRAME_c_attack908, + FRAME_c_attack909, + FRAME_c_attack910, + FRAME_c_attack911, + FRAME_c_attack912, + FRAME_c_attack913, + FRAME_c_attack914, + FRAME_c_attack915, + FRAME_c_attack916, + FRAME_c_attack917, + FRAME_c_attack918, + FRAME_c_attack919, + FRAME_c_duck01, + FRAME_c_duck02, + FRAME_c_duckstep01, + FRAME_c_duckstep02, + FRAME_c_duckstep03, + FRAME_c_duckstep04, + FRAME_c_duckstep05, + FRAME_c_duckstep06, + FRAME_c_duckpain01, + FRAME_c_duckpain02, + FRAME_c_duckpain03, + FRAME_c_duckpain04, + FRAME_c_duckpain05, + FRAME_c_duckdeath01, + FRAME_c_duckdeath02, + FRAME_c_duckdeath03, + FRAME_c_duckdeath04, + FRAME_c_duckdeath05, + FRAME_c_duckdeath06, + FRAME_c_duckdeath07, + FRAME_c_duckdeath08, + FRAME_c_duckdeath09, + FRAME_c_duckdeath10, + FRAME_c_duckdeath11, + FRAME_c_duckdeath12, + FRAME_c_duckdeath13, + FRAME_c_duckdeath14, + FRAME_c_duckdeath15, + FRAME_c_duckdeath16, + FRAME_c_duckdeath17, + FRAME_c_duckdeath18, + FRAME_c_duckdeath19, + FRAME_c_duckdeath20, + FRAME_c_duckdeath21, + FRAME_c_duckdeath22, + FRAME_c_duckdeath23, + FRAME_c_duckdeath24, + FRAME_c_duckdeath25, + FRAME_c_duckdeath26, + FRAME_c_duckdeath27, + FRAME_c_duckdeath28, + FRAME_c_duckdeath29 +}; + +constexpr float MODEL_SCALE = 1.150000f; diff --git a/src/quake2/monsterframes/m_infantry.h b/src/quake2/monsterframes/m_infantry.h index d0314541..8c29a668 100644 --- a/src/quake2/monsterframes/m_infantry.h +++ b/src/quake2/monsterframes/m_infantry.h @@ -209,5 +209,62 @@ #define FRAME_attak206 204 #define FRAME_attak207 205 #define FRAME_attak208 206 +#define FRAME_jump01 207 +#define FRAME_jump02 208 +#define FRAME_jump03 209 +#define FRAME_jump04 210 +#define FRAME_jump05 211 +#define FRAME_jump06 212 +#define FRAME_jump07 213 +#define FRAME_jump08 214 +#define FRAME_jump09 215 +#define FRAME_jump10 216 +#define FRAME_attak301 217 +#define FRAME_attak302 218 +#define FRAME_attak303 219 +#define FRAME_attak304 220 +#define FRAME_attak305 221 +#define FRAME_attak306 222 +#define FRAME_attak307 223 +#define FRAME_attak308 224 +#define FRAME_attak309 225 +#define FRAME_attak310 226 +#define FRAME_attak311 227 +#define FRAME_attak312 228 +#define FRAME_attak313 229 +#define FRAME_attak314 230 +#define FRAME_attak315 231 +#define FRAME_run201 232 +#define FRAME_run202 233 +#define FRAME_run203 234 +#define FRAME_run204 235 +#define FRAME_run205 236 +#define FRAME_run206 237 +#define FRAME_run207 238 +#define FRAME_run208 239 +#define FRAME_attak401 240 +#define FRAME_attak402 241 +#define FRAME_attak403 242 +#define FRAME_attak404 243 +#define FRAME_attak405 244 +#define FRAME_attak406 245 +#define FRAME_attak407 246 +#define FRAME_attak408 247 +#define FRAME_attak409 248 +#define FRAME_attak410 249 +#define FRAME_attak411 250 +#define FRAME_attak412 251 +#define FRAME_attak413 252 +#define FRAME_attak414 253 +#define FRAME_attak415 254 +#define FRAME_attak416 255 +#define FRAME_attak417 256 +#define FRAME_attak418 257 +#define FRAME_attak419 258 +#define FRAME_attak420 259 +#define FRAME_attak421 260 +#define FRAME_attak422 261 +#define FRAME_attak423 262 +#define FRAME_attak424 263 #define MODEL_SCALE 1.000000 diff --git a/src/quake2/monsterframes/m_mutant.h b/src/quake2/monsterframes/m_mutant.h index 7afd6930..f56d00bd 100644 --- a/src/quake2/monsterframes/m_mutant.h +++ b/src/quake2/monsterframes/m_mutant.h @@ -152,4 +152,11 @@ #define FRAME_walk22 147 #define FRAME_walk23 148 +// ROGUE +#define FRAME_jump01 149 +#define FRAME_jump02 150 +#define FRAME_jump03 151 +#define FRAME_jump04 152 +#define FRAME_jump05 153 + #define MODEL_SCALE 1.000000 diff --git a/src/quake2/monsterframes/m_parasite.h b/src/quake2/monsterframes/m_parasite.h index d895ce1d..2c768cfa 100644 --- a/src/quake2/monsterframes/m_parasite.h +++ b/src/quake2/monsterframes/m_parasite.h @@ -121,4 +121,14 @@ #define FRAME_stand34 116 #define FRAME_stand35 117 +// ROGUE +#define FRAME_jump01 118 +#define FRAME_jump02 119 +#define FRAME_jump03 120 +#define FRAME_jump04 121 +#define FRAME_jump05 122 +#define FRAME_jump06 123 +#define FRAME_jump07 124 +#define FRAME_jump08 125 + #define MODEL_SCALE 1.000000 diff --git a/src/quake2/monsterframes/m_redmutant.h b/src/quake2/monsterframes/m_redmutant.h new file mode 100644 index 00000000..b8089e95 --- /dev/null +++ b/src/quake2/monsterframes/m_redmutant.h @@ -0,0 +1,156 @@ +#pragma once +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. +// RedMutant + +// This file generated by ModelGen - Do NOT Modify + +enum +{ + FRAME_attack101, + FRAME_attack102, + FRAME_attack103, + FRAME_attack104, + FRAME_attack105, + FRAME_attack106, + FRAME_attack107, + FRAME_attack108, + FRAME_attack109, + FRAME_attack110, + FRAME_attack111, + FRAME_attack112, + FRAME_attack113, + FRAME_attack114, + FRAME_attack115, + FRAME_attack116, + FRAME_attack117, + FRAME_attack118, + FRAME_attack119, + FRAME_attack120, + FRAME_attack121, + FRAME_attack122, + FRAME_attack123, + FRAME_attack124, + FRAME_attack125, + FRAME_attack126, + FRAME_death101, + FRAME_death102, + FRAME_death103, + FRAME_death104, + FRAME_death105, + FRAME_death106, + FRAME_death107, + FRAME_death108, + FRAME_death109, + FRAME_death110, + FRAME_death111, + FRAME_death112, + FRAME_death113, + FRAME_death114, + FRAME_death115, + FRAME_death116, + FRAME_death117, + FRAME_death118, + FRAME_death119, + FRAME_death120, + FRAME_death201, + FRAME_death202, + FRAME_death203, + FRAME_death204, + FRAME_death205, + FRAME_death206, + FRAME_death207, + FRAME_pain101, + FRAME_pain102, + FRAME_pain103, + FRAME_pain104, + FRAME_pain105, + FRAME_pain201, + FRAME_pain202, + FRAME_pain203, + FRAME_pain204, + FRAME_pain205, + FRAME_pain206, + FRAME_pain301, + FRAME_pain302, + FRAME_pain303, + FRAME_pain304, + FRAME_pain305, + FRAME_pain306, + FRAME_pain307, + FRAME_pain308, + FRAME_pain309, + FRAME_pain310, + FRAME_pain311, + FRAME_run03, + FRAME_run04, + FRAME_run05, + FRAME_run06, + FRAME_run07, + FRAME_run08, + FRAME_stand101, + FRAME_stand102, + FRAME_stand103, + FRAME_stand104, + FRAME_stand105, + FRAME_stand106, + FRAME_stand107, + FRAME_stand108, + FRAME_stand109, + FRAME_stand110, + FRAME_stand111, + FRAME_stand112, + FRAME_stand201, + FRAME_stand202, + FRAME_stand203, + FRAME_stand204, + FRAME_stand205, + FRAME_stand206, + FRAME_stand207, + FRAME_stand208, + FRAME_stand209, + FRAME_stand210, + FRAME_stand211, + FRAME_stand212, + FRAME_stand213, + FRAME_stand214, + FRAME_stand215, + FRAME_stand216, + FRAME_stand217, + FRAME_stand218, + FRAME_stand219, + FRAME_stand220, + FRAME_stand221, + FRAME_stand222, + FRAME_stand223, + FRAME_stand224, + FRAME_stand225, + FRAME_stand226, + FRAME_stand227, + FRAME_stand228, + FRAME_walk01, + FRAME_walk02, + FRAME_walk03, + FRAME_walk04, + FRAME_walk05, + FRAME_walk06, + FRAME_walk07, + FRAME_walk08, + FRAME_walk09, + FRAME_walk10, + FRAME_walk11, + FRAME_walk12, + FRAME_walk13, + FRAME_walk14, + FRAME_walk15, + FRAME_walk16, + FRAME_walk17, + FRAME_walk18, + FRAME_walk19, + FRAME_walk20, + FRAME_walk21, + FRAME_walk22, + FRAME_walk23, +}; + +constexpr float MODEL_SCALE = 1.000000f; diff --git a/src/quake2/monsterframes/m_rogue_carrier.h b/src/quake2/monsterframes/m_rogue_carrier.h new file mode 100644 index 00000000..ec94e855 --- /dev/null +++ b/src/quake2/monsterframes/m_rogue_carrier.h @@ -0,0 +1,86 @@ +// Licensed under the GNU General Public License 2.0. +// xpack/models/monsters/carrier + +enum +{ + FRAME_search01, + FRAME_search02, + FRAME_search03, + FRAME_search04, + FRAME_search05, + FRAME_search06, + FRAME_search07, + FRAME_search08, + FRAME_search09, + FRAME_search10, + FRAME_search11, + FRAME_search12, + FRAME_search13, + FRAME_firea01, + FRAME_firea02, + FRAME_firea03, + FRAME_firea04, + FRAME_firea05, + FRAME_firea06, + FRAME_firea07, + FRAME_firea08, + FRAME_firea09, + FRAME_firea10, + FRAME_firea11, + FRAME_firea12, + FRAME_firea13, + FRAME_firea14, + FRAME_firea15, + FRAME_fireb01, + FRAME_fireb02, + FRAME_fireb03, + FRAME_fireb04, + FRAME_fireb05, + FRAME_fireb06, + FRAME_fireb07, + FRAME_fireb08, + FRAME_fireb09, + FRAME_fireb10, + FRAME_fireb11, + FRAME_fireb12, + FRAME_fireb13, + FRAME_fireb14, + FRAME_fireb15, + FRAME_fireb16, + FRAME_spawn01, + FRAME_spawn02, + FRAME_spawn03, + FRAME_spawn04, + FRAME_spawn05, + FRAME_spawn06, + FRAME_spawn07, + FRAME_spawn08, + FRAME_spawn09, + FRAME_spawn10, + FRAME_spawn11, + FRAME_spawn12, + FRAME_spawn13, + FRAME_spawn14, + FRAME_spawn15, + FRAME_spawn16, + FRAME_spawn17, + FRAME_spawn18, + FRAME_death01, + FRAME_death02, + FRAME_death03, + FRAME_death04, + FRAME_death05, + FRAME_death06, + FRAME_death07, + FRAME_death08, + FRAME_death09, + FRAME_death10, + FRAME_death11, + FRAME_death12, + FRAME_death13, + FRAME_death14, + FRAME_death15, + FRAME_death16 +}; + +constexpr float MODEL_SCALE = 2.000000f; diff --git a/src/quake2/monsterframes/m_rogue_stalker.h b/src/quake2/monsterframes/m_rogue_stalker.h new file mode 100644 index 00000000..160851a4 --- /dev/null +++ b/src/quake2/monsterframes/m_rogue_stalker.h @@ -0,0 +1,104 @@ +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. +// /expanse/quake2/xpack/models/monsters/stalker + +// This file generated by qdata - Do NOT Modify + +enum +{ + FRAME_idle01, + FRAME_idle02, + FRAME_idle03, + FRAME_idle04, + FRAME_idle05, + FRAME_idle06, + FRAME_idle07, + FRAME_idle08, + FRAME_idle09, + FRAME_idle10, + FRAME_idle11, + FRAME_idle12, + FRAME_idle13, + FRAME_idle14, + FRAME_idle15, + FRAME_idle16, + FRAME_idle17, + FRAME_idle18, + FRAME_idle19, + FRAME_idle20, + FRAME_idle21, + FRAME_idle201, + FRAME_idle202, + FRAME_idle203, + FRAME_idle204, + FRAME_idle205, + FRAME_idle206, + FRAME_idle207, + FRAME_idle208, + FRAME_idle209, + FRAME_idle210, + FRAME_idle211, + FRAME_idle212, + FRAME_idle213, + FRAME_walk01, + FRAME_walk02, + FRAME_walk03, + FRAME_walk04, + FRAME_walk05, + FRAME_walk06, + FRAME_walk07, + FRAME_walk08, + FRAME_jump01, + FRAME_jump02, + FRAME_jump03, + FRAME_jump04, + FRAME_jump05, + FRAME_jump06, + FRAME_jump07, + FRAME_run01, + FRAME_run02, + FRAME_run03, + FRAME_run04, + FRAME_attack01, + FRAME_attack02, + FRAME_attack03, + FRAME_attack04, + FRAME_attack05, + FRAME_attack06, + FRAME_attack07, + FRAME_attack08, + FRAME_attack11, + FRAME_attack12, + FRAME_attack13, + FRAME_attack14, + FRAME_attack15, + FRAME_pain01, + FRAME_pain02, + FRAME_pain03, + FRAME_pain04, + FRAME_death01, + FRAME_death02, + FRAME_death03, + FRAME_death04, + FRAME_death05, + FRAME_death06, + FRAME_death07, + FRAME_death08, + FRAME_death09, + FRAME_twitch01, + FRAME_twitch02, + FRAME_twitch03, + FRAME_twitch04, + FRAME_twitch05, + FRAME_twitch06, + FRAME_twitch07, + FRAME_twitch08, + FRAME_twitch09, + FRAME_twitch10, + FRAME_reactive01, + FRAME_reactive02, + FRAME_reactive03, + FRAME_reactive04 +}; + +constexpr float MODEL_SCALE = 1.000000f; diff --git a/src/quake2/monsterframes/m_runnertank.h b/src/quake2/monsterframes/m_runnertank.h new file mode 100644 index 00000000..65fe22bd --- /dev/null +++ b/src/quake2/monsterframes/m_runnertank.h @@ -0,0 +1,266 @@ +#pragma once +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. +// G:\quake2\baseq2\models/monsters/parasite + +// This file generated by ModelGen - Do NOT Modify + +enum +{ + FRAME_stand01, + FRAME_stand02, + FRAME_stand03, + FRAME_stand04, + FRAME_stand05, + FRAME_stand06, + FRAME_stand07, + FRAME_stand08, + FRAME_stand09, + FRAME_stand10, + FRAME_stand11, + FRAME_stand12, + FRAME_stand13, + FRAME_stand14, + FRAME_stand15, + FRAME_stand16, + FRAME_stand17, + FRAME_stand18, + FRAME_stand19, + FRAME_stand20, + FRAME_stand21, + FRAME_stand22, + FRAME_stand23, + FRAME_stand24, + FRAME_stand25, + FRAME_stand26, + FRAME_stand27, + FRAME_stand28, + FRAME_stand29, + FRAME_stand30, + FRAME_walk01, + FRAME_walk02, + FRAME_walk03, + FRAME_walk04, + FRAME_walk05, + FRAME_walk06, + FRAME_walk07, + FRAME_walk08, + FRAME_walk09, + FRAME_walk10, + FRAME_walk11, + FRAME_walk12, + FRAME_walk13, + FRAME_walk14, + FRAME_walk15, + FRAME_walk16, + FRAME_walk17, + FRAME_walk18, + FRAME_walk19, + FRAME_walk20, + FRAME_walk21, + FRAME_walk22, + FRAME_walk23, + FRAME_walk24, + FRAME_walk25, + FRAME_walk26, + FRAME_walk27, + FRAME_walk28, + FRAME_walk29, + FRAME_walk30, + FRAME_walk31, + FRAME_walk32, + FRAME_walk33, + FRAME_walk34, + FRAME_walk35, + FRAME_walk36, + FRAME_walk37, + FRAME_walk38, + FRAME_run01, + FRAME_run02, + FRAME_run03, + FRAME_run04, + FRAME_run05, + FRAME_run06, + FRAME_run07, + FRAME_run08, + FRAME_run09, + FRAME_run10, + FRAME_attak101, + FRAME_attak102, + FRAME_attak103, + FRAME_attak104, + FRAME_attak105, + FRAME_attak106, + FRAME_attak107, + FRAME_attak108, + FRAME_attak109, + FRAME_attak110, + FRAME_attak111, + FRAME_attak112, + FRAME_attak113, + FRAME_attak114, + FRAME_attak115, + FRAME_attak116, + FRAME_attak117, + FRAME_attak118, + FRAME_attak119, + FRAME_attak120, + FRAME_attak121, + FRAME_attak122, + FRAME_attak201, + FRAME_attak202, + FRAME_attak203, + FRAME_attak204, + FRAME_attak205, + FRAME_attak206, + FRAME_attak207, + FRAME_attak208, + FRAME_attak209, + FRAME_attak210, + FRAME_attak211, + FRAME_attak212, + FRAME_attak213, + FRAME_attak214, + FRAME_attak215, + FRAME_attak216, + FRAME_attak217, + FRAME_attak218, + FRAME_attak219, + FRAME_attak220, + FRAME_attak221, + FRAME_attak222, + FRAME_attak223, + FRAME_attak224, + FRAME_attak225, + FRAME_attak226, + FRAME_attak227, + FRAME_attak228, + FRAME_attak229, + FRAME_attak230, + FRAME_attak231, + FRAME_attak232, + FRAME_attak233, + FRAME_attak234, + FRAME_attak235, + FRAME_attak236, + FRAME_attak237, + FRAME_attak238, + FRAME_attak301, + FRAME_attak302, + FRAME_attak303, + FRAME_attak304, + FRAME_attak305, + FRAME_attak306, + FRAME_attak307, + FRAME_attak308, + FRAME_attak309, + FRAME_attak310, + FRAME_attak311, + FRAME_attak312, + FRAME_attak313, + FRAME_attak314, + FRAME_attak315, + FRAME_attak316, + FRAME_attak317, + FRAME_attak318, + FRAME_attak319, + FRAME_attak320, + FRAME_attak321, + FRAME_attak322, + FRAME_attak323, + FRAME_attak324, + FRAME_attak325, + FRAME_attak326, + FRAME_attak327, + FRAME_attak328, + FRAME_attak329, + FRAME_attak330, + FRAME_attak331, + FRAME_attak332, + FRAME_attak333, + FRAME_attak334, + FRAME_attak335, + FRAME_attak401, + FRAME_attak402, + FRAME_attak403, + FRAME_attak404, + FRAME_attak405, + FRAME_attak406, + FRAME_attak407, + FRAME_attak408, + FRAME_attak409, + FRAME_attak410, + FRAME_attak411, + FRAME_attak412, + FRAME_attak413, + FRAME_attak414, + FRAME_attak415, + FRAME_attak416, + FRAME_attak417, + FRAME_attak418, + FRAME_attak419, + FRAME_attak420, + FRAME_attak421, + FRAME_attak422, + FRAME_attak423, + FRAME_attak424, + FRAME_attak425, + FRAME_attak426, + FRAME_attak427, + FRAME_attak428, + FRAME_attak429, + FRAME_pain201, + FRAME_pain202, + FRAME_pain203, + FRAME_pain204, + FRAME_pain301, + FRAME_pain302, + FRAME_pain303, + FRAME_pain304, + FRAME_pain305, + FRAME_pain306, + FRAME_pain307, + FRAME_pain308, + FRAME_pain309, + FRAME_pain310, + FRAME_pain311, + FRAME_pain312, + FRAME_pain313, + FRAME_pain314, + FRAME_pain315, + FRAME_pain316, + FRAME_death01, + FRAME_death02, + FRAME_death03, + FRAME_death04, + FRAME_death05, + FRAME_death06, + FRAME_death07, + FRAME_death08, + FRAME_death09, + FRAME_death10, + FRAME_death11, + FRAME_death12, + FRAME_death13, + FRAME_death14, + FRAME_death15, + FRAME_death16, + FRAME_death17, + FRAME_death18, + FRAME_death19, + FRAME_death20, + FRAME_death21, + FRAME_death22, + FRAME_death23, + FRAME_death24, + FRAME_death25, + FRAME_death26, + FRAME_death27, + FRAME_death28, + FRAME_death29, + FRAME_death30, + FRAME_death31, + FRAME_death32, +}; + +constexpr float MODEL_SCALE = 1.000000f; diff --git a/src/quake2/monsterframes/m_soldier.h b/src/quake2/monsterframes/m_soldier.h index a14b8cc4..07bf5d14 100644 --- a/src/quake2/monsterframes/m_soldier.h +++ b/src/quake2/monsterframes/m_soldier.h @@ -477,5 +477,105 @@ #define FRAME_death608 472 #define FRAME_death609 473 #define FRAME_death610 474 +#define FRAME_stand401 475 +#define FRAME_stand402 476 +#define FRAME_stand403 477 +#define FRAME_stand404 478 +#define FRAME_stand405 479 +#define FRAME_stand406 480 +#define FRAME_stand407 481 +#define FRAME_stand408 482 +#define FRAME_stand409 483 +#define FRAME_stand410 484 +#define FRAME_stand411 485 +#define FRAME_stand412 486 +#define FRAME_stand413 487 +#define FRAME_stand414 488 +#define FRAME_stand415 489 +#define FRAME_stand416 490 +#define FRAME_stand417 491 +#define FRAME_stand418 492 +#define FRAME_stand419 493 +#define FRAME_stand420 494 +#define FRAME_stand421 495 +#define FRAME_stand422 496 +#define FRAME_stand423 497 +#define FRAME_stand424 498 +#define FRAME_stand425 499 +#define FRAME_stand426 500 +#define FRAME_stand427 501 +#define FRAME_stand428 502 +#define FRAME_stand429 503 +#define FRAME_stand430 504 +#define FRAME_stand431 505 +#define FRAME_stand432 506 +#define FRAME_stand433 507 +#define FRAME_stand434 508 +#define FRAME_stand435 509 +#define FRAME_stand436 510 +#define FRAME_stand437 511 +#define FRAME_stand438 512 +#define FRAME_stand439 513 +#define FRAME_stand440 514 +#define FRAME_stand441 515 +#define FRAME_stand442 516 +#define FRAME_stand443 517 +#define FRAME_stand444 518 +#define FRAME_stand445 519 +#define FRAME_stand446 520 +#define FRAME_stand447 521 +#define FRAME_stand448 522 +#define FRAME_stand449 523 +#define FRAME_stand450 524 +#define FRAME_stand451 525 +#define FRAME_stand452 526 +#define FRAME_stand201 527 +#define FRAME_stand202 528 +#define FRAME_stand203 529 +#define FRAME_stand204 530 +#define FRAME_stand205 531 +#define FRAME_stand206 532 +#define FRAME_stand207 533 +#define FRAME_stand208 534 +#define FRAME_stand209 535 +#define FRAME_stand210 536 +#define FRAME_stand211 537 +#define FRAME_stand212 538 +#define FRAME_stand213 539 +#define FRAME_stand214 540 +#define FRAME_stand215 541 +#define FRAME_stand216 542 +#define FRAME_stand217 543 +#define FRAME_stand218 544 +#define FRAME_stand219 545 +#define FRAME_stand220 546 +#define FRAME_stand221 547 +#define FRAME_stand222 548 +#define FRAME_stand223 549 +#define FRAME_stand224 550 +#define FRAME_stand225 551 +#define FRAME_stand226 552 +#define FRAME_stand227 553 +#define FRAME_stand228 554 +#define FRAME_stand229 555 +#define FRAME_stand230 556 +#define FRAME_stand231 557 +#define FRAME_stand232 558 +#define FRAME_stand233 559 +#define FRAME_stand234 560 +#define FRAME_stand235 561 +#define FRAME_stand236 562 +#define FRAME_stand237 563 +#define FRAME_stand238 564 +#define FRAME_stand239 565 +#define FRAME_stand240 566 +#define FRAME_attak501 567 +#define FRAME_attak502 568 +#define FRAME_attak503 569 +#define FRAME_attak504 570 +#define FRAME_attak505 571 +#define FRAME_attak506 572 +#define FRAME_attak507 573 +#define FRAME_attak508 574 #define MODEL_SCALE 1.200000 diff --git a/src/quake2/monsterframes/m_xatrix_gekk.h b/src/quake2/monsterframes/m_xatrix_gekk.h new file mode 100644 index 00000000..db7ebb84 --- /dev/null +++ b/src/quake2/monsterframes/m_xatrix_gekk.h @@ -0,0 +1,361 @@ +// Copyright (c) ZeniMax Media Inc. +// Licensed under the GNU General Public License 2.0. +// ./gekk + +// This file generated by ModelGen - Do NOT Modify + +enum +{ + FRAME_stand_01, + FRAME_stand_02, + FRAME_stand_03, + FRAME_stand_04, + FRAME_stand_05, + FRAME_stand_06, + FRAME_stand_07, + FRAME_stand_08, + FRAME_stand_09, + FRAME_stand_10, + FRAME_stand_11, + FRAME_stand_12, + FRAME_stand_13, + FRAME_stand_14, + FRAME_stand_15, + FRAME_stand_16, + FRAME_stand_17, + FRAME_stand_18, + FRAME_stand_19, + FRAME_stand_20, + FRAME_stand_21, + FRAME_stand_22, + FRAME_stand_23, + FRAME_stand_24, + FRAME_stand_25, + FRAME_stand_26, + FRAME_stand_27, + FRAME_stand_28, + FRAME_stand_29, + FRAME_stand_30, + FRAME_stand_31, + FRAME_stand_32, + FRAME_stand_33, + FRAME_stand_34, + FRAME_stand_35, + FRAME_stand_36, + FRAME_stand_37, + FRAME_stand_38, + FRAME_stand_39, + FRAME_run_01, + FRAME_run_02, + FRAME_run_03, + FRAME_run_04, + FRAME_run_05, + FRAME_run_06, + FRAME_clawatk3_01, + FRAME_clawatk3_02, + FRAME_clawatk3_03, + FRAME_clawatk3_04, + FRAME_clawatk3_05, + FRAME_clawatk3_06, + FRAME_clawatk3_07, + FRAME_clawatk3_08, + FRAME_clawatk3_09, + FRAME_clawatk4_01, + FRAME_clawatk4_02, + FRAME_clawatk4_03, + FRAME_clawatk4_04, + FRAME_clawatk4_05, + FRAME_clawatk4_06, + FRAME_clawatk4_07, + FRAME_clawatk4_08, + FRAME_clawatk5_01, + FRAME_clawatk5_02, + FRAME_clawatk5_03, + FRAME_clawatk5_04, + FRAME_clawatk5_05, + FRAME_clawatk5_06, + FRAME_clawatk5_07, + FRAME_clawatk5_08, + FRAME_clawatk5_09, + FRAME_leapatk_01, + FRAME_leapatk_02, + FRAME_leapatk_03, + FRAME_leapatk_04, + FRAME_leapatk_05, + FRAME_leapatk_06, + FRAME_leapatk_07, + FRAME_leapatk_08, + FRAME_leapatk_09, + FRAME_leapatk_10, + FRAME_leapatk_11, + FRAME_leapatk_12, + FRAME_leapatk_13, + FRAME_leapatk_14, + FRAME_leapatk_15, + FRAME_leapatk_16, + FRAME_leapatk_17, + FRAME_leapatk_18, + FRAME_leapatk_19, + FRAME_pain3_01, + FRAME_pain3_02, + FRAME_pain3_03, + FRAME_pain3_04, + FRAME_pain3_05, + FRAME_pain3_06, + FRAME_pain3_07, + FRAME_pain3_08, + FRAME_pain3_09, + FRAME_pain3_10, + FRAME_pain3_11, + FRAME_pain4_01, + FRAME_pain4_02, + FRAME_pain4_03, + FRAME_pain4_04, + FRAME_pain4_05, + FRAME_pain4_06, + FRAME_pain4_07, + FRAME_pain4_08, + FRAME_pain4_09, + FRAME_pain4_10, + FRAME_pain4_11, + FRAME_pain4_12, + FRAME_pain4_13, + FRAME_death1_01, + FRAME_death1_02, + FRAME_death1_03, + FRAME_death1_04, + FRAME_death1_05, + FRAME_death1_06, + FRAME_death1_07, + FRAME_death1_08, + FRAME_death1_09, + FRAME_death1_10, + FRAME_death2_01, + FRAME_death2_02, + FRAME_death2_03, + FRAME_death2_04, + FRAME_death2_05, + FRAME_death2_06, + FRAME_death2_07, + FRAME_death2_08, + FRAME_death2_09, + FRAME_death2_10, + FRAME_death2_11, + FRAME_death3_01, + FRAME_death3_02, + FRAME_death3_03, + FRAME_death3_04, + FRAME_death3_05, + FRAME_death3_06, + FRAME_death3_07, + FRAME_death4_01, + FRAME_death4_02, + FRAME_death4_03, + FRAME_death4_04, + FRAME_death4_05, + FRAME_death4_06, + FRAME_death4_07, + FRAME_death4_08, + FRAME_death4_09, + FRAME_death4_10, + FRAME_death4_11, + FRAME_death4_12, + FRAME_death4_13, + FRAME_death4_14, + FRAME_death4_15, + FRAME_death4_16, + FRAME_death4_17, + FRAME_death4_18, + FRAME_death4_19, + FRAME_death4_20, + FRAME_death4_21, + FRAME_death4_22, + FRAME_death4_23, + FRAME_death4_24, + FRAME_death4_25, + FRAME_death4_26, + FRAME_death4_27, + FRAME_death4_28, + FRAME_death4_29, + FRAME_death4_30, + FRAME_death4_31, + FRAME_death4_32, + FRAME_death4_33, + FRAME_death4_34, + FRAME_death4_35, + FRAME_rduck_01, + FRAME_rduck_02, + FRAME_rduck_03, + FRAME_rduck_04, + FRAME_rduck_05, + FRAME_rduck_06, + FRAME_rduck_07, + FRAME_rduck_08, + FRAME_rduck_09, + FRAME_rduck_10, + FRAME_rduck_11, + FRAME_rduck_12, + FRAME_rduck_13, + FRAME_lduck_01, + FRAME_lduck_02, + FRAME_lduck_03, + FRAME_lduck_04, + FRAME_lduck_05, + FRAME_lduck_06, + FRAME_lduck_07, + FRAME_lduck_08, + FRAME_lduck_09, + FRAME_lduck_10, + FRAME_lduck_11, + FRAME_lduck_12, + FRAME_lduck_13, + FRAME_idle_01, + FRAME_idle_02, + FRAME_idle_03, + FRAME_idle_04, + FRAME_idle_05, + FRAME_idle_06, + FRAME_idle_07, + FRAME_idle_08, + FRAME_idle_09, + FRAME_idle_10, + FRAME_idle_11, + FRAME_idle_12, + FRAME_idle_13, + FRAME_idle_14, + FRAME_idle_15, + FRAME_idle_16, + FRAME_idle_17, + FRAME_idle_18, + FRAME_idle_19, + FRAME_idle_20, + FRAME_idle_21, + FRAME_idle_22, + FRAME_idle_23, + FRAME_idle_24, + FRAME_idle_25, + FRAME_idle_26, + FRAME_idle_27, + FRAME_idle_28, + FRAME_idle_29, + FRAME_idle_30, + FRAME_idle_31, + FRAME_idle_32, + FRAME_spit_01, + FRAME_spit_02, + FRAME_spit_03, + FRAME_spit_04, + FRAME_spit_05, + FRAME_spit_06, + FRAME_spit_07, + FRAME_amb_01, + FRAME_amb_02, + FRAME_amb_03, + FRAME_amb_04, + FRAME_wdeath_01, + FRAME_wdeath_02, + FRAME_wdeath_03, + FRAME_wdeath_04, + FRAME_wdeath_05, + FRAME_wdeath_06, + FRAME_wdeath_07, + FRAME_wdeath_08, + FRAME_wdeath_09, + FRAME_wdeath_10, + FRAME_wdeath_11, + FRAME_wdeath_12, + FRAME_wdeath_13, + FRAME_wdeath_14, + FRAME_wdeath_15, + FRAME_wdeath_16, + FRAME_wdeath_17, + FRAME_wdeath_18, + FRAME_wdeath_19, + FRAME_wdeath_20, + FRAME_wdeath_21, + FRAME_wdeath_22, + FRAME_wdeath_23, + FRAME_wdeath_24, + FRAME_wdeath_25, + FRAME_wdeath_26, + FRAME_wdeath_27, + FRAME_wdeath_28, + FRAME_wdeath_29, + FRAME_wdeath_30, + FRAME_wdeath_31, + FRAME_wdeath_32, + FRAME_wdeath_33, + FRAME_wdeath_34, + FRAME_wdeath_35, + FRAME_wdeath_36, + FRAME_wdeath_37, + FRAME_wdeath_38, + FRAME_wdeath_39, + FRAME_wdeath_40, + FRAME_wdeath_41, + FRAME_wdeath_42, + FRAME_wdeath_43, + FRAME_wdeath_44, + FRAME_wdeath_45, + FRAME_swim_01, + FRAME_swim_02, + FRAME_swim_03, + FRAME_swim_04, + FRAME_swim_05, + FRAME_swim_06, + FRAME_swim_07, + FRAME_swim_08, + FRAME_swim_09, + FRAME_swim_10, + FRAME_swim_11, + FRAME_swim_12, + FRAME_swim_13, + FRAME_swim_14, + FRAME_swim_15, + FRAME_swim_16, + FRAME_swim_17, + FRAME_swim_18, + FRAME_swim_19, + FRAME_swim_20, + FRAME_swim_21, + FRAME_swim_22, + FRAME_swim_23, + FRAME_swim_24, + FRAME_swim_25, + FRAME_swim_26, + FRAME_swim_27, + FRAME_swim_28, + FRAME_swim_29, + FRAME_swim_30, + FRAME_swim_31, + FRAME_swim_32, + FRAME_attack_01, + FRAME_attack_02, + FRAME_attack_03, + FRAME_attack_04, + FRAME_attack_05, + FRAME_attack_06, + FRAME_attack_07, + FRAME_attack_08, + FRAME_attack_09, + FRAME_attack_10, + FRAME_attack_11, + FRAME_attack_12, + FRAME_attack_13, + FRAME_attack_14, + FRAME_attack_15, + FRAME_attack_16, + FRAME_attack_17, + FRAME_attack_18, + FRAME_attack_19, + FRAME_attack_20, + FRAME_attack_21, + FRAME_pain_01, + FRAME_pain_02, + FRAME_pain_03, + FRAME_pain_04, + FRAME_pain_05, + FRAME_pain_06 +}; + +constexpr float MODEL_SCALE = 1.000000f; diff --git a/src/server/v_luasettings.c b/src/server/v_luasettings.c index 826c2654..3e46a17e 100644 --- a/src/server/v_luasettings.c +++ b/src/server/v_luasettings.c @@ -1075,6 +1075,10 @@ void Lua_LoadVariables() M_MUTANT_ADDON_HEALTH = vrx_lua_get_variable("M_MUTANT_ADDON_HEALTH", 75); M_MUTANT_INITIAL_ARMOR = vrx_lua_get_variable("M_MUTANT_INITIAL_ARMOR", 0); M_MUTANT_ADDON_ARMOR = vrx_lua_get_variable("M_MUTANT_ADDON_ARMOR", 0); + M_REDMUTANT_INITIAL_HEALTH = vrx_lua_get_variable("M_REDMUTANT_INITIAL_HEALTH", 150); + M_REDMUTANT_ADDON_HEALTH = vrx_lua_get_variable("M_REDMUTANT_ADDON_HEALTH", 85); + M_REDMUTANT_INITIAL_ARMOR = vrx_lua_get_variable("M_REDMUTANT_INITIAL_ARMOR", 0); + M_REDMUTANT_ADDON_ARMOR = vrx_lua_get_variable("M_REDMUTANT_ADDON_ARMOR", 0); M_PARASITE_INITIAL_HEALTH = vrx_lua_get_variable("M_PARASITE_INITIAL_HEALTH", 75); M_PARASITE_ADDON_HEALTH = vrx_lua_get_variable("M_PARASITE_ADDON_HEALTH", 30); @@ -1094,6 +1098,103 @@ void Lua_LoadVariables() M_SHAMBLER_INITIAL_HEALTH = vrx_lua_get_variable("M_SHAMBLER_INITIAL_HEALTH", 250); M_SHAMBLER_ADDON_HEALTH = vrx_lua_get_variable("M_SHAMBLER_ADDON_HEALTH", 125); + M_CHICK_HEAT_INITIAL_HEALTH = vrx_lua_get_variable("M_CHICK_HEAT_INITIAL_HEALTH", 50); + M_CHICK_HEAT_ADDON_HEALTH = vrx_lua_get_variable("M_CHICK_HEAT_ADDON_HEALTH", 15); + M_CHICK_HEAT_INITIAL_ARMOR = vrx_lua_get_variable("M_CHICK_HEAT_INITIAL_ARMOR", 25); + M_CHICK_HEAT_ADDON_ARMOR = vrx_lua_get_variable("M_CHICK_HEAT_ADDON_ARMOR", 15); + M_DAEDALUS_INITIAL_HEALTH = vrx_lua_get_variable("M_DAEDALUS_INITIAL_HEALTH", 50); + M_DAEDALUS_ADDON_HEALTH = vrx_lua_get_variable("M_DAEDALUS_ADDON_HEALTH", 20); + M_DAEDALUS_INITIAL_ARMOR = vrx_lua_get_variable("M_DAEDALUS_INITIAL_ARMOR", 150); + M_DAEDALUS_ADDON_ARMOR = vrx_lua_get_variable("M_DAEDALUS_ADDON_ARMOR", 60); + M_RUNNERTANK_INITIAL_HEALTH = vrx_lua_get_variable("M_RUNNERTANK_INITIAL_HEALTH", 100); + M_RUNNERTANK_ADDON_HEALTH = vrx_lua_get_variable("M_RUNNERTANK_ADDON_HEALTH", 65); + M_RUNNERTANK_INITIAL_ARMOR = vrx_lua_get_variable("M_RUNNERTANK_INITIAL_ARMOR", 200); + M_RUNNERTANK_ADDON_ARMOR = vrx_lua_get_variable("M_RUNNERTANK_ADDON_ARMOR", 105); + M_GEKK_INITIAL_HEALTH = vrx_lua_get_variable("M_GEKK_INITIAL_HEALTH", 150); + M_GEKK_ADDON_HEALTH = vrx_lua_get_variable("M_GEKK_ADDON_HEALTH", 85); + M_GEKK_INITIAL_ARMOR = vrx_lua_get_variable("M_GEKK_INITIAL_ARMOR", 0); + M_GEKK_ADDON_ARMOR = vrx_lua_get_variable("M_GEKK_ADDON_ARMOR", 0); + M_STALKER_INITIAL_HEALTH = vrx_lua_get_variable("M_STALKER_INITIAL_HEALTH", 150); + M_STALKER_ADDON_HEALTH = vrx_lua_get_variable("M_STALKER_ADDON_HEALTH", 85); + M_STALKER_INITIAL_ARMOR = vrx_lua_get_variable("M_STALKER_INITIAL_ARMOR", 0); + M_STALKER_ADDON_ARMOR = vrx_lua_get_variable("M_STALKER_ADDON_ARMOR", 0); + M_MEDIC_COMMANDER_INITIAL_HEALTH = vrx_lua_get_variable("M_MEDIC_COMMANDER_INITIAL_HEALTH", 200); + M_MEDIC_COMMANDER_ADDON_HEALTH = vrx_lua_get_variable("M_MEDIC_COMMANDER_ADDON_HEALTH", 20); + M_MEDIC_COMMANDER_INITIAL_ARMOR = vrx_lua_get_variable("M_MEDIC_COMMANDER_INITIAL_ARMOR", 200); + M_MEDIC_COMMANDER_ADDON_ARMOR = vrx_lua_get_variable("M_MEDIC_COMMANDER_ADDON_ARMOR", 40); + M_GUNCMDR_INITIAL_HEALTH = vrx_lua_get_variable("M_GUNCMDR_INITIAL_HEALTH", 100); + M_GUNCMDR_ADDON_HEALTH = vrx_lua_get_variable("M_GUNCMDR_ADDON_HEALTH", 65); + M_GUNCMDR_INITIAL_ARMOR = vrx_lua_get_variable("M_GUNCMDR_INITIAL_ARMOR", 200); + M_GUNCMDR_ADDON_ARMOR = vrx_lua_get_variable("M_GUNCMDR_ADDON_ARMOR", 105); + M_GLADB_INITIAL_HEALTH = vrx_lua_get_variable("M_GLADB_INITIAL_HEALTH", 100); + M_GLADB_ADDON_HEALTH = vrx_lua_get_variable("M_GLADB_ADDON_HEALTH", 10); + M_GLADB_INITIAL_ARMOR = vrx_lua_get_variable("M_GLADB_INITIAL_ARMOR", 100); + M_GLADB_ADDON_ARMOR = vrx_lua_get_variable("M_GLADB_ADDON_ARMOR", 20); + M_GLADC_INITIAL_HEALTH = vrx_lua_get_variable("M_GLADC_INITIAL_HEALTH", 100); + M_GLADC_ADDON_HEALTH = vrx_lua_get_variable("M_GLADC_ADDON_HEALTH", 10); + M_GLADC_INITIAL_ARMOR = vrx_lua_get_variable("M_GLADC_INITIAL_ARMOR", 100); + M_GLADC_ADDON_ARMOR = vrx_lua_get_variable("M_GLADC_ADDON_ARMOR", 20); + M_ARACHNID_INITIAL_HEALTH = vrx_lua_get_variable("M_ARACHNID_INITIAL_HEALTH", 100); + M_ARACHNID_ADDON_HEALTH = vrx_lua_get_variable("M_ARACHNID_ADDON_HEALTH", 10); + M_ARACHNID_INITIAL_ARMOR = vrx_lua_get_variable("M_ARACHNID_INITIAL_ARMOR", 100); + M_ARACHNID_ADDON_ARMOR = vrx_lua_get_variable("M_ARACHNID_ADDON_ARMOR", 20); + M_CARRIER_INITIAL_HEALTH = vrx_lua_get_variable("M_CARRIER_INITIAL_HEALTH", 2200); + M_CARRIER_ADDON_HEALTH = vrx_lua_get_variable("M_CARRIER_ADDON_HEALTH", 650); + M_CARRIER_INITIAL_ARMOR = vrx_lua_get_variable("M_CARRIER_INITIAL_ARMOR", 500); + M_CARRIER_ADDON_ARMOR = vrx_lua_get_variable("M_CARRIER_ADDON_ARMOR", 350); + M_WIDOW_INITIAL_HEALTH = vrx_lua_get_variable("M_WIDOW_INITIAL_HEALTH", 2400); + M_WIDOW_ADDON_HEALTH = vrx_lua_get_variable("M_WIDOW_ADDON_HEALTH", 700); + M_WIDOW_INITIAL_ARMOR = vrx_lua_get_variable("M_WIDOW_INITIAL_ARMOR", 600); + M_WIDOW_ADDON_ARMOR = vrx_lua_get_variable("M_WIDOW_ADDON_ARMOR", 350); + M_WIDOW2_INITIAL_HEALTH = vrx_lua_get_variable("M_WIDOW2_INITIAL_HEALTH", 3000); + M_WIDOW2_ADDON_HEALTH = vrx_lua_get_variable("M_WIDOW2_ADDON_HEALTH", 900); + M_WIDOW2_INITIAL_ARMOR = vrx_lua_get_variable("M_WIDOW2_INITIAL_ARMOR", 750); + M_WIDOW2_ADDON_ARMOR = vrx_lua_get_variable("M_WIDOW2_ADDON_ARMOR", 450); + M_FIXBOT_INITIAL_HEALTH = vrx_lua_get_variable("M_FIXBOT_INITIAL_HEALTH", 250); + M_FIXBOT_ADDON_HEALTH = vrx_lua_get_variable("M_FIXBOT_ADDON_HEALTH", 90); + M_FIXBOT_INITIAL_ARMOR = vrx_lua_get_variable("M_FIXBOT_INITIAL_ARMOR", 120); + M_FIXBOT_ADDON_ARMOR = vrx_lua_get_variable("M_FIXBOT_ADDON_ARMOR", 45); + M_FIXBOT_BOSS_INITIAL_HEALTH = vrx_lua_get_variable("M_FIXBOT_BOSS_INITIAL_HEALTH", 3200); + M_FIXBOT_BOSS_ADDON_HEALTH = vrx_lua_get_variable("M_FIXBOT_BOSS_ADDON_HEALTH", 850); + M_FIXBOT_BOSS_INITIAL_ARMOR = vrx_lua_get_variable("M_FIXBOT_BOSS_INITIAL_ARMOR", 700); + M_FIXBOT_BOSS_ADDON_ARMOR = vrx_lua_get_variable("M_FIXBOT_BOSS_ADDON_ARMOR", 350); + M_ROGUE_TURRET_INITIAL_HEALTH = vrx_lua_get_variable("M_ROGUE_TURRET_INITIAL_HEALTH", 50); + M_ROGUE_TURRET_ADDON_HEALTH = vrx_lua_get_variable("M_ROGUE_TURRET_ADDON_HEALTH", 25); + M_ROGUE_TURRET_INITIAL_ARMOR = vrx_lua_get_variable("M_ROGUE_TURRET_INITIAL_ARMOR", 50); + M_ROGUE_TURRET_ADDON_ARMOR = vrx_lua_get_variable("M_ROGUE_TURRET_ADDON_ARMOR", 15); + M_BOSS2_INITIAL_HEALTH = vrx_lua_get_variable("M_BOSS2_INITIAL_HEALTH", 2400); + M_BOSS2_ADDON_HEALTH = vrx_lua_get_variable("M_BOSS2_ADDON_HEALTH", 700); + M_BOSS2_INITIAL_ARMOR = vrx_lua_get_variable("M_BOSS2_INITIAL_ARMOR", 600); + M_BOSS2_ADDON_ARMOR = vrx_lua_get_variable("M_BOSS2_ADDON_ARMOR", 350); + M_BOSS2_SMALL_INITIAL_HEALTH = vrx_lua_get_variable("M_BOSS2_SMALL_INITIAL_HEALTH", 250); + M_BOSS2_SMALL_ADDON_HEALTH = vrx_lua_get_variable("M_BOSS2_SMALL_ADDON_HEALTH", 100); + M_BOSS2_SMALL_INITIAL_ARMOR = vrx_lua_get_variable("M_BOSS2_SMALL_INITIAL_ARMOR", 160); + M_BOSS2_SMALL_ADDON_ARMOR = vrx_lua_get_variable("M_BOSS2_SMALL_ADDON_ARMOR", 70); + M_GUARDIAN_INITIAL_HEALTH = vrx_lua_get_variable("M_GUARDIAN_INITIAL_HEALTH", 6500); + M_GUARDIAN_ADDON_HEALTH = vrx_lua_get_variable("M_GUARDIAN_ADDON_HEALTH", 1200); + M_GUARDIAN_INITIAL_ARMOR = vrx_lua_get_variable("M_GUARDIAN_INITIAL_ARMOR", 550); + M_GUARDIAN_ADDON_ARMOR = vrx_lua_get_variable("M_GUARDIAN_ADDON_ARMOR", 250); + M_JANITOR_INITIAL_HEALTH = vrx_lua_get_variable("M_JANITOR_INITIAL_HEALTH", 125); + M_JANITOR_ADDON_HEALTH = vrx_lua_get_variable("M_JANITOR_ADDON_HEALTH", 50); + M_JANITOR_INITIAL_ARMOR = vrx_lua_get_variable("M_JANITOR_INITIAL_ARMOR", 300); + M_JANITOR_ADDON_ARMOR = vrx_lua_get_variable("M_JANITOR_ADDON_ARMOR", 100); + M_MINIGUARDIAN_INITIAL_HEALTH = vrx_lua_get_variable("M_MINIGUARDIAN_INITIAL_HEALTH", 400); + M_MINIGUARDIAN_ADDON_HEALTH = vrx_lua_get_variable("M_MINIGUARDIAN_ADDON_HEALTH", 100); + M_MINIGUARDIAN_INITIAL_ARMOR = vrx_lua_get_variable("M_MINIGUARDIAN_INITIAL_ARMOR", 450); + M_MINIGUARDIAN_ADDON_ARMOR = vrx_lua_get_variable("M_MINIGUARDIAN_ADDON_ARMOR", 150); + M_SOLDIER_RIPPER_INITIAL_HEALTH = vrx_lua_get_variable("M_SOLDIER_RIPPER_INITIAL_HEALTH", 100); + M_SOLDIER_RIPPER_ADDON_HEALTH = vrx_lua_get_variable("M_SOLDIER_RIPPER_ADDON_HEALTH", 40); + M_SOLDIER_RIPPER_INITIAL_ARMOR = vrx_lua_get_variable("M_SOLDIER_RIPPER_INITIAL_ARMOR", 50); + M_SOLDIER_RIPPER_ADDON_ARMOR = vrx_lua_get_variable("M_SOLDIER_RIPPER_ADDON_ARMOR", 20); + M_SOLDIER_BLUEBLASTER_INITIAL_HEALTH = vrx_lua_get_variable("M_SOLDIER_BLUEBLASTER_INITIAL_HEALTH", 100); + M_SOLDIER_BLUEBLASTER_ADDON_HEALTH = vrx_lua_get_variable("M_SOLDIER_BLUEBLASTER_ADDON_HEALTH", 40); + M_SOLDIER_BLUEBLASTER_INITIAL_ARMOR = vrx_lua_get_variable("M_SOLDIER_BLUEBLASTER_INITIAL_ARMOR", 50); + M_SOLDIER_BLUEBLASTER_ADDON_ARMOR = vrx_lua_get_variable("M_SOLDIER_BLUEBLASTER_ADDON_ARMOR", 20); + M_SOLDIER_LASER_INITIAL_HEALTH = vrx_lua_get_variable("M_SOLDIER_LASER_INITIAL_HEALTH", 120); + M_SOLDIER_LASER_ADDON_HEALTH = vrx_lua_get_variable("M_SOLDIER_LASER_ADDON_HEALTH", 45); + M_SOLDIER_LASER_INITIAL_ARMOR = vrx_lua_get_variable("M_SOLDIER_LASER_INITIAL_ARMOR", 70); + M_SOLDIER_LASER_ADDON_ARMOR = vrx_lua_get_variable("M_SOLDIER_LASER_ADDON_ARMOR", 25); + M_BRAIN_INITIAL_PULL = vrx_lua_get_variable("M_BRAIN_INITIAL_PULL", -60); M_BRAIN_ADDON_PULL = vrx_lua_get_variable("M_BRAIN_ADDON_PULL", -2); M_BRAIN_MAX_PULL = vrx_lua_get_variable("M_BRAIN_MAX_PULL", -100); @@ -1125,6 +1226,10 @@ void Lua_LoadVariables() M_RAILGUN_DMG_ADDON = vrx_lua_get_variable("M_RAILGUN_DMG_ADDON", 15); M_RAILGUN_DMG_MAX = vrx_lua_get_variable("M_RAILGUN_DMG_MAX", 0); + M_DABEAM_DMG_BASE = vrx_lua_get_variable("M_DABEAM_DMG_BASE", 2); + M_DABEAM_DMG_ADDON = vrx_lua_get_variable("M_DABEAM_DMG_ADDON", 1); + M_DABEAM_DMG_MAX = vrx_lua_get_variable("M_DABEAM_DMG_MAX", 0); + M_MELEE_DMG_BASE = vrx_lua_get_variable("M_MELEE_DMG_BASE", 50); M_MELEE_DMG_ADDON = vrx_lua_get_variable("M_MELEE_DMG_ADDON", 25); M_MELEE_DMG_MAX = vrx_lua_get_variable("M_MELEE_DMG_MAX", 0); @@ -1145,6 +1250,21 @@ void Lua_LoadVariables() M_BLASTER_SPEED_ADDON = vrx_lua_get_variable("M_BLASTER_SPEED_ADDON", 50); M_BLASTER_SPEED_MAX = vrx_lua_get_variable("M_BLASTER_SPEED_MAX", 1500); + M_BLASTER2_DMG_BASE = vrx_lua_get_variable("M_BLASTER2_DMG_BASE", 50); + M_BLASTER2_DMG_ADDON = vrx_lua_get_variable("M_BLASTER2_DMG_ADDON", 2); + M_BLASTER2_DMG_MAX = vrx_lua_get_variable("M_BLASTER2_DMG_MAX", 0); + M_BLASTER2_SPEED_BASE = vrx_lua_get_variable("M_BLASTER2_SPEED_BASE", 500); + M_BLASTER2_SPEED_ADDON = vrx_lua_get_variable("M_BLASTER2_SPEED_ADDON", 0); + M_BLASTER2_SPEED_MAX = vrx_lua_get_variable("M_BLASTER2_SPEED_MAX", 0); + + M_PLASMA_DMG_BASE = vrx_lua_get_variable("M_PLASMA_DMG_BASE", 24); + M_PLASMA_DMG_ADDON = vrx_lua_get_variable("M_PLASMA_DMG_ADDON", 8); + M_PLASMA_DMG_MAX = vrx_lua_get_variable("M_PLASMA_DMG_MAX", 0); + M_PLASMA_SPEED_BASE = vrx_lua_get_variable("M_PLASMA_SPEED_BASE", 850); + M_PLASMA_SPEED_ADDON = vrx_lua_get_variable("M_PLASMA_SPEED_ADDON", 50); + M_PLASMA_SPEED_MAX = vrx_lua_get_variable("M_PLASMA_SPEED_MAX", 1500); + M_PLASMA_DAMAGE_RADIUS = vrx_lua_get_variable("M_PLASMA_DAMAGE_RADIUS", 40); + M_20MM_RANGE_BASE = vrx_lua_get_variable("M_20MM_RANGE_BASE", 750); M_20MM_RANGE_ADDON = vrx_lua_get_variable("M_20MM_RANGE_ADDON", 0); M_20MM_RANGE_MAX = vrx_lua_get_variable("M_20MM_RANGE_MAX", 750); @@ -1152,6 +1272,27 @@ void Lua_LoadVariables() M_20MM_DMG_ADDON = vrx_lua_get_variable("M_20MM_DMG_ADDON", 3); M_20MM_DMG_MAX = vrx_lua_get_variable("M_20MM_DMG_MAX", 0); + M_ETFRIFLE_DMG_BASE = vrx_lua_get_variable("M_ETFRIFLE_DMG_BASE", 10); + M_ETFRIFLE_DMG_ADDON = vrx_lua_get_variable("M_ETFRIFLE_DMG_ADDON", 3); + M_ETFRIFLE_DMG_MAX = vrx_lua_get_variable("M_ETFRIFLE_DMG_MAX", 0); + M_ETFRIFLE_SPEED_BASE = vrx_lua_get_variable("M_ETFRIFLE_SPEED_BASE", 1000); + M_ETFRIFLE_SPEED_ADDON = vrx_lua_get_variable("M_ETFRIFLE_SPEED_ADDON", 50); + M_ETFRIFLE_SPEED_MAX = vrx_lua_get_variable("M_ETFRIFLE_SPEED_MAX", 1500); + + M_IONRIPPER_DMG_BASE = vrx_lua_get_variable("M_IONRIPPER_DMG_BASE", 20); + M_IONRIPPER_DMG_ADDON = vrx_lua_get_variable("M_IONRIPPER_DMG_ADDON", 3); + M_IONRIPPER_DMG_MAX = vrx_lua_get_variable("M_IONRIPPER_DMG_MAX", 0); + M_IONRIPPER_SPEED_BASE = vrx_lua_get_variable("M_IONRIPPER_SPEED_BASE", 1000); + M_IONRIPPER_SPEED_ADDON = vrx_lua_get_variable("M_IONRIPPER_SPEED_ADDON", 50); + M_IONRIPPER_SPEED_MAX = vrx_lua_get_variable("M_IONRIPPER_SPEED_MAX", 1500); + + M_DISRUPTOR_DMG_BASE = vrx_lua_get_variable("M_DISRUPTOR_DMG_BASE", 40); + M_DISRUPTOR_DMG_ADDON = vrx_lua_get_variable("M_DISRUPTOR_DMG_ADDON", 3); + M_DISRUPTOR_DMG_MAX = vrx_lua_get_variable("M_DISRUPTOR_DMG_MAX", 0); + M_DISRUPTOR_SPEED_BASE = vrx_lua_get_variable("M_DISRUPTOR_SPEED_BASE", 1000); + M_DISRUPTOR_SPEED_ADDON = vrx_lua_get_variable("M_DISRUPTOR_SPEED_ADDON", 50); + M_DISRUPTOR_SPEED_MAX = vrx_lua_get_variable("M_DISRUPTOR_SPEED_MAX", 1500); + BERSERK_SLASH_INITIAL_DAMAGE = vrx_lua_get_variable("BERSERK_SLASH_INITIAL_DAMAGE", 100); BERSERK_SLASH_ADDON_DAMAGE = vrx_lua_get_variable("BERSERK_SLASH_ADDON_DAMAGE", 20); BERSERK_SLASH_RANGE = vrx_lua_get_variable("BERSERK_SLASH_RANGE", 96);