472 lines
14 KiB
C#
472 lines
14 KiB
C#
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// DepositBagAction — Navigate to own vault and deposit carried bags.
|
|
/// Score increases with more bags carried and when vault is nearby.
|
|
/// </summary>
|
|
public class DepositBagAction : AIAction
|
|
{
|
|
public override string Name => "DepositBag";
|
|
|
|
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.isDead || !bb.isCarrying) return 0f;
|
|
if (bb.myVault == null) return 0f;
|
|
|
|
float score = 0.4f; // Base desire to deposit
|
|
|
|
// More bags = stronger urge to deposit (risk increases)
|
|
score += bb.carriedBagCount * 0.2f;
|
|
|
|
// High value = deposit urgently
|
|
if (bb.totalCarriedValue >= 200f)
|
|
score += 0.15f;
|
|
|
|
// Enemies nearby while carrying = deposit NOW
|
|
if (bb.nearbyEnemyCount > 0 && bb.nearestEnemyDistance < 12f)
|
|
score += 0.25f;
|
|
|
|
// Low health while carrying = deposit ASAP
|
|
if (bb.healthPercent < 0.4f)
|
|
score += 0.2f;
|
|
|
|
// Close to vault = just do it
|
|
float vaultDist = Vector3.Distance(
|
|
bb.myVault.transform.position,
|
|
bb.myVault.transform.position // Will be overridden by brain position
|
|
);
|
|
|
|
// Bonus if losing (need to score to catch up)
|
|
if (bb.scoreDifference < -200f)
|
|
score += 0.1f;
|
|
|
|
return Mathf.Clamp01(score);
|
|
}
|
|
|
|
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.myVault == null) return;
|
|
|
|
brain.NavigateTo(bb.myVault.transform.position);
|
|
|
|
// Check if close enough to deposit
|
|
float dist = Vector3.Distance(brain.transform.position, bb.myVault.transform.position);
|
|
if (dist < 3f)
|
|
{
|
|
CashCarrier carrier = brain.GetComponent<CashCarrier>();
|
|
if (carrier != null && carrier.IsCarrying)
|
|
{
|
|
carrier.DepositToVault(bb.myVault);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// AttackEnemyAction — Engage nearest enemy. Prioritizes bag carriers and low-HP targets.
|
|
/// Respects team engagement limits (max 2 AI per enemy).
|
|
/// </summary>
|
|
public class AttackEnemyAction : AIAction
|
|
{
|
|
public override string Name => "AttackEnemy";
|
|
public override bool CanInterrupt => true;
|
|
|
|
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.isDead) return 0f;
|
|
if (bb.nearbyEnemyCount == 0) return 0f;
|
|
|
|
float score = 0.35f; // Base aggression
|
|
|
|
// Enemy carrier spotted — HIGH priority
|
|
if (bb.nearestEnemyCarrier != null)
|
|
{
|
|
float carrierDist = Vector3.Distance(bb.nearestEnemyCarrier.transform.position,
|
|
bb.myVault != null ? bb.myVault.transform.position : Vector3.zero);
|
|
score += 0.35f;
|
|
|
|
// More bags on carrier = more urgent
|
|
score += bb.nearestEnemyCarrier.BagCount * 0.1f;
|
|
}
|
|
|
|
// Close enemy = react aggressively
|
|
float proximityFactor = InverseCurve(bb.nearestEnemyDistance, 15f);
|
|
score *= (0.5f + proximityFactor * 0.5f);
|
|
|
|
// Outnumber advantage
|
|
if (bb.nearbyAllyCount > bb.nearbyEnemyCount)
|
|
score += 0.1f;
|
|
|
|
// Don't be aggressive when low health (unless enemy carrier)
|
|
if (bb.healthPercent < 0.3f && bb.nearestEnemyCarrier == null)
|
|
score *= 0.3f;
|
|
|
|
// Carrying bags = less aggressive (protect the payload)
|
|
if (bb.carriedBagCount > 0)
|
|
score *= 0.5f;
|
|
|
|
return Mathf.Clamp01(score);
|
|
}
|
|
|
|
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
// Choose target: carrier > low HP > nearest
|
|
Transform target = null;
|
|
float bestPriority = float.MinValue;
|
|
|
|
for (int i = 0; i < bb.perceivedEnemies.Count; i++)
|
|
{
|
|
PerceivedEntity enemy = bb.perceivedEnemies[i];
|
|
if (enemy.gameObject == null) continue;
|
|
|
|
// Skip if too many teammates already targeting
|
|
int engagedCount = tb.GetEnemyTargetCount(enemy.transform);
|
|
if (engagedCount >= 2) continue;
|
|
|
|
float priority = 0f;
|
|
|
|
// Carrier priority
|
|
if (enemy.isCarryingBag)
|
|
priority += 50f + enemy.carriedBagCount * 20f;
|
|
|
|
// Low HP priority
|
|
priority += (1f - enemy.healthPercent) * 30f;
|
|
|
|
// Distance priority (closer = higher)
|
|
priority += InverseCurve(enemy.distance, 15f) * 20f;
|
|
|
|
// Visible bonus
|
|
if (enemy.isVisible) priority += 10f;
|
|
|
|
if (priority > bestPriority)
|
|
{
|
|
bestPriority = priority;
|
|
target = enemy.transform;
|
|
}
|
|
}
|
|
|
|
if (target == null) return;
|
|
|
|
// Register targeting
|
|
bb.currentTarget = target;
|
|
tb.AddEnemyTarget(target);
|
|
|
|
// Delegate combat to CharacterAIController
|
|
brain.EngageTarget(target);
|
|
}
|
|
|
|
public override void OnExit(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.currentTarget != null)
|
|
tb.RemoveEnemyTarget(bb.currentTarget);
|
|
bb.currentTarget = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// DefendVaultAction — Rush to own vault when under attack.
|
|
/// Highest priority interrupt action.
|
|
/// </summary>
|
|
public class DefendVaultAction : AIAction
|
|
{
|
|
public override string Name => "DefendVault";
|
|
public override bool CanInterrupt => true;
|
|
|
|
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.isDead) return 0f;
|
|
if (!bb.myVaultUnderAttack) return 0f;
|
|
|
|
float score = 0.9f; // Very high base — vault defense is critical
|
|
|
|
// Closer to vault = we should definitely defend
|
|
if (bb.myVault != null)
|
|
{
|
|
float dist = Vector3.Distance(bb.myVault.transform.position, bb.myVault.transform.position);
|
|
score += InverseCurve(dist, 30f) * 0.1f;
|
|
}
|
|
|
|
return Mathf.Clamp01(score);
|
|
}
|
|
|
|
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.myVault == null) return;
|
|
|
|
// If carrying, deposit first
|
|
CashCarrier carrier = brain.GetComponent<CashCarrier>();
|
|
float vaultDist = Vector3.Distance(brain.transform.position, bb.myVault.transform.position);
|
|
|
|
if (carrier != null && carrier.IsCarrying && vaultDist < 3f)
|
|
{
|
|
carrier.DepositToVault(bb.myVault);
|
|
}
|
|
|
|
// Check for enemies near vault
|
|
Transform nearestEnemyAtVault = null;
|
|
float nearestDist = float.MaxValue;
|
|
|
|
for (int i = 0; i < bb.perceivedEnemies.Count; i++)
|
|
{
|
|
PerceivedEntity enemy = bb.perceivedEnemies[i];
|
|
if (enemy.gameObject == null) continue;
|
|
|
|
float enemyVaultDist = Vector3.Distance(enemy.transform.position, bb.myVault.transform.position);
|
|
if (enemyVaultDist < 10f && enemyVaultDist < nearestDist)
|
|
{
|
|
nearestDist = enemyVaultDist;
|
|
nearestEnemyAtVault = enemy.transform;
|
|
}
|
|
}
|
|
|
|
if (nearestEnemyAtVault != null)
|
|
{
|
|
brain.EngageTarget(nearestEnemyAtVault);
|
|
}
|
|
else
|
|
{
|
|
brain.NavigateTo(bb.myVault.transform.position);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// PatrolAction — Wander randomly via NavMesh when no other objective.
|
|
/// Lowest-priority fallback action.
|
|
/// </summary>
|
|
public class PatrolAction : AIAction
|
|
{
|
|
public override string Name => "Patrol";
|
|
|
|
private float _pauseEndTime;
|
|
|
|
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.isDead) return 0f;
|
|
|
|
// Patrol is the default — returns low score so anything else takes priority
|
|
return 0.1f;
|
|
}
|
|
|
|
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (Time.time < _pauseEndTime) return;
|
|
|
|
if (!bb.hasPatrolDestination || brain.HasReachedDestination())
|
|
{
|
|
if (bb.hasPatrolDestination)
|
|
{
|
|
// Pause at patrol point
|
|
_pauseEndTime = Time.time + Random.Range(1f, 3f);
|
|
bb.hasPatrolDestination = false;
|
|
return;
|
|
}
|
|
|
|
// Generate new patrol destination
|
|
Vector3 randomPoint = brain.transform.position + Random.insideUnitSphere * 12f;
|
|
randomPoint.y = brain.transform.position.y;
|
|
|
|
UnityEngine.AI.NavMeshHit hit;
|
|
if (UnityEngine.AI.NavMesh.SamplePosition(randomPoint, out hit, 12f, UnityEngine.AI.NavMesh.AllAreas))
|
|
{
|
|
bb.patrolDestination = hit.position;
|
|
bb.hasPatrolDestination = true;
|
|
brain.NavigateTo(bb.patrolDestination);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
brain.NavigateTo(bb.patrolDestination);
|
|
}
|
|
}
|
|
|
|
public override void OnExit(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
bb.hasPatrolDestination = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// RetreatAction — Fall back to vault when low health.
|
|
/// Deposits any carried bags along the way.
|
|
/// </summary>
|
|
public class RetreatAction : AIAction
|
|
{
|
|
public override string Name => "Retreat";
|
|
public override bool CanInterrupt => true;
|
|
|
|
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.isDead) return 0f;
|
|
|
|
// Only retreat when health is low
|
|
if (bb.healthPercent > 0.3f) return 0f;
|
|
|
|
float score = 0.7f; // High priority when triggered
|
|
|
|
// Lower health = more urgent
|
|
score += InverseCurve(bb.healthPercent, 0.3f) * 0.3f;
|
|
|
|
// Even more urgent if carrying bags
|
|
if (bb.isCarrying)
|
|
score += 0.1f;
|
|
|
|
// Less urgent if no enemies nearby
|
|
if (bb.nearbyEnemyCount == 0)
|
|
score *= 0.6f;
|
|
|
|
return Mathf.Clamp01(score);
|
|
}
|
|
|
|
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.myVault == null) return;
|
|
|
|
brain.NavigateTo(bb.myVault.transform.position);
|
|
|
|
// Deposit if close to vault
|
|
float vaultDist = Vector3.Distance(brain.transform.position, bb.myVault.transform.position);
|
|
if (vaultDist < 3f)
|
|
{
|
|
CashCarrier carrier = brain.GetComponent<CashCarrier>();
|
|
if (carrier != null && carrier.IsCarrying)
|
|
carrier.DepositToVault(bb.myVault);
|
|
}
|
|
|
|
// If health recovered, exit retreat
|
|
if (bb.healthPercent > 0.5f)
|
|
{
|
|
// Score will drop, brain will switch naturally
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// EscortCarrierAction — Follow and protect an ally who is carrying bags.
|
|
/// Maintains distance, engages threats approaching the carrier.
|
|
/// </summary>
|
|
public class EscortCarrierAction : AIAction
|
|
{
|
|
public override string Name => "EscortCarrier";
|
|
|
|
private const float ESCORT_DISTANCE = 4f;
|
|
private const float THREAT_RANGE = 10f;
|
|
|
|
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.isDead) return 0f;
|
|
if (bb.isCarrying) return 0f; // Don't escort if carrying ourselves
|
|
|
|
// Need an ally carrier to escort
|
|
if (bb.nearestAllyCarrier == null) return 0f;
|
|
|
|
float score = 0.45f;
|
|
|
|
// More bags on ally = more valuable to escort
|
|
score += bb.nearestAllyCarrier.BagCount * 0.1f;
|
|
|
|
// Enemies near the carrier = escort more important
|
|
float carrierPos = 0;
|
|
for (int i = 0; i < bb.perceivedEnemies.Count; i++)
|
|
{
|
|
float enemyDist = Vector3.Distance(
|
|
bb.perceivedEnemies[i].transform.position,
|
|
bb.nearestAllyCarrier.transform.position);
|
|
if (enemyDist < THREAT_RANGE)
|
|
score += 0.15f;
|
|
}
|
|
|
|
// Already close to carrier = maintain escort
|
|
float distToCarrier = Vector3.Distance(bb.nearestAllyCarrier.transform.position, bb.myVault.transform.position);
|
|
if (distToCarrier < ESCORT_DISTANCE * 2f)
|
|
score += 0.1f;
|
|
|
|
return Mathf.Clamp01(score);
|
|
}
|
|
|
|
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.nearestAllyCarrier == null) return;
|
|
|
|
Vector3 carrierPos = bb.nearestAllyCarrier.transform.position;
|
|
|
|
// Check for threats near carrier
|
|
Transform nearestThreat = null;
|
|
float nearestThreatDist = float.MaxValue;
|
|
|
|
for (int i = 0; i < bb.perceivedEnemies.Count; i++)
|
|
{
|
|
PerceivedEntity enemy = bb.perceivedEnemies[i];
|
|
float distToCarrier = Vector3.Distance(enemy.transform.position, carrierPos);
|
|
if (distToCarrier < THREAT_RANGE && distToCarrier < nearestThreatDist)
|
|
{
|
|
nearestThreatDist = distToCarrier;
|
|
nearestThreat = enemy.transform;
|
|
}
|
|
}
|
|
|
|
if (nearestThreat != null)
|
|
{
|
|
// Engage threat
|
|
brain.EngageTarget(nearestThreat);
|
|
}
|
|
else
|
|
{
|
|
// Follow carrier at distance
|
|
float distToCarrier = Vector3.Distance(brain.transform.position, carrierPos);
|
|
if (distToCarrier > ESCORT_DISTANCE)
|
|
{
|
|
brain.NavigateTo(carrierPos);
|
|
}
|
|
else
|
|
{
|
|
brain.StopNavigation();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// InterceptCarrierAction — Chase and attack an enemy who is carrying bags.
|
|
/// Highest combat priority.
|
|
/// </summary>
|
|
public class InterceptCarrierAction : AIAction
|
|
{
|
|
public override string Name => "InterceptCarrier";
|
|
public override bool CanInterrupt => true;
|
|
|
|
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.isDead) return 0f;
|
|
if (bb.nearestEnemyCarrier == null) return 0f;
|
|
|
|
float score = 0.75f; // Very high base priority
|
|
|
|
// More bags = more vital to intercept
|
|
score += bb.nearestEnemyCarrier.BagCount * 0.08f;
|
|
|
|
// Close to our vault = CRITICAL
|
|
if (bb.myVault != null)
|
|
{
|
|
float carrierToVault = Vector3.Distance(
|
|
bb.nearestEnemyCarrier.transform.position,
|
|
bb.myVault.transform.position);
|
|
if (carrierToVault < 15f)
|
|
score += 0.15f;
|
|
}
|
|
|
|
// Low health = less interception
|
|
if (bb.healthPercent < 0.25f)
|
|
score *= 0.4f;
|
|
|
|
return Mathf.Clamp01(score);
|
|
}
|
|
|
|
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
|
{
|
|
if (bb.nearestEnemyCarrier == null) return;
|
|
|
|
brain.EngageTarget(bb.nearestEnemyCarrier.transform);
|
|
}
|
|
}
|