#using scripts\shared\array_shared; #using scripts\shared\math_shared; #using scripts\shared\util_shared; #using scripts\shared\bots\_bot; #using scripts\shared\bots\bot_buttons; #namespace bot_combat; // Combat Think //======================================== function combat_think() { if ( self has_threat() ) { // Assume the 'death' notification is always going to come up for threats if ( self threat_is_alive() ) { self update_threat(); } else { self thread [[level.botThreatDead]](); } } if ( !self has_threat() && !self get_new_threat() ) { return; } else if ( self has_threat() ) { if ( !self threat_visible() || self.bot.threat.lastDistanceSq > level.botSettings.threatRadiusMaxSq ) { self get_new_threat( level.botSettings.threatRadiusMin ); } } if ( self threat_visible() ) { self thread [[level.botUpdateThreatGoal]](); self thread [[level.botThreatEngage]](); } else { self thread [[level.botThreatLost]](); } } // Default Combat Queries //======================================== function is_alive( entity ) { // TODO: Add killstreak checks for mp return IsAlive( entity ); } function get_bot_threats( maxDistance ) { if(!isdefined(maxDistance))maxDistance=0; return self BotGetThreats( maxDistance ); } function get_ai_threats() { return GetAITeamArray( "axis" ); } function ignore_none( entity ) { return false; } function ignore_non_sentient( entity ) { return !IsSentient( entity ); } // Threat CRUD //======================================== function has_threat() { return ( isdefined( self.bot.threat.entity ) ); } function threat_visible() { return self has_threat() && self.bot.threat.visible; } function threat_is_alive() { if ( !self has_threat() ) { return false; } if ( isdefined( level.botThreatIsAlive) ) { return self [[level.botThreatIsAlive]]( self.bot.threat.entity ); } return IsAlive( self.bot.threat.entity ); } function set_threat( entity ) { self.bot.threat.entity = entity; self.bot.threat.aimOffset = self get_aim_offset( entity ); self update_threat( true ); } function clear_threat() { self.bot.threat.entity = undefined; self clear_threat_aim(); self BotLookForward(); /* self.bot.threat.lastPosition = ( 0, 0, 0 ); self.bot.threat.lastVisibleTime = 0; self.bot.threat.lastUpdateTime = 0; */ } function update_threat( newThreat ) { if ( ( isdefined( newThreat ) && newThreat ) ) { self.bot.threat.wasVisible = false; self clear_threat_aim(); } else { self.bot.threat.wasVisible = self.bot.threat.visible; } velocity = self.bot.threat.entity GetVelocity(); distanceSq = DistanceSquared( self GetEye(), self.bot.threat.entity.origin ); predictionTime = (isdefined(level.botSettings.thinkInterval)?level.botSettings.thinkInterval:0.05); predictedPosition = self.bot.threat.entity.origin + ( velocity * predictionTime ); aimPoint = predictedPosition + self.bot.threat.aimOffset; dot = self bot::fwd_dot( aimPoint ); fov = self BotGetFov(); if ( ( isdefined( newThreat ) && newThreat ) ) { self.bot.threat.visible = true; } else if ( dot < fov || !self BotSightTrace( self.bot.threat.entity ) ) { // TODO: Be able to pass the aimPoint into the trace self.bot.threat.visible = false; return; } self.bot.threat.visible = true; self.bot.threat.lastVisibleTime = GetTime(); self.bot.threat.lastDistanceSq = distanceSq; self.bot.threat.lastVelocity = velocity; self.bot.threat.lastPosition = predictedPosition; self.bot.threat.aimPoint = aimPoint; self.bot.threat.dot = dot; weapon = self GetCurrentWeapon(); weaponRange = weapon_range( weapon ); self.bot.threat.inRange = distanceSq < ( weaponRange * weaponRange ); weaponRangeClose = weapon_range_close( weapon ); self.bot.threat.inCloseRange = distanceSq < ( weaponRangeClose * weaponRangeClose ); } // Get Threats //======================================== function get_new_threat( maxDistance ) { // TODO: New threat Difficult delay entity = self get_greatest_threat( maxDistance ); if ( isdefined( entity ) && entity !== self.bot.threat.entity ) { self set_threat( entity ); return true; } return false; } function get_greatest_threat( maxDistance ) { // TODO: factor in current threat? // TODO: Factor in damage threats = self [[level.botGetThreats]]( maxDistance ); if ( !isdefined( threats ) ) { return undefined; } foreach( entity in threats ) { if ( self [[level.botIgnoreThreat]]( entity ) ) { continue; } return entity; } return undefined; } // Threat Engagement //======================================== function engage_threat() { if ( !self.bot.threat.wasVisible && self.bot.threat.visible && !self IsThrowingGrenade() && !self FragButtonPressed() && !self SecondaryOffhandButtonPressed() && !self IsSwitchingWeapons() ) { visibleRoll = RandomInt( 100 ); rollWeight = (isdefined(level.botSettings.lethalWeight)?level.botSettings.lethalWeight:0); if ( visibleRoll < rollWeight && self.bot.threat.lastDistanceSq >= level.botSettings.lethalDistanceMinSq && self.bot.threat.lastDistanceSq <= level.botSettings.lethalDistanceMaxSq && self GetWeaponAmmoStock( self.grenadeTypePrimary ) ) { self clear_threat_aim(); self throw_grenade( self.grenadeTypePrimary, self.bot.threat.lastPosition ); return; } visibleRoll -= rollWeight; rollWeight = (isdefined(level.botSettings.tacticalWeight)?level.botSettings.tacticalWeight:0); if ( visibleRoll >= 0 && visibleRoll < rollWeight && self.bot.threat.lastDistanceSq >= level.botSettings.tacticalDistanceMinSq && self.bot.threat.lastDistanceSq <= level.botSettings.tacticalDistanceMaxSq && self GetWeaponAmmoStock( self.grenadeTypeSecondary ) ) { self clear_threat_aim(); self throw_grenade( self.grenadeTypeSecondary, self.bot.threat.lastPosition ); return; } //visbileRoll -= rollWeight; // TODO: Fancier hero gadget stuff // Retarget self.bot.threat.aimOffset = self get_aim_offset( self.bot.threat.entity ); } if ( self FragButtonPressed() ) { self throw_grenade( self.grenadeTypePrimary, self.bot.threat.lastPosition ); return; } else if ( self SecondaryOffhandButtonPressed() ) { self throw_grenade( self.grenadeTypeSecondary, self.bot.threat.lastPosition ); return; } self update_weapon_aim(); if ( self IsReloading() || self IsSwitchingWeapons() || self IsThrowingGrenade() || self FragButtonPressed() || self SecondaryOffhandButtonPressed() || self IsMeleeing() ) { return; } if ( melee_attack() ) { return; } self update_weapon_ads(); self fire_weapon(); } // Combat Movment //======================================== function update_threat_goal() { if ( self BotUnderManualControl() ) { return; } if ( self BotGoalSet() && ( self.bot.threat.wasVisible || !self.bot.threat.visible ) ) { return; } radius = get_threat_goal_radius(); radiusSq = radius * radius; threatDistSq = Distance2DSquared( self.origin, self.bot.threat.lastPosition ); if ( threatDistSq < radiusSq || !self BotSetGoal( self.bot.threat.lastPosition, radius ) ) { self combat_strafe(); } } function get_threat_goal_radius( ) { weapon = self GetCurrentWeapon(); if ( RandomInt( 100 ) < 10 || weapon.weapClass == "melee" || ( !self GetWeaponAmmoClip( weapon ) && !self GetWeaponAmmoStock( weapon ) ) ) { return level.botSettings.meleeRange; } return RandomIntRange( level.botSettings.threatRadiusMin, level.botSettings.threatRadiusMax ); } // Attack //======================================== function fire_weapon() { if ( !self.bot.threat.inRange ) { return; } weapon = self GetCurrentWeapon(); if ( weapon == level.weaponNone || !self GetWeaponAmmoClip( weapon ) || self.bot.threat.dot < weapon_fire_dot( weapon ) ) { return; } if ( weapon.fireType == "Single Shot" || weapon.fireType == "Burst" || weapon.fireType == "Charge Shot" ) { if ( self AttackButtonPressed() ) { return; } } self bot::press_attack_button(); if ( weapon.isDualWield ) { self bot::press_throw_button(); } } function melee_attack() { // TODO: Check for shotgun/ammo and armblades if ( self.bot.threat.dot < level.botSettings.meleeDot ) { return false; } if ( DistanceSquared( self.origin, self.bot.threat.lastPosition ) > level.botSettings.meleeRangeSq ) { return false; } self bot::tap_melee_button(); return true; } // Threat Chase //======================================== function chase_threat() { if ( self BotUnderManualControl() ) { return; } // TODO: factor in HasPerk( "specialty_tracker" ) and threat HasPerk( "specialty_trackerjammer" ) if ( self.bot.threat.wasVisible && !self.bot.threat.visible ) { self clear_threat_aim(); self BotSetGoal( self.bot.threat.lastPosition ); self bot::sprint_to_goal(); return; } if ( self.bot.threat.lastVisibleTime + (isdefined(level.botSettings.chaseThreatTime)?level.botSettings.chaseThreatTime:0) < GetTime() ) { // Give up looking for this threat self clear_threat(); return; } if ( !self BotGoalSet() ) { self bot::navmesh_wander( self.bot.threat.lastVelocity, self.botSettings.chaseWanderMin, self.botSettings.chaseWanderMax, self.botSettings.chaseWanderSpacing, self.botSettings.chaseWanderFwdDot ); self clear_threat(); //self bot::sprint_to_goal(); } } // Aim //======================================== function get_aim_offset( entity ) { if ( IsSentient( entity ) && RandomInt( 100 ) < (isdefined(level.botSettings.headshotWeight)?level.botSettings.headshotWeight:0) ) { return entity GetEye() - entity.origin; } return entity GetCentroid() - entity.origin; } function update_weapon_aim() { if ( !isdefined( self.bot.threat.aimStartTime ) ) { self start_threat_aim(); } aimTime = GetTime() - self.bot.threat.aimStartTime; if ( aimTime < 0 ) { return; } if ( aimTime >= self.bot.threat.aimTime || !isdefined( self.bot.threat.aimError ) ) { self BotLookAtPoint( self.bot.threat.aimPoint ); return; } eyePoint = self GetEye(); threatAngles = VectorToAngles( self.bot.threat.aimPoint - eyePoint ); initialAngles = threatAngles + self.bot.threat.aimError; currAngles = VectorLerp( initialAngles, threatAngles, aimTime / self.bot.threat.aimTime ); playerAngles = self GetPlayerAngles(); self BotSetLookAngles( AnglesToForward( currAngles ) ); } function start_threat_aim() { self.bot.threat.aimStartTime = GetTime() + (isdefined(level.botSettings.aimDelay)?level.botSettings.aimDelay:0) * 1000; self.bot.threat.aimTime = (isdefined(level.botSettings.aimTime)?level.botSettings.aimTime:0) * 1000; pitchError = angleError( (isdefined(level.botSettings.aimErrorMinPitch)?level.botSettings.aimErrorMinPitch:0), (isdefined(level.botSettings.aimErrorMaxPitch)?level.botSettings.aimErrorMaxPitch:0) ); yawError = angleError( (isdefined(level.botSettings.aimErrorMinYaw)?level.botSettings.aimErrorMinYaw:0), (isdefined(level.botSettings.aimErrorMaxYaw)?level.botSettings.aimErrorMaxYaw:0) ); self.bot.threat.aimError = ( pitchError, yawError, 0 ); } function angleError( angleMin, angleMax ) { angle = angleMax - angleMin; angle *= RandomFloatRange( -1, 1 ); if ( angle < 0 ) { angle -= angleMin; } else { angle += angleMin; } return angle; } function clear_threat_aim() { if ( !isdefined( self.bot.threat.aimStartTime ) ) { return; } self.bot.threat.aimStartTime = undefined; self.bot.threat.aimTime = undefined; self.bot.threat.aimError = undefined; } // Pre Combat //======================================== function bot_pre_combat() { if ( self has_threat() ) { return; } // Look for whoever is shooting at the bot if ( isdefined( self.bot.damage.time ) && self.bot.damage.time + 1500 > GetTime() ) { if ( self has_threat() && self.bot.damage.time > self.bot.threat.lastVisibleTime ) { self clear_threat(); } self bot::navmesh_wander( self.bot.damage.attackDir, level.botSettings.damageWanderMin, level.botSettings.damageWanderMax, level.botSettings.damageWanderSpacing, level.botSettings.damageWanderFwdDot ); self bot::end_sprint_to_goal(); self bot_combat::clear_damage(); } } // Post Combat //======================================== function bot_post_combat() { } // Weapon Stuff //======================================== function update_weapon_ads() { if ( !self.bot.threat.inRange || self.bot.threat.inCloseRange ) { return; } weapon = self GetCurrentWeapon(); if ( weapon == level.weaponNone || weapon.isDualWield || weapon.weapClass == "melee" || self GetWeaponAmmoClip( weapon ) <= 0 ) { return; } if ( self.bot.threat.dot < weapon_ads_dot( weapon ) ) { return; } // TODO: Set ADS time self bot::press_ads_button(); } function weapon_ads_dot( weapon ) { if ( weapon.isSniperWeapon ) { return level.botSettings.sniperAds; } else if ( weapon.isRocketLauncher ) { return level.botSettings.rocketLauncherAds; } switch( weapon.weapClass ) { case "mg": return level.botSettings.mgAds; case "smg": return level.botSettings.smgAds; case "spread": return level.botSettings.spreadAds; case "pistol": return level.botSettings.pistolAds; case "rifle": return level.botSettings.rifleAds; } return level.botSettings.defaultAds; } function weapon_fire_dot( weapon ) { if ( weapon.isSniperWeapon ) { return level.botSettings.sniperFire; } else if ( weapon.isRocketLauncher ) { return level.botSettings.rocketLauncherFire; } switch( weapon.weapClass ) { case "mg": return level.botSettings.mgFire; case "smg": return level.botSettings.smgFire; case "spread": return level.botSettings.spreadFire; case "pistol": return level.botSettings.pistolFire; case "rifle": return level.botSettings.rifleFire; } return level.botSettings.defaultFire; } function weapon_range( weapon ) { if ( weapon.isSniperWeapon ) { return level.botSettings.sniperRange; } else if ( weapon.isRocketLauncher ) { return level.botSettings.rocketLauncherRange; } switch( weapon.weapClass ) { case "mg": return level.botSettings.mgRange; case "smg": return level.botSettings.smgRange; case "spread": return level.botSettings.spreadRange; case "pistol": return level.botSettings.pistolRange; case "rifle": return level.botSettings.rifleRange; } return level.botSettings.defaultRange; } function weapon_range_close( weapon ) { if ( weapon.isSniperWeapon ) { return level.botSettings.sniperRangeClose; } else if ( weapon.isRocketLauncher ) { return level.botSettings.rocketLauncherRangeClose; } switch( weapon.weapClass ) { case "mg": return level.botSettings.mgRangeClose; case "smg": return level.botSettings.smgRangeClose; case "spread": return level.botSettings.spreadRangeClose; case "pistol": return level.botSettings.pistolRangeClose; case "rifle": return level.botSettings.rifleRangeClose; } return level.botSettings.defaultRangeClose; } function switch_weapon() { currentWeapon = self GetCurrentWeapon(); if ( self IsSwitchingWeapons() || currentWeapon.isHeroWeapon || currentWeapon.isItem ) { return false; } weapon = bot::get_ready_gadget(); if ( weapon != level.weaponNone ) { if ( !isdefined( level.enemyEmpActive ) || !self [[level.enemyEmpActive]]() ) { self bot::activate_hero_gadget( weapon ); return true; } } weapons = self GetWeaponsListPrimaries(); // Switch away from a 'sidearm' if ( currentWeapon == level.weaponNone || currentWeapon.weapClass == "melee" || currentWeapon.weapClass == "rocketLauncher" || currentWeapon.weapClass == "pistol" ) { foreach( weapon in weapons ) { if ( weapon == currentWeapon ) { continue; } if ( self GetWeaponAmmoClip( weapon ) || self GetWeaponAmmoStock( weapon ) ) { self BotSwitchToWeapon( weapon ); return true; } } return false; } currentAmmoStock = self GetWeaponAmmoStock( currentWeapon ); if ( currentAmmoStock ) { return false; } switchFrac = 0.3; currentClipFrac = self weapon_clip_frac( currentWeapon ); if ( currentClipFrac > switchFrac ) { return false; } foreach( weapon in weapons ) { if ( self GetWeaponAmmoStock( weapon ) || self weapon_clip_frac( weapon ) > switchFrac ) { self BotSwitchToWeapon( weapon ); return true; } } return false; } function threat_switch_weapon() { currentWeapon = self GetCurrentWeapon(); if ( self IsSwitchingWeapons() || self GetWeaponAmmoClip( currentWeapon ) || currentWeapon.isItem ) { return; } currentAmmoStock = self GetWeaponAmmoStock( currentWeapon ); weapons = self GetWeaponsListPrimaries(); foreach( weapon in weapons ) { // TODO: Check if the target is a scorestreak laong with .requireLockOnToFire if ( weapon == currentWeapon || weapon.requireLockOnToFire ) { continue; } if ( weapon.weapClass == "melee" ) { // if we have ammo, low chance to switch to melee weapon if ( currentAmmoStock && RandomIntRange( 0, 100 ) < 75 ) { continue; } } else { // Dont' switch if we'd have to reload the new weapon, and we can reload this one if( !self GetWeaponAmmoClip( weapon ) && currentAmmoStock ) { continue; } weaponAmmoStock = self GetWeaponAmmoStock( weapon ); // Don't switch if neither has any ammo if ( !currentAmmoStock && !weaponAmmoStock ) { continue; } // Maybe don't switch if it's not to a pistol if ( weapon.weapClass != "pistol" && RandomIntRange( 0, 100 ) < 75 ) { continue; } } self BotSwitchToWeapon( weapon ); } } function reload_weapon() { weapon = self GetCurrentWeapon(); if ( !self GetWeaponAmmoStock( weapon ) ) { return false; } reloadFrac = 0.5; if ( weapon.weapClass == "mg" ) { reloadFrac = 0.25; } if ( self weapon_clip_frac( weapon ) < reloadFrac ) { self bot::tap_reload_button(); return true; } return false; } function weapon_clip_frac( weapon ) { if ( weapon.clipSize <= 0 ) { return 1; } clipAmmo = self GetWeaponAmmoClip( weapon ); return ( clipAmmo / weapon.clipSize ); } // Grenades //======================================== function throw_grenade( weapon, target ) { if ( !isdefined( self.bot.threat.aimStartTime ) ) { self aim_grenade( weapon, target ); self press_grenade_button( weapon ); return; } if ( self.bot.threat.aimStartTime + self.bot.threat.aimTime > GetTime() ) { return; } if ( self will_hit_target( weapon, target ) ) { return; } self press_grenade_button( weapon ); } function press_grenade_button( weapon ) { if ( weapon == self.grenadeTypePrimary ) { self bot::press_frag_button(); } else if ( weapon == self.grenadeTypeSecondary ) { self bot::press_offhand_button(); } } function aim_grenade( weapon, target ) { // Just look above the target some aimPeak = target + ( 0, 0, 100 ); self.bot.threat.aimStartTime = GetTime(); self.bot.threat.aimTime = 1500; self BotSetLookAnglesFromPoint( aimPeak ); } function will_hit_target( weapon, target ) { velocity = get_throw_velocity( weapon ); throwOrigin = self GetEye(); xyDist = Distance2D( throwOrigin, target ); xySpeed = Distance2D( velocity, ( 0, 0, 0 ) ); t = xyDist / xySpeed; gravity = -GetDvarFloat( "bg_gravity" ); tHeight = throwOrigin[2] + velocity[2] * t + ( gravity * t * t * .5 ); return Abs( tHeight - target[2] ) < 20; } function get_throw_velocity( weapon ) { angles = self GetPlayerAngles(); forward = AnglesToForward( angles ); // Precomputed velocity of most grenades return forward * 928; /* TODO: These weapon values aren't actually accessable velocity = forward * weapon.projectileSpeed; velocity += AnglesToUp( angles ) * weapon.projectileSpeedRelativeUp; velocity = ( velocity[0], velocity[1], velocity[2] + weapon.projectileSpeedUp ); if ( weapon.projectileSpeedForward ) { xyForward = VectorNormalize( ( forward[0], forward[1], 0 ) ); velocity += xyForward * weapon.projectileSpeedForward; } return velocity; */ } function get_lethal_grenade() { weaponsList = self GetWeaponsList(); foreach( weapon in weaponsList ) { if ( weapon.type == "grenade" && self GetWeaponAmmoStock( weapon ) ) { return weapon; } } return level.weaponNone; } // Damage //======================================== function wait_damage_loop() { self endon( "death" ); level endon( "game_ended" ); while( 1 ) { self waittill( "damage", damage, attacker, direction, point, mod, unused1, unused2, unused3, weapon, flags, inflictor ); self.bot.damage.entity = attacker; self.bot.damage.amount = damage; self.bot.damage.attackDir = VectorNormalize( attacker.origin - self.origin ); self.bot.damage.weapon = weapon; self.bot.damage.mod = mod; self.bot.damage.time = GetTime(); self thread [[level.onBotDamage]](); } } function clear_damage() { self.bot.damage.entity = undefined; self.bot.damage.amount = undefined; self.bot.damage.direction = undefined; self.bot.damage.weapon = undefined; self.bot.damage.mod = undefined; self.bot.damage.time = undefined; } // Movement //======================================== function combat_strafe( radiusMin, radiusMax, spacing, sideDotMin, sideDotMax ) { if(!isdefined(radiusMin))radiusMin=(isdefined(level.botSettings.strafeMin)?level.botSettings.strafeMin:0); if(!isdefined(radiusMax))radiusMax=(isdefined(level.botSettings.strafeMax)?level.botSettings.strafeMax:0); if(!isdefined(spacing))spacing=(isdefined(level.botSettings.strafeSpacing)?level.botSettings.strafeSpacing:0); if(!isdefined(sideDotMin))sideDotMin=(isdefined(level.botSettings.strafeSideDotMin)?level.botSettings.strafeSideDotMin:0); if(!isdefined(sideDotMax))sideDotMax=(isdefined(level.botSettings.strafeSideDotMax)?level.botSettings.strafeSideDotMax:0); fwd = AnglesToForward( self.angles ); /# //Circle( self.origin, radiusMin, ( 1, 1, 1 ), false, true, 3 * 20 ); //Circle( self.origin, radiusMax, ( 1, 1, 1 ), false, true, 3 * 20 ); //Line( self.origin, self.origin + ( fwd * radiusMax ), ( 1, 1, 1 ), 1, false, 3 * 20 ); #/ queryResult = PositionQuery_Source_Navigation( self.origin, radiusMin, radiusMax, 64, spacing, self ); best_point = undefined; foreach ( point in queryResult.data ) { moveDir = VectorNormalize( point.origin - self.origin ); dot = VectorDot( moveDir, fwd ); // Don't even consider points outside of the strafe cones if ( dot >= sideDotMin && dot <= sideDotMax ) { point.score = MapFloat( radiusMin, radiusMax, 0, 50, point.distToOrigin2D ); point.score += randomFloatRange( 0, 50 ); } /# //Line( point.origin, point.origin + ( 0, 0, point.score ), ( 0, 0, 1 ), 1, false, 3 * 20 ); #/ if ( !isdefined( best_point ) || point.score > best_point.score ) { best_point = point; } } if( isdefined( best_point ) ) { /# //Circle( best_point.origin, 16, ( 0, 1, 0 ), false, true, 3 * 20 ); #/ self BotSetGoal( best_point.origin ); self bot::end_sprint_to_goal(); } }