chunk 1: core gameplay scripts scenes runtime assets

This commit is contained in:
2026-04-06 11:02:34 +03:00
parent fa0388bc79
commit 0d11a097d8
703 changed files with 2292651 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
using UnityEngine;
/// <summary>
/// UtilityScorer — Evaluates all AI actions and returns the highest-scoring one.
/// Stateless, zero-allocation. Called by AIBrain on each decision tick.
///
/// Uses Response Curves for nuanced decisions rather than hard thresholds.
/// Each AIAction.Score() returns 0-1 utility; UtilityScorer scales by role weights.
/// </summary>
public class UtilityScorer
{
// ─── Role Weight Profiles ────────────────────────────────
public enum AIRole
{
Balanced,
Collector,
Defender,
Aggressor
}
[System.Serializable]
public struct RoleWeights
{
public float collectWeight;
public float depositWeight;
public float attackWeight;
public float defendWeight;
public float retreatWeight;
public float escortWeight;
public float interceptWeight;
public float patrolWeight;
public static RoleWeights Balanced => new RoleWeights
{
collectWeight = 1.0f,
depositWeight = 1.0f,
attackWeight = 1.0f,
defendWeight = 1.2f,
retreatWeight = 1.0f,
escortWeight = 0.8f,
interceptWeight = 1.0f,
patrolWeight = 1.0f
};
public static RoleWeights Collector => new RoleWeights
{
collectWeight = 1.5f,
depositWeight = 1.3f,
attackWeight = 0.6f,
defendWeight = 0.8f,
retreatWeight = 1.2f,
escortWeight = 0.5f,
interceptWeight = 0.7f,
patrolWeight = 1.0f
};
public static RoleWeights Defender => new RoleWeights
{
collectWeight = 0.6f,
depositWeight = 0.8f,
attackWeight = 1.2f,
defendWeight = 2.0f,
retreatWeight = 0.8f,
escortWeight = 1.5f,
interceptWeight = 1.3f,
patrolWeight = 0.6f
};
public static RoleWeights Aggressor => new RoleWeights
{
collectWeight = 0.5f,
depositWeight = 0.7f,
attackWeight = 1.8f,
defendWeight = 0.8f,
retreatWeight = 0.6f,
escortWeight = 0.4f,
interceptWeight = 1.6f,
patrolWeight = 0.5f
};
public static RoleWeights ForRole(AIRole role)
{
switch (role)
{
case AIRole.Collector: return Collector;
case AIRole.Defender: return Defender;
case AIRole.Aggressor: return Aggressor;
default: return Balanced;
}
}
}
// ─── Pre-allocated action instances (no allocation per tick) ──
private readonly CollectBagAction _collectAction = new CollectBagAction();
private readonly DepositBagAction _depositAction = new DepositBagAction();
private readonly AttackEnemyAction _attackAction = new AttackEnemyAction();
private readonly DefendVaultAction _defendAction = new DefendVaultAction();
private readonly PatrolAction _patrolAction = new PatrolAction();
private readonly RetreatAction _retreatAction = new RetreatAction();
private readonly EscortCarrierAction _escortAction = new EscortCarrierAction();
private readonly InterceptCarrierAction _interceptAction = new InterceptCarrierAction();
private readonly AIAction[] _allActions;
private RoleWeights _weights;
// ─── Decision State ──────────────────────────────────────
private AIAction _currentAction;
private float _currentActionScore;
private float _minSwitchTime = 1.5f;
private float _actionStartTime;
// ─── Debug ───────────────────────────────────────────────
public AIAction CurrentAction => _currentAction;
public float CurrentActionScore => _currentActionScore;
public string CurrentActionName => _currentAction?.Name ?? "None";
// Expose for debug display
private readonly float[] _lastScores = new float[8];
public float[] LastScores => _lastScores;
public UtilityScorer(AIRole role = AIRole.Balanced)
{
_weights = RoleWeights.ForRole(role);
_allActions = new AIAction[]
{
_collectAction, // 0
_depositAction, // 1
_attackAction, // 2
_defendAction, // 3
_patrolAction, // 4
_retreatAction, // 5
_escortAction, // 6
_interceptAction // 7
};
}
/// <summary>
/// Evaluate all actions and return the best one.
/// Zero allocation — all actions are pre-allocated.
/// </summary>
public AIAction Evaluate(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
{
float bestScore = float.MinValue;
AIAction bestAction = null;
for (int i = 0; i < _allActions.Length; i++)
{
float rawScore = _allActions[i].Score(bb, tb);
float weight = GetWeight(i);
float weightedScore = rawScore * weight;
_lastScores[i] = weightedScore; // Store for debug
if (weightedScore > bestScore)
{
bestScore = weightedScore;
bestAction = _allActions[i];
}
}
// Hysteresis: don't switch unless new action is significantly better
// (prevents oscillation between similar-scoring actions)
if (_currentAction != null && bestAction != _currentAction)
{
bool timeLocked = (Time.time - _actionStartTime) < _minSwitchTime;
bool canInterrupt = bestAction.CanInterrupt;
bool significantlyBetter = bestScore > _currentActionScore * 1.2f;
if (timeLocked && !canInterrupt && !significantlyBetter)
{
return _currentAction; // Stay with current action
}
}
// Switch action
if (bestAction != _currentAction)
{
_currentAction?.OnExit(brain, bb, tb);
_currentAction = bestAction;
_currentActionScore = bestScore;
_actionStartTime = Time.time;
_currentAction?.OnEnter(brain, bb, tb);
}
return _currentAction;
}
/// <summary>Force reset (e.g., on death/respawn).</summary>
public void Reset()
{
_currentAction = null;
_currentActionScore = 0f;
}
private float GetWeight(int actionIndex)
{
switch (actionIndex)
{
case 0: return _weights.collectWeight;
case 1: return _weights.depositWeight;
case 2: return _weights.attackWeight;
case 3: return _weights.defendWeight;
case 4: return _weights.patrolWeight;
case 5: return _weights.retreatWeight;
case 6: return _weights.escortWeight;
case 7: return _weights.interceptWeight;
default: return 1f;
}
}
public void SetRole(AIRole role)
{
_weights = RoleWeights.ForRole(role);
}
}