Simple Pigman AI

This commit is contained in:
Michal Pikulski
2025-12-04 08:52:59 +01:00
committed by Michal Pikulski
parent e60d516e7e
commit edb83ef13d
27 changed files with 7452 additions and 428 deletions

View File

@@ -1,24 +1,51 @@
using System.Collections;
using System.Collections.Generic;
using Core;
using Core.Lifecycle;
using Minigames.FortFight.Core;
using Minigames.FortFight.Data;
using Minigames.FortFight.Fort;
using UnityEngine;
namespace Minigames.FortFight.AI
{
/// <summary>
/// AI controller for the PigMan opponent.
/// Phase 1: Stubbed implementation - just simulates taking a turn.
/// Phase 4: Full implementation with trajectory calculation and target selection.
/// AI controller for Fort Fight opponent.
/// Implements trajectory calculation, target selection, and shot execution with configurable difficulty.
/// Includes debug visualization and thinking animations.
/// </summary>
public class FortFightAIController : ManagedBehaviour
{
[Header("AI Settings (Stubbed)")]
[SerializeField] private float aiThinkTime = 1.5f; // Time AI "thinks" before acting
#region Inspector Properties
private TurnManager turnManager;
private bool isThinking = false;
[Header("AI References")]
[SerializeField] private SlingshotController aiSlingshot;
[Tooltip("Optional: Animator for AI character thinking animations")]
[SerializeField] private Animator aiAnimator;
[Header("UI References")]
[SerializeField] private GameObject thinkingIndicator;
#endregion
#region Private State
private TurnManager _turnManager;
private AmmunitionManager _ammoManager;
private FortManager _fortManager;
private bool _isExecutingTurn = false;
// Current target info
private FortBlock _targetBlock;
private Vector2 _targetPosition;
private ProjectileType _selectedAmmo;
// Settings cache
private AppleHills.Core.Settings.IFortFightSettings _cachedSettings;
private AppleHills.Core.Settings.FortFightDeveloperSettings _cachedDevSettings;
private AIDifficultyData _currentDifficultyData;
#endregion
#region Initialization
@@ -27,19 +54,57 @@ namespace Minigames.FortFight.AI
/// </summary>
public void Initialize()
{
// Get reference to turn manager via singleton
turnManager = TurnManager.Instance;
// Load settings
_cachedSettings = GameManager.GetSettingsObject<AppleHills.Core.Settings.IFortFightSettings>();
_cachedDevSettings = GameManager.GetDeveloperSettings<AppleHills.Core.Settings.FortFightDeveloperSettings>();
if (turnManager == null)
if (_cachedSettings == null)
{
Logging.Error("[FortFightAIController] Failed to load FortFightSettings!");
}
// Load difficulty configuration from settings
if (_cachedSettings != null)
{
AIDifficulty difficulty = _cachedSettings.DefaultAIDifficulty;
_currentDifficultyData = _cachedSettings.GetAIDifficultyData(difficulty);
Logging.Debug($"[FortFightAIController] Loaded AI difficulty: {difficulty} - Angle: ±{_currentDifficultyData.angleDeviation}°, Force: ±{_currentDifficultyData.forceDeviation * 100}%");
}
// Get references to managers via singletons
_turnManager = TurnManager.Instance;
_ammoManager = AmmunitionManager.Instance;
_fortManager = FortManager.Instance;
if (_turnManager == null)
{
Logging.Error("[FortFightAIController] TurnManager not found!");
return;
}
// Subscribe to turn events
turnManager.OnTurnStarted += OnTurnStarted;
if (_ammoManager == null)
{
Logging.Error("[FortFightAIController] AmmunitionManager not found!");
return;
}
Logging.Debug("[FortFightAIController] AI initialized");
if (_fortManager == null)
{
Logging.Error("[FortFightAIController] FortManager not found!");
return;
}
if (aiSlingshot == null)
{
Logging.Error("[FortFightAIController] AI Slingshot not assigned!");
}
// Subscribe to turn events
_turnManager.OnTurnStarted += OnTurnStarted;
bool debugVisualsEnabled = _cachedDevSettings?.ShowAIDebugVisuals ?? false;
AIDifficulty activeDifficulty = _cachedSettings?.DefaultAIDifficulty ?? AIDifficulty.Medium;
Logging.Debug($"[FortFightAIController] AI initialized - Difficulty: {activeDifficulty}, Debug Visuals: {debugVisualsEnabled}");
}
#endregion
@@ -52,49 +117,403 @@ namespace Minigames.FortFight.AI
private void OnTurnStarted(PlayerData currentPlayer, TurnState turnState)
{
// Only act if it's AI's turn
if (turnState == TurnState.AITurn && !isThinking)
if (turnState == TurnState.AITurn && !_isExecutingTurn)
{
StartCoroutine(ExecuteAITurn());
}
}
/// <summary>
/// Execute the AI's turn (stubbed for Phase 1)
/// Execute the AI's turn with thinking phases and shot execution
/// </summary>
private IEnumerator ExecuteAITurn()
{
isThinking = true;
_isExecutingTurn = true;
Logging.Debug($"[FortFightAIController] AI is thinking... (for {aiThinkTime}s)");
Logging.Debug("[FortFightAIController] === AI TURN START ===");
// Simulate AI "thinking"
yield return new WaitForSeconds(aiThinkTime);
// Phase 1: Thinking - Target Selection
yield return StartCoroutine(ThinkingPhase("Analyzing targets..."));
_targetBlock = SelectBestTarget();
// STUBBED: Perform AI action
Logging.Debug("[FortFightAIController] AI takes action! (STUBBED - no actual projectile fired yet)");
if (_targetBlock == null)
{
Logging.Warning("[FortFightAIController] No valid target found! Skipping turn.");
_isExecutingTurn = false;
yield break;
}
// TODO Phase 4: AI should trigger its slingshot to fire projectile here
// Turn will automatically advance when AI's projectile settles (via ProjectileTurnAction)
// Do NOT manually call EndTurn() - it's now private and automatic
_targetPosition = _targetBlock.transform.position;
Logging.Debug($"[FortFightAIController] Target selected: {_targetBlock.gameObject.name} at {_targetPosition}");
// NOTE: For now, AI turn will hang until Phase 4 AI projectile system is implemented
// To test without AI, use TwoPlayer mode
// Phase 2: Thinking - Ammunition Selection
yield return StartCoroutine(ThinkingPhase("Selecting ammunition..."));
_selectedAmmo = ChooseProjectile();
Logging.Debug($"[FortFightAIController] Ammo selected: {_selectedAmmo}");
isThinking = false;
Logging.Warning("[FortFightAIController] AI turn stubbed - Phase 4 needed for AI projectile firing");
// Phase 3: Thinking - Trajectory Calculation
yield return StartCoroutine(ThinkingPhase("Calculating trajectory..."));
Vector2 launchVector = CalculateTrajectoryWithDeviation(aiSlingshot.transform.position, _targetPosition);
Logging.Debug($"[FortFightAIController] Trajectory calculated: {launchVector}");
// Phase 4: Execute Shot
yield return new WaitForSeconds(0.3f); // Brief pause before shot
ExecuteShot(launchVector);
Logging.Debug("[FortFightAIController] === AI TURN SHOT EXECUTED ===");
_isExecutingTurn = false;
// Turn will automatically end when projectile settles (handled by TurnManager)
}
/// <summary>
/// Thinking phase with random duration and optional animation trigger
/// </summary>
private IEnumerator ThinkingPhase(string thinkingAction)
{
float thinkTime = Random.Range(_currentDifficultyData.thinkTimeMin, _currentDifficultyData.thinkTimeMax);
Logging.Debug($"[FortFightAIController] {thinkingAction} ({thinkTime:F1}s)");
// TODO: Play some funny stuff here?
// TODO: Placeholder "thinking" exlamation point
if (thinkingIndicator != null)
{
thinkingIndicator.SetActive(true);
}
yield return new WaitForSeconds(thinkTime);
if (thinkingIndicator != null)
{
thinkingIndicator.SetActive(false);
}
}
#endregion
#region AI Logic
/// <summary>
/// Select the best target block to aim at
/// Priority: Weak points > Low HP blocks > Random block
/// </summary>
private FortBlock SelectBestTarget()
{
if (_fortManager.PlayerFort == null)
{
Logging.Error("[FortFightAIController] Player fort not found!");
return null;
}
List<FortBlock> weakPoints = _fortManager.PlayerFort.GetWeakPoints();
// Priority 1: Target weak points
if (weakPoints != null && weakPoints.Count > 0)
{
FortBlock target = weakPoints[Random.Range(0, weakPoints.Count)];
Logging.Debug($"[FortFightAIController] Targeting weak point: {target.gameObject.name}");
return target;
}
// Priority 2: Target lowest HP block
List<FortBlock> allBlocks = new List<FortBlock>();
foreach (Transform child in _fortManager.PlayerFort.transform)
{
FortBlock block = child.GetComponent<FortBlock>();
if (block != null && !block.IsDestroyed)
{
allBlocks.Add(block);
}
}
if (allBlocks.Count == 0)
{
Logging.Warning("[FortFightAIController] No blocks available to target!");
return null;
}
// Sort by HP and pick from bottom 30%
allBlocks.Sort((a, b) => a.CurrentHp.CompareTo(b.CurrentHp));
int targetRange = Mathf.Max(1, allBlocks.Count / 3);
FortBlock lowestHpTarget = allBlocks[Random.Range(0, targetRange)];
Logging.Debug($"[FortFightAIController] Targeting low HP block: {lowestHpTarget.gameObject.name} (HP: {lowestHpTarget.CurrentHp})");
return lowestHpTarget;
}
/// <summary>
/// Choose the best projectile type for current situation
/// </summary>
private ProjectileType ChooseProjectile()
{
// Get AI player index
int playerIndex = _turnManager.CurrentPlayer.PlayerIndex;
// Get available projectiles (check cooldowns via ammo manager)
var availableTypes = new List<ProjectileType>();
foreach (ProjectileType type in System.Enum.GetValues(typeof(ProjectileType)))
{
// Check if ammo is available (not on cooldown)
if (_ammoManager.IsAmmoAvailable(type, playerIndex))
{
availableTypes.Add(type);
}
}
if (availableTypes.Count == 0)
{
Logging.Warning("[FortFightAIController] No ammo available! Using Toaster as default.");
return ProjectileType.Toaster;
}
float enemyHpPercent = _fortManager.PlayerFort.HpPercentage;
// Strategic selection based on fort HP
if (enemyHpPercent > 70f && availableTypes.Contains(ProjectileType.TrashBag))
{
return ProjectileType.TrashBag; // Spread damage early game
}
else if (enemyHpPercent > 40f && availableTypes.Contains(ProjectileType.Vacuum))
{
return ProjectileType.Vacuum; // Destruction machine mid-game
}
else if (_targetBlock != null && _targetBlock.IsWeakPoint && availableTypes.Contains(ProjectileType.CeilingFan))
{
return ProjectileType.CeilingFan; // Drop on weak points
}
// Default to first available
return availableTypes[Random.Range(0, availableTypes.Count)];
}
/// <summary>
/// Calculate ballistic trajectory to hit target using proper parabolic flight physics
/// Returns velocity vector needed to hit the target
/// </summary>
private Vector2 CalculateTrajectoryWithDeviation(Vector2 from, Vector2 to)
{
// Get gravity from Physics2D settings
float gravity = Mathf.Abs(Physics2D.gravity.y);
if (gravity < 0.1f) gravity = 9.81f; // Fallback to standard gravity
// Calculate displacement
Vector2 displacement = to - from;
float horizontalDist = displacement.x;
float verticalDist = displacement.y;
// Calculate perfect launch velocity using ballistic trajectory equations
// We'll use a 45-degree angle as base (optimal for distance) and adjust
Vector2 perfectVelocity = CalculateBallisticVelocity(horizontalDist, verticalDist, gravity);
if (perfectVelocity == Vector2.zero)
{
// Fallback if physics calculation fails
Logging.Warning("[FortFightAIController] Ballistic calculation failed, using fallback");
perfectVelocity = displacement.normalized * 15f;
}
// Apply difficulty-based deviation to velocity
Vector2 deviatedVelocity = ApplyDeviation(perfectVelocity);
// Log detailed trajectory info
float angle = Mathf.Atan2(deviatedVelocity.y, deviatedVelocity.x) * Mathf.Rad2Deg;
float speed = deviatedVelocity.magnitude;
Logging.Debug($"[FortFightAIController] Ballistic Trajectory: angle={angle:F1}°, speed={speed:F1} m/s, distance={displacement.magnitude:F1}m, gravity={gravity:F1}");
return deviatedVelocity;
}
/// <summary>
/// Calculate required velocity to hit target using ballistic equations
/// Uses quadratic formula to solve for launch angle given distance and height
/// </summary>
private Vector2 CalculateBallisticVelocity(float dx, float dy, float gravity)
{
// Try multiple launch angles and pick the first valid solution
// We prefer lower angles (30-60 degrees) for more direct shots
float[] preferredAngles = { 45f, 40f, 50f, 35f, 55f, 30f, 60f };
foreach (float angleDegrees in preferredAngles)
{
float angleRad = angleDegrees * Mathf.Deg2Rad;
// Calculate required speed for this angle
// Formula: v = sqrt((g * dx^2) / (2 * cos^2(θ) * (dx * tan(θ) - dy)))
float cosTheta = Mathf.Cos(angleRad);
float tanTheta = Mathf.Tan(angleRad);
float denominator = 2f * cosTheta * cosTheta * (dx * tanTheta - dy);
if (denominator > 0)
{
float speedSquared = (gravity * dx * dx) / denominator;
if (speedSquared > 0)
{
float speed = Mathf.Sqrt(speedSquared);
// Clamp speed to reasonable range (5-25 m/s)
speed = Mathf.Clamp(speed, 5f, 25f);
// Calculate velocity components
float vx = speed * cosTheta * Mathf.Sign(dx);
float vy = speed * Mathf.Sin(angleRad);
return new Vector2(vx, vy);
}
}
}
// If no valid solution found, use simple physics
// Estimate time of flight and calculate velocity
float distance = Mathf.Sqrt(dx * dx + dy * dy);
float estimatedTime = Mathf.Sqrt(2f * distance / gravity);
if (estimatedTime > 0)
{
float vx = dx / estimatedTime;
float vy = dy / estimatedTime + 0.5f * gravity * estimatedTime;
return new Vector2(vx, vy);
}
return Vector2.zero;
}
/// <summary>
/// Apply difficulty-based deviation to velocity
/// </summary>
private Vector2 ApplyDeviation(Vector2 perfectVelocity)
{
float angleDeviation = GetAngleDeviation();
float speedDeviation = GetForceDeviation(); // Reuse for speed deviation
// Convert velocity to polar coordinates
float speed = perfectVelocity.magnitude;
float angle = Mathf.Atan2(perfectVelocity.y, perfectVelocity.x) * Mathf.Rad2Deg;
// Apply angle deviation
angle += Random.Range(-angleDeviation, angleDeviation);
// Apply speed deviation
speed *= Random.Range(1f - speedDeviation, 1f + speedDeviation);
// Convert back to cartesian
float angleRad = angle * Mathf.Deg2Rad;
return new Vector2(Mathf.Cos(angleRad), Mathf.Sin(angleRad)) * speed;
}
/// <summary>
/// Get angle deviation based on current difficulty data
/// </summary>
private float GetAngleDeviation()
{
return _currentDifficultyData.angleDeviation;
}
/// <summary>
/// Get force deviation based on current difficulty data
/// </summary>
private float GetForceDeviation()
{
return _currentDifficultyData.forceDeviation;
}
/// <summary>
/// Execute the shot by launching projectile from AI slingshot with calculated velocity
/// </summary>
private void ExecuteShot(Vector2 launchVelocity)
{
if (aiSlingshot == null)
{
Logging.Error("[FortFightAIController] AI Slingshot not assigned! Cannot execute shot.");
return;
}
// Set ammunition type for AI player
int playerIndex = _turnManager.CurrentPlayer.PlayerIndex;
_ammoManager.SelectAmmo(_selectedAmmo, playerIndex);
// Get the selected ammo config and set it on the slingshot
ProjectileConfig ammoConfig = _ammoManager.GetSelectedAmmoConfig(playerIndex);
if (ammoConfig != null)
{
aiSlingshot.SetAmmo(ammoConfig);
}
else
{
Logging.Error("[FortFightAIController] Failed to get ammo config!");
return;
}
// Fire projectile using velocity-based launch (proper ballistic physics)
aiSlingshot.LaunchWithVelocity(launchVelocity);
// Trigger shot animation if animator exists
if (aiAnimator != null)
{
aiAnimator.SetTrigger("Shoot");
}
Logging.Debug($"[FortFightAIController] Shot executed: {_selectedAmmo} with velocity {launchVelocity}");
}
#endregion
#region Debug Visualization
private void OnDrawGizmos()
{
// Get developer settings (works in both editor and play mode)
var devSettings = AppleHills.SettingsAccess.GetDeveloperSettingsForEditor<AppleHills.Core.Settings.FortFightDeveloperSettings>();
if (devSettings == null || !devSettings.ShowAIDebugVisuals) return;
// Only draw during play mode when AI is actively executing
if (!Application.isPlaying) return;
// Draw target circle
if (_targetBlock != null && _isExecutingTurn)
{
Gizmos.color = devSettings.AIDebugTargetColor;
DrawCircle(_targetPosition, devSettings.AIDebugCircleRadius);
// Draw line from slingshot to target
if (aiSlingshot != null)
{
Gizmos.color = devSettings.AIDebugTrajectoryColor;
Gizmos.DrawLine(aiSlingshot.transform.position, _targetPosition);
}
}
}
/// <summary>
/// Draw a circle gizmo (approximation)
/// </summary>
private void DrawCircle(Vector2 center, float radius)
{
int segments = 32;
float angleStep = 360f / segments;
Vector3 prevPoint = center + new Vector2(radius, 0);
for (int i = 1; i <= segments; i++)
{
float angle = angleStep * i * Mathf.Deg2Rad;
Vector3 newPoint = center + new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
Gizmos.DrawLine(prevPoint, newPoint);
prevPoint = newPoint;
}
}
#endregion
#region Cleanup
internal override void OnManagedDestroy()
{
base.OnManagedDestroy();
if (turnManager != null)
if (_turnManager != null)
{
turnManager.OnTurnStarted -= OnTurnStarted;
_turnManager.OnTurnStarted -= OnTurnStarted;
}
}