diff --git a/include/ai_patterns.hpp b/include/ai_patterns.hpp
new file mode 100644
index 0000000..9b5ac18
--- /dev/null
+++ b/include/ai_patterns.hpp
@@ -0,0 +1,79 @@
+/*
+ Biplanes Revival
+ Copyright (C) 2019-2025 Regular-dev community
+ https://regular-dev.org
+ regular.dev.org@gmail.com
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+#pragma once
+
+#include
+#include
+#include
+
+#include
+#include
+
+// Forward declarations
+class Plane;
+
+// Use the same DIFFICULTY format as in the rest of the project
+using DifficultyType = DIFFICULTY::DIFFICULTY;
+
+// New structure for tracking movement patterns
+struct AiPatternTracker
+{
+ static constexpr size_t MAX_POSITIONS = 32;
+
+ std::vector mRecentPositions {};
+ std::vector mRecentDirections {};
+ Timer mPursuitTimer {0.0f};
+ Timer mPatternBreakCooldown {0.0f};
+ bool mIsInCircularPattern {false};
+ bool mIsStalePursuit {false};
+ SDL_Vector mLastOpponentPosition {};
+ float mLastOpponentDirection {};
+
+ void update(const Plane& self, const Plane& opponent);
+ bool isCircularPattern() const;
+ bool isStalePursuit() const;
+ bool shouldBreakCircularPattern(const Plane& self, const Plane& opponent) const;
+ void reset();
+ SDL_Vector getLastOpponentPosition() const { return mLastOpponentPosition; }
+ const std::vector& getRecentPositions() const { return mRecentPositions; }
+};
+
+// Structure for defining bot "personality"
+struct AiPersonality
+{
+ enum class Type {
+ AGGRESSIVE, // Aggressive - always attacks
+ DEFENSIVE, // Defensive - prefers to avoid conflicts
+ BALANCED, // Balanced - mixed strategy
+ PREDICTIVE // Predictive - uses complex tactics
+ };
+
+ Type type {Type::BALANCED};
+ float aggressiveness {0.5f}; // 0.0 - very cautious, 1.0 - very aggressive
+ float predictionWeight {0.5f}; // Weight of prediction in decision making
+ float riskTolerance {0.5f}; // Risk tolerance
+ float adaptability {0.5f}; // Ability to adapt to opponent's style
+
+ void updateBasedOnDifficulty(DifficultyType difficulty);
+ bool shouldUseAggressiveStrategy(const Plane& self, const Plane& opponent) const;
+ bool shouldUseDefensiveStrategy(const Plane& self, const Plane& opponent) const;
+ float getModifiedAimCone(float baseAimCone) const;
+};
diff --git a/include/constants.hpp b/include/constants.hpp
index 5cc6739..671bdee 100644
--- a/include/constants.hpp
+++ b/include/constants.hpp
@@ -385,6 +385,10 @@ namespace constants
static constexpr float aimConeEasy {2.f};
static constexpr float shootCooldownEasy {2.f * plane::shootCooldown};
+ static constexpr float circularPatternThreshold {0.23f};
+ static constexpr float stalePursuitThreshold {3.0f};
+ static constexpr float movementPredictionTime {0.15f};
+
namespace debug
{
static constexpr float dangerMagnitude {2.f * plane::sizeX};
diff --git a/src/ai_patterns.cpp b/src/ai_patterns.cpp
new file mode 100644
index 0000000..7f83f60
--- /dev/null
+++ b/src/ai_patterns.cpp
@@ -0,0 +1,275 @@
+/*
+ Biplanes Revival
+ Copyright (C) 2019-2025 Regular-dev community
+ https://regular-dev.org
+ regular.dev.org@gmail.com
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+*/
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+
+void
+AiPatternTracker::update(const Plane& self, const Plane& opponent)
+{
+ // Update pattern break cooldown timer
+ mPatternBreakCooldown.Update();
+
+ // Add current opponent position and direction to history
+ mRecentPositions.push_back({opponent.x(), opponent.y()});
+ mRecentDirections.push_back(opponent.dir());
+
+ // Maintain fixed-size history by removing oldest entries
+ if (mRecentPositions.size() > MAX_POSITIONS)
+ {
+ mRecentPositions.erase(mRecentPositions.begin());
+ mRecentDirections.erase(mRecentDirections.begin());
+ }
+
+ // Detect if opponent is moving or stationary
+ // This helps identify when opponent is stuck in a pattern
+ const auto currentOpponentPos = SDL_Vector{opponent.x(), opponent.y()};
+ const auto distanceMoved = (currentOpponentPos - mLastOpponentPosition).length();
+ const auto directionChanged = std::abs(opponent.dir() - mLastOpponentDirection);
+
+ // If opponent hasn't moved significantly, increment stale pursuit timer
+ // This helps detect when we're stuck following the same pattern
+ if (distanceMoved < 0.01f && directionChanged < 5.0f)
+ {
+ mPursuitTimer.Update();
+ }
+ else
+ {
+ mPursuitTimer.Reset();
+ }
+
+ // Store current position and direction for next frame comparison
+ mLastOpponentPosition = currentOpponentPos;
+ mLastOpponentDirection = opponent.dir();
+
+ // Update pattern detection flags
+ mIsInCircularPattern = isCircularPattern();
+ mIsStalePursuit = isStalePursuit();
+}
+
+bool
+AiPatternTracker::isCircularPattern() const
+{
+ // Need minimum number of positions to detect a pattern
+ if (mRecentPositions.size() < 8)
+ return false;
+
+ // Calculate center of mass (centroid) of all opponent positions
+ SDL_Vector center{0.0f, 0.0f};
+ for (const auto& pos : mRecentPositions)
+ {
+ center.x += pos.x;
+ center.y += pos.y;
+ }
+ center.x /= mRecentPositions.size();
+ center.y /= mRecentPositions.size();
+
+ // Calculate average distance from center
+ float avgDistance = 0.0f;
+ for (const auto& pos : mRecentPositions)
+ {
+ avgDistance += (pos - center).length();
+ }
+ avgDistance /= mRecentPositions.size();
+
+ // Calculate variance of distances from center
+ // Low variance indicates points are roughly equidistant from center (circular pattern)
+ float variance = 0.0f;
+ for (const auto& pos : mRecentPositions)
+ {
+ const float distance = (pos - center).length();
+ variance += std::abs(distance - avgDistance);
+ }
+ variance /= mRecentPositions.size();
+
+ // Use adaptive threshold to prevent rapid switching between pattern states
+ // This creates hysteresis and makes pattern detection more stable
+ const float adjustedThreshold = constants::ai::circularPatternThreshold * 2.0f;
+
+ // If we were recently in a circular pattern, be more lenient
+ // This prevents flickering between pattern and non-pattern states
+ const bool wasRecentlyCircular = mIsInCircularPattern && mRecentPositions.size() >= 6;
+ const float finalThreshold = wasRecentlyCircular ? adjustedThreshold * 1.5f : adjustedThreshold;
+
+ // Pattern is circular if variance is low and average distance is significant
+ return variance < finalThreshold && avgDistance > 0.05f;
+}
+
+bool
+AiPatternTracker::isStalePursuit() const
+{
+ // Check if we've been pursuing the same pattern for too long
+ return mPursuitTimer.remainderTime() < (mPursuitTimer.timeout() - constants::ai::stalePursuitThreshold);
+}
+
+bool
+AiPatternTracker::shouldBreakCircularPattern(const Plane& self, const Plane& opponent) const
+{
+ if (!mIsInCircularPattern)
+ return false;
+
+ // Calculate center and radius of the circular pattern
+ SDL_Vector center{0.0f, 0.0f};
+ for (const auto& pos : mRecentPositions)
+ {
+ center.x += pos.x;
+ center.y += pos.y;
+ }
+ center.x /= mRecentPositions.size();
+ center.y /= mRecentPositions.size();
+
+ // Calculate average radius of the pattern
+ float radius = 0.0f;
+ for (const auto& pos : mRecentPositions)
+ {
+ radius += (pos - center).length();
+ }
+ radius /= mRecentPositions.size();
+
+ // Check our position relative to the pattern center
+ const auto selfPos = SDL_Vector{self.x(), self.y()};
+ const auto distanceToCenter = get_distance_between_points(selfPos, center);
+
+ // Multiple conditions for breaking out of circular pattern:
+
+ // 1. We're close to the center - good position to break out
+ const bool closeToCenter = distanceToCenter < radius * 0.8f;
+
+ // 2. Pattern has been going on too long
+ const bool patternTooLong = mPursuitTimer.remainderTime() < (mPursuitTimer.timeout() - constants::ai::stalePursuitThreshold * 0.5f);
+
+ // 3. We've been in pattern for more than 0.5 seconds
+ const bool patternTimeExceeded = mPursuitTimer.remainderTime() < (mPursuitTimer.timeout() - 0.5f);
+
+ // 4. Opponent is too close - dangerous situation, need to break away
+ const auto distanceToOpponent = get_distance_between_points(selfPos, {opponent.x(), opponent.y()});
+ const bool opponentTooClose = distanceToOpponent < 0.35f;
+
+ return closeToCenter || patternTooLong || patternTimeExceeded || opponentTooClose;
+}
+
+void
+AiPatternTracker::reset()
+{
+ // Clear all pattern tracking data
+ mRecentPositions.clear();
+ mRecentDirections.clear();
+ mPursuitTimer.Reset();
+ mPatternBreakCooldown.Reset();
+ mIsInCircularPattern = false;
+ mIsStalePursuit = false;
+}
+
+void
+AiPersonality::updateBasedOnDifficulty(DifficultyType difficulty)
+{
+ // Configure personality parameters based on difficulty level
+ // Each difficulty has unique characteristics that affect AI behavior
+ switch (difficulty)
+ {
+ case DIFFICULTY::EASY:
+ type = Type::DEFENSIVE;
+ aggressiveness = 0.6f;
+ predictionWeight = 0.4f;
+ riskTolerance = 0.4f;
+ adaptability = 0.4f;
+ break;
+
+ case DIFFICULTY::MEDIUM:
+ type = Type::BALANCED;
+ aggressiveness = 0.8f;
+ predictionWeight = 0.7f;
+ riskTolerance = 0.7f;
+ adaptability = 0.6f;
+ break;
+
+ case DIFFICULTY::HARD:
+ type = Type::AGGRESSIVE;
+ aggressiveness = 0.9f;
+ predictionWeight = 0.9f;
+ riskTolerance = 0.9f;
+ adaptability = 0.9f;
+ break;
+
+ case DIFFICULTY::DEVELOPER:
+ type = Type::PREDICTIVE;
+ aggressiveness = 1.0f;
+ predictionWeight = 1.0f;
+ riskTolerance = 1.0f;
+ adaptability = 1.0f;
+ break;
+ }
+}
+
+bool
+AiPersonality::shouldUseAggressiveStrategy(const Plane& self, const Plane& opponent) const
+{
+ // Calculate health ratios for tactical decision making
+ const auto selfHp = static_cast(self.hp()) / constants::plane::maxHp;
+ const auto opponentHp = static_cast(opponent.hp()) / constants::plane::maxHp;
+
+ // Calculate distance to opponent
+ const auto distance = get_distance_between_points(
+ {self.x(), self.y()},
+ {opponent.x(), opponent.y()}
+ );
+
+ // Use aggressive strategy if:
+ // 1. We have significantly more health than opponent AND are close enough to engage
+ // 2. OR our personality is naturally very aggressive
+ return (selfHp > opponentHp * 0.8f && distance < 0.4f) || aggressiveness > 0.7f;
+}
+
+bool
+AiPersonality::shouldUseDefensiveStrategy(const Plane& self, const Plane& opponent) const
+{
+ // Calculate health ratios for tactical decision making
+ const auto selfHp = static_cast(self.hp()) / constants::plane::maxHp;
+ const auto opponentHp = static_cast(opponent.hp()) / constants::plane::maxHp;
+
+ // Calculate distance to opponent
+ const auto distance = get_distance_between_points(
+ {self.x(), self.y()},
+ {opponent.x(), opponent.y()}
+ );
+
+ // Use defensive strategy if:
+ // 1. We have significantly less health than opponent
+ // 2. OR opponent is dangerously close
+ // 3. OR our personality is naturally very defensive
+ return (selfHp < opponentHp * 0.6f || distance < 0.2f) || aggressiveness < 0.3f;
+}
+
+float
+AiPersonality::getModifiedAimCone(float baseAimCone) const
+{
+ // Adjust aim accuracy based on personality's prediction weight
+ // Higher prediction weight = more accurate aiming
+ const float accuracyModifier = 1.0f - (predictionWeight * 0.5f);
+ return baseAimCone * accuracyModifier;
+}
diff --git a/src/ai_stuff.cpp b/src/ai_stuff.cpp
index 94aef41..a229a5c 100644
--- a/src/ai_stuff.cpp
+++ b/src/ai_stuff.cpp
@@ -19,6 +19,7 @@
*/
#include
+#include
#include
#include
#include
@@ -93,6 +94,72 @@ isPlaneStalling(
return plane.y() >= constants::plane::groundCollision - gravity;
}
+// Enhanced shooting logic with movement prediction
+static bool
+shouldShootWithPrediction(
+ const Plane& self,
+ const Plane& opponent,
+ const SDL_Vector& pos,
+ const float dirToTargetRelative,
+ const DIFFICULTY::DIFFICULTY botDifficulty,
+ const AiPersonality& personality,
+ const AiPatternTracker& patternTracker )
+{
+ namespace plane = constants::plane;
+ const auto bulletOffset = self.bulletSpawnOffset();
+ const float baseAimCone =
+ botDifficulty == DIFFICULTY::EASY
+ ? constants::ai::aimConeEasy
+ : constants::ai::aimConeDefault;
+
+ // Personality affects aim accuracy - higher prediction weight = better aim
+ const float aimCone = personality.getModifiedAimCone(baseAimCone);
+
+ // Calculate movement prediction for better accuracy
+ const auto opponentVelocity = SDL_Vector{
+ opponent.x() - patternTracker.getLastOpponentPosition().x,
+ opponent.y() - patternTracker.getLastOpponentPosition().y
+ };
+
+ // Enhanced prediction using multiple points
+ const float predictionTime = constants::ai::movementPredictionTime;
+
+ // Linear prediction (current velocity)
+ const auto linearPredictedPos = SDL_Vector{
+ opponent.x() + opponentVelocity.x * predictionTime,
+ opponent.y() + opponentVelocity.y * predictionTime
+ };
+
+ // Lead prediction (aim ahead based on opponent's direction)
+ const auto leadPredictedPos = SDL_Vector{
+ opponent.x() + opponentVelocity.x * predictionTime * 1.5f,
+ opponent.y() + opponentVelocity.y * predictionTime * 1.5f
+ };
+
+ // Calculate angles to both predicted positions
+ const auto dirToLinearPredictedAbsolute = get_angle_to_point(pos, linearPredictedPos);
+ const auto dirToLinearPredictedRelative = get_angle_relative(self.dir(), dirToLinearPredictedAbsolute);
+
+ const auto dirToLeadPredictedAbsolute = get_angle_to_point(pos, leadPredictedPos);
+ const auto dirToLeadPredictedRelative = get_angle_relative(self.dir(), dirToLeadPredictedAbsolute);
+
+ // Use current position and both predictions for better accuracy
+ const bool willHitCurrent = std::abs(dirToTargetRelative) <= plane::pitchStep * aimCone;
+ const bool willHitLinearPredicted = std::abs(dirToLinearPredictedRelative) <= plane::pitchStep * aimCone * 1.1f;
+ const bool willHitLeadPredicted = std::abs(dirToLeadPredictedRelative) <= plane::pitchStep * aimCone * 1.3f;
+ const bool canHitInstantly = opponent.isHit(
+ self.x() + bulletOffset.x,
+ self.y() + bulletOffset.y ) && botDifficulty > DIFFICULTY::EASY;
+
+ if ( willHitCurrent == true || willHitLinearPredicted == true || willHitLeadPredicted == true || canHitInstantly == true )
+ {
+ if ( opponent.hasJumped() == false || botDifficulty > DIFFICULTY::EASY )
+ return true;
+ }
+
+ return false;
+}
+
static bool
isOpponentCloseBehind(
const Plane& opponent,
@@ -546,6 +613,8 @@ class AiStatePlane : public AiState
{
protected:
AiOscillationFixer mOscillationFixer {};
+ AiPatternTracker mPatternTracker {};
+ AiPersonality mPersonality {};
public:
@@ -566,6 +635,8 @@ AiStatePlane::AiStatePlane(
mInterestMap = {constants::plane::directionCount};
mDangerMap = {constants::plane::directionCount};
+ mPersonality.updateBasedOnDifficulty(gameState().botDifficulty);
+
constexpr auto ReactionTimeToWeights = &AiTemperature::Weights::FromTime;
auto throttleWeight = ReactionTimeToWeights(0.1f, 0.1f);
@@ -639,6 +710,9 @@ AiStatePlane::update(
for ( auto& [action, temperature] : mActions )
temperature.set(0.f);
+ if ( self.isDead() == true )
+ mPatternTracker.reset();
+
return;
}
@@ -649,6 +723,36 @@ AiStatePlane::update(
mInterestMap.reinit();
mDangerMap.reinit();
+ // Update pattern tracker
+ mPatternTracker.update(self, opponent);
+
+ // Update personality based on current difficulty
+ mPersonality.updateBasedOnDifficulty(botDifficulty);
+
+ // Calculate positions and distances to opponent in advance
+ const SDL_Vector pos {self.x(), self.y()};
+ const SDL_Vector opponentPos {opponent.pilot.x(), opponent.pilot.y()};
+
+ const SDL_Vector opponentPosL = opponentPos - SDL_Vector{1.f, 0.f};
+ const SDL_Vector opponentPosR = opponentPos + SDL_Vector{1.f, 0.f};
+
+ const auto opponentDistance = get_distance_between_points(pos, opponentPos);
+ const auto opponentDistanceL = get_distance_between_points(pos, opponentPosL);
+ const auto opponentDistanceR = get_distance_between_points(pos, opponentPosR);
+
+ std::multimap opponentPositionsSorted
+ {
+ {opponentDistance, opponentPos},
+ {opponentDistanceL, opponentPosL},
+ {opponentDistanceR, opponentPosR},
+ };
+ opponentPositionsSorted.erase(--opponentPositionsSorted.end());
+
+ const auto [opponentShortestDistance, opponentShortestPos] =
+ *opponentPositionsSorted.begin();
+
+
+
const auto speed = 0.5f * std::clamp(
self.speed() * constants::tickRate,
plane::maxSpeedBase,
@@ -813,29 +917,6 @@ AiStatePlane::update(
std::vector actions {};
- const SDL_Vector pos {self.x(), self.y()};
- const SDL_Vector opponentPos {opponent.pilot.x(), opponent.pilot.y()};
-
- const SDL_Vector opponentPosL = opponentPos - SDL_Vector{1.f, 0.f};
- const SDL_Vector opponentPosR = opponentPos + SDL_Vector{1.f, 0.f};
-
- const auto opponentDistance = get_distance_between_points(pos, opponentPos);
- const auto opponentDistanceL = get_distance_between_points(pos, opponentPosL);
- const auto opponentDistanceR = get_distance_between_points(pos, opponentPosR);
-
-
- std::multimap opponentPositionsSorted
- {
- {opponentDistance, opponentPos},
- {opponentDistanceL, opponentPosL},
- {opponentDistanceR, opponentPosR},
- };
- opponentPositionsSorted.erase(--opponentPositionsSorted.end());
-
-
- const auto [opponentShortestDistance, opponentShortestPos] =
- *opponentPositionsSorted.begin();
-
const auto dirToOpponentAbsolute = get_angle_to_point(
pos, opponentShortestPos );
@@ -856,49 +937,108 @@ AiStatePlane::update(
if ( opponent.isDead() == false )
- for ( const auto& [distance, position] : opponentPositionsSorted )
+ {
+ // Advanced pattern recognition and response system
+ // This system detects when AI is stuck in circular pursuit and responds appropriately
+
+ // Check if we're stuck in circular pursuit or stale pattern
+ const bool shouldUseInterceptStrategy =
+ mPatternTracker.isCircularPattern() ||
+ mPatternTracker.isStalePursuit();
+
+ // Check if we should break out of circular pattern
+ // This applies to ALL difficulty levels to prevent infinite loops
+ const bool shouldBreakPattern = mPatternTracker.shouldBreakCircularPattern(self, opponent);
+
+ if ( shouldUseInterceptStrategy && !shouldBreakPattern )
{
- const auto dirToTargetAbsolute = get_angle_to_point(
- pos, position );
-
- const auto dirToTargetRelative = get_angle_relative(
- self.dir(), dirToTargetAbsolute );
+ // Pattern detected but not breaking - use normal pursuit with enhanced positioning
+ const auto dirToOpponentAbsolute = get_angle_to_point(pos, {opponent.x(), opponent.y()});
+ const auto dirToOpponentRelative = get_angle_relative(self.dir(), dirToOpponentAbsolute);
- const auto bulletOffset = self.bulletSpawnOffset();
+ // Use enhanced shooting logic with movement prediction
+ if (shouldShootWithPrediction(self, opponent, pos, dirToOpponentRelative, botDifficulty, mPersonality, mPatternTracker))
+ {
+ actions.push_back(AiAction::Shoot);
+ }
- const float aimCone =
- botDifficulty == DIFFICULTY::EASY
- ? constants::ai::aimConeEasy
- : constants::ai::aimConeDefault;
+ // Set interest to opponent's current position (normal pursuit behavior)
+ const size_t opponentDirIndex = std::round(dirToOpponentAbsolute / plane::pitchStep);
+ const auto distanceToOpponent = get_distance_between_points(pos, {opponent.x(), opponent.y()});
+ mInterestMap[opponentDirIndex % mInterestMap.size()] = distanceToOpponent;
- const bool canHitInstantly = opponent.isHit(
- self.x() + bulletOffset.x,
- self.y() + bulletOffset.y ) &&
- botDifficulty > DIFFICULTY::EASY;
+ // For high difficulty levels, add tactical positioning interest points
+ // This helps AI find better angles for attack
+ if ( botDifficulty >= DIFFICULTY::HARD )
+ {
+ // Add interest in perpendicular directions for tactical positioning
+ const auto perpDir1 = dirToOpponentAbsolute + 45.0f;
+ const auto perpDir2 = dirToOpponentAbsolute - 45.0f;
- const bool willBulletHit =
- std::abs(dirToTargetRelative) <= plane::pitchStep * aimCone;
+ const size_t perpDirIndex1 = std::round(perpDir1 / plane::pitchStep);
+ const size_t perpDirIndex2 = std::round(perpDir2 / plane::pitchStep);
- if ( willBulletHit == true || canHitInstantly == true )
+ mInterestMap[perpDirIndex1 % mInterestMap.size()] = distanceToOpponent * 0.3f;
+ mInterestMap[perpDirIndex2 % mInterestMap.size()] = distanceToOpponent * 0.3f;
+ }
+ }
+ else if (shouldBreakPattern)
+ {
+ // Pattern breaking strategy - force AI to change course
+ // This prevents infinite circular pursuit loops
+ const auto dirToOpponent = get_angle_to_point(pos, {opponent.x(), opponent.y()});
+ const auto oppositeDir = dirToOpponent + 180.0f;
+
+ // Create strong interest in opposite direction to break the pattern
+ const size_t oppositeDirIndex = std::round(oppositeDir / plane::pitchStep);
+ mInterestMap[oppositeDirIndex % mInterestMap.size()] = 2.0f;
+
+ // Add interest in perpendicular directions for escape options
+ const size_t perpDir1Index = std::round((oppositeDir + 90.0f) / plane::pitchStep);
+ const size_t perpDir2Index = std::round((oppositeDir - 90.0f) / plane::pitchStep);
+ mInterestMap[perpDir1Index % mInterestMap.size()] = 1.2f;
+ mInterestMap[perpDir2Index % mInterestMap.size()] = 1.2f;
+
+ // Clear interest in opponent's direction to force pattern break
+ const size_t opponentDirIndex = std::round(dirToOpponent / plane::pitchStep);
+ mInterestMap[opponentDirIndex % mInterestMap.size()] = 0.0f;
+
+ // Pattern break cooldown prevents immediate return to pattern
+ // This is handled automatically by the pattern tracker
+ }
+ else
+ {
+ // Normal pursuit logic
+ for ( const auto& [distance, position] : opponentPositionsSorted )
{
- if ( opponent.hasJumped() == false || botDifficulty > DIFFICULTY::EASY )
+ const auto dirToTargetAbsolute = get_angle_to_point(
+ pos, position );
+
+ const auto dirToTargetRelative = get_angle_relative(
+ self.dir(), dirToTargetAbsolute );
+
+ // Use enhanced shooting logic with movement prediction
+ if (shouldShootWithPrediction(self, opponent, pos, dirToTargetRelative, botDifficulty, mPersonality, mPatternTracker))
+ {
actions.push_back(AiAction::Shoot);
- }
+ }
- const size_t dirIndex = std::round(dirToTargetAbsolute / plane::pitchStep);
+ const size_t dirIndex = std::round(dirToTargetAbsolute / plane::pitchStep);
- if ( isOpponentBehind == true &&
- opponent.y() > self.y() &&
- opponent.speed() < plane::maxSpeedBase )
- {
- mInterestMap[5] = 0.01f;
- mInterestMap[11] = 0.01f;
+ if ( isOpponentBehind == true &&
+ opponent.y() > self.y() &&
+ opponent.speed() < plane::maxSpeedBase )
+ {
+ mInterestMap[5] = 0.01f;
+ mInterestMap[11] = 0.01f;
- continue;
- }
+ continue;
+ }
- mInterestMap[dirIndex % mInterestMap.size()] = distance;
+ mInterestMap[dirIndex % mInterestMap.size()] = distance;
+ }
}
+ }
const auto maxDanger = SDL_Vector{plane::maxSpeedBoosted, plane::maxSpeedBoosted}.length();
@@ -926,12 +1066,47 @@ AiStatePlane::update(
}
+
+ // Personality-based strategy application
+ // This system modifies AI behavior based on personality traits and tactical situation
+ if ( mPersonality.shouldUseDefensiveStrategy(self, opponent) )
+ {
+ // Defensive strategy: create danger zone around opponent to avoid engagement
+ // Used when AI has low health, opponent is stronger, or personality is defensive
+ const auto dirToOpponentAbsolute = get_angle_to_point(pos, opponentShortestPos);
+ const size_t opponentDirIndex = std::round(dirToOpponentAbsolute / plane::pitchStep);
+
+ // Create wide danger zone (7 directions) around opponent
+ // Danger strength inversely proportional to aggressiveness
+ for ( int i = -3; i <= 3; ++i )
+ {
+ const size_t dangerIndex = (opponentDirIndex + i + mDangerMap.size()) % mDangerMap.size();
+ mDangerMap[dangerIndex] += 0.4f * (1.0f - mPersonality.aggressiveness);
+ }
+ }
+ else if ( mPersonality.shouldUseAggressiveStrategy(self, opponent) )
+ {
+ // Aggressive strategy: create interest zone around opponent to encourage engagement
+ // Used when AI has health advantage, is close to opponent, or personality is aggressive
+ const auto dirToOpponentAbsolute = get_angle_to_point(pos, opponentShortestPos);
+ const size_t opponentDirIndex = std::round(dirToOpponentAbsolute / plane::pitchStep);
+
+ // Create focused interest zone (3 directions) around opponent
+ // Interest strength proportional to aggressiveness
+ for ( int i = -1; i <= 1; ++i )
+ {
+ const size_t interestIndex = (opponentDirIndex + i + mInterestMap.size()) % mInterestMap.size();
+ mInterestMap[interestIndex] += 0.3f * mPersonality.aggressiveness;
+ }
+ }
+
+
const bool shouldClimb =
opponent.isDead() == true ||
opponent.protectionRemainder() >= 0.5f * plane::spawnProtectionCooldown;
const bool shouldRescue =
- botDifficulty > DIFFICULTY::EASY &&
+ botDifficulty > DIFFICULTY::EASY &&
self.hp() < plane::maxHp &&
(opponent.isDead() == true || opponent.hasJumped() == true);