Simple Pigman AI
This commit is contained in:
committed by
Michal Pikulski
parent
e60d516e7e
commit
edb83ef13d
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user