chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
471
Assets/Scripts/AI/Actions/AIActions.cs
Normal file
471
Assets/Scripts/AI/Actions/AIActions.cs
Normal file
@@ -0,0 +1,471 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user