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

8
Assets/Scripts/AI.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e9cc74237c7d2433eb1585fde95783ef
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bbb831ed5741e5286af289e75349418b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
using UnityEngine;
/// <summary>
/// AIAction — Abstract base class for all AI actions.
/// Each action knows how to score itself (utility) and execute.
/// Stateless scoring — all context comes from AIBlackboard.
///
/// Subclasses: CollectBagAction, DepositBagAction, AttackEnemyAction,
/// DefendVaultAction, PatrolAction, RetreatAction,
/// EscortCarrierAction, InterceptCarrierAction
/// </summary>
public abstract class AIAction
{
/// <summary>Display name for debug.</summary>
public abstract string Name { get; }
/// <summary>
/// Evaluate how desirable this action is (0-1 range, higher = more desirable).
/// Called every AI decision tick. Must be allocation-free.
/// </summary>
/// <param name="blackboard">Per-AI knowledge store</param>
/// <param name="teamBoard">Shared team knowledge</param>
/// <returns>Utility score 0-1</returns>
public abstract float Score(AIBlackboard blackboard, TeamBlackboard teamBoard);
/// <summary>
/// Execute this action for one tick.
/// Called every AI tick while this is the active action.
/// </summary>
/// <param name="brain">The AI brain executing this action</param>
/// <param name="blackboard">Per-AI knowledge</param>
/// <param name="teamBoard">Team knowledge</param>
public abstract void Execute(AIBrain brain, AIBlackboard blackboard, TeamBlackboard teamBoard);
/// <summary>Called when this action becomes active (optional setup).</summary>
public virtual void OnEnter(AIBrain brain, AIBlackboard blackboard, TeamBlackboard teamBoard) { }
/// <summary>Called when this action is replaced by another (optional cleanup).</summary>
public virtual void OnExit(AIBrain brain, AIBlackboard blackboard, TeamBlackboard teamBoard) { }
/// <summary>
/// Whether this action can interrupt the current action.
/// High-priority actions (defend vault, retreat) return true.
/// </summary>
public virtual bool CanInterrupt => false;
// ─── Utility Helpers ─────────────────────────────────────
/// <summary>Smooth curve: high when value is low, low when value is high.</summary>
protected float InverseCurve(float value, float max)
{
return Mathf.Clamp01(1f - (value / max));
}
/// <summary>Smooth curve: high when value is high.</summary>
protected float LinearCurve(float value, float max)
{
return Mathf.Clamp01(value / max);
}
/// <summary>Step function: 1 if above threshold, 0 otherwise.</summary>
protected float StepAbove(float value, float threshold)
{
return value >= threshold ? 1f : 0f;
}
/// <summary>Step function: 1 if below threshold, 0 otherwise.</summary>
protected float StepBelow(float value, float threshold)
{
return value < threshold ? 1f : 0f;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fde981af550c7988ebf29c73e80a06b2

View 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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ba24431079dff36f3822b97125261be6

View File

@@ -0,0 +1,107 @@
using UnityEngine;
/// <summary>
/// CollectBagAction — Navigate to nearest unclaimed available/dropped cash box.
/// Prioritizes hot-drop boxes (bonus value) and closer boxes.
/// Respects TeamBlackboard claims to prevent clustering.
/// </summary>
public class CollectBagAction : AIAction
{
public override string Name => "CollectBag";
public override float Score(AIBlackboard bb, TeamBlackboard tb)
{
// Can't collect if dead or already full
if (bb.isDead) return 0f;
if (bb.carriedBagCount >= 3) return 0f;
// No bags nearby
int totalNearby = bb.nearbyAvailableBags.Count + bb.nearbyDroppedBags.Count;
if (totalNearby == 0) return 0f;
float score = 0.5f; // Base desire to collect
// Bonus for hot-drop bags (time-sensitive)
for (int i = 0; i < bb.nearbyDroppedBags.Count; i++)
{
if (bb.nearbyDroppedBags[i].IsHotDrop)
{
score += 0.2f;
break;
}
}
// Distance factor — closer bags = higher score
float distanceFactor = InverseCurve(bb.nearestBagDistance, 25f);
score *= (0.5f + distanceFactor * 0.5f);
// Less desire to collect when already carrying (deposit instead)
if (bb.carriedBagCount >= 2)
score *= 0.3f;
else if (bb.carriedBagCount >= 1)
score *= 0.6f;
// Reduce if nearby enemies (risky to collect)
if (bb.nearbyEnemyCount > 0 && bb.nearestEnemyDistance < 8f)
score *= 0.6f;
// Low health = less collecting
score *= (0.3f + bb.healthPercent * 0.7f);
return Mathf.Clamp01(score);
}
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
{
// Find best bag (prefer dropped hot-drops, then closest available)
CashBag bestBag = null;
float bestScore = float.MinValue;
// Check dropped bags first (higher priority)
for (int i = 0; i < bb.nearbyDroppedBags.Count; i++)
{
CashBag bag = bb.nearbyDroppedBags[i];
if (bag == null || bag.IsCarried) continue;
if (tb.IsBagClaimed(bag, brain)) continue;
float dist = Vector3.Distance(brain.transform.position, bag.transform.position);
float bagScore = bag.GetEffectiveValue() / Mathf.Max(dist, 1f);
if (bag.IsHotDrop) bagScore *= 2f; // Big bonus for hot drops
if (bagScore > bestScore) { bestScore = bagScore; bestBag = bag; }
}
// Then available bags
if (bestBag == null)
{
for (int i = 0; i < bb.nearbyAvailableBags.Count; i++)
{
CashBag bag = bb.nearbyAvailableBags[i];
if (bag == null || bag.IsCarried) continue;
if (tb.IsBagClaimed(bag, brain)) continue;
float dist = Vector3.Distance(brain.transform.position, bag.transform.position);
float bagScore = bag.Value / Mathf.Max(dist, 1f);
if (bagScore > bestScore) { bestScore = bagScore; bestBag = bag; }
}
}
if (bestBag == null)
{
// No unclaimed bags, go idle
return;
}
// Claim and navigate
tb.TryClaimBag(bestBag, brain);
bb.targetBag = bestBag;
brain.NavigateTo(bestBag.transform.position);
}
public override void OnExit(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
{
tb.ReleaseBagClaim(brain);
bb.targetBag = null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 12d6d7471a2b608a38e68d2073778452

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bf57712f1987ec7d7b6160acc2e1fa7f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,274 @@
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// AIBlackboard — Per-AI knowledge store.
/// Holds perception results, targets, and state without allocations.
/// Updated by AIPerceptionSystem and read by AIActions/UtilityScorer.
/// </summary>
[System.Serializable]
public class AIBlackboard
{
// ─── Identity ────────────────────────────────────────────
public string teamTag;
public TeamMember.Team team;
// ─── Current Targets ─────────────────────────────────────
public Transform currentTarget;
public CashBag targetBag;
public TeamVault myVault;
public TeamVault enemyVault;
public CashCarrier nearestEnemyCarrier; // Priority target
public Transform escortTarget; // Ally to escort
// ─── Self State ──────────────────────────────────────────
public float currentHealth;
public float maxHealth;
public float healthPercent;
public bool isDead;
public int carriedBagCount;
public float totalCarriedValue;
public bool isCarrying;
// ─── Perception Results (updated by AIPerceptionSystem) ──
public readonly List<PerceivedEntity> perceivedEnemies = new List<PerceivedEntity>(8);
public readonly List<PerceivedEntity> perceivedAllies = new List<PerceivedEntity>(8);
public readonly List<CashBag> nearbyAvailableBags = new List<CashBag>(8);
public readonly List<CashBag> nearbyDroppedBags = new List<CashBag>(4);
public CashCarrier nearestAllyCarrier;
public int nearbyEnemyCount;
public int nearbyAllyCount;
public float nearestEnemyDistance;
public float nearestBagDistance;
// ─── Team State (from TeamBlackboard) ────────────────────
public float myTeamScore;
public float enemyTeamScore;
public bool myVaultUnderAttack;
public float scoreDifference; // positive = winning
// ─── Navigation ──────────────────────────────────────────
public Vector3 patrolDestination;
public bool hasPatrolDestination;
public float timeAtCurrentState;
/// <summary>Clear all perception data (called before perception update).</summary>
public void ClearPerception()
{
perceivedEnemies.Clear();
perceivedAllies.Clear();
nearbyAvailableBags.Clear();
nearbyDroppedBags.Clear();
nearestEnemyCarrier = null;
nearestAllyCarrier = null;
nearbyEnemyCount = 0;
nearbyAllyCount = 0;
nearestEnemyDistance = float.MaxValue;
nearestBagDistance = float.MaxValue;
}
}
/// <summary>
/// Data about a perceived entity (enemy or ally).
/// Reused from pool to avoid allocation.
/// </summary>
[System.Serializable]
public struct PerceivedEntity
{
public Transform transform;
public GameObject gameObject;
public float distance;
public float sqrDistance;
public float healthPercent;
public bool isCarryingBag;
public int carriedBagCount;
public bool isVisible; // In line of sight
public Vector3 lastKnownPosition;
public float timeSinceLastSeen;
}
/// <summary>
/// TeamBlackboard — Shared knowledge between all AI on a team.
/// Prevents clustering and enables coordination.
/// Singleton per team — accessed via TeamBlackboard.Get(teamTag).
/// </summary>
public class TeamBlackboard : MonoBehaviour
{
// ─── Static Access ───────────────────────────────────────
private static TeamBlackboard _playerTeam;
private static TeamBlackboard _enemyTeam;
public static TeamBlackboard Get(string teamTag)
{
if (teamTag == "Player")
{
if (_playerTeam == null)
{
var go = new GameObject("TeamBlackboard_Player");
_playerTeam = go.AddComponent<TeamBlackboard>();
_playerTeam._teamTag = "Player";
}
return _playerTeam;
}
else
{
if (_enemyTeam == null)
{
var go = new GameObject("TeamBlackboard_Enemy");
_enemyTeam = go.AddComponent<TeamBlackboard>();
_enemyTeam._teamTag = "Enemy";
}
return _enemyTeam;
}
}
public static void ClearAll()
{
if (_playerTeam != null) { Destroy(_playerTeam.gameObject); _playerTeam = null; }
if (_enemyTeam != null) { Destroy(_enemyTeam.gameObject); _enemyTeam = null; }
}
// ─── Team Data ───────────────────────────────────────────
[SerializeField] private string _teamTag;
/// <summary>Which bags are claimed by AI (prevents all AI chasing same bag).</summary>
public readonly Dictionary<CashBag, AIBrain> claimedBags = new Dictionary<CashBag, AIBrain>(8);
/// <summary>Which enemies are being targeted (limit engagement per enemy).</summary>
public readonly Dictionary<Transform, int> targetedEnemies = new Dictionary<Transform, int>(8);
/// <summary>Who on our team is carrying bags?</summary>
public readonly List<CashCarrier> teamCarriers = new List<CashCarrier>(4);
/// <summary>Is our vault under attack?</summary>
public bool vaultUnderAttack;
/// <summary>Our team's score.</summary>
public float teamScore;
// ─── Claim System ────────────────────────────────────────
/// <summary>Try to claim a bag for this AI. Returns true if claimed successfully.</summary>
public bool TryClaimBag(CashBag bag, AIBrain claimant)
{
if (bag == null) return false;
// Clean stale claims
if (claimedBags.TryGetValue(bag, out AIBrain existing))
{
if (existing == null || !existing.gameObject.activeInHierarchy)
{
claimedBags[bag] = claimant;
return true;
}
return existing == claimant; // Already claimed by us
}
claimedBags[bag] = claimant;
return true;
}
/// <summary>Release claim on a bag.</summary>
public void ReleaseBagClaim(AIBrain claimant)
{
CashBag toRemove = null;
foreach (var kvp in claimedBags)
{
if (kvp.Value == claimant) { toRemove = kvp.Key; break; }
}
if (toRemove != null) claimedBags.Remove(toRemove);
}
/// <summary>Is this bag already claimed by a teammate?</summary>
public bool IsBagClaimed(CashBag bag, AIBrain excludeSelf = null)
{
if (!claimedBags.TryGetValue(bag, out AIBrain claimant)) return false;
if (claimant == null || !claimant.gameObject.activeInHierarchy)
{
claimedBags.Remove(bag);
return false;
}
return claimant != excludeSelf;
}
/// <summary>Register an enemy as being targeted. Returns current count.</summary>
public int AddEnemyTarget(Transform enemy)
{
if (enemy == null) return 0;
if (!targetedEnemies.ContainsKey(enemy))
targetedEnemies[enemy] = 0;
targetedEnemies[enemy]++;
return targetedEnemies[enemy];
}
/// <summary>Unregister from targeting an enemy.</summary>
public void RemoveEnemyTarget(Transform enemy)
{
if (enemy == null) return;
if (targetedEnemies.ContainsKey(enemy))
{
targetedEnemies[enemy]--;
if (targetedEnemies[enemy] <= 0)
targetedEnemies.Remove(enemy);
}
}
/// <summary>How many teammates are targeting this enemy?</summary>
public int GetEnemyTargetCount(Transform enemy)
{
if (enemy == null) return 0;
return targetedEnemies.TryGetValue(enemy, out int count) ? count : 0;
}
/// <summary>Update team carriers list.</summary>
public void RefreshCarriers()
{
teamCarriers.Clear();
var allCarriers = EntityRegistry<CashCarrier>.All;
for (int i = 0; i < allCarriers.Count; i++)
{
if (allCarriers[i] != null && allCarriers[i].TeamTag == _teamTag && allCarriers[i].IsCarrying)
teamCarriers.Add(allCarriers[i]);
}
}
/// <summary>Clean stale entries (call periodically).</summary>
public void CleanStaleEntries()
{
// Clean bag claims
var staleBags = new List<CashBag>();
foreach (var kvp in claimedBags)
{
if (kvp.Key == null || kvp.Value == null || !kvp.Value.gameObject.activeInHierarchy)
staleBags.Add(kvp.Key);
}
for (int i = 0; i < staleBags.Count; i++)
claimedBags.Remove(staleBags[i]);
// Clean enemy targets
var staleTargets = new List<Transform>();
foreach (var kvp in targetedEnemies)
{
if (kvp.Key == null) staleTargets.Add(kvp.Key);
}
for (int i = 0; i < staleTargets.Count; i++)
targetedEnemies.Remove(staleTargets[i]);
}
private float _lastCleanTime;
private void Update()
{
if (Time.time - _lastCleanTime > 3f)
{
CleanStaleEntries();
RefreshCarriers();
_lastCleanTime = Time.time;
}
}
private void OnDestroy()
{
if (_playerTeam == this) _playerTeam = null;
if (_enemyTeam == this) _enemyTeam = null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f851e5eb528f0fa12a0efb0e7ecde6f6

View File

@@ -0,0 +1,352 @@
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// AIBrain — Central AI orchestrator. Replaces CashSystemAI.
/// Runs perception → utility scoring → action execution on a configurable tick.
///
/// Features:
/// - AI LOD (distance-based tick scaling)
/// - Zero FindObjectOfType calls
/// - Modular scoring via UtilityScorer
/// - Delegates combat to existing CharacterAIController
/// - Delegates navigation to AINavigationHelper
///
/// Attach to all AI characters (both player-team AI and enemy-team AI).
/// </summary>
public class AIBrain : MonoBehaviour
{
[Header("AI Settings")]
[SerializeField] private UtilityScorer.AIRole role = UtilityScorer.AIRole.Balanced;
[SerializeField] private bool autoAssignRole = true;
[Header("Tick Rate (AI LOD)")]
[SerializeField] private float baseTick = 0.3f; // Tier 0: on-screen, close
[SerializeField] private float midTick = 0.5f; // Tier 1: on-screen, far
[SerializeField] private float farTick = 1.0f; // Tier 2: off-screen
[SerializeField] private float lodMidDistance = 15f;
[SerializeField] private float lodFarDistance = 30f;
[Header("Behavior")]
[SerializeField] private float minActionDuration = 1.5f; // Minimum time before switching actions
// ─── Components ──────────────────────────────────────────
private AIPerceptionSystem _perception;
private AINavigationHelper _navigation;
private CharacterAIController _aiController;
private CashCarrier _carrier;
private HealthNew _health;
private TeamMember _teamMember;
// ─── AI State ────────────────────────────────────────────
private AIBlackboard _blackboard;
private TeamBlackboard _teamBlackboard;
private UtilityScorer _scorer;
private AIAction _currentAction;
private float _lastTickTime;
private float _currentTick;
private Transform _mainCamera;
private bool _combatEnabled;
// ─── Properties ──────────────────────────────────────────
public AIBlackboard Blackboard => _blackboard;
public AIAction CurrentAction => _currentAction;
public UtilityScorer.AIRole Role => role;
public string CurrentActionName => _scorer?.CurrentActionName ?? "Initializing";
// ─── Lifecycle ───────────────────────────────────────────
private void Awake()
{
// Ensure components exist
_perception = GetComponent<AIPerceptionSystem>();
if (_perception == null) _perception = gameObject.AddComponent<AIPerceptionSystem>();
_navigation = GetComponent<AINavigationHelper>();
if (_navigation == null) _navigation = gameObject.AddComponent<AINavigationHelper>();
}
private void Start()
{
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
{
enabled = false;
return;
}
// Cache components
_aiController = GetComponent<CharacterAIController>();
_carrier = GetComponent<CashCarrier>();
_health = GetComponent<HealthNew>();
_teamMember = GetComponent<TeamMember>();
// Find camera for LOD
if (Camera.main != null)
_mainCamera = Camera.main.transform;
// Initialize blackboard
_blackboard = new AIBlackboard();
_blackboard.teamTag = gameObject.tag;
_blackboard.team = _teamMember != null ? _teamMember.CurrentTeam : TeamMember.Team.EnemyTeam;
// Initialize perception
_perception.Initialize(_blackboard.teamTag);
// Find vaults (one-time, cached)
FindVaults();
// Auto-assign role
if (autoAssignRole)
AssignRandomRole();
// Initialize scorer
_scorer = new UtilityScorer(role);
// Get team blackboard
_teamBlackboard = TeamBlackboard.Get(_blackboard.teamTag);
_currentTick = baseTick;
// Subscribe to events
GameEvents.OnCombatEnabled += OnCombatEnabled;
GameEvents.OnVaultUnderAttack += OnVaultAttacked;
GameEvents.OnMatchEnd += OnMatchEnd;
}
private void OnDestroy()
{
GameEvents.OnCombatEnabled -= OnCombatEnabled;
GameEvents.OnVaultUnderAttack -= OnVaultAttacked;
GameEvents.OnMatchEnd -= OnMatchEnd;
// Release claims
_teamBlackboard?.ReleaseBagClaim(this);
}
private void Update()
{
if (!_combatEnabled) return;
if (_health != null && _health.isDead) return;
// AI LOD — adjust tick rate based on distance to camera
UpdateAILOD();
// Tick check
if (Time.time - _lastTickTime < _currentTick) return;
_lastTickTime = Time.time;
// 1. Update self state on blackboard
UpdateSelfState();
// 2. Run perception
_perception.UpdatePerception(_blackboard);
// 3. Update team state
UpdateTeamState();
// 4. Score and select action
AIAction bestAction = _scorer.Evaluate(this, _blackboard, _teamBlackboard);
// 5. Execute action
if (bestAction != null)
{
_currentAction = bestAction;
_currentAction.Execute(this, _blackboard, _teamBlackboard);
}
}
// ─── State Updates ───────────────────────────────────────
private void UpdateSelfState()
{
if (_health != null)
{
_blackboard.currentHealth = _health.currentHealth;
_blackboard.maxHealth = _health.maxHealth;
_blackboard.healthPercent = _health.maxHealth > 0 ? _health.currentHealth / _health.maxHealth : 1f;
_blackboard.isDead = _health.isDead;
}
if (_carrier != null)
{
_blackboard.carriedBagCount = _carrier.BagCount;
_blackboard.totalCarriedValue = _carrier.TotalValue;
_blackboard.isCarrying = _carrier.IsCarrying;
}
}
private void UpdateTeamState()
{
if (_teamBlackboard != null)
{
_blackboard.myVaultUnderAttack = _teamBlackboard.vaultUnderAttack;
_blackboard.myTeamScore = _teamBlackboard.teamScore;
}
// Get enemy team score
string enemyTag = _blackboard.teamTag == "Player" ? "Enemy" : "Player";
TeamBlackboard enemyBoard = TeamBlackboard.Get(enemyTag);
if (enemyBoard != null)
{
_blackboard.enemyTeamScore = enemyBoard.teamScore;
}
_blackboard.scoreDifference = _blackboard.myTeamScore - _blackboard.enemyTeamScore;
}
private void UpdateAILOD()
{
if (_mainCamera == null)
{
_currentTick = baseTick;
return;
}
float distToCamera = Vector3.Distance(transform.position, _mainCamera.position);
if (distToCamera > lodFarDistance)
_currentTick = farTick;
else if (distToCamera > lodMidDistance)
_currentTick = midTick;
else
_currentTick = baseTick;
}
private void FindVaults()
{
var vaults = EntityRegistry<TeamVault>.All;
for (int i = 0; i < vaults.Count; i++)
{
if (vaults[i] == null) continue;
if (vaults[i].TeamTag == _blackboard.teamTag)
_blackboard.myVault = vaults[i];
else
_blackboard.enemyVault = vaults[i];
}
// Fallback to FindObjectsOfType (only at initialization)
if (_blackboard.myVault == null || _blackboard.enemyVault == null)
{
TeamVault[] allVaults = FindObjectsOfType<TeamVault>();
foreach (var vault in allVaults)
{
if (vault.TeamTag == _blackboard.teamTag)
_blackboard.myVault = vault;
else
_blackboard.enemyVault = vault;
}
}
}
private void AssignRandomRole()
{
float rand = Random.value;
if (rand < 0.35f) role = UtilityScorer.AIRole.Balanced;
else if (rand < 0.60f) role = UtilityScorer.AIRole.Collector;
else if (rand < 0.80f) role = UtilityScorer.AIRole.Defender;
else role = UtilityScorer.AIRole.Aggressor;
}
// ─── Public API (called by AIActions) ────────────────────
/// <summary>Navigate to a world position.</summary>
public void NavigateTo(Vector3 position)
{
_navigation.SetDestination(position);
}
/// <summary>Stop all navigation.</summary>
public void StopNavigation()
{
_navigation.Stop();
}
/// <summary>Has the AI reached its current navigation destination?</summary>
public bool HasReachedDestination()
{
return _navigation.HasReachedDestination();
}
/// <summary>Engage a target in combat (delegates to CharacterAIController).</summary>
public void EngageTarget(Transform target)
{
if (_aiController != null)
{
_aiController.SetTarget(target.gameObject);
_aiController.NavigateTo(target.position);
_aiController.attack = true;
}
else
{
// Fallback: just navigate toward
NavigateTo(target.position);
}
}
/// <summary>Disengage from combat.</summary>
public void DisengageTarget()
{
if (_aiController != null)
{
_aiController.Stop();
}
}
// ─── Event Handlers ──────────────────────────────────────
private void OnCombatEnabled(bool enabled)
{
_combatEnabled = enabled;
}
private void OnVaultAttacked(TeamVault vault)
{
if (vault.TeamTag == _blackboard.teamTag)
{
_blackboard.myVaultUnderAttack = true;
if (_teamBlackboard != null)
_teamBlackboard.vaultUnderAttack = true;
// Force immediate decision (bypass tick timer)
_lastTickTime = 0;
}
}
private void OnMatchEnd(string winnerTag)
{
_combatEnabled = false;
StopNavigation();
}
// ─── Debug ───────────────────────────────────────────────
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (_blackboard == null) return;
// Show current action
UnityEditor.Handles.Label(transform.position + Vector3.up * 2.5f,
$"AI: {CurrentActionName}\n" +
$"Role: {role}\n" +
$"HP: {_blackboard.healthPercent:P0}\n" +
$"Bags: {_blackboard.carriedBagCount}\n" +
$"Enemies: {_blackboard.nearbyEnemyCount}\n" +
$"Tick: {_currentTick:F2}s");
// Show target
if (_blackboard.currentTarget != null)
{
Gizmos.color = Color.red;
Gizmos.DrawLine(transform.position + Vector3.up, _blackboard.currentTarget.position + Vector3.up);
}
// Show target bag
if (_blackboard.targetBag != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position + Vector3.up, _blackboard.targetBag.transform.position);
}
}
#endif
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5557a60f10e7a97e79823d421b552c2d

View File

@@ -0,0 +1,241 @@
using UnityEngine;
using System.Collections.Generic;
/// <summary>
/// AIPerceptionSystem — Unified vision/detection system for AI.
/// Uses Physics.OverlapSphereNonAlloc for zero-allocation detection.
/// Populates the AIBlackboard with perception results.
///
/// Call UpdatePerception() on the AI tick (not every frame).
/// </summary>
public class AIPerceptionSystem : MonoBehaviour
{
[Header("Vision Settings")]
[SerializeField] private float detectionRadius = 18f;
[SerializeField] private float fovAngle = 160f; // Forward cone
[SerializeField] private float closeRangeRadius = 5f; // Always detect within this range (no FOV check)
[Header("Bag Detection")]
[SerializeField] private float bagDetectionRadius = 25f;
[Header("Line of Sight")]
[SerializeField] private bool requireLineOfSight = true;
[SerializeField] private LayerMask obstructionLayers;
[SerializeField] private float losHeightOffset = 1.2f; // Eye height
[Header("Memory")]
[SerializeField] private float memoryDuration = 5f; // Remember entities for this long after losing sight
// ─── Detection Buffer (zero-alloc) ───────────────────────
private static readonly Collider[] _overlapBuffer = new Collider[32];
// ─── Threat Memory ───────────────────────────────────────
private readonly Dictionary<Transform, float> _lastSeenTime = new Dictionary<Transform, float>(16);
private readonly Dictionary<Transform, Vector3> _lastKnownPositions = new Dictionary<Transform, Vector3>(16);
// ─── Component Cache ─────────────────────────────────────
private Transform _transform;
private string _teamTag;
private void Awake()
{
_transform = transform;
}
public void Initialize(string teamTag)
{
_teamTag = teamTag;
}
/// <summary>
/// Update perception and populate blackboard.
/// Call this on the AI decision tick, NOT every frame.
/// </summary>
public void UpdatePerception(AIBlackboard blackboard)
{
blackboard.ClearPerception();
Vector3 myPos = _transform.position;
Vector3 eyePos = myPos + Vector3.up * losHeightOffset;
// ─── Detect Characters ───────────────────────────────
var allMembers = TeamMember.AllMembers;
for (int i = 0; i < allMembers.Count; i++)
{
TeamMember member = allMembers[i];
if (member == null || member.gameObject == gameObject) continue;
Transform target = member.transform;
float sqrDist = (target.position - myPos).sqrMagnitude;
float dist = Mathf.Sqrt(sqrDist);
// Range check
if (dist > detectionRadius) continue;
// FOV check (skip for close range)
bool inFOV = dist <= closeRangeRadius || IsInFOV(target.position);
// LOS check
bool hasLOS = !requireLineOfSight || dist <= closeRangeRadius || CheckLineOfSight(eyePos, target.position + Vector3.up * losHeightOffset);
bool isVisible = inFOV && hasLOS;
// Update memory
if (isVisible)
{
_lastSeenTime[target] = Time.time;
_lastKnownPositions[target] = target.position;
}
// Check memory (even if not currently visible)
bool isRemembered = false;
float timeSinceLastSeen = float.MaxValue;
Vector3 lastKnownPos = target.position;
if (_lastSeenTime.TryGetValue(target, out float lastSeen))
{
timeSinceLastSeen = Time.time - lastSeen;
isRemembered = timeSinceLastSeen < memoryDuration;
if (_lastKnownPositions.TryGetValue(target, out Vector3 lkp))
lastKnownPos = lkp;
}
if (!isVisible && !isRemembered) continue;
// Build perceived entity
HealthNew health = member.GetComponent<HealthNew>();
CashCarrier carrier = member.GetComponent<CashCarrier>();
PerceivedEntity entity = new PerceivedEntity
{
transform = target,
gameObject = member.gameObject,
distance = dist,
sqrDistance = sqrDist,
healthPercent = health != null ? (health.currentHealth / health.maxHealth) : 1f,
isCarryingBag = carrier != null && carrier.IsCarrying,
carriedBagCount = carrier != null ? carrier.BagCount : 0,
isVisible = isVisible,
lastKnownPosition = lastKnownPos,
timeSinceLastSeen = timeSinceLastSeen
};
bool isEnemy = member.gameObject.tag != _teamTag;
if (isEnemy)
{
blackboard.perceivedEnemies.Add(entity);
blackboard.nearbyEnemyCount++;
if (dist < blackboard.nearestEnemyDistance)
blackboard.nearestEnemyDistance = dist;
// Track nearest enemy carrier
if (entity.isCarryingBag && carrier != null)
{
if (blackboard.nearestEnemyCarrier == null ||
dist < Vector3.Distance(myPos, blackboard.nearestEnemyCarrier.transform.position))
{
blackboard.nearestEnemyCarrier = carrier;
}
}
}
else
{
blackboard.perceivedAllies.Add(entity);
blackboard.nearbyAllyCount++;
// Track nearest ally carrier (for escort)
if (entity.isCarryingBag && carrier != null)
{
if (blackboard.nearestAllyCarrier == null ||
dist < Vector3.Distance(myPos, blackboard.nearestAllyCarrier.transform.position))
{
blackboard.nearestAllyCarrier = carrier;
}
}
}
}
// ─── Detect Cash Bags ─────────────────────────────────
var allBags = EntityRegistry<CashBag>.All;
for (int i = 0; i < allBags.Count; i++)
{
CashBag bag = allBags[i];
if (bag == null || bag.IsCarried) continue;
float dist = Vector3.Distance(myPos, bag.transform.position);
if (dist > bagDetectionRadius) continue;
if (dist < blackboard.nearestBagDistance)
blackboard.nearestBagDistance = dist;
if (bag.IsAvailable)
blackboard.nearbyAvailableBags.Add(bag);
else if (bag.IsDropped)
blackboard.nearbyDroppedBags.Add(bag);
}
}
// ─── Vision Helpers ──────────────────────────────────────
private bool IsInFOV(Vector3 targetPos)
{
Vector3 dirToTarget = (targetPos - _transform.position).normalized;
dirToTarget.y = 0; // Horizontal only
float angle = Vector3.Angle(_transform.forward, dirToTarget);
return angle <= fovAngle * 0.5f;
}
private bool CheckLineOfSight(Vector3 from, Vector3 to)
{
Vector3 dir = to - from;
float dist = dir.magnitude;
if (dist < 0.1f) return true;
return !Physics.Raycast(from, dir.normalized, dist, obstructionLayers);
}
// ─── Memory Management ───────────────────────────────────
/// <summary>Clear all threat memory (call on state reset).</summary>
public void ClearMemory()
{
_lastSeenTime.Clear();
_lastKnownPositions.Clear();
}
/// <summary>Get last known position of a target (for pursuit).</summary>
public bool TryGetLastKnownPosition(Transform target, out Vector3 position)
{
return _lastKnownPositions.TryGetValue(target, out position);
}
// ─── Debug ───────────────────────────────────────────────
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (_transform == null) _transform = transform;
// Detection radius
Gizmos.color = new Color(1f, 1f, 0f, 0.1f);
Gizmos.DrawWireSphere(_transform.position, detectionRadius);
// Close range
Gizmos.color = new Color(1f, 0.5f, 0f, 0.15f);
Gizmos.DrawWireSphere(_transform.position, closeRangeRadius);
// FOV cone
Gizmos.color = new Color(0f, 1f, 0f, 0.3f);
Vector3 fovLeft = Quaternion.Euler(0, -fovAngle * 0.5f, 0) * _transform.forward * detectionRadius;
Vector3 fovRight = Quaternion.Euler(0, fovAngle * 0.5f, 0) * _transform.forward * detectionRadius;
Gizmos.DrawLine(_transform.position, _transform.position + fovLeft);
Gizmos.DrawLine(_transform.position, _transform.position + fovRight);
// Bag detection radius
Gizmos.color = new Color(1f, 0.84f, 0f, 0.05f);
Gizmos.DrawWireSphere(_transform.position, bagDetectionRadius);
}
#endif
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 09984091d0b1426349f45490e2b656e7

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1513669be5125c8c2bee7148a001ff8d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,145 @@
using UnityEngine;
using UnityEngine.AI;
/// <summary>
/// AINavigationHelper — NavMeshAgent wrapper with path caching and mobile optimization.
/// Prevents unnecessary path recalculations and provides clean navigation API.
///
/// Only recalculates path when target moves >2m or path becomes invalid.
/// </summary>
public class AINavigationHelper : MonoBehaviour
{
[Header("Navigation Settings")]
[SerializeField] private float pathRecalcThreshold = 2f; // Minimum target movement to recalc
[SerializeField] private float stoppingDistance = 1.5f;
[SerializeField] private float reachThreshold = 2f;
[Header("Avoidance")]
[SerializeField] private int avoidancePriority = 50; // 0=highest priority, 99=lowest
// ─── Components ──────────────────────────────────────────
private NavMeshAgent _agent;
private CharacterAIController _aiController;
// ─── Path Cache ──────────────────────────────────────────
private Vector3 _lastTargetPosition;
private bool _hasDestination;
// ─── Properties ──────────────────────────────────────────
public bool HasArrived => _hasDestination && _agent != null && _agent.enabled &&
!_agent.pathPending &&
_agent.remainingDistance <= reachThreshold;
public bool IsNavigating => _hasDestination && _agent != null && _agent.enabled &&
_agent.hasPath && _agent.remainingDistance > reachThreshold;
public float RemainingDistance => _agent != null && _agent.enabled ? _agent.remainingDistance : float.MaxValue;
private void Awake()
{
_agent = GetComponent<NavMeshAgent>();
_aiController = GetComponent<CharacterAIController>();
}
private void Start()
{
if (_agent != null)
{
_agent.stoppingDistance = stoppingDistance;
_agent.avoidancePriority = avoidancePriority;
}
}
/// <summary>
/// Navigate to a position. Only recalculates path if target moved significantly.
/// </summary>
public void SetDestination(Vector3 target)
{
// Use CharacterAIController if available (it handles NavMesh warping)
if (_aiController != null)
{
float sqrDist = (target - _lastTargetPosition).sqrMagnitude;
if (_hasDestination && sqrDist < pathRecalcThreshold * pathRecalcThreshold)
return; // Target hasn't moved enough, keep current path
_aiController.NavigateTo(target);
_lastTargetPosition = target;
_hasDestination = true;
return;
}
// Direct NavMeshAgent fallback
if (_agent == null || !_agent.enabled) return;
float sqrDistDirect = (target - _lastTargetPosition).sqrMagnitude;
if (_hasDestination && sqrDistDirect < pathRecalcThreshold * pathRecalcThreshold)
return;
_agent.SetDestination(target);
_lastTargetPosition = target;
_hasDestination = true;
}
/// <summary>Stop navigation immediately.</summary>
public void Stop()
{
if (_aiController != null)
{
_aiController.Stop();
}
else if (_agent != null && _agent.enabled)
{
_agent.ResetPath();
}
_hasDestination = false;
}
/// <summary>Check if destination is approximately reached.</summary>
public bool HasReachedDestination()
{
if (!_hasDestination) return true;
if (_agent != null && _agent.enabled)
{
return !_agent.pathPending && _agent.remainingDistance <= reachThreshold;
}
// Fallback distance check
return Vector3.SqrMagnitude(transform.position - _lastTargetPosition) < reachThreshold * reachThreshold;
}
/// <summary>Set avoidance priority (carriers get higher priority = lower number).</summary>
public void SetAvoidancePriority(int priority)
{
avoidancePriority = priority;
if (_agent != null) _agent.avoidancePriority = priority;
}
/// <summary>Set movement speed.</summary>
public void SetSpeed(float speed)
{
if (_agent != null) _agent.speed = speed;
}
/// <summary>Warp to a position on the NavMesh.</summary>
public bool WarpTo(Vector3 position)
{
if (_agent == null) return false;
NavMeshHit hit;
if (NavMesh.SamplePosition(position, out hit, 5f, NavMesh.AllAreas))
{
_agent.Warp(hit.position);
return true;
}
return false;
}
/// <summary>Force path recalculation on next SetDestination call.</summary>
public void InvalidatePath()
{
_lastTargetPosition = Vector3.zero;
_hasDestination = false;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dfe5b90516a45b1bb9448ea8c0a67baa

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f4bc8a91e3ceecf97816ea2d740ae97f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 84ff338ac6954704cbaa771ed42b9e3e

View File

@@ -0,0 +1,240 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Advertisements;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class AdsManager : MonoBehaviour, IUnityAdsInitializationListener, IUnityAdsLoadListener, IUnityAdsShowListener
{
// public string GAME_ID = "5486175"; //replace with your gameID from dashboard. note: will be different for each platform.
// private const string BANNER_PLACEMENT = "banner";
// private const string VIDEO_PLACEMENT = "video";
// private const string REWARDED_VIDEO_PLACEMENT = "rewardedVideo";
[SerializeField] private BannerPosition bannerPosition = BannerPosition.BOTTOM_CENTER;
// private bool testMode = true;
private bool showBanner = false;
//utility wrappers for debuglog
public delegate void DebugEvent(string msg);
public static event DebugEvent OnDebugLog;
// public GameObject missions_gameobject;
//Game ID based on platform
[SerializeField] string _androidGameId = "5486175";
[SerializeField] string _iOSGameId = "5486174";
// [SerializeField] string _androidGameId = "5491357";
// [SerializeField] string _iOSGameId = "5491356";
[SerializeField] bool _testMode = false; //Currently testing
private string _gameId;
[SerializeField] string _rewardedVideo_androidAdUnitId = "rewardedVideo";
[SerializeField] string _rewardedVideo_iOsAdUnitId = "rewardedVideo";
// [SerializeField] string _rewardedVideo_androidAdUnitId = "Rewarded_Android";
// [SerializeField] string _rewardedVideo_iOsAdUnitId = "Rewarded_Android";
string _adUnitId;
// public TextMeshPro isSubscribedText;
// public TextMeshProUGUI details;
// public TextMeshProUGUI debug_msg;
IEnumerator LoadAdCoroutine(){
while (true){
Advertisement.Load(_adUnitId, this);
yield return new WaitForSeconds(2f); // Adjust wait time based on your needs
}
}
MissionHandler mission_handler;
MissionsManager missions_manager;
public GameObject missionsManager_GO;
public static string AdShowbutton;
void OnEnable(){
mission_handler = GetComponent<MissionHandler>();
missions_manager = missionsManager_GO.GetComponent<MissionsManager>();
}
void Awake(){
Initialize();
StartCoroutine(LoadAdCoroutine()); //This code starts a coroutine that continuously calls LoadRewardedAd every 5 seconds (adjust as needed).
}
void Update(){
}
//Initialize method is responsible for initializing Unity Ads with a given GAME_ID
public void Initialize()
{
//Check if Unity Ads is supported on the current platform
// if (Advertisement.isSupported)
// {
// DebugLog(Application.platform + " supported by Advertisement");
// }
//Set Game ID based on the platform, Android or iPhone
#if UNITY_IOS
_gameId = _iOSGameId;
#elif UNITY_ANDROID
_gameId = _androidGameId;
#elif UNITY_EDITOR
_gameId = _androidGameId; //Only for testing the functionality in the Editor
#endif
if (!Advertisement.isInitialized && Advertisement.isSupported)
{
Advertisement.Initialize(_gameId, _testMode, this);
}
// Get the Ad Unit ID for the current platform:
_adUnitId = (Application.platform == RuntimePlatform.IPhonePlayer)
? _rewardedVideo_iOsAdUnitId
: _rewardedVideo_androidAdUnitId;
try{
Advertisement.Initialize(_gameId, _testMode, this);
}catch(Exception e){
// debug_msg.text = "Initialize_Error: " + e.ToString();
}
}
//Toggles between showing and hiding the banner ad
public void ToggleBanner()
{
// showBanner = !showBanner;
// if (showBanner)
// {
// //Sets the banner position
// Advertisement.Banner.SetPosition(bannerPosition);
// Advertisement.Banner.Show(BANNER_PLACEMENT);
// }
// else
// {
// Advertisement.Banner.Hide(false);
// }
}
//Loads rewarded video ads
public void LoadRewardedAd()
{
Advertisement.Load(_adUnitId, this);
}
//Show rewarded video ads
public void ShowRewardedAd(){
string buttonName = gameObject.name;
AdShowbutton = buttonName;
Debug.LogWarning("Clicked button name: " + buttonName);
try{
Advertisement.Show(_adUnitId, this);
}catch(Exception e){
// ads_not_ready_popup_text.text = "Show_Error: " + e.ToString();
}
}
//Loads Interstitial (Non-Rewarded) Video Ads
public void LoadNonRewardedAd()
{
Advertisement.Load(_adUnitId, this);
}
#region Interface Implementations
public void OnInitializationComplete()
{
DebugLog("Init Success");
}
public void OnInitializationFailed(UnityAdsInitializationError error, string message)
{
// ads_not_ready_popup_text.text = "InitializationFailed: " + message;
DebugLog($"Init Failed: [{error}]: {message}");
}
public void OnUnityAdsAdLoaded(string placementId)
{
// ShowRewardedAd();
DebugLog($"Load Success: {placementId}");
}
public void OnUnityAdsFailedToLoad(string placementId, UnityAdsLoadError error, string message)
{
// ads_not_ready_popup_text.text = "LoadFailed: " + message;
DebugLog($"Load Failed: [{error}:{placementId}] {message}");
}
public void OnUnityAdsShowFailure(string placementId, UnityAdsShowError error, string message)
{
// ads_not_ready_popup_text.text = "ShowFailed: " + message;
DebugLog($"OnUnityAdsShowFailure: [{error}]: {message}");
}
public void OnUnityAdsShowStart(string placementId)
{
DebugLog($"OnUnityAdsShowStart: {placementId}");
}
public void OnUnityAdsShowClick(string placementId)
{
DebugLog($"OnUnityAdsShowClick: {placementId}");
}
//Called when the rewarded video ad has been shown to completion
public void OnUnityAdsShowComplete(string placementId, UnityAdsShowCompletionState showCompletionState)
{
//Check if the Ad selected was a Rewarded video
if (((placementId == _rewardedVideo_androidAdUnitId) || (placementId == _rewardedVideo_iOsAdUnitId)) && showCompletionState == UnityAdsShowCompletionState.COMPLETED)
{
//// Reward the user here
// RewardUser();
mission_handler.AdsWatched();
Debug.LogWarning("Reward Given!");
// missions_gameobject.SetActive(false);
// missions_gameobject.SetActive(true);
missions_manager.UpdateAdMissionUI(AdShowbutton);
}
DebugLog($"OnUnityAdsShowComplete: [{showCompletionState}]: {placementId}");
}
#endregion
public void OnGameIDFieldChanged(string newInput)
{
_gameId = newInput;
}
public void ToggleTestMode(bool isOn)
{
_testMode = isOn;
}
//wrapper around debug.log to allow broadcasting log strings to the UI
void DebugLog(string msg)
{
OnDebugLog?.Invoke(msg);
Debug.Log(msg);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea6ba061c4b33e741b3de0fa3565dc76
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,226 @@
using UnityEngine;
/// <summary>
/// All available animation names for the game
/// </summary>
public static class AnimationNames
{
// Legacy Animations (Still Used)
public const string Right = "Right";
public const string LeftHook = "LeftHook";
public const string RightHook = "RightHook";
public const string LeftElbow = "LeftElbow";
public const string RightElbow = "RightElbow";
public const string RightBody = "RightBody";
public const string PowerPunchLeft = "PowerPunchLeft";
public const string SupermanPunch = "SupermanPunch";
public const string SpecialBackfist = "SpecialBackfist";
public const string FrontKickRight = "FrontKickRight";
public const string BodyKickRight = "BodyKickRight";
public const string KneeRight = "KneeRight";
public const string BackSideKick = "BackSideKick";
public const string LegKickRight = "LegKickRight";
public const string LegPowerKickRight = "LegPowerKickRight";
public const string JumpBack = "JumpBack";
public const string JumpLeft = "JumpLeft";
public const string JumpRight = "JumpRight";
public const string BlockStepBack = "BlockStepBack";
public const string BlockBodyLeft = "BlockBodyLeft";
public const string BlockLeg = "BlockLeg";
// Legacy Hit Reactions (Still Used)
public const string RightHookHitReaction = "RightHookHitReaction";
public const string BodyKickHitReaction = "BodyKickHitReaction";
public const string HookHitReaction = "HookHitReaction";
public const string JabHitReaction = "JabHitReaction";
public const string LegKickHitReaction = "LegKickHitReaction";
public const string UppercutPowerHitReaction = "UppercutPowerHitReaction";
public const string OverhandHitReaction = "OverhandHitReaction";
// Legacy alternative spellings (for backward compatibility)
public const string SwantomBomb = "SwantomBomb";
// Basic Move Animations
public const string Jab = "Jab";
public const string Chop = "Chop";
public const string Haymaker = "Haymaker";
public const string GroundPound = "GroundPound";
public const string LowKick = "LowKick";
public const string LowKick1 = "LowKick1"; // Additional variant from input system
public const string BasicKick = "BasicKick"; // New consolidated basic kick
public const string AirPunch = "AirPunch";
public const string AirKick = "AirKick";
public const string DropKick = "DropKick";
public const string ElbowSmash = "ElbowSmash";
public const string SpinningBackfist = "SpinningBackfist";
public const string IrishWhip = "IrishWhip";
public const string ChargedPunch = "ChargedPunch";
public const string Block = "Block";
public const string DashBackward = "DashBackward";
public const string DashSide = "DashSide";
public const string Move = "Move"; // Basic locomotion
public const string Look = "Look"; // Camera look
public const string Sprint = "Sprint"; // Running
public const string Strike = "Strike"; // Basic strike
public const string Pause = "Pause"; // Game pause
// Basic Moves Reaction Animations
public const string JabReaction = "StaggerReaction";
public const string ChopReaction = "SideStagger";
public const string HaymakerReaction = "JumpSideStaggerReaction";
public const string GroundPoundReaction = "GroundPoundReaction";
public const string LowKickReaction = "JumpSideStagger";
public const string AirPunchReaction = "Stagger";
public const string AirKickReaction = "AirKickReaction";
public const string DropKickReaction = "KnockBackReaction";
public const string ElbowSmashReaction = "KnockBackReaction";
public const string SpinningBackfistReaction = "KnockBackReaction";
public const string IrishWhipReaction = "IrishWhipReaction";
public const string ChargedPunchReaction = "KnockBackReaction";
public const string SignSwingReaction = "SignSwingReaction";
public const string SignOverheadSlamReaction = "SignOverheadSlamReaction";
public const string SignChargeAttackReaction = "SignChargeAttackReaction";
// Vicious Moves Animations
public const string Chokeslam = "Chokeslam";
public const string Suplex = "Suplex";
public const string GiantSwing = "GiantSwing";
public const string DiamondCrusher = "DiamondCrusher";
public const string SumoSlap = "SumoSlap";
public const string Uppercut = "Uppercut";
public const string JavelinTackle = "JavelinTackle";
public const string RocketJump = "RocketJump";
public const string BullRush = "BullRush";
public const string SuperKick = "SuperKick";
public const string F5 = "F5";
// Vicious Moves Reaction Animations
public const string ChokeslamReaction = "KnockBackReaction";
public const string SuplexReaction = "SuplexReaction";
public const string GiantSwingReaction = "GiantSwingReaction";
public const string DiamondCrusherReaction = "DiamondCrusherReaction";
public const string SumoSlapReaction = "KnockBackReaction";
public const string UppercutReaction = "KnockBackReaction";
public const string JavelinTackleReaction = "JavelinTackleReaction";
public const string RocketJumpReaction = "KnockedDownReaction";
public const string BullRushReaction = "KnockBackReaction";
public const string SuperKickReaction = "KnockBackReaction";
public const string F5Reaction = "F5Reaction";
// Special Moves/Finishers Animations
public const string Clothesline = "Clothesline";
public const string Spear = "Spear";
public const string RKO = "RKO";
public const string Claymore = "Claymore";
public const string SuperDDT = "SuperDDT";
public const string RockBottom = "RockBottom";
public const string TroubleInParadise = "TroubleInParadise";
public const string SwantonBomb = "SwantonBomb";
public const string Pedigree = "Pedigree";
public const string GorillaPress = "GorillaPress";
public const string Headbutt = "Headbutt";
public const string KOPunch = "KOPunch";
// Special Moves Reaction Animations
public const string ClotheslineReaction = "ClotheslineReaction";
public const string SpearReaction = "SpearReaction";
public const string RKOReaction = "RKOReaction";
public const string ClaymoreReaction = "ShortKnockBackReaction";
public const string SuperDDTReaction = "SuperDDTReaction";
public const string RockBottomReaction = "RockBottomReaction";
public const string TroubleInParadiseReaction = "KnockBackReaction";
public const string SwantonBombReaction = "KnockedDownReaction";
public const string PedigreeReaction = "PedigreeReaction";
public const string GorillaPressReaction = "GorillaPressReaction";
public const string HeadbuttReaction = "Stagger";
public const string KOPunchReaction = "KnockBackReaction";
// Weapon Animations
public const string BatOne = "BatOne";
public const string BatTwo = "BatTwo";
public const string HammerOne = "HammerOne"; // if using Hammer
public const string HammerTwo = "HammerTwo";
// Utility/Non-attack Animations
public const string WeaponPickup = "WeaponPickup";
public static string GetReactionForAttack(string attack)
{
switch (attack)
{
// Face/Head area hits
case Jab: return JabReaction;
case Haymaker: return HaymakerReaction;
case Uppercut: return UppercutReaction;
case ElbowSmash: return ElbowSmashReaction;
case Headbutt: return HeadbuttReaction;
// Body/Torso hits
case Chop: return ChopReaction;
case GroundPound: return GroundPoundReaction;
case SpinningBackfist: return SpinningBackfistReaction;
// Leg/Lower body hits
case LowKick: return LowKickReaction;
case LowKick1: return LowKickReaction;
case BasicKick: return "KnockBackReaction";
// Aerial attacks
case AirPunch: return AirPunchReaction;
case AirKick: return AirKickReaction;
case DropKick: return DropKickReaction;
case SwantonBomb: return SwantonBombReaction;
case SuperKick: return SuperKickReaction;
// Special moves - high impact
case ChargedPunch: return ChargedPunchReaction;
case IrishWhip: return IrishWhipReaction;
case Clothesline: return ClotheslineReaction;
case Spear: return SpearReaction;
case RKO: return RKOReaction;
case Claymore: return ClaymoreReaction;
case SuperDDT: return SuperDDTReaction;
case RockBottom: return RockBottomReaction;
case TroubleInParadise: return TroubleInParadiseReaction;
case Pedigree: return PedigreeReaction;
case GorillaPress: return GorillaPressReaction;
case KOPunch: return KOPunchReaction;
case F5: return F5Reaction;
// Vicious moves
case Chokeslam: return ChokeslamReaction;
case Suplex: return SuplexReaction;
case GiantSwing: return GiantSwingReaction;
case DiamondCrusher: return DiamondCrusherReaction;
case SumoSlap: return SumoSlapReaction;
case JavelinTackle: return JavelinTackleReaction;
case RocketJump: return RocketJumpReaction;
case BullRush: return BullRushReaction;
// Weapons
case BatOne: return "KnockBackReaction";
case BatTwo: return "KnockBackReaction";
case HammerOne: return "KnockBackReaction";
case HammerTwo: return "KnockBackReaction";
// Legacy mappings for backward compatibility
case RightHook: return RightHookHitReaction;
case BodyKickRight: return BodyKickHitReaction;
case LeftHook: return HookHitReaction;
case LegKickRight: return LegKickHitReaction;
case PowerPunchLeft: return UppercutPowerHitReaction;
case SupermanPunch: return OverhandHitReaction;
case Right: return RightHookHitReaction;
// Default fallback cases based on animation name pattern
default:
// Try to find a matching reaction by name convention
if (attack.Contains("Kick"))
return "KnockBackReaction";
if (attack.Contains("Punch"))
return "Stagger";
// If all else fails
return null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f05e5247740dc4a5a9577cf064b81494

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d813156f460e49243b205b9d1425da84
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,170 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
/*
DESCRIPTION:
1. announcement_progression has buttons which are used to navigate throught the content in announcement_content
2. The logic of this script is as follows:
- announcement_progression[1] only is visible when showing content in announcement_content[0]
- announcement_progression[0] and announcement_progression[1] should be visible when showing content in announcement_content[1]
- announcement_progression[0] and announcement_progression[2] should be visible when showing content in announcement_content[3]
*/
public class Announcement : MonoBehaviour
{
public Button[] announcement_progression; //Buttons for navigating throught the Announcement Content | 0. Left, 1. Right
public GameObject[] announcement_content; //Gameobjects to be shown for the Announcement Content | 0, 1, 2
private int currentIndex = 0; //Which Announcement content is currently being shown
[SerializeField]
GameObject homeScreen;
void OnEnable(){
// Set all content to false/not visible
foreach (GameObject obj in announcement_content){
obj.SetActive(false);
}
currentIndex = 0; //Start with the first GameObject/Announcement Content
// Initialize the content visibility
UpdateContentVisibility();
}
// Start is called before the first frame update
void Start()
{
// Initialize button click listeners, for the Next and Previous buttons
announcement_progression[0].onClick.AddListener(ShowPrevious);
announcement_progression[1].onClick.AddListener(ShowNext);
// Change UI after Announcement, "Next" Button, in the last page of the Announcements
announcement_progression[2].onClick.AddListener(AfterAnnouncement);
//select the right arrow key
announcement_progression[1].Select();
// PlayerPrefs code, Check the status of the PLAYERPREFS related to Announcement, referenced as "announcementStatus"
// Check if the "announcement" key exists in PlayerPrefs
if (PlayerPrefs.HasKey("announcementStatus")){
// Get the value of the "announcement" key
string announcementValue = PlayerPrefs.GetString("announcementStatus");
// Check if the value is "0", if it is 0 means the Announcements have not yet been shown in the game
/*if (announcementValue == "0"){
Debug.Log("The 'announcement' key is assigned and its value is '0'.");
}
else{
Debug.Log("The 'announcement' key is assigned but its value is not '0'.");
}*/
}
else{
PlayerPrefs.SetString("announcementStatus", "0");
//Debug.Log("The 'announcement' key is not assigned.");
}
}
//Function for the button to go to the Previous Announcement Content
void ShowPrevious()
{
if (currentIndex <= 0)
{
// currentIndex = announcement_content.Length - 1; // Wrap around to the last element
currentIndex = currentIndex; // Wrap around to the last element
}else{
// Decrease the index
currentIndex--;
}
announcement_progression[0].Select();
if(currentIndex == 0)//if on the first page
announcement_progression[1].Select();
// Update the content visibility
UpdateContentVisibility();
print("pressed");
}
//Function for the button to go to the Next Announcement Content
void ShowNext()
{
if (currentIndex == (announcement_content.Length - 1))
{
// currentIndex = 0; // Wrap around to the first element
currentIndex = currentIndex; // Wrap around to the first element
}else{
// Increase the index
currentIndex++;
}
announcement_progression[1].Select();
if(currentIndex == 2)//if on the last page
announcement_progression[0].Select();
// Update the content visibility
UpdateContentVisibility();
print("pressed");
}
//After the last page of the Announcements, set the status to 1
void AfterAnnouncement(){
PlayerPrefs.SetString("announcementStatus", "1"); //Announcements Done
gameObject.SetActive(false); //Set Announcements UI to false
homeScreen.GetComponent<HomeSrceen>().primaryButton.Select();
print("pressed");
}
//Function to Show the Announcement content based on the current index
void UpdateContentVisibility()
{
// Hide all content elements
for (int i = 0; i < announcement_content.Length; i++){
announcement_content[i].SetActive(false);
}
// Show the current content element
if (announcement_content.Length > 0){
announcement_content[currentIndex].SetActive(true);
}
// Shows the first Announcement content
if (currentIndex <= 0){
announcement_content[0].SetActive(true); //Sets the first element to be visible
ShowOrHideButton(announcement_progression[0], false);
ShowOrHideButton(announcement_progression[2], false);
}
// Shows the second Announcement content
if ((currentIndex > 0) && (currentIndex < (announcement_content.Length - 1))){
ShowOrHideButton(announcement_progression[0], true);
ShowOrHideButton(announcement_progression[1], true);
ShowOrHideButton(announcement_progression[2], false);
}
// Shows the third Announcement content
if (currentIndex >= (announcement_content.Length - 1)){
announcement_content[2].SetActive(true); //Sets the third element to be visible
//Takes you to next UI here
ShowOrHideButton(announcement_progression[1], false);
ShowOrHideButton(announcement_progression[2], true);
}
}
//Function to dynamically set the visibility of gameobject which takes a button as an argument
void ShowOrHideButton(Button btn, bool buttonStatus){
((btn).gameObject).SetActive(buttonStatus);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 814b47897efd4d14fa880dc57b2e2ccb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,548 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;
using UnityEngine.Analytics;
using CustomAssetBundleSystem;
/*
DESCRIPTION:
1. When the Loading_Screen gameobject with this script is enabled, the loading slider animates to show progress when changing
between UI's or scenes
*/
public class LoadingScreen : MonoBehaviour
{
public GameObject BackgroundImg_GameObject; //Reference to the background image in the loading UI
public GameObject Hints_GameObject; //Reference to Hints text in the loading UI
public Slider loadingSlider; //Reference to the slider in the loading UI
//For the "Loading..." text shown
public TextMeshProUGUI loadingText;
public string loadingBaseText; //Text showing "Loading..." whereas the dots appear to animate
public string og_text;
public string ui_to_show;
public int to_change_to; //1. Scene, 2. UI
public Sprite[] backgroundImages; // Array of background images
public string[] hintsArray; //Array of hints to be displayed while loading
private int maxBackgroundChanges = 1, backgroundChangeCount = 0; // Maximum number of background changes
private int maxhintsChanges = 1, hintsChangeCount = 0; // Counter to keep track of background changes
private int maxLoaderChanges = 3, loaderChangeCount = 0; // Counter to keep track of background changes
bool flag = true; //flag to check if loading has been initialized
private GameManager gameManager;
private float barProgess;
private bool pendingInitialization;
private string targetSceneName; // remember scene we are loading so we can react on load
private bool sceneHideHookRegistered;
private bool fallbackHideHookRegistered; // subscribed when we didn't initialize via to_change_to==1
// Scenes on which we should auto-hide the loading UI even if we didn't initiate the load via this component.
// Keeps prior behavior intact while fixing cases like WaitingSceneManager.
[SerializeField]
private string[] autoHideOnSceneNames = new[] { "Game" };
// AssetBundle Download Integration
[Header("AssetBundle Download Settings")]
[SerializeField] private bool enableAssetBundleDownload = false; // DISABLED: Using Build Settings scenes instead
[SerializeField] private bool downloadBeforeMenu = false; // DISABLED: Using Build Settings scenes instead
private AssetBundleDownloadManager downloadManager;
// Force disable AssetBundle system at runtime (overrides serialized Inspector values)
private void Awake()
{
// IMPORTANT: Force disable AssetBundle downloading until you want to re-enable it
enableAssetBundleDownload = false;
downloadBeforeMenu = false;
}
private void OnEnable()
{
gameManager = GameManager.Instance;
if (gameManager == null)
{
gameManager = GetComponent<GameManager>() ?? GetComponentInParent<GameManager>();
}
if (gameManager == null)
{
#if UNITY_2022_2_OR_NEWER
gameManager = FindFirstObjectByType<GameManager>();
#else
gameManager = FindObjectOfType<GameManager>();
#endif
}
// Initialize AssetBundle download manager
if (enableAssetBundleDownload && downloadManager == null)
{
downloadManager = gameObject.AddComponent<AssetBundleDownloadManager>();
// Pass UI references to download manager
var downloadManagerType = downloadManager.GetType();
var sliderField = downloadManagerType.GetField("loadingSlider", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var loadingTextField = downloadManagerType.GetField("loadingText", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var hintsTextField = downloadManagerType.GetField("hintsText", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (sliderField != null) sliderField.SetValue(downloadManager, loadingSlider);
if (loadingTextField != null) loadingTextField.SetValue(downloadManager, loadingText);
if (hintsTextField != null) hintsTextField.SetValue(downloadManager, Hints_GameObject.GetComponent<TextMeshProUGUI>());
}
// Hints to be shown
hintsArray = new string[]
{
"Complete Weekly Missions to gain XP",
"Complete a 20 Wins streak to level up in Rank",
"Missions change every Week"
};
og_text = loadingText.text; // "Loading..." text
loadingBaseText = loadingText.text;
if (pendingInitialization)
{
InitializeLoading();
}
// Always attach a lightweight fallback so if another system loads the scene (e.g., WaitingSceneManager),
// we still hide the loading UI when the gameplay scene is ready.
if (!fallbackHideHookRegistered)
{
SceneManager.sceneLoaded += FallbackSceneLoadedHide;
fallbackHideHookRegistered = true;
}
}
private void OnDisable()
{
StopAllCoroutines();
// Ensure we unsubscribe any scene hooks to avoid leaks.
if (sceneHideHookRegistered)
{
SceneManager.sceneLoaded -= HandleSceneLoadedInternal;
sceneHideHookRegistered = false;
}
if (fallbackHideHookRegistered)
{
SceneManager.sceneLoaded -= FallbackSceneLoadedHide;
fallbackHideHookRegistered = false;
}
}
// Function called when loading has been initialized
public void InitializeLoading()
{
pendingInitialization = false;
StopAllCoroutines();
backgroundChangeCount = 0;
hintsChangeCount = 0;
loaderChangeCount = 0;
flag = true;
if (loadingSlider != null)
{
loadingSlider.value = 0f;
}
if (!string.IsNullOrEmpty(og_text) && loadingText != null)
{
loadingBaseText = og_text;
loadingText.text = og_text;
}
ChangeBackgroundImage();
ChangeHintsText();
StartCoroutine(ChangeBackgroundWithDelay());
StartCoroutine(ChangeHintsWithDelay());
// Check if we should download AssetBundles BEFORE loading menu
if (enableAssetBundleDownload && downloadBeforeMenu && ui_to_show == "MenuScene" && to_change_to == 1)
{
Debug.Log("[LoadingScreen] Starting AssetBundle download before MenuScene...");
StartCoroutine(DownloadThenLoadScene(ui_to_show));
return; // Don't proceed with normal loading yet
}
if (to_change_to == 1)
{
if (GameManager.Instance != null)
{
SceneManager.sceneLoaded += GameManager.Instance.OnSceneLoaded;
}
targetSceneName = ui_to_show;
// Also register a local handler to guarantee the loading UI hides even if external code path changes.
if (!sceneHideHookRegistered)
{
SceneManager.sceneLoaded += HandleSceneLoadedInternal;
sceneHideHookRegistered = true;
}
StartCoroutine(LoadSceneAsync(ui_to_show));
}
else
{
StartCoroutine(AnimateSliderFill(5f));//duration should be 3 for editor simulation, 5 for mobile apk lower than that the corouting gets jammed
}
}
/// <summary>
/// Download AssetBundles first, then load the target scene
/// </summary>
private IEnumerator DownloadThenLoadScene(string sceneName)
{
if (downloadManager == null)
{
Debug.LogError("[LoadingScreen] DownloadManager not initialized!");
// Fallback to normal loading
StartCoroutine(LoadSceneAsync(sceneName));
yield break;
}
Debug.Log("[LoadingScreen] Starting AssetBundle download process...");
bool downloadSuccess = false;
// Start download with callback
yield return downloadManager.StartDownloadProcess((success) =>
{
downloadSuccess = success;
});
if (downloadSuccess)
{
Debug.Log("[LoadingScreen] ✓ AssetBundle download complete! Loading scene...");
// Now proceed with scene loading
if (GameManager.Instance != null)
{
SceneManager.sceneLoaded += GameManager.Instance.OnSceneLoaded;
}
targetSceneName = sceneName;
if (!sceneHideHookRegistered)
{
SceneManager.sceneLoaded += HandleSceneLoadedInternal;
sceneHideHookRegistered = true;
}
yield return LoadSceneAsync(sceneName);
}
else
{
Debug.LogError("[LoadingScreen] ✗ AssetBundle download failed! Cannot proceed.");
// Show error message - user must retry or have internet
if (loadingText != null)
{
loadingText.text = "Download Failed - Check Internet Connection";
}
if (Hints_GameObject != null)
{
Hints_GameObject.GetComponent<TextMeshProUGUI>().text = "Please connect to the internet and restart the game";
}
// Don't proceed to menu - block here
}
}
// Function to animate the loading slider being filled from empty to full
private IEnumerator AnimateSliderFill(float duration)
{
loadingBaseText = "LOADING";
int dotsCount = 3;
float timer = 0f;
float startValue = loadingSlider.value;
float targetValue = 1f; // Assuming the slider's range is 0 to 1
float fakeProgress = 0;
loadingSlider.value = 0.01f;
while (timer < duration)
{
if (fakeProgress >= 0.25f && fakeProgress < 0.26f)
{
yield return new WaitForSeconds(0.15f); // Set a delay of 0.5 seconds
}
if (fakeProgress >= 0.75f && fakeProgress < 0.76f)
{
yield return new WaitForSeconds(0.25f); // Set a delay of 0.5 seconds
}
fakeProgress = Mathf.Lerp(startValue, targetValue, timer / duration);
if (loadingSlider != null)
{
loadingSlider.value = fakeProgress;
}
//Create ... animation effect on the Loading... text
int dotsToShow = Mathf.FloorToInt(timer % (dotsCount + 1));
string dots = new string('.', dotsToShow);
loadingText.text = loadingBaseText + dots;
timer += Time.deltaTime;
if ((loaderChangeCount < maxLoaderChanges) && (fakeProgress >= 0.01f && fakeProgress < 0.4f ||
fakeProgress >= 0.5f && fakeProgress < 0.65f ||
fakeProgress >= 0.8f && fakeProgress < 0.9f))
{
float randomLoaderDelay = Mathf.Round(Random.Range(0.01f, 0.09f) * 100) / 100; // Random value between 1 and 2 with 2 decimal places
//Debug.Log("randomdelay " + randomLoaderDelay);
yield return new WaitForSeconds(randomLoaderDelay);
loaderChangeCount++;
}
yield return null;
}
//loadingSlider.value = targetValue;
loadingText.text = "LOADING."; // Reset text to the original after completion
yield return new WaitForSeconds(0.5f);
if (to_change_to == 2) // UI
{
GameObject uiObject = GameObject.Find("UI");
GameObject UI_to_be_shown = uiObject.transform.Find(ui_to_show).gameObject;
UI_to_be_shown.SetActive(true);
}
}
// Function to load scene, and update slider value based on the loading progress
private IEnumerator LoadSceneAsync(string sceneName)
{
// Start a guarded async scene load (Single mode) and prevent duplicates
SceneLoader.SceneLoadHandle asyncLoad = SceneLoader.LoadSceneAsyncOnce(sceneName, LoadSceneMode.Single, allowSceneActivation: false);
// If a different scene is already loading, try to reuse the current operation if available
if (asyncLoad == null)
{
asyncLoad = SceneLoader.CurrentOperation;
if (asyncLoad == null)
yield break; // Nothing to wait on
}
if (gameManager != null)
{
gameManager.ShowLoadingUI();
}
yield return new WaitForSeconds(2f);
float timer = 0f;
float previousValue = 0;
while (!asyncLoad.isDone)
{
if (loadingSlider.value >= 0.25f && loadingSlider.value < 0.26f)
{
yield return new WaitForSeconds(0.15f); // Set a delay of 0.5 seconds
}
if (loadingSlider.value >= 0.75f && loadingSlider.value < 0.76f)
{
yield return new WaitForSeconds(0.25f); // Set a delay of 0.5 seconds
}
if (previousValue < Mathf.Lerp(0, asyncLoad.progress, timer / 4f))
{
previousValue = Mathf.Lerp(0, asyncLoad.progress, timer / 4f);
loadingSlider.value = Mathf.Lerp(0, asyncLoad.progress, timer / 4f);
}
else
loadingSlider.value = previousValue;
int dotsToShow = Mathf.FloorToInt(timer % (3 + 1));
string dots = new string('.', dotsToShow);
loadingText.text = loadingBaseText + dots;
timer += Time.deltaTime;
if ((loaderChangeCount < maxLoaderChanges) && (loadingSlider.value >= 0.01f && loadingSlider.value < 0.4f ||
loadingSlider.value >= 0.5f && loadingSlider.value < 0.65f ||
loadingSlider.value >= 0.8f && loadingSlider.value < 0.9f))
{
float randomLoaderDelay = Mathf.Round(Random.Range(0.01f, 0.09f) * 100) / 100; // Random value between 1 and 2 with 2 decimal places
yield return new WaitForSeconds(randomLoaderDelay);
loaderChangeCount++;
}
if (loadingSlider.value >= 0.9f)
{
loadingText.text = "LOADING...";
loadingSlider.value = 1.0f;
float randomLoaderDelay = Mathf.Round(Random.Range(0.01f, 0.09f) * 100) / 100; // Random value between 1 and 2 with 2 decimal places
yield return new WaitForSeconds(randomLoaderDelay);
loaderChangeCount++;
// Activate the scene once ready
asyncLoad.allowSceneActivation = true;
Debug.Log("[LoadingScreen] Scene activation triggered, waiting for isDone...");
}
yield return null;
}
Debug.Log("[LoadingScreen] Scene load complete (asyncLoad.isDone=true), hiding loading UI...");
// Use GameManager.Instance singleton instead of cached reference (which may be stale after scene change)
if (GameManager.Instance != null)
{
GameManager.Instance.HideLoadingUI();
}
else if (gameManager != null)
{
gameManager.HideLoadingUI();
}
else
{
Debug.LogWarning("[LoadingScreen] No GameManager available to hide loading UI!");
// Fallback: try to disable our own root
if (transform.root != null)
{
transform.root.gameObject.SetActive(false);
}
}
}
// Internal safety: ensure we always hide loading when the intended scene finishes loading.
private void HandleSceneLoadedInternal(Scene scene, LoadSceneMode mode)
{
if (!string.IsNullOrEmpty(targetSceneName) && scene.name == targetSceneName)
{
// Stop anim coroutines.
StopAllCoroutines();
// Hide through GameManager if available.
if (GameManager.Instance != null)
{
GameManager.Instance.HideLoadingUI();
}
else if (gameObject.activeInHierarchy)
{
// Fallback: disable our root canvas (assumes parent = loadingUI)
transform.root.gameObject.SetActive(false);
}
// cleanup subscription
SceneManager.sceneLoaded -= HandleSceneLoadedInternal;
sceneHideHookRegistered = false;
targetSceneName = null;
}
}
// Fallback hide when we didn't initiate the load, or targetSceneName wasn't set.
private void FallbackSceneLoadedHide(Scene scene, LoadSceneMode mode)
{
// If InitializeLoading set a specific target, let HandleSceneLoadedInternal manage it.
if (!string.IsNullOrEmpty(targetSceneName))
return;
if (autoHideOnSceneNames != null)
{
for (int i = 0; i < autoHideOnSceneNames.Length; i++)
{
var name = autoHideOnSceneNames[i];
if (!string.IsNullOrEmpty(name) && scene.name == name)
{
// Stop any visuals if they were running.
StopAllCoroutines();
if (GameManager.Instance != null)
{
GameManager.Instance.HideLoadingUI();
}
else if (gameObject.activeInHierarchy)
{
transform.root.gameObject.SetActive(false);
}
// Once done, remove fallback hook
SceneManager.sceneLoaded -= FallbackSceneLoadedHide;
fallbackHideHookRegistered = false;
break;
}
}
}
}
// Function to change the background image of the loading UI
private IEnumerator ChangeBackgroundWithDelay()
{
while (true)
{
if (backgroundChangeCount < maxBackgroundChanges)
{
float randomDelay = Mathf.Round(Random.Range(1f, 2f) * 100) / 100; // Random value between 1 and 2 with 2 decimal places
yield return new WaitForSeconds(randomDelay); // Adjust the time to wait as needed (2 seconds in this case)
ChangeBackgroundImage();
backgroundChangeCount++;
}
else
{
yield break; // Exit coroutine when maximum number of background changes is reached
}
yield return null;
}
}
// Function to change the hints being displayed in the loading UI while the player waits the loading to be completed
private IEnumerator ChangeHintsWithDelay()
{
while (true)
{
if (hintsChangeCount < maxhintsChanges)
{
float randomDelay = Mathf.Round(Random.Range(0.75f, 2.25f) * 100) / 100; // Random value between 1 and 2 with 2 decimal places
yield return new WaitForSeconds(randomDelay); // Adjust the time to wait as needed (2 seconds in this case)
ChangeHintsText();
hintsChangeCount++;
}
else
{
yield break; // Exit coroutine when maximum number of background changes is reached
}
yield return null;
}
}
// Function to change the hints text being displayed
private void ChangeHintsText()
{
// Remove stray literal text assignment; pick a random hint safely.
if (hintsArray.Length > 0)
{
int randomIndex = Random.Range(0, hintsArray.Length);
Hints_GameObject.GetComponent<TextMeshProUGUI>().text = hintsArray[randomIndex];
}
}
// Function to change the background image in the loading UI
private void ChangeBackgroundImage()
{
if (backgroundImages.Length > 0)
{
int randomIndex = Random.Range(0, backgroundImages.Length);
BackgroundImg_GameObject.GetComponent<Image>().sprite = backgroundImages[randomIndex];
}
}
// Function that is referenced/called in other script which specifies what Scene or UI to change to after the loading plays out
public void UI_to_show_AfterLoading(int x, string ui_name)
{
to_change_to = x;
ui_to_show = ui_name;
backgroundChangeCount = 0;
hintsChangeCount = 0;
loaderChangeCount = 0;
flag = true;
pendingInitialization = true;
if (isActiveAndEnabled)
{
InitializeLoading();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 006b3e96af949e44aaa312f9acd935b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e292ac785a1575376b06013ba96ee85b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,347 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;
namespace CustomAssetBundleSystem
{
/// <summary>
/// Manages AssetBundle downloads during LoadingScreen with UI feedback
/// Checks cache, downloads if needed, handles offline mode
/// </summary>
public class AssetBundleDownloadManager : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private Slider loadingSlider;
[SerializeField] private TextMeshProUGUI loadingText;
[SerializeField] private TextMeshProUGUI hintsText;
[SerializeField] private GameObject noInternetPanel; // Optional: Shows when internet required but not available
[Header("Download Settings")]
[SerializeField] private string[] bundlesToPreload = new[] { "gamescene" };
[SerializeField] private float minDisplayTime = 2f; // Minimum time to show loading (for UX)
[Header("Hints")]
[SerializeField] private string[] downloadHints = new[]
{
"Downloading game content...",
"Preparing assets for better performance...",
"First time setup - this only happens once!",
"Future launches will be instant!"
};
[SerializeField] private string[] cachedHints = new[]
{
"Complete Weekly Missions to gain XP",
"Complete a 20 Wins streak to level up in Rank",
"Missions change every Week"
};
// State
private bool _isDownloading = false;
private bool _downloadComplete = false;
private bool _hasInternet = true;
private RemoteAssetBundleManager _bundleManager;
// Events
public event Action<bool> OnDownloadComplete; // true = success, false = failed
private void Awake()
{
_bundleManager = RemoteAssetBundleManager.Instance;
}
/// <summary>
/// Start the download process - call this from LoadingScreen
/// </summary>
public IEnumerator StartDownloadProcess(Action<bool> onComplete = null)
{
if (_isDownloading)
{
Debug.LogWarning("[DownloadManager] Download already in progress");
yield break;
}
_isDownloading = true;
_downloadComplete = false;
float startTime = Time.time;
Debug.Log("[DownloadManager] Starting AssetBundle download process...");
// Step 1: Initialize RemoteAssetBundleManager (downloads catalog)
yield return InitializeBundleSystem();
if (!_bundleManager.IsInitialized)
{
Debug.LogError("[DownloadManager] Failed to initialize bundle system");
HandleDownloadFailed("Failed to connect to server");
onComplete?.Invoke(false);
OnDownloadComplete?.Invoke(false);
yield break;
}
// Step 2: Check if bundles are already cached
bool allCached = CheckIfBundlesCached();
if (allCached)
{
Debug.Log("[DownloadManager] ✓ All bundles already cached! (Offline play available)");
UpdateUI(1f, "Ready!", cachedHints);
// Show success for minimum display time
float elapsed = Time.time - startTime;
if (elapsed < minDisplayTime)
{
yield return new WaitForSeconds(minDisplayTime - elapsed);
}
_downloadComplete = true;
_isDownloading = false;
onComplete?.Invoke(true);
OnDownloadComplete?.Invoke(true);
yield break;
}
// Step 3: Need to download - check internet connectivity
_hasInternet = Application.internetReachability != NetworkReachability.NotReachable;
if (!_hasInternet)
{
Debug.LogWarning("[DownloadManager] No internet connection and bundles not cached");
HandleNoInternet();
onComplete?.Invoke(false);
OnDownloadComplete?.Invoke(false);
yield break;
}
// Step 4: Download bundles with progress updates
Debug.Log($"[DownloadManager] Downloading {bundlesToPreload.Length} bundle(s)...");
UpdateUI(0f, "Downloading game content", downloadHints);
bool downloadSuccess = true;
int bundleIndex = 0;
foreach (string bundleName in bundlesToPreload)
{
bundleIndex++;
Debug.Log($"[DownloadManager] Downloading bundle {bundleIndex}/{bundlesToPreload.Length}: {bundleName}");
bool bundleLoaded = false;
float bundleProgress = 0f;
// Subscribe to progress updates
void ProgressHandler(string name, float progress)
{
if (name == bundleName)
{
bundleProgress = progress;
float overallProgress = (bundleIndex - 1f + progress) / bundlesToPreload.Length;
UpdateUI(overallProgress, $"Downloading {bundleName}", downloadHints);
}
}
_bundleManager.OnBundleDownloadProgress += ProgressHandler;
// Start download
yield return _bundleManager.LoadBundleAsync(bundleName, (bundle) =>
{
bundleLoaded = (bundle != null);
});
_bundleManager.OnBundleDownloadProgress -= ProgressHandler;
if (!bundleLoaded)
{
Debug.LogError($"[DownloadManager] Failed to download bundle: {bundleName}");
downloadSuccess = false;
break;
}
Debug.Log($"[DownloadManager] ✓ Bundle '{bundleName}' ready!");
}
if (!downloadSuccess)
{
HandleDownloadFailed("Failed to download game content");
onComplete?.Invoke(false);
OnDownloadComplete?.Invoke(false);
yield break;
}
// Step 5: Success!
UpdateUI(1f, "Download complete!", downloadHints);
Debug.Log("[DownloadManager] ✓ All bundles downloaded and cached successfully!");
// Show success for minimum display time
float totalElapsed = Time.time - startTime;
if (totalElapsed < minDisplayTime)
{
yield return new WaitForSeconds(minDisplayTime - totalElapsed);
}
_downloadComplete = true;
_isDownloading = false;
onComplete?.Invoke(true);
OnDownloadComplete?.Invoke(true);
}
/// <summary>
/// Initialize the bundle system (downloads catalog)
/// </summary>
private IEnumerator InitializeBundleSystem()
{
Debug.Log("[DownloadManager] Initializing bundle system...");
UpdateUI(0.05f, "Connecting to server", downloadHints);
yield return _bundleManager.InitializeAsync();
if (_bundleManager.IsInitialized)
{
Debug.Log("[DownloadManager] ✓ Bundle system initialized");
}
}
/// <summary>
/// Check if all required bundles are already cached
/// </summary>
private bool CheckIfBundlesCached()
{
foreach (string bundleName in bundlesToPreload)
{
if (!_bundleManager.IsBundleCached(bundleName))
{
Debug.Log($"[DownloadManager] Bundle '{bundleName}' not cached - download required");
return false;
}
}
return true;
}
/// <summary>
/// Update UI elements with progress and hints
/// </summary>
private void UpdateUI(float progress, string statusText, string[] hints)
{
if (loadingSlider != null)
{
loadingSlider.value = progress;
}
if (loadingText != null)
{
loadingText.text = statusText;
}
if (hintsText != null && hints != null && hints.Length > 0)
{
// Show random hint or cycle through them
int hintIndex = Mathf.FloorToInt(progress * hints.Length);
hintIndex = Mathf.Clamp(hintIndex, 0, hints.Length - 1);
hintsText.text = hints[hintIndex];
}
}
/// <summary>
/// Handle no internet connection scenario
/// </summary>
private void HandleNoInternet()
{
Debug.LogWarning("[DownloadManager] ⚠️ No internet connection!");
if (loadingText != null)
{
loadingText.text = "No Internet Connection";
}
if (hintsText != null)
{
hintsText.text = "Please connect to the internet to download game content";
}
if (noInternetPanel != null)
{
noInternetPanel.SetActive(true);
}
// Don't proceed - block user until they have internet
_isDownloading = false;
}
/// <summary>
/// Handle download failure
/// </summary>
private void HandleDownloadFailed(string errorMessage)
{
Debug.LogError($"[DownloadManager] Download failed: {errorMessage}");
if (loadingText != null)
{
loadingText.text = "Download Failed";
}
if (hintsText != null)
{
hintsText.text = errorMessage;
}
_isDownloading = false;
}
/// <summary>
/// Check if download is complete and successful
/// </summary>
public bool IsDownloadComplete()
{
return _downloadComplete;
}
/// <summary>
/// Retry button handler (for no internet panel)
/// </summary>
public void RetryDownload()
{
if (noInternetPanel != null)
{
noInternetPanel.SetActive(false);
}
// Restart the download process
StartCoroutine(StartDownloadProcess());
}
/// <summary>
/// Get cache status for UI display
/// </summary>
public string GetCacheStatus()
{
if (!_bundleManager.IsInitialized)
{
return "Not initialized";
}
int cachedCount = 0;
foreach (string bundleName in bundlesToPreload)
{
if (_bundleManager.IsBundleCached(bundleName))
{
cachedCount++;
}
}
if (cachedCount == bundlesToPreload.Length)
{
return "All content cached (Offline play available)";
}
else if (cachedCount > 0)
{
return $"{cachedCount}/{bundlesToPreload.Length} bundles cached";
}
else
{
return "Download required";
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 89228db1021bc63ddaafaef772de5318

View File

@@ -0,0 +1,181 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using CustomAssetBundleSystem;
/// <summary>
/// Example integration: Replace Addressables with custom AssetBundle system
/// Drop-in replacement for your existing SceneLoader
/// </summary>
public class CustomAssetBundleSceneLoader : MonoBehaviour
{
[Header("UI References (Optional)")]
public Slider progressSlider;
public Text statusText;
[Header("Settings")]
public bool initializeOnStart = true;
private bool isInitialized = false;
void Start()
{
if (initializeOnStart)
{
StartCoroutine(InitializeAndLoadGameScene());
}
}
/// <summary>
/// Initialize the AssetBundle system and load Game scene
/// Call this instead of Addressables.InitializeAsync()
/// </summary>
public IEnumerator InitializeAndLoadGameScene()
{
UpdateStatus("Initializing AssetBundle system...");
// Initialize the catalog
yield return RemoteAssetBundleManager.Instance.InitializeAsync();
if (!RemoteAssetBundleManager.Instance.IsInitialized)
{
UpdateStatus("✗ Failed to initialize AssetBundle system");
Debug.LogError("[CustomAssetBundleLoader] Initialization failed!");
yield break;
}
isInitialized = true;
UpdateStatus("✓ AssetBundle system initialized");
// Optional: Show catalog info
var catalog = RemoteAssetBundleManager.Instance.GetCatalog();
Debug.Log($"[CustomAssetBundleLoader] Catalog version: {catalog.version}");
Debug.Log($"[CustomAssetBundleLoader] Available bundles: {catalog.bundles.Count}");
// Load the Game scene
yield return LoadGameScene();
}
/// <summary>
/// Load the Game scene from remote AssetBundle
/// </summary>
public IEnumerator LoadGameScene()
{
if (!isInitialized)
{
Debug.LogError("[CustomAssetBundleLoader] Not initialized! Call InitializeAndLoadGameScene() first.");
yield break;
}
UpdateStatus("Loading Game scene...");
bool sceneLoaded = false;
// Subscribe to download progress
RemoteAssetBundleManager.Instance.OnBundleDownloadProgress += (bundleName, progress) =>
{
UpdateProgress(progress);
UpdateStatus($"Downloading {bundleName}: {progress * 100:F1}%");
};
// Load the scene
yield return RemoteAssetBundleManager.Instance.LoadSceneAsync("Game", (success) =>
{
sceneLoaded = success;
});
if (sceneLoaded)
{
UpdateStatus("✓ Game scene loaded!");
Debug.Log("[CustomAssetBundleLoader] ✓ Game scene loaded successfully!");
}
else
{
UpdateStatus("✗ Failed to load Game scene");
Debug.LogError("[CustomAssetBundleLoader] ✗ Failed to load Game scene!");
}
}
/// <summary>
/// Load any scene by name
/// </summary>
public void LoadScene(string sceneName)
{
StartCoroutine(LoadSceneCoroutine(sceneName));
}
private IEnumerator LoadSceneCoroutine(string sceneName)
{
if (!isInitialized)
{
Debug.LogError("[CustomAssetBundleLoader] Not initialized!");
yield break;
}
UpdateStatus($"Loading {sceneName}...");
bool success = false;
yield return RemoteAssetBundleManager.Instance.LoadSceneAsync(sceneName, (loaded) =>
{
success = loaded;
});
if (success)
{
UpdateStatus($"✓ {sceneName} loaded!");
}
else
{
UpdateStatus($"✗ Failed to load {sceneName}");
}
}
#region UI Helpers
private void UpdateProgress(float progress)
{
if (progressSlider != null)
{
progressSlider.value = progress;
}
}
private void UpdateStatus(string message)
{
if (statusText != null)
{
statusText.text = message;
}
Debug.Log($"[CustomAssetBundleLoader] {message}");
}
#endregion
#region Public API - Replace your existing SceneLoader calls
/// <summary>
/// Check if the system is ready (replaces Addressables initialization check)
/// </summary>
public bool IsReady()
{
return RemoteAssetBundleManager.Instance.IsInitialized;
}
/// <summary>
/// Get memory usage info
/// </summary>
public string GetMemoryInfo()
{
return RemoteAssetBundleManager.Instance.GetMemoryStats();
}
/// <summary>
/// Unload all bundles to free memory
/// </summary>
public void UnloadAllBundles()
{
RemoteAssetBundleManager.Instance.UnloadAllBundles(true);
}
#endregion
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c5a2bd36bf588caa4835bea95fcc3985

View File

@@ -0,0 +1,663 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.SceneManagement;
using UnityEngine.Profiling;
namespace CustomAssetBundleSystem
{
/// <summary>
/// Custom AssetBundle manager - replaces Unity Addressables
/// Full manual control, no Unity 6 bugs, lightweight and fast
/// </summary>
public class RemoteAssetBundleManager : MonoBehaviour
{
private static RemoteAssetBundleManager _instance;
public static RemoteAssetBundleManager Instance
{
get
{
if (_instance == null)
{
var go = new GameObject("[RemoteAssetBundleManager]");
_instance = go.AddComponent<RemoteAssetBundleManager>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
// Configuration
[Header("Remote Catalog Settings")]
public string catalogUrl = "https://assetbundle.uncaptured.co/catalog.json";
public int downloadTimeout = 900; // seconds (15 minutes) for large bundles
public int maxRetries = 3;
[Tooltip("If progress does not change for this many seconds, abort and retry.")]
public float downloadStallTimeout = 45f;
[Tooltip("Base delay between retries (multiplied by attempt index).")]
public float retryBackoffSeconds = 2f;
[Header("Memory Management")]
[Tooltip("Unloads the scene bundle and its dependencies once the scene is loaded to free GPU/CPU memory (recommended on mobile).")]
public bool unloadSceneBundlesAfterLoad = true;
[Tooltip("Calls Resources.UnloadUnusedAssets and GC.Collect before and after heavy scene loads.")]
public bool runMemoryCleanupAroundSceneLoad = true;
[Tooltip("Logs runtime memory usage after cleanup steps.")]
public bool logMemoryStats = false;
// State
private RemoteAssetCatalog _catalog;
private Dictionary<string, AssetBundle> _loadedBundles = new Dictionary<string, AssetBundle>();
private Dictionary<string, Coroutine> _activeDownloads = new Dictionary<string, Coroutine>();
private bool _isInitialized = false;
// Events
public event Action<bool, string> OnCatalogLoaded;
public event Action<string, float> OnBundleDownloadProgress;
public event Action<string, bool> OnBundleLoaded;
void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
#region Initialization
/// <summary>
/// Initialize the system by downloading the remote catalog
/// </summary>
public IEnumerator InitializeAsync()
{
if (_isInitialized)
{
Debug.Log("[AssetBundleManager] Already initialized");
yield break;
}
Debug.Log($"[AssetBundleManager] Downloading catalog from: {catalogUrl}");
bool success = false;
string errorMessage = "";
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
using (UnityWebRequest www = UnityWebRequest.Get(catalogUrl))
{
www.timeout = downloadTimeout;
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
try
{
string jsonText = www.downloadHandler.text;
_catalog = JsonUtility.FromJson<RemoteAssetCatalog>(jsonText);
if (_catalog != null && _catalog.bundles != null)
{
Debug.Log($"[AssetBundleManager] ✓ Catalog loaded successfully!");
Debug.Log($" Version: {_catalog.version}");
Debug.Log($" Base URL: {_catalog.baseUrl}");
Debug.Log($" Bundles: {_catalog.bundles.Count}");
_isInitialized = true;
success = true;
break;
}
else
{
errorMessage = "Catalog JSON is invalid or empty";
}
}
catch (Exception ex)
{
errorMessage = $"JSON parsing failed: {ex.Message}";
Debug.LogError($"[AssetBundleManager] {errorMessage}");
}
}
else
{
errorMessage = $"Download failed: {www.error}";
Debug.LogWarning($"[AssetBundleManager] Attempt {attempt}/{maxRetries}: {errorMessage}");
if (attempt < maxRetries)
{
yield return new WaitForSeconds(2f); // Wait before retry
}
}
}
}
OnCatalogLoaded?.Invoke(success, errorMessage);
if (!success)
{
Debug.LogError($"[AssetBundleManager] ✗ Failed to load catalog after {maxRetries} attempts: {errorMessage}");
}
}
public bool IsInitialized => _isInitialized;
#endregion
#region Bundle Loading
/// <summary>
/// Load an AssetBundle by name (with dependencies)
/// </summary>
public IEnumerator LoadBundleAsync(string bundleName, Action<AssetBundle> onComplete = null)
{
if (!_isInitialized)
{
Debug.LogError("[AssetBundleManager] Not initialized! Call InitializeAsync() first.");
onComplete?.Invoke(null);
yield break;
}
// Check if already loaded
if (_loadedBundles.TryGetValue(bundleName, out AssetBundle cachedBundle))
{
Debug.Log($"[AssetBundleManager] Bundle '{bundleName}' already loaded (using cached)");
onComplete?.Invoke(cachedBundle);
yield break;
}
// Find bundle entry in catalog
var bundleEntry = _catalog.FindBundleByName(bundleName);
if (bundleEntry == null)
{
Debug.LogError($"[AssetBundleManager] Bundle '{bundleName}' not found in catalog!");
onComplete?.Invoke(null);
yield break;
}
// Load dependencies first
if (bundleEntry.dependencies != null && bundleEntry.dependencies.Count > 0)
{
Debug.Log($"[AssetBundleManager] Loading {bundleEntry.dependencies.Count} dependencies for '{bundleName}'...");
foreach (var depName in bundleEntry.dependencies)
{
if (!_loadedBundles.ContainsKey(depName))
{
bool depLoaded = false;
yield return LoadBundleAsync(depName, (bundle) => { depLoaded = bundle != null; });
if (!depLoaded)
{
Debug.LogError($"[AssetBundleManager] Failed to load dependency '{depName}' for '{bundleName}'");
onComplete?.Invoke(null);
yield break;
}
}
}
}
// Download and load the bundle WITH CACHING
string bundleUrl = bundleEntry.GetFullUrl(_catalog.baseUrl);
// Check if bundle is already cached
bool isCached = Caching.IsVersionCached(bundleUrl, Hash128.Parse(bundleEntry.hash));
if (isCached)
{
Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' found in cache (INSTANT LOAD)");
}
else
{
Debug.Log($"[AssetBundleManager] Downloading bundle: {bundleUrl}");
Debug.Log($" Size: {bundleEntry.size / 1024f / 1024f:F2} MB");
}
AssetBundle downloadedBundle = null;
string lastError = null;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
if (Application.internetReachability == NetworkReachability.NotReachable)
{
lastError = "No internet connection detected";
Debug.LogWarning($"[AssetBundleManager] Internet not reachable while downloading '{bundleName}'. Attempt {attempt}/{maxRetries}.");
yield return new WaitForSeconds(retryBackoffSeconds * attempt);
continue;
}
using (UnityWebRequest www = UnityWebRequestAssetBundle.GetAssetBundle(bundleUrl, Hash128.Parse(bundleEntry.hash), 0))
{
www.timeout = downloadTimeout;
www.disposeDownloadHandlerOnDispose = true;
var operation = www.SendWebRequest();
float lastProgress = -1f;
float stalledTimer = 0f;
// Report download progress (only shows progress if downloading)
while (!operation.isDone)
{
float progress = Mathf.Clamp01(www.downloadProgress);
if (!isCached)
{
OnBundleDownloadProgress?.Invoke(bundleName, progress);
}
if (!isCached && downloadStallTimeout > 0f)
{
if (Mathf.Approximately(progress, lastProgress))
{
stalledTimer += Time.unscaledDeltaTime;
if (stalledTimer >= downloadStallTimeout)
{
Debug.LogWarning($"[AssetBundleManager] Download stalled for '{bundleName}' (progress {progress:P0}) after {stalledTimer:F1}s. Aborting attempt {attempt}/{maxRetries}.");
www.Abort();
break;
}
}
else
{
lastProgress = progress;
stalledTimer = 0f;
}
}
yield return null;
}
if (www.result == UnityWebRequest.Result.Success)
{
downloadedBundle = DownloadHandlerAssetBundle.GetContent(www);
if (downloadedBundle != null)
{
_loadedBundles[bundleName] = downloadedBundle;
if (isCached)
{
Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' loaded from cache!");
}
else
{
Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' downloaded and cached!");
Debug.Log($" Cached location: {Caching.defaultCache.path}");
}
OnBundleLoaded?.Invoke(bundleName, true);
onComplete?.Invoke(downloadedBundle);
yield break;
}
lastError = "Bundle extraction returned null";
Debug.LogError($"[AssetBundleManager] ✗ Failed to extract bundle from download: {bundleName}");
}
else
{
lastError = string.IsNullOrEmpty(www.error) ? www.result.ToString() : www.error;
Debug.LogWarning($"[AssetBundleManager] Attempt {attempt}/{maxRetries} failed for '{bundleName}': {lastError}");
}
}
if (attempt < maxRetries)
{
float delay = retryBackoffSeconds * attempt;
Debug.Log($"[AssetBundleManager] Retrying '{bundleName}' in {delay:F1}s...");
yield return new WaitForSeconds(delay);
}
}
Debug.LogError($"[AssetBundleManager] ✗ Failed to download bundle '{bundleName}': {lastError}");
OnBundleLoaded?.Invoke(bundleName, false);
onComplete?.Invoke(null);
yield break;
}
/// <summary>
/// Load a scene from a remote AssetBundle by scene name
/// </summary>
public IEnumerator LoadSceneAsync(string sceneName, Action<bool> onComplete = null)
{
if (!_isInitialized)
{
Debug.LogError("[AssetBundleManager] Not initialized! Call InitializeAsync() first.");
onComplete?.Invoke(false);
yield break;
}
// Find which bundle contains this scene
var bundleEntry = _catalog.FindBundleByScene(sceneName);
if (bundleEntry == null)
{
Debug.LogError($"[AssetBundleManager] Scene '{sceneName}' not found in any bundle!");
onComplete?.Invoke(false);
yield break;
}
Debug.Log($"[AssetBundleManager] Scene '{sceneName}' found in bundle '{bundleEntry.bundleName}'");
var dependencyEntries = _catalog.GetAllDependencies(bundleEntry);
if (runMemoryCleanupAroundSceneLoad)
{
yield return RunMemoryCleanupAsync($"[AssetBundleManager] Pre-load memory cleanup before '{sceneName}'");
}
// Load the bundle (and dependencies)
bool bundleLoaded = false;
yield return LoadBundleAsync(bundleEntry.bundleName, (bundle) => { bundleLoaded = bundle != null; });
if (!bundleLoaded)
{
Debug.LogError($"[AssetBundleManager] Failed to load bundle for scene '{sceneName}'");
onComplete?.Invoke(false);
yield break;
}
// CRITICAL FIX: Load scene in SINGLE mode to avoid Unity bug with additive loading
// The bug causes scene loading to hang at 0.2% when using Additive mode from AssetBundles
Debug.Log($"[AssetBundleManager] Loading scene '{sceneName}' from bundle (SINGLE mode to avoid Unity hang bug)...");
// Extra instrumentation: timestamp, loaded scenes, memory
Debug.Log($"[AssetBundleManager] StartTime: {DateTime.UtcNow:O}");
Debug.Log($"[AssetBundleManager] Current loaded scenes: {SceneManager.sceneCount}");
for (int si = 0; si < SceneManager.sceneCount; si++)
{
var s = SceneManager.GetSceneAt(si);
Debug.Log($"[AssetBundleManager] Scene[{si}] name={s.name} isLoaded={s.isLoaded}");
}
var sceneLoadOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
if (sceneLoadOperation == null)
{
Debug.LogError($"[AssetBundleManager] Failed to start scene load: {sceneName}");
onComplete?.Invoke(false);
yield break;
}
// Allow scene to activate immediately (no manual activation needed)
sceneLoadOperation.allowSceneActivation = true;
// Wait for scene to load with timeout protection and stall detection
float timeout = 30f; // 30 second timeout
float elapsed = 0f;
// Track progress samples to detect stalls (common symptom: stuck at ~0.2)
float lastProgress = -1f;
float progressUnchangedTime = 0f;
const float stallThresholdSeconds = 8f; // if progress unchanged for this long, treat as stall
bool attemptedRetry = false;
while (!sceneLoadOperation.isDone && elapsed < timeout)
{
float progress = sceneLoadOperation.progress; // 0..1
// Log only when progress changes or at periodic intervals to avoid log spam
if (Mathf.Approximately(progress, lastProgress))
{
progressUnchangedTime += Time.deltaTime;
}
else
{
lastProgress = progress;
progressUnchangedTime = 0f;
Debug.Log($"[AssetBundleManager] Scene load progress: {progress * 100:F1}% (elapsed {elapsed:F2}s)");
}
// If we detect a stall near 0.2 (historical hang), attempt a single retry
if (!attemptedRetry && progress <= 0.21f && progressUnchangedTime > stallThresholdSeconds)
{
Debug.LogWarning($"[AssetBundleManager] Detected scene-load stall at {progress * 100:F1}% for {progressUnchangedTime:F1}s. Attempting one retry...");
// Attempt to unload bundle and retry once after small delay
try
{
string retryBundle = bundleEntry.bundleName;
if (_loadedBundles.ContainsKey(retryBundle))
{
Debug.Log($"[AssetBundleManager] Unloading bundle '{retryBundle}' to attempt retry...");
UnloadBundle(retryBundle, true);
}
}
catch (Exception ex)
{
Debug.LogException(ex);
}
// wait a short moment and restart the load operation
yield return new WaitForSeconds(0.5f);
attemptedRetry = true;
elapsed = 0f;
lastProgress = -1f;
progressUnchangedTime = 0f;
Debug.Log("[AssetBundleManager] Starting retry of SceneManager.LoadSceneAsync...");
sceneLoadOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
if (sceneLoadOperation == null)
{
Debug.LogError($"[AssetBundleManager] Retry failed to start scene load: {sceneName}");
onComplete?.Invoke(false);
yield break;
}
sceneLoadOperation.allowSceneActivation = true;
}
elapsed += Time.deltaTime;
yield return null;
}
if (elapsed >= timeout && !sceneLoadOperation.isDone)
{
Debug.LogError($"[AssetBundleManager] Scene load timed out after {timeout} seconds! progress={sceneLoadOperation.progress * 100:F1}% attemptedRetry={attemptedRetry}");
// dump some diagnostic info
Debug.Log($"[AssetBundleManager] Loaded bundles: {string.Join(",", GetLoadedBundleNames())}");
Debug.Log($"[AssetBundleManager] Memory stats: {GetMemoryStats()}");
onComplete?.Invoke(false);
yield break;
}
Debug.Log($"[AssetBundleManager] ✓ Scene '{sceneName}' loaded successfully in {elapsed:F2} seconds! attemptedRetry={attemptedRetry}");
if (unloadSceneBundlesAfterLoad)
{
yield return null; // ensure the scene has a frame to settle before we release bundles
ReleaseSceneBundles(bundleEntry, dependencyEntries);
}
if (runMemoryCleanupAroundSceneLoad)
{
yield return RunMemoryCleanupAsync($"[AssetBundleManager] Post-load memory cleanup after '{sceneName}'");
}
onComplete?.Invoke(true);
}
#endregion
#region Cache Management
/// <summary>
/// Check if a bundle is cached on disk
/// </summary>
public bool IsBundleCached(string bundleName)
{
if (!_isInitialized || _catalog == null)
return false;
var bundleEntry = _catalog.FindBundleByName(bundleName);
if (bundleEntry == null)
return false;
string bundleUrl = bundleEntry.GetFullUrl(_catalog.baseUrl);
return Caching.IsVersionCached(bundleUrl, Hash128.Parse(bundleEntry.hash));
}
/// <summary>
/// Get cache information
/// </summary>
public void GetCacheInfo()
{
Debug.Log($"[AssetBundleManager] Cache Info:");
Debug.Log($" Cache path: {Caching.defaultCache.path}");
Debug.Log($" Cache space used: {Caching.defaultCache.spaceOccupied / 1024f / 1024f:F2} MB");
Debug.Log($" Max cache size: {Caching.defaultCache.maximumAvailableStorageSpace / 1024f / 1024f / 1024f:F2} GB");
Debug.Log($" Caching ready: {Caching.ready}");
}
/// <summary>
/// Clear all cached bundles (forces re-download on next load)
/// </summary>
public bool ClearCache()
{
bool success = Caching.ClearCache();
if (success)
{
Debug.Log($"[AssetBundleManager] ✓ Cache cleared successfully!");
}
else
{
Debug.LogError($"[AssetBundleManager] ✗ Failed to clear cache");
}
return success;
}
/// <summary>
/// Clear only old versions of cached bundles (keeps current version)
/// </summary>
public void CleanupOldCachedVersions()
{
bool success = Caching.ClearAllCachedVersions(Caching.defaultCache.path);
if (success)
{
Debug.Log($"[AssetBundleManager] ✓ Cleaned up old cached versions");
}
else
{
Debug.LogWarning($"[AssetBundleManager] Failed to cleanup old cached versions");
}
}
#endregion
#region Memory Management
/// <summary>
/// Unload a specific bundle
/// </summary>
public void UnloadBundle(string bundleName, bool unloadAllLoadedObjects = false)
{
if (_loadedBundles.TryGetValue(bundleName, out AssetBundle bundle))
{
bundle.Unload(unloadAllLoadedObjects);
_loadedBundles.Remove(bundleName);
Debug.Log($"[AssetBundleManager] Unloaded bundle: {bundleName}");
}
}
/// <summary>
/// Unload all bundles
/// </summary>
public void UnloadAllBundles(bool unloadAllLoadedObjects = false)
{
foreach (var kvp in _loadedBundles)
{
kvp.Value.Unload(unloadAllLoadedObjects);
}
_loadedBundles.Clear();
Debug.Log($"[AssetBundleManager] Unloaded all bundles");
}
/// <summary>
/// Get memory usage stats
/// </summary>
public string GetMemoryStats()
{
long totalMemory = 0;
foreach (var kvp in _loadedBundles)
{
// AssetBundle doesn't expose size directly, but we can estimate
totalMemory += kvp.Value.GetAllAssetNames().Length * 1024; // Rough estimate
}
return $"Loaded Bundles: {_loadedBundles.Count}, Estimated Memory: {totalMemory / 1024f / 1024f:F2} MB";
}
private void ReleaseSceneBundles(RemoteAssetCatalog.BundleEntry rootBundle, List<RemoteAssetCatalog.BundleEntry> dependencyEntries)
{
if (rootBundle == null)
return;
var bundlesToRelease = new List<RemoteAssetCatalog.BundleEntry> { rootBundle };
if (dependencyEntries != null)
{
bundlesToRelease.AddRange(dependencyEntries);
}
foreach (var entry in bundlesToRelease)
{
if (entry == null)
continue;
if (_loadedBundles.ContainsKey(entry.bundleName))
{
Debug.Log($"[AssetBundleManager] Releasing bundle '{entry.bundleName}' after scene load");
UnloadBundle(entry.bundleName, false);
}
}
}
private IEnumerator RunMemoryCleanupAsync(string reason)
{
Debug.Log(reason);
// Give the UI a frame to update before we start releasing memory heavy objects.
yield return null;
yield return Resources.UnloadUnusedAssets();
GC.Collect();
if (logMemoryStats)
{
Debug.Log($"[AssetBundleManager] Runtime memory usage: {GetRuntimeMemoryStats()}");
}
}
private string GetRuntimeMemoryStats()
{
long allocated = Profiler.GetTotalAllocatedMemoryLong();
long reserved = Profiler.GetTotalReservedMemoryLong();
long unused = Profiler.GetTotalUnusedReservedMemoryLong();
return $"Allocated: {allocated / (1024f * 1024f):F1} MB | Reserved: {reserved / (1024f * 1024f):F1} MB | Unused Reserved: {unused / (1024f * 1024f):F1} MB | Bundles Cached: {GetMemoryStats()}";
}
#endregion
#region Catalog Info
/// <summary>
/// Get the loaded catalog (null if not initialized)
/// </summary>
public RemoteAssetCatalog GetCatalog() => _catalog;
/// <summary>
/// Check if a bundle is loaded
/// </summary>
public bool IsBundleLoaded(string bundleName) => _loadedBundles.ContainsKey(bundleName);
/// <summary>
/// Get list of all loaded bundle names
/// </summary>
public List<string> GetLoadedBundleNames() => new List<string>(_loadedBundles.Keys);
#endregion
void OnDestroy()
{
UnloadAllBundles(true);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 17480a2e38229ac01a32a1cc37aa82a0

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace CustomAssetBundleSystem
{
/// <summary>
/// Simple JSON catalog structure for remote AssetBundles
/// No Unity Addressables bugs, full control, lightweight
/// </summary>
[Serializable]
public class RemoteAssetCatalog
{
public string version = "1.0.0";
public long timestamp;
public string baseUrl = "https://addressable.uncaptured.co/AssetBundles/Android";
public List<BundleEntry> bundles = new List<BundleEntry>();
[Serializable]
public class BundleEntry
{
public string bundleName; // e.g., "gamescene"
public string fileName; // e.g., "gamescene.bundle"
public string hash; // MD5/CRC for verification
public long size; // File size in bytes
public List<string> scenes; // Scenes contained in this bundle
public List<string> dependencies; // Other bundles this depends on
public string GetFullUrl(string baseUrl)
{
return $"{baseUrl}/{fileName}";
}
}
/// <summary>
/// Find bundle entry by scene name
/// </summary>
public BundleEntry FindBundleByScene(string sceneName)
{
foreach (var bundle in bundles)
{
if (bundle.scenes != null && bundle.scenes.Contains(sceneName))
{
return bundle;
}
}
return null;
}
/// <summary>
/// Find bundle entry by bundle name
/// </summary>
public BundleEntry FindBundleByName(string bundleName)
{
return bundles.Find(b => b.bundleName == bundleName);
}
/// <summary>
/// Get all dependencies recursively
/// </summary>
public List<BundleEntry> GetAllDependencies(BundleEntry bundle, HashSet<string> visited = null)
{
if (visited == null) visited = new HashSet<string>();
var result = new List<BundleEntry>();
if (bundle.dependencies == null || bundle.dependencies.Count == 0)
return result;
foreach (var depName in bundle.dependencies)
{
if (visited.Contains(depName)) continue;
visited.Add(depName);
var depBundle = FindBundleByName(depName);
if (depBundle != null)
{
result.Add(depBundle);
result.AddRange(GetAllDependencies(depBundle, visited));
}
}
return result;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b442d4ceb7eb5b9a78189a4b3dedd9bf

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e1487159000ff9b40a0c860d9055714a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c47f8738dc6b2014f82a6949aac64682
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,215 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using TMPro;
using UnityEngine.SceneManagement;
using UnityEngine.EventSystems;
// UserLogin: Clean single class implementation.
// Features: email/password login, guest panel (show/accept/decline), guest login, menu transition, click SFX.
public class UserLogin : MonoBehaviour
{
[Header("Credentials UI")] public TMP_InputField emailInput;
public TMP_InputField passwordInput;
public TextMeshProUGUI feedbackText;
public GameObject loadingIndicator;
[Header("Guest Flow UI")] public GameObject guestPanel; // Confirmation panel
public GameObject loginLandingPanel; // Landing buttons/panel (should stay visible behind guest panel)
[Header("Guest Flow Buttons (Optional Auto-Wire)")]
public UnityEngine.UI.Button guestButton;
public UnityEngine.UI.Button guestAcceptButton;
public UnityEngine.UI.Button guestDeclineButton;
[Header("Audio (Optional)")] public AudioClip clickSfx;
private AudioSource uiAudio;
private const string MenuSceneName = "MenuScene";
private const string AuthTokenKey = "AuthToken";
[SerializeField] private string loginUrl = "http://127.0.0.1:8001/auth/token/login/";
private GameInitialize gameInitialize;
private string authToken;
void Awake()
{
gameInitialize = GameInitialize.Instance;
if (!gameInitialize) Debug.LogError("[UserLogin] GameInitialize not found.");
if (!guestPanel) guestPanel = GameObject.Find("GuestPanel");
if (!loginLandingPanel) loginLandingPanel = GameObject.Find("Login Landing Panel");
// Auto-wire buttons (by name) if not assigned explicitly
if (!guestButton)
{
var go = GameObject.Find("GuestButton");
if (go) guestButton = go.GetComponent<UnityEngine.UI.Button>();
}
if (!guestAcceptButton)
{
var go = GameObject.Find("Accept");
if (go) guestAcceptButton = go.GetComponent<UnityEngine.UI.Button>();
}
if (!guestDeclineButton)
{
var go = GameObject.Find("Decline");
if (go) guestDeclineButton = go.GetComponent<UnityEngine.UI.Button>();
}
if (guestButton)
{
guestButton.onClick.RemoveListener(ShowGuestPanel);
guestButton.onClick.AddListener(ShowGuestPanel);
}
if (guestAcceptButton)
{
guestAcceptButton.onClick.RemoveListener(GuestAccept);
guestAcceptButton.onClick.AddListener(GuestAccept);
}
if (guestDeclineButton)
{
guestDeclineButton.onClick.RemoveListener(GuestDecline);
guestDeclineButton.onClick.AddListener(GuestDecline);
}
uiAudio = GetComponent<AudioSource>();
if (!uiAudio) { uiAudio = gameObject.AddComponent<AudioSource>(); uiAudio.playOnAwake = false; }
if (!EventSystem.current) Debug.LogWarning("[UserLogin] No EventSystem present UI buttons won't work.");
}
void Start()
{
if (guestPanel && guestPanel.activeSelf) guestPanel.SetActive(false);
if (loginLandingPanel && !loginLandingPanel.activeSelf) loginLandingPanel.SetActive(true);
Debug.Log($"[UserLogin] Button wiring -> Guest:{(guestButton?"OK":"MISSING")} Accept:{(guestAcceptButton?"OK":"MISSING")} Decline:{(guestDeclineButton?"OK":"MISSING")}");
}
// ===== Standard Login =====
public void LoginUser()
{
PlayClick();
if (!emailInput || !passwordInput) { Debug.LogError("[UserLogin] Input fields missing."); return; }
string email = emailInput.text; string password = passwordInput.text;
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(password)) { SetFeedback("Email and password are required!"); return; }
StartCoroutine(LoginCoroutine(email, password));
}
IEnumerator LoginCoroutine(string email, string password)
{
ShowLoadingIndicator(true);
var payload = new LoginRequest { email = email, password = password };
string json = JsonUtility.ToJson(payload);
using (var req = new UnityWebRequest(loginUrl, "POST"))
{
req.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
yield return req.SendWebRequest();
if (req.result == UnityWebRequest.Result.Success)
{
bool ok = false; AuthResponse resp = null;
try { resp = JsonUtility.FromJson<AuthResponse>(req.downloadHandler.text); ok = resp != null && !string.IsNullOrEmpty(resp.auth_token); }
catch (System.Exception ex) { Debug.LogError("[UserLogin] Parse error: " + ex.Message); }
if (ok)
{
authToken = resp.auth_token;
PlayerPrefs.SetString(AuthTokenKey, authToken); PlayerPrefs.Save();
if (gameInitialize) gameInitialize.SetUserAuthenticated(true);
SetFeedback("Login Successful! Redirecting...");
yield return new WaitForSeconds(1f);
TriggerMenuLoad();
}
else { SetFeedback("Unexpected response."); ShowLoadingIndicator(false); }
}
else
{
string body = req.downloadHandler.text;
SetFeedback(string.IsNullOrEmpty(body) ? "Login Failed." : $"Login Failed: {body}");
ShowLoadingIndicator(false);
}
}
}
// ===== Guest Panel Flow =====
public void ShowGuestPanel()
{
PlayClick();
Debug.Log("[UserLogin] ShowGuestPanel invoked");
if (!guestPanel)
{
Debug.LogWarning("[UserLogin] guestPanel missing (cannot display).");
return;
}
// Keep landing panel visible behind; just layer guest panel above
if (loginLandingPanel && !loginLandingPanel.activeSelf)
{
loginLandingPanel.SetActive(true);
Debug.Log("[UserLogin] Reactivating landing panel for background");
}
guestPanel.transform.SetAsLastSibling();
guestPanel.SetActive(true);
Debug.Log("[UserLogin] Guest panel active (landing stays visible)");
}
public void GuestAccept()
{
PlayClick();
Debug.Log("[UserLogin] GuestAccept clicked");
LoginAsGuest();
}
public void GuestDecline()
{
PlayClick();
Debug.Log("[UserLogin] GuestDecline clicked -> hiding only guest panel");
if (guestPanel) guestPanel.SetActive(false);
if (loginLandingPanel && !loginLandingPanel.activeSelf) loginLandingPanel.SetActive(true);
}
public void LoginAsGuest()
{
Debug.Log("[UserLogin] LoginAsGuest starting (clearing token & loading menu)");
PlayerPrefs.DeleteKey(AuthTokenKey);
if (gameInitialize) gameInitialize.SetUserAuthenticated(true);
SetFeedback("Entering as guest...");
TriggerMenuLoad();
}
// ===== Helpers =====
void TriggerMenuLoad()
{
ShowLoadingIndicator(true);
Debug.Log("[UserLogin] TriggerMenuLoad invoked. Using SceneLoader -> " + MenuSceneName);
var op = SceneLoader.LoadSceneAsyncOnce(MenuSceneName, LoadSceneMode.Single, true);
if (op == null)
{
// Fallback if another load blocked it (rare if user spam clicks)
Debug.LogWarning("[UserLogin] SceneLoader returned null (maybe already loading). Attempting fallback GameManager/SceneManager.");
if (GameManager.Instance)
{
GameManager.Instance.LoadSceneWithLoading(MenuSceneName);
}
else
{
SceneManager.LoadScene(MenuSceneName);
}
}
else
{
Debug.Log("[UserLogin] Async operation started (progress will advance automatically)." );
}
}
void ShowLoadingIndicator(bool show) { if (loadingIndicator) loadingIndicator.SetActive(show); }
void SetFeedback(string msg) { if (feedbackText) feedbackText.text = msg; }
void PlayClick() { if (clickSfx) { if (!uiAudio) { uiAudio = gameObject.AddComponent<AudioSource>(); uiAudio.playOnAwake = false; } uiAudio.PlayOneShot(clickSfx); } }
public void LogoutUser()
{ PlayerPrefs.DeleteKey(AuthTokenKey); if (gameInitialize) gameInitialize.SetUserAuthenticated(false); authToken = null; SetFeedback("Logged out."); }
[System.Serializable] class AuthResponse { public string auth_token; }
[System.Serializable] class LoginRequest { public string email; public string password; }
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 430bcebc27f4a6c4da036ebc42b18ee2

View File

@@ -0,0 +1,115 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using TMPro;
public class UserRegistration : MonoBehaviour
{
public TMP_InputField usernameInput;
public TMP_InputField nameInput;
public TMP_InputField countryInput;
public TMP_InputField emailInput;
public TMP_InputField passwordInput;
public TMP_InputField rePasswordInput;
public TextMeshProUGUI feedbackText;
public GameObject registrationPanel;
public GameObject loginPanel;
private string registerUrl = "http://127.0.0.1:8001/auth/users/";
public void RegisterUser()
{
// Debug message for button press
Debug.Log("Register button clicked! Starting registration process.");
// Collect user input
string username = usernameInput.text;
string name = nameInput.text;
string country = countryInput.text;
string email = emailInput.text;
string password = passwordInput.text;
string rePassword = rePasswordInput.text;
// Log the collected data
Debug.Log($"Collected registration data - Username: {username}, Name: {name}, Email: {email}");
// Validate input
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(name) ||
string.IsNullOrEmpty(country) || string.IsNullOrEmpty(email) ||
string.IsNullOrEmpty(password) || string.IsNullOrEmpty(rePassword))
{
feedbackText.text = "All fields are required!";
Debug.Log("Validation failed: Some fields are missing.");
return;
}
if (password != rePassword)
{
feedbackText.text = "Passwords do not match!";
Debug.Log("Validation failed: Passwords do not match.");
return;
}
// Validation passed
Debug.Log("Validation passed. Starting API call...");
StartCoroutine(Register(username, name, country, email, password));
}
private IEnumerator Register(string username, string name, string country, string email, string password)
{
Debug.Log("Register coroutine started");
string jsonData = JsonUtility.ToJson(new RegistrationRequest
{
username = username,
name = name,
country = country,
email = email,
password = password,
re_password = password
});
Debug.Log($"Sending registration request to: {registerUrl}");
Debug.Log($"Request data: {jsonData}");
using (UnityWebRequest request = new UnityWebRequest(registerUrl, "POST"))
{
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonData);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
Debug.Log("Sending web request...");
yield return request.SendWebRequest();
Debug.Log($"Request completed with result: {request.result}");
if (request.result == UnityWebRequest.Result.Success)
{
Debug.Log("API call successful. Response: " + request.downloadHandler.text);
feedbackText.text = "Registration Successful! Redirecting to login...";
registrationPanel.SetActive(false);
loginPanel.SetActive(true);
}
else
{
Debug.LogError($"API call failed: {request.error}");
Debug.LogError($"Response from server: {request.downloadHandler.text}");
feedbackText.text = $"Registration failed: {request.downloadHandler.text}";
}
}
}
}
[System.Serializable]
public class RegistrationRequest
{
public string username;
public string name;
public string country;
public string email;
public string password;
public string re_password;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 43b25d92836f2c649ba0e90035bf3b12

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9f469b33c1b32264fa1a89726a11b981
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f8fc13f43eb65864db04c81601dcf0cc
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerAnimationBehaviour : MonoBehaviour
{
[Header("Component References")]
public Animator playerAnimator;
//Animation String IDs
private int playerMovementAnimationID;
private int playerAttackAnimationID;
public void SetupBehaviour()
{
SetupAnimationIDs();
}
void SetupAnimationIDs()
{
playerMovementAnimationID = Animator.StringToHash("Movement");
playerAttackAnimationID = Animator.StringToHash("Attack");
}
public void UpdateMovementAnimation(float movementBlendValue)
{
playerAnimator.SetFloat(playerMovementAnimationID, movementBlendValue);
}
public void PlayAttackAnimation()
{
// If the unified input-to-animation system is present, avoid double-triggering
if (GetComponent<InputToAnimation>() != null)
return;
playerAnimator.SetTrigger(playerAttackAnimationID);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 733c62aa19a5f384ea007d48fd472e50
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,209 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.EventSystems;
public class PlayerController : MonoBehaviour
{
//Player ID
private int playerID;
[Header("Sub Behaviours")]
public PlayerMovementBehaviour playerMovementBehaviour;
public PlayerAnimationBehaviour playerAnimationBehaviour;
public PlayerVisualsBehaviour playerVisualsBehaviour;
[Header("Input Settings")]
public PlayerInput playerInput;
public float movementSmoothingSpeed = 1f;
private Vector3 rawInputMovement;
private Vector3 smoothInputMovement;
//Action Maps
private string actionMapPlayerControls = "Player Controls";
private string actionMapMenuControls = "Menu Controls";
//Current Control Scheme
private string currentControlScheme;
//This is called from the GameManager; when the game is being setup.
public void SetupPlayer(int newPlayerID)
{
playerID = newPlayerID;
currentControlScheme = playerInput.currentControlScheme;
playerMovementBehaviour.SetupBehaviour();
playerAnimationBehaviour.SetupBehaviour();
playerVisualsBehaviour.SetupBehaviour(playerID, playerInput);
}
//INPUT SYSTEM ACTION METHODS --------------
//This is called from PlayerInput; when a joystick or arrow keys has been pushed.
//It stores the input Vector as a Vector3 to then be used by the smoothing function.
public void OnMovement(InputAction.CallbackContext value)
{
Vector2 inputMovement = value.ReadValue<Vector2>();
rawInputMovement = new Vector3(inputMovement.x, 0, inputMovement.y);
}
//This is called from PlayerInput, when a button has been pushed, that corresponds with the 'Attack' action
public void OnAttack(InputAction.CallbackContext value)
{
// If the unified InputToAnimation exists on this GameObject, let it handle attacks exclusively
if (GetComponent<InputToAnimation>() != null)
return;
if(value.started)
{
playerAnimationBehaviour.PlayAttackAnimation();
}
}
//This is called from Player Input, when a button has been pushed, that correspons with the 'TogglePause' action
public void OnTogglePause(InputAction.CallbackContext value)
{
if(value.started)
{
//GameObject.Find("GameManager").GetComponent<GameManager>().TogglePauseState(this);
}
}
//INPUT SYSTEM AUTOMATIC CALLBACKS --------------
//This is automatically called from PlayerInput, when the input device has changed
//(IE: Keyboard -> Xbox Controller)
public void OnControlsChanged()
{
if(playerInput.currentControlScheme != currentControlScheme)
{
currentControlScheme = playerInput.currentControlScheme;
playerVisualsBehaviour.UpdatePlayerVisuals();
RemoveAllBindingOverrides();
}
}
//This is automatically called from PlayerInput, when the input device has been disconnected and can not be identified
//IE: Device unplugged or has run out of batteries
public void OnDeviceLost()
{
playerVisualsBehaviour.SetDisconnectedDeviceVisuals();
}
public void OnDeviceRegained()
{
StartCoroutine(WaitForDeviceToBeRegained());
}
IEnumerator WaitForDeviceToBeRegained()
{
yield return new WaitForSeconds(0.1f);
playerVisualsBehaviour.UpdatePlayerVisuals();
}
//Update Loop - Used for calculating frame-based data
void Update()
{
CalculateMovementInputSmoothing();
UpdatePlayerMovement();
UpdatePlayerAnimationMovement();
}
//Input's Axes values are raw
void CalculateMovementInputSmoothing()
{
smoothInputMovement = Vector3.Lerp(smoothInputMovement, rawInputMovement, Time.deltaTime * movementSmoothingSpeed);
}
void UpdatePlayerMovement()
{
playerMovementBehaviour.UpdateMovementData(smoothInputMovement);
}
void UpdatePlayerAnimationMovement()
{
playerAnimationBehaviour.UpdateMovementAnimation(smoothInputMovement.magnitude);
}
public void SetInputActiveState(bool gameIsPaused)
{
switch (gameIsPaused)
{
case true:
playerInput.DeactivateInput();
break;
case false:
playerInput.ActivateInput();
break;
}
}
void RemoveAllBindingOverrides()
{
InputActionRebindingExtensions.RemoveAllBindingOverrides(playerInput.currentActionMap);
}
//Switching Action Maps ----
public void EnableGameplayControls()
{
playerInput.SwitchCurrentActionMap(actionMapPlayerControls);
}
public void EnablePauseMenuControls()
{
playerInput.SwitchCurrentActionMap(actionMapMenuControls);
}
//Get Data ----
public int GetPlayerID()
{
return playerID;
}
public InputActionAsset GetActionAsset()
{
return playerInput.actions;
}
public PlayerInput GetPlayerInput()
{
return playerInput;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e20fb9a3b0e7eae4e9e630fc6e8b1a3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,75 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerMovementBehaviour : MonoBehaviour
{
[Header("Component References")]
public Rigidbody playerRigidbody;
[Header("Movement Settings")]
public float movementSpeed = 3f;
public float turnSpeed = 0.1f;
//Stored Values
private Camera mainCamera;
private Vector3 movementDirection;
public void SetupBehaviour()
{
SetGameplayCamera();
}
void SetGameplayCamera()
{
mainCamera = CameraManager.Instance.GetGameplayCamera();
}
public void UpdateMovementData(Vector3 newMovementDirection)
{
movementDirection = newMovementDirection;
}
void FixedUpdate()
{
MoveThePlayer();
TurnThePlayer();
}
void MoveThePlayer()
{
Vector3 movement = CameraDirection(movementDirection) * movementSpeed * Time.deltaTime;
playerRigidbody.MovePosition(transform.position + movement);
}
void TurnThePlayer()
{
if(movementDirection.sqrMagnitude > 0.01f)
{
Quaternion rotation = Quaternion.Slerp(playerRigidbody.rotation,
Quaternion.LookRotation (CameraDirection(movementDirection)),
turnSpeed);
playerRigidbody.MoveRotation(rotation);
}
}
Vector3 CameraDirection(Vector3 movementDirection)
{
var cameraForward = mainCamera.transform.forward;
var cameraRight = mainCamera.transform.right;
cameraForward.y = 0f;
cameraRight.y = 0f;
return cameraForward * movementDirection.z + cameraRight * movementDirection.x;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63b2534bfb74738418425a76f03357fe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class PlayerUIDisplayBehaviour : MonoBehaviour
{
public TextMeshProUGUI IDDisplayText;
public TextMeshProUGUI deviceNameDisplayText;
public Image deviceDisplayIcon;
public void UpdatePlayerIDDisplayText(int newPlayerID)
{
IDDisplayText.SetText((newPlayerID + 1).ToString());
}
public void UpdatePlayerDeviceNameDisplayText(string newDeviceName)
{
deviceNameDisplayText.SetText(newDeviceName);
}
public void UpdatePlayerIconDisplayColor(Color newColor)
{
deviceDisplayIcon.color = newColor;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90ce320e2fa6597478b575fc077d0c27
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerVisualsBehaviour : MonoBehaviour
{
//Player ID
private int playerID;
private PlayerInput playerInput;
[Header("Device Display Settings")]
public DeviceDisplayConfigurator deviceDisplaySettings;
[Header("Sub Behaviours")]
public PlayerUIDisplayBehaviour playerUIDisplayBehaviour;
[Header("Player Material")]
public SkinnedMeshRenderer playerSkinnedMeshRenderer;
private int clothingTintShaderID;
public void SetupBehaviour(int newPlayerID, PlayerInput newPlayerInput)
{
playerID = newPlayerID;
playerInput = newPlayerInput;
SetupShaderIDs();
UpdatePlayerVisuals();
}
void SetupShaderIDs()
{
clothingTintShaderID = Shader.PropertyToID("_Clothing_Tint");
}
public void UpdatePlayerVisuals()
{
UpdateUIDisplay();
UpdateCharacterShader();
}
void UpdateUIDisplay()
{
playerUIDisplayBehaviour.UpdatePlayerIDDisplayText(playerID);
string deviceName = deviceDisplaySettings.GetDeviceName(playerInput);
playerUIDisplayBehaviour.UpdatePlayerDeviceNameDisplayText(deviceName);
Color deviceColor = deviceDisplaySettings.GetDeviceColor(playerInput);
playerUIDisplayBehaviour.UpdatePlayerIconDisplayColor(deviceColor);
}
void UpdateCharacterShader()
{
Color deviceColor = deviceDisplaySettings.GetDeviceColor(playerInput);
playerSkinnedMeshRenderer.material.SetColor(clothingTintShaderID, deviceColor);
}
public void SetDisconnectedDeviceVisuals()
{
string disconnectedName = deviceDisplaySettings.GetDisconnectedName();
playerUIDisplayBehaviour.UpdatePlayerDeviceNameDisplayText(disconnectedName);
Color disconnectedColor = deviceDisplaySettings.GetDisconnectedColor();
playerUIDisplayBehaviour.UpdatePlayerIconDisplayColor(disconnectedColor);
playerSkinnedMeshRenderer.material.SetColor(clothingTintShaderID, disconnectedColor);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b9c7226a9b7db047a442efcb4f263ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 045edeee3a7092d418dde20ef8ae339b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
using UnityEngine;
using System.Collections;
[ExecuteAlways]
public class UIBillboardBehaviour : MonoBehaviour
{
private Transform gameplayCameraTransform;
void OnEnable()
{
gameplayCameraTransform = CameraManager.Instance.GetGameplayCameraTransform();
}
void OnDisable()
{
gameplayCameraTransform = null;
}
void LateUpdate()
{
if(gameplayCameraTransform)
{
transform.LookAt(transform.position + gameplayCameraTransform.rotation * Vector3.forward, gameplayCameraTransform.rotation * Vector3.up);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0d237ecbece995f4b99ac5ebab0f1baf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIContextPanelsBehaviour : MonoBehaviour
{
public GameObject[] contextPanels;
private int currentContextPanelID;
public void SetupContextPanels()
{
currentContextPanelID = 0;
for(int i = 0; i < contextPanels.Length; i++)
{
contextPanels[i].SetActive(false);
}
contextPanels[currentContextPanelID].SetActive(true);
}
public void UpdateContextPanels(int newContextPanelID)
{
contextPanels[currentContextPanelID].SetActive(false);
currentContextPanelID = newContextPanelID;
contextPanels[currentContextPanelID].SetActive(true);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a55701e00d9d61e4d857a2040c6c872d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,100 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class UIMenuBehaviour : MonoBehaviour
{
[Header("Sub-Behaviours")]
public UIContextPanelsBehaviour uiContextPanelsBehaviour;
public UIPanelRebindBehaviour uiPanelRebindBehaviour;
[Header("Core Object References")]
public GameObject uiMenuCameraObject;
public GameObject uiMenuCanvasObject;
[Header("Default Selected")]
public GameObject defaultSelectedGameObject;
[Header("Player Display")]
public DeviceDisplayConfigurator deviceDisplayconfigurator;
public Image deviceDisplayIcon;
public TextMeshProUGUI IDDisplayText;
public void SetupBehaviour()
{
UpdateUIMenuState(false);
}
public void UpdateUIMenuState(bool newState)
{
switch (newState)
{
case true:
ResetContextPanels();
UpdateEventSystemDefaultSelected();
UpdateEventSystemUIInputModule();
UpdateUIPlayerDisplay();
UpdateUIRebindActions();
break;
case false:
break;
}
UpdateCoreUIObjectsState(newState);
}
void ResetContextPanels()
{
uiContextPanelsBehaviour.SetupContextPanels();
}
void UpdateCoreUIObjectsState(bool newState)
{
uiMenuCameraObject.SetActive(newState);
uiMenuCanvasObject.SetActive(newState);
}
void UpdateUIPlayerDisplay()
{
/*PlayerController focusedPlayerController = GameObject.Find("GameManager").GetComponent<GameManager>().GetFocusedPlayerController();
//ID
int focusedPlayerID = focusedPlayerController.GetPlayerID();
IDDisplayText.SetText((focusedPlayerID + 1).ToString());
//Color
Color focusedPlayerDeviceColor = deviceDisplayconfigurator.GetDeviceColor(focusedPlayerController.GetPlayerInput());
deviceDisplayIcon.color = focusedPlayerDeviceColor;*/
}
void UpdateEventSystemDefaultSelected()
{
EventSystemManager.Instance.SetCurrentSelectedGameObject(defaultSelectedGameObject);
}
void UpdateEventSystemUIInputModule()
{
EventSystemManager.Instance.UpdateActionAssetToFocusedPlayer();
}
void UpdateUIRebindActions()
{
uiPanelRebindBehaviour.UpdateRebindActions();
}
public void SwitchUIContextPanels(int selectedPanelID)
{
uiContextPanelsBehaviour.UpdateContextPanels(selectedPanelID);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d4cf0d34234797b4f8e05fdf8db216f7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIMenuOptionBehaviour : MonoBehaviour
{
public int contextPanelID;
public void ButtonOptionSelected()
{
UIManager.Instance.MenuButtonOptionSelected(contextPanelID);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ab404cb73cfd59041ae6fa39d672efef
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
public class UIMenuPlayerDeviceDisplayBehaviour : MonoBehaviour
{
[Header("Device Display")]
public DeviceDisplayConfigurator deviceDisplayConfigurator;
[Header("Display References")]
public TextMeshProUGUI playerIDDisplay;
public TextMeshProUGUI playerDeviceDisplay;
public Image playerDeviceDisplayIcon;
public void SetPlayerDeviceDisplay(int playerID, string playerDevicePath)
{
playerIDDisplay.SetText((playerID + 1).ToString());
UpdatePlayerDeviceDisplay(playerDevicePath);
}
public void UpdatePlayerDeviceDisplay(string playerDevicePath)
{
/*
playerDeviceDisplay.SetText(deviceDisplayConfigurator.GetDeviceDisplayName(playerDevicePath));
playerDeviceDisplayIcon.color = deviceDisplayConfigurator.GetDeviceDisplayColor(playerDevicePath);
*/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb9fa96d7e2cd5d429bb276c06a46c72
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIPanelQuitBehaviour : MonoBehaviour
{
public void ButtonPressedQuitGame()
{
QuitGame();
}
void QuitGame()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dda3a4598827131489bee68ae015c43e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIPanelRebindBehaviour : MonoBehaviour
{
public UIRebindActionBehaviour[] uiRebindActionBehaviours;
public void UpdateRebindActions()
{
for(int i = 0; i < uiRebindActionBehaviours.Length; i++)
{
uiRebindActionBehaviours[i].UpdateBehaviour();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d0ee8987180cc640b92a50bdf409025
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,144 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using TMPro;
public class UIRebindActionBehaviour : MonoBehaviour
{
//Input System Stored Data
private InputActionAsset focusedInputActionAsset;
private PlayerInput focusedPlayerInput;
private InputAction focusedInputAction;
private InputActionRebindingExtensions.RebindingOperation rebindOperation;
[Header("Rebind Settings")]
public string actionName;
[Header("Device Display Settings")]
public DeviceDisplayConfigurator deviceDisplaySettings;
[Header("UI Display - Action Text")]
public TextMeshProUGUI actionNameDisplayText;
[Header("UI Display - Binding Text or Icon")]
public TextMeshProUGUI bindingNameDisplayText;
public Image bindingIconDisplayImage;
[Header("UI Display - Buttons")]
public GameObject rebindButtonObject;
public GameObject resetButtonObject;
[Header("UI Display - Listening Text")]
public GameObject listeningForInputObject;
public void UpdateBehaviour()
{
GetFocusedPlayerInput();
SetupFocusedInputAction();
UpdateActionDisplayUI();
UpdateBindingDisplayUI();
}
void GetFocusedPlayerInput()
{
/*PlayerController focusedPlayerController = GameObject.Find("GameManager").GetComponent<GameManager>().GetFocusedPlayerController();
focusedPlayerInput = focusedPlayerController.GetPlayerInput();*/
}
void SetupFocusedInputAction()
{
focusedInputAction = focusedPlayerInput.actions.FindAction(actionName);
}
public void ButtonPressedStartRebind()
{
StartRebindProcess();
}
void StartRebindProcess()
{
ToggleGameObjectState(rebindButtonObject, false);
ToggleGameObjectState(resetButtonObject, false);
ToggleGameObjectState(listeningForInputObject, true);
rebindOperation = focusedInputAction.PerformInteractiveRebinding()
.WithControlsExcluding("<Mouse>/position")
.WithControlsExcluding("<Mouse>/delta")
.WithControlsExcluding("<Gamepad>/Start")
.WithControlsExcluding("<Keyboard>/p")
.WithControlsExcluding("<Keyboard>/escape")
.OnMatchWaitForAnother(0.1f)
.OnComplete(operation => RebindCompleted());
rebindOperation.Start();
}
void RebindCompleted()
{
rebindOperation.Dispose();
rebindOperation = null;
ToggleGameObjectState(rebindButtonObject, true);
ToggleGameObjectState(resetButtonObject, true);
ToggleGameObjectState(listeningForInputObject, false);
UpdateActionDisplayUI();
UpdateBindingDisplayUI();
}
public void ButtonPressedResetBinding()
{
ResetBinding();
}
public void ResetBinding()
{
InputActionRebindingExtensions.RemoveAllBindingOverrides(focusedInputAction);
UpdateBindingDisplayUI();
}
void UpdateActionDisplayUI()
{
actionNameDisplayText.SetText(actionName);
}
void UpdateBindingDisplayUI()
{
int controlBindingIndex = focusedInputAction.GetBindingIndexForControl(focusedInputAction.controls[0]);
string currentBindingInput = InputControlPath.ToHumanReadableString(focusedInputAction.bindings[controlBindingIndex].effectivePath, InputControlPath.HumanReadableStringOptions.OmitDevice);
Sprite currentDisplayIcon = deviceDisplaySettings.GetDeviceBindingIcon(focusedPlayerInput, currentBindingInput);
if(currentDisplayIcon)
{
ToggleGameObjectState(bindingNameDisplayText.gameObject, false);
ToggleGameObjectState(bindingIconDisplayImage.gameObject, true);
bindingIconDisplayImage.sprite = currentDisplayIcon;
} else if(currentDisplayIcon == null)
{
ToggleGameObjectState(bindingNameDisplayText.gameObject, true);
ToggleGameObjectState(bindingIconDisplayImage.gameObject, false);
bindingNameDisplayText.SetText(currentBindingInput);
}
}
void ToggleGameObjectState(GameObject targetGameObject, bool newState)
{
targetGameObject.SetActive(newState);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d6005e3b6c6ebbd4a98d34a9488cf066
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6f7a99d17f33abe4ab6dc9c57bb6bcc2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
public class ButtonClickDetector : MonoBehaviour, IPointerClickHandler
{
public UnityEvent onClick;
public bool Clicked { get; set; }
public void OnPointerClick(PointerEventData eventData)
{
Clicked = true;
onClick.Invoke();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bee16f2af4e522b4c82a66e80d442828
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,40 @@
using UnityEngine;
using UnityEngine.UI;
public class ButtonClickSound : MonoBehaviour
{
public AudioClip clickSound;
private Button button;
private AudioSource audioSource;
private void Awake()
{
button = GetComponent<Button>();
audioSource = GetComponent<AudioSource>();
// Create an AudioSource component if one doesn't already exist
if (audioSource == null)
{
audioSource = gameObject.AddComponent<AudioSource>();
}
// Set the AudioClip to play when the button is clicked
audioSource.clip = clickSound;
// Make sure the AudioSource doesn't play automatically
audioSource.playOnAwake = false;
}
private void Start()
{
// Attach the button click event to the PlayClickSound() method
button.onClick.AddListener(PlayClickSound);
}
public void PlayClickSound()
{
// Play the click sound using the Manager
GameManager.Instance.PlaySound(clickSound);
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 8d4618d29e8ff4346b4cf90d10e23bb8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- clickSound: {fileID: 8300000, guid: 712edf32f2eaf0247abd5118428699d2, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3d82fc89bd2e16343bf0f53bd58b742b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,182 @@
using UnityEngine;
using Cinemachine;
using System.Collections;
/// <summary>
/// Automatically configures Cinemachine FreeLook camera targets on scene start.
/// Correctly separates Follow (character root) from LookAt (head bone)
/// to prevent orbit collapse and camera looking at the sky.
/// </summary>
public class AutoAddCinemachineCamera : MonoBehaviour
{
[TagField]
public string Tag = string.Empty;
void Start()
{
var vcam = GetComponent<CinemachineFreeLook>();
if (vcam != null && Tag.Length > 0)
{
bool isCashSystemMode = SelectionOptions.Instance != null && SelectionOptions.Instance.isCashSystemSelected;
if (isCashSystemMode)
{
// In Cash System mode, characters spawn after a delay
StartCoroutine(FindPlayerControlledTargetDelayed(vcam, 1.0f));
}
else
{
// Normal mode - find immediately
var targets = GameObject.FindGameObjectsWithTag(Tag);
if (targets.Length > 0)
{
SetCameraTargets(vcam, targets[0]);
}
}
}
}
private IEnumerator FindPlayerControlledTargetDelayed(CinemachineFreeLook vcam, float delay)
{
yield return new WaitForSeconds(delay);
// If CameraManager already set targets, don't override
if (CameraManager.Instance != null && CameraManager.Instance.GetCurrentTarget() != null)
{
Debug.Log("[AutoAddCinemachineCamera] CameraManager already handling camera — skipping.");
yield break;
}
var targets = GameObject.FindGameObjectsWithTag(Tag);
// Find the character with IsPlayerControlled = true
foreach (var target in targets)
{
TeamMember teamMember = target.GetComponent<TeamMember>();
if (teamMember != null && teamMember.IsPlayerControlled)
{
// Route through CameraManager if available
if (CameraManager.Instance != null)
{
CameraManager.Instance.SetTarget(teamMember);
Debug.Log($"[AutoAddCinemachineCamera] Routed to CameraManager for {target.name}");
}
else
{
SetCameraTargets(vcam, target);
Debug.Log($"[AutoAddCinemachineCamera] Set targets directly for {target.name}");
}
yield break;
}
}
// Fallback if no TeamMember found
if (targets.Length > 0)
{
Debug.LogWarning("[AutoAddCinemachineCamera] No player-controlled character found, using first target");
SetCameraTargets(vcam, targets[0]);
}
}
/// <summary>
/// Set Follow to character ROOT (orbit center) and LookAt to HEAD (aim point).
/// This is the correct configuration for CinemachineFreeLook.
/// </summary>
private void SetCameraTargets(CinemachineFreeLook vcam, GameObject character)
{
// Follow = character root (stable orbit center at ground level)
Transform followTarget = character.transform;
// LookAt = head bone (camera aims at upper body)
Transform lookAtTarget = FindHeadTransform(character) ?? character.transform;
vcam.Follow = followTarget;
vcam.LookAt = lookAtTarget;
// CRITICAL: Start at middle rig (0.5), not bottom (0)
// Without this, camera defaults to looking UP at the character from below
vcam.m_YAxis.Value = 0.5f;
// Force Cinemachine to recalculate from scratch (forget cached underground position)
vcam.PreviousStateIsValid = false;
// Clear input axis names — CinemachineCameraInput handles all input
vcam.m_XAxis.m_InputAxisName = "";
vcam.m_YAxis.m_InputAxisName = "";
// Set professional orbit heights (consistent with CameraManager)
vcam.m_Orbits = new CinemachineFreeLook.Orbit[] {
new CinemachineFreeLook.Orbit(4.5f, 5f), // TopRig
new CinemachineFreeLook.Orbit(2.5f, 6f), // MiddleRig
new CinemachineFreeLook.Orbit(0.5f, 5f), // BottomRig
};
// Configure Composer aim on each rig
for (int i = 0; i < 3; i++)
{
var rig = vcam.GetRig(i);
if (rig == null) continue;
var composer = rig.GetCinemachineComponent<CinemachineComposer>();
if (composer != null)
{
composer.m_ScreenX = 0.5f;
composer.m_ScreenY = 0.4f;
composer.m_DeadZoneWidth = 0.1f;
composer.m_DeadZoneHeight = 0.1f;
composer.m_SoftZoneWidth = 0.8f;
composer.m_SoftZoneHeight = 0.8f;
}
}
// Add CinemachineCollider to prevent camera clipping below terrain
EnsureCinemachineCollider(vcam);
Debug.Log($"[AutoAddCinemachineCamera] Follow={followTarget.name}, LookAt={lookAtTarget.name}, YAxis=0.5");
}
/// <summary>
/// Adds a CinemachineCollider extension so the camera stays above terrain
/// and doesn't clip through the environment after changing levels.
/// </summary>
private void EnsureCinemachineCollider(CinemachineFreeLook vcam)
{
var colliderExt = vcam.GetComponent<CinemachineCollider>();
if (colliderExt == null)
{
colliderExt = vcam.gameObject.AddComponent<CinemachineCollider>();
}
colliderExt.m_Strategy = CinemachineCollider.ResolutionStrategy.PullCameraForward;
colliderExt.m_CollideAgainst = LayerMask.GetMask("Default", "Ground");
colliderExt.m_IgnoreTag = "Player";
colliderExt.m_CameraRadius = 0.2f;
colliderExt.m_Damping = 0f;
colliderExt.m_MinimumDistanceFromTarget = 1f;
Debug.Log("[AutoAddCinemachineCamera] CinemachineCollider extension configured for terrain avoidance");
}
private Transform FindHeadTransform(GameObject character)
{
string[] headNames = { "Head", "head", "Bip001 Head", "mixamorig:Head", "Bip01 Head" };
foreach (string headName in headNames)
{
Transform head = FindChildRecursive(character.transform, headName);
if (head != null) return head;
}
return null;
}
private Transform FindChildRecursive(Transform parent, string name)
{
foreach (Transform child in parent)
{
if (child.name == name) return child;
Transform found = FindChildRecursive(child, name);
if (found != null) return found;
}
return null;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0638b26c337ab6d459ed0f3d377f69f5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Cinemachine;
/// <summary>
/// Legacy camera follow script. Auto-disables when CinemachineBrain is present
/// on the same camera to prevent LateUpdate transform conflicts.
/// Only active as fallback when NO Cinemachine pipeline exists.
/// </summary>
public class CameraFollow : MonoBehaviour
{
public Transform target;
public Vector3 offset;
public float smoothSpeed = 0.1f;
private bool cinemachineActive = false;
private void Start()
{
// Check if CinemachineBrain is controlling this camera
CinemachineBrain brain = GetComponent<CinemachineBrain>();
if (brain == null)
{
// Also check Camera.main in case CameraFollow is on a child
Camera mainCam = Camera.main;
if (mainCam != null)
{
brain = mainCam.GetComponent<CinemachineBrain>();
}
}
if (brain != null && brain.enabled)
{
cinemachineActive = true;
Debug.Log($"[CameraFollow] CinemachineBrain detected — disabling CameraFollow to prevent LateUpdate conflict.");
enabled = false;
return;
}
// Legacy fallback: compute offset from target
if (target != null)
{
offset = transform.position - target.position;
}
}
private void LateUpdate()
{
// Double-check: if Cinemachine got enabled after Start, bail out
if (cinemachineActive) return;
if (target == null) return;
SmoothFollow();
}
public void SmoothFollow()
{
if (target == null) return;
Vector3 targetPos = target.position + offset;
Vector3 smoothFollow = Vector3.Lerp(transform.position, targetPos, smoothSpeed);
transform.position = smoothFollow;
transform.LookAt(target);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5946991bb03d5b341872abb3e254971d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,188 @@
using UnityEngine;
using Cinemachine;
using System.Collections;
/// <summary>
/// Resets the Main Camera to a known-good position BEFORE CinemachineBrain takes over,
/// then snaps the camera to the correct position AFTER the character spawns.
///
/// This solves the camera-underground problem with a two-phase approach:
/// Phase 1 (Awake): Force camera above ground, set CinemachineBrain blend to Cut
/// Phase 2 (Coroutine): Wait for player character, then force camera to correct orbit position
///
/// Attach this to the MainCamera GameObject.
/// </summary>
[DefaultExecutionOrder(-100)] // Run before everything else
public class CameraInitializer : MonoBehaviour
{
[Header("Safe Starting Position")]
[Tooltip("Camera starts here before Cinemachine takes control")]
public Vector3 resetPosition = new Vector3(0f, 10f, -10f);
public Vector3 resetRotation = new Vector3(20f, 0f, 0f);
[Header("Spawn Wait")]
[Tooltip("Maximum time to wait for the player character to spawn")]
[SerializeField] private float maxSpawnWaitTime = 5f;
[Tooltip("How often to check for the player character")]
[SerializeField] private float spawnCheckInterval = 0.1f;
[Header("Debug")]
[SerializeField] private bool debugLogging = true;
private void Awake()
{
// ===== PHASE 1: Force camera to a known-good position =====
transform.position = resetPosition;
transform.rotation = Quaternion.Euler(resetRotation);
if (debugLogging)
Debug.Log($"[CameraInitializer] Phase 1: Camera reset to position={resetPosition}");
// Set CinemachineBrain to Cut (instant snap, no blending from underground)
CinemachineBrain brain = GetComponent<CinemachineBrain>();
if (brain != null)
{
brain.m_DefaultBlend = new CinemachineBlendDefinition(
CinemachineBlendDefinition.Style.Cut, 0f
);
if (debugLogging)
Debug.Log("[CameraInitializer] CinemachineBrain set to CUT blend");
}
// Invalidate all FreeLook camera states
CinemachineFreeLook[] freeLooks = FindObjectsOfType<CinemachineFreeLook>();
foreach (var fl in freeLooks)
{
fl.m_YAxis.Value = 0.5f;
fl.PreviousStateIsValid = false;
// Set orbit heights
fl.m_Orbits = new CinemachineFreeLook.Orbit[] {
new CinemachineFreeLook.Orbit(4.5f, 5f), // TopRig
new CinemachineFreeLook.Orbit(2.5f, 6f), // MiddleRig
new CinemachineFreeLook.Orbit(0.5f, 5f), // BottomRig
};
// CRITICAL: Clear default input axis names so Cinemachine doesn't fight
// with CinemachineCameraInput. Without this, Cinemachine reads Input.GetAxis("Mouse X")
// which returns 0 and snaps Y axis to bottom rig.
fl.m_XAxis.m_InputAxisName = "";
fl.m_YAxis.m_InputAxisName = "";
if (debugLogging)
Debug.Log($"[CameraInitializer] FreeLook '{fl.name}': YAxis=0.5, axes cleared, state invalidated");
}
}
private IEnumerator Start()
{
// ===== PHASE 2: Wait for player character, then snap camera =====
float elapsed = 0f;
Transform playerTransform = null;
while (elapsed < maxSpawnWaitTime)
{
// Try to find the player character
playerTransform = FindPlayerCharacter();
if (playerTransform != null)
{
if (debugLogging)
Debug.Log($"[CameraInitializer] Phase 2: Found player '{playerTransform.name}' at {playerTransform.position} after {elapsed:F1}s");
break;
}
yield return new WaitForSeconds(spawnCheckInterval);
elapsed += spawnCheckInterval;
}
if (playerTransform == null)
{
Debug.LogWarning("[CameraInitializer] Phase 2: No player found within timeout — camera may be incorrect");
yield break;
}
// Force camera to the correct position relative to the character
SnapCameraToCharacter(playerTransform);
// Wait a frame for Cinemachine to process the snap
yield return null;
yield return null;
// Restore smooth blending for character switching
CinemachineBrain brain = GetComponent<CinemachineBrain>();
if (brain != null)
{
brain.m_DefaultBlend = new CinemachineBlendDefinition(
CinemachineBlendDefinition.Style.EaseInOut, 1f
);
if (debugLogging)
Debug.Log("[CameraInitializer] Phase 2: Blend restored to EaseInOut");
}
}
/// <summary>
/// Force-snap the camera to the correct orbit position relative to the character.
/// </summary>
private void SnapCameraToCharacter(Transform character)
{
CinemachineFreeLook[] freeLooks = FindObjectsOfType<CinemachineFreeLook>();
foreach (var fl in freeLooks)
{
if (!fl.isActiveAndEnabled) continue;
// Ensure target is set
if (fl.Follow == null)
{
fl.Follow = character;
fl.LookAt = character;
}
// Reset to middle rig
fl.m_YAxis.Value = 0.5f;
fl.PreviousStateIsValid = false;
// Force camera transform to the correct position
// MiddleRig: Height 2.5, Radius 6 — camera should be behind and above
Vector3 targetPos = character.position;
Vector3 cameraPos = targetPos + new Vector3(0f, 2.5f, -6f);
// Set the Main Camera transform directly
transform.position = cameraPos;
transform.LookAt(targetPos + Vector3.up * 1.5f);
if (debugLogging)
{
Debug.Log($"[CameraInitializer] Camera snapped to {cameraPos} (character at {targetPos})");
Debug.Log($"[CameraInitializer] FreeLook Follow={fl.Follow?.name}, LookAt={fl.LookAt?.name}");
}
}
}
/// <summary>
/// Find the player-controlled character in the scene.
/// </summary>
private Transform FindPlayerCharacter()
{
// Try CameraManager first
if (CameraManager.Instance != null)
{
var target = CameraManager.Instance.GetCurrentTarget();
if (target != null) return target.transform;
}
// Try finding by tag
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null) return player.transform;
// Try TeamMember with IsPlayerControlled
TeamMember[] teamMembers = FindObjectsOfType<TeamMember>();
foreach (var tm in teamMembers)
{
if (tm.IsPlayerControlled) return tm.transform;
}
return null;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 16d9747b21ad6547384cf9a5e0f0abab

View File

@@ -0,0 +1,50 @@
using UnityEngine;
using Cinemachine;
using System.Collections;
public class CameraShake : MonoBehaviour
{
private CinemachineVirtualCamera virtualCamera;
private CinemachineBasicMultiChannelPerlin perlin;
private Coroutine shakeCoroutine;
[Header("Shake Settings")]
public float defaultShakeAmplitude = 2.0f;
public float defaultShakeFrequency = 2.0f;
public float defaultShakeDuration = 0.3f;
void Awake()
{
virtualCamera = GetComponent<CinemachineVirtualCamera>();
if (virtualCamera != null)
perlin = virtualCamera.GetCinemachineComponent<CinemachineBasicMultiChannelPerlin>();
}
public void ShakeCamera(float amplitude, float frequency, float duration)
{
if (shakeCoroutine != null)
StopCoroutine(shakeCoroutine);
shakeCoroutine = StartCoroutine(DoShake(amplitude, frequency, duration));
}
public void ShakeCamera() // Overload for default values
{
ShakeCamera(defaultShakeAmplitude, defaultShakeFrequency, defaultShakeDuration);
}
private IEnumerator DoShake(float amplitude, float frequency, float duration)
{
if (perlin == null)
yield break;
perlin.m_AmplitudeGain = amplitude;
perlin.m_FrequencyGain = frequency;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
yield return null;
}
perlin.m_AmplitudeGain = 0f;
perlin.m_FrequencyGain = 0f;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e80f5ecd3b46f8c448c32ad78d00ce04

View File

@@ -0,0 +1,203 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using Cinemachine;
/// <summary>
/// Custom Cinemachine input handler that replaces CinemachineInputProvider.
/// Uses CinemachineCore.GetInputAxis delegate for total control over camera orbit.
///
/// Supports:
/// - Gamepad right stick (X = horizontal orbit, Y = vertical orbit)
/// - Mobile touch drag (right half of screen, ignoring UI)
/// - Mouse delta (desktop/editor)
///
/// Attach this to the CinemachineFreeLook camera GameObject.
/// Remove or disable CinemachineInputProvider if present.
/// </summary>
public class CinemachineCameraInput : MonoBehaviour
{
[Header("Sensitivity")]
[SerializeField] private float gamepadSensitivityX = 200f;
[SerializeField] private float gamepadSensitivityY = 3f;
[SerializeField] private float touchSensitivityX = 0.15f;
[SerializeField] private float touchSensitivityY = 0.003f;
[SerializeField] private float mouseSensitivityX = 3f;
[SerializeField] private float mouseSensitivityY = 0.03f;
[Header("Touch Settings")]
[Tooltip("Only process touches on the right portion of the screen (0.5 = right half)")]
[Range(0f, 1f)]
[SerializeField] private float touchActiveScreenRatio = 0.4f;
[Tooltip("Ignore touches over UI elements (buttons, joysticks, etc.)")]
[SerializeField] private bool ignoreUITouches = true;
[Header("Filtering")]
[SerializeField] private float stickDeadzone = 0.15f;
[SerializeField] private float smoothing = 0.1f;
// Touch tracking
private int activeTouchId = -1;
private Vector2 lastTouchPos;
// Smoothed output
private float smoothedX;
private float smoothedY;
private float rawX;
private float rawY;
// Axis names used by CinemachineFreeLook
private const string XAxisName = "Mouse X";
private const string YAxisName = "Mouse Y";
private CinemachineFreeLook freeLookCam;
private void Awake()
{
freeLookCam = GetComponent<CinemachineFreeLook>();
}
private void OnEnable()
{
// Override Cinemachine's input gathering with our custom handler
CinemachineCore.GetInputAxis = HandleCinemachineInput;
// Disable CinemachineInputProvider if present (we replace it)
var provider = GetComponent<CinemachineInputProvider>();
if (provider != null)
{
provider.enabled = false;
Debug.Log("[CinemachineCameraInput] Disabled CinemachineInputProvider — using custom input handler");
}
// Clear the default input axis names on the FreeLook camera
// This prevents Cinemachine from also reading legacy Input Manager axes
if (freeLookCam != null)
{
freeLookCam.m_XAxis.m_InputAxisName = "";
freeLookCam.m_YAxis.m_InputAxisName = "";
}
Debug.Log("[CinemachineCameraInput] Custom camera input enabled (gamepad + touch + mouse)");
}
private void OnDisable()
{
// Restore default input handling when disabled
CinemachineCore.GetInputAxis = UnityEngine.Input.GetAxis;
}
private void Update()
{
// Gather raw input from all sources
rawX = 0f;
rawY = 0f;
ReadGamepadInput();
ReadTouchInput();
ReadMouseInput();
// Smooth the input
if (smoothing > 0.001f)
{
smoothedX = Mathf.Lerp(smoothedX, rawX, Time.unscaledDeltaTime / smoothing);
smoothedY = Mathf.Lerp(smoothedY, rawY, Time.unscaledDeltaTime / smoothing);
}
else
{
smoothedX = rawX;
smoothedY = rawY;
}
}
/// <summary>
/// Delegate callback — Cinemachine calls this instead of Input.GetAxis
/// </summary>
private float HandleCinemachineInput(string axisName)
{
if (axisName == XAxisName) return smoothedX;
if (axisName == YAxisName) return smoothedY;
return 0f;
}
// ======================== GAMEPAD ========================
private void ReadGamepadInput()
{
if (Gamepad.current == null) return;
Vector2 rightStick = Gamepad.current.rightStick.ReadValue();
// Apply deadzone
if (rightStick.magnitude < stickDeadzone)
return;
rawX += rightStick.x * gamepadSensitivityX * Time.unscaledDeltaTime;
rawY += rightStick.y * gamepadSensitivityY * Time.unscaledDeltaTime;
}
// ======================== TOUCH ========================
private void ReadTouchInput()
{
if (Touchscreen.current == null) return;
var touches = Touchscreen.current.touches;
bool foundActiveTouch = false;
for (int i = 0; i < touches.Count; i++)
{
var touch = touches[i];
if (!touch.press.isPressed) continue;
int id = touch.touchId.ReadValue();
Vector2 pos = touch.position.ReadValue();
// If no active touch, try to start tracking this one
if (activeTouchId == -1)
{
// Only track touches on the right portion of the screen
if (pos.x < Screen.width * touchActiveScreenRatio)
continue;
// Ignore touches over UI elements
if (ignoreUITouches && EventSystem.current != null &&
EventSystem.current.IsPointerOverGameObject(id))
continue;
activeTouchId = id;
lastTouchPos = pos;
continue;
}
if (touch.touchId.ReadValue() != activeTouchId)
continue;
foundActiveTouch = true;
Vector2 delta = pos - lastTouchPos;
lastTouchPos = pos;
rawX += delta.x * touchSensitivityX;
rawY += delta.y * touchSensitivityY;
}
// Reset tracking if active touch released
if (activeTouchId != -1 && !foundActiveTouch)
{
activeTouchId = -1;
}
}
// ======================== MOUSE ========================
private void ReadMouseInput()
{
if (Mouse.current == null) return;
// Only use mouse if right button is held (prevents accidental camera rotation)
if (!Mouse.current.rightButton.isPressed) return;
Vector2 mouseDelta = Mouse.current.delta.ReadValue();
rawX += mouseDelta.x * mouseSensitivityX * Time.unscaledDeltaTime;
rawY += mouseDelta.y * mouseSensitivityY * Time.unscaledDeltaTime;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 85b5c64e9ac2e5829b1e858392b5955c

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4a1d7453d1c6c1304b8c80446b3214fe
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,415 @@
using UnityEngine;
/// <summary>
/// CashBag — Collectible cash bag for Cash System mode.
/// Neutral gold color when spawned, takes team color on pickup.
/// Tracks state: Available → Carried → Dropped → Available/Destroyed.
/// Registers with EntityRegistry for zero-allocation queries.
/// Fires GameEvents instead of using FindObjectOfType.
/// The vault (TeamVault) is the "CashBox" where bags are deposited.
/// </summary>
public class CashBag : MonoBehaviour
{
public enum BagState { Available, Carried, Dropped }
[Header("Cash Settings")]
[SerializeField] private float cashValue = 100f;
[SerializeField] private float hotDropBonusPercent = 10f; // +10% if picked during hot drop window
[Header("Visual Feedback")]
[SerializeField] private float rotationSpeed = 30f;
[SerializeField] private float bobSpeed = 1f;
[SerializeField] private float bobHeight = 0.2f;
[SerializeField] private float floatOffset = 0.3f;
[Header("Scale")]
[SerializeField] private Vector3 targetScale = new Vector3(0.35f, 0.35f, 0.35f);
[Header("Drop Settings")]
[SerializeField] private float hotDropWindow = 3f; // Seconds after drop where pickup gives bonus
[SerializeField] private float dropDespawnTime = 30f; // Despawn dropped bag after this time
[SerializeField] private float pickupGracePeriod = 1.5f; // Seconds after spawn/enable before pickup allowed
[Header("Visual References")]
[SerializeField] private MeshRenderer meshRenderer;
[SerializeField] private ParticleSystem idleParticles;
[SerializeField] private ParticleSystem pickupBurstEffect;
[SerializeField] private ParticleSystem dropImpactEffect;
[SerializeField] private GameObject glowObject; // Point light or glow sprite
[SerializeField] private GameObject worldIndicator; // "!" icon for dropped bags
[Header("Colors")]
[SerializeField] private Color neutralColor = new Color(1f, 0.84f, 0f); // Gold
[SerializeField] private Color playerTeamColor = new Color(0.13f, 0.59f, 0.95f);// Blue
[SerializeField] private Color enemyTeamColor = new Color(0.96f, 0.26f, 0.21f); // Red
[SerializeField] private Color droppedPulseColor = new Color(1f, 0.84f, 0f); // Pulsing gold
[Header("Glow Settings")]
[SerializeField] private float glowLightRange = 8f;
[SerializeField] private float glowLightIntensity = 2f;
[SerializeField] private Color glowLightColor = new Color(1f, 0.84f, 0f); // Gold
[SerializeField] private float glowPulseSpeed = 2f;
[SerializeField] private float glowPulseMin = 0.6f;
[SerializeField] private float glowPulseMax = 1f;
[Header("Audio")]
[Tooltip("Sound effect played when this bag is picked up")]
[SerializeField] private AudioClip pickupSound;
[SerializeField] private float pickupSoundVolume = 3f;
// ─── Runtime State ───────────────────────────────────────
private BagState _state = BagState.Available;
private CashCarrier _carrier;
private Vector3 _spawnPosition;
private float _dropTime;
private float _despawnTimer;
private Collider _collider;
private Light _glowLight;
private float _enabledTime; // Time.time when this bag was last enabled
// Bob animation (randomized phase to desync multiple bags)
private float _bobPhase;
// ─── Properties ──────────────────────────────────────────
public BagState State => _state;
public float CashValue => cashValue;
public float Value => cashValue; // Alias for AI compatibility
public bool IsAvailable => _state == BagState.Available;
public bool IsDropped => _state == BagState.Dropped;
public bool IsCarried => _state == BagState.Carried;
public bool IsHotDrop => _state == BagState.Dropped && (Time.time - _dropTime) < hotDropWindow;
public float HotDropBonusPercent => hotDropBonusPercent;
public CashCarrier CurrentCarrier => _carrier;
public Vector3 SpawnPosition => _spawnPosition;
// ─── Lifecycle ───────────────────────────────────────────
private void Awake()
{
if (meshRenderer == null) meshRenderer = GetComponent<MeshRenderer>();
_collider = GetComponent<Collider>();
if (_collider != null) _collider.isTrigger = true;
_bobPhase = Random.Range(0f, Mathf.PI * 2f); // Desync bobs between bags
// Create glow point light if none assigned
CreateGlow();
}
private void OnEnable()
{
EntityRegistry<CashBag>.Register(this);
_spawnPosition = transform.position;
_enabledTime = Time.time;
SetAvailable();
}
private void Reset()
{
// Ensure reasonable default scale when added in editor
transform.localScale = targetScale;
}
private void OnDisable()
{
EntityRegistry<CashBag>.Unregister(this);
}
private void Update()
{
switch (_state)
{
case BagState.Available:
AnimateIdle();
break;
case BagState.Carried:
// Position managed by CashCarrier
break;
case BagState.Dropped:
AnimateDropped();
_despawnTimer += Time.deltaTime;
if (_despawnTimer >= dropDespawnTime)
Despawn();
break;
}
}
// ─── State Transitions ───────────────────────────────────
/// <summary>Reset to neutral available state.</summary>
public void SetAvailable()
{
_state = BagState.Available;
_carrier = null;
_despawnTimer = 0f;
if (_collider != null) _collider.enabled = true;
if (meshRenderer != null) meshRenderer.enabled = true;
gameObject.SetActive(true);
// Ensure consistent visual scale
transform.localScale = targetScale;
SetColor(neutralColor);
SetParticles(idleParticles, true);
if (worldIndicator != null) worldIndicator.SetActive(false);
// Enable glow so players can see the bag from far away
SetGlowActive(true);
// Show minimap marker when available
var marker = GetComponent<MinimapObjectMarker>();
if (marker != null) marker.SetVisible(true);
}
/// <summary>Picked up by a carrier.</summary>
public void PickUp(CashCarrier carrier)
{
if (_state == BagState.Carried || carrier == null) return;
_state = BagState.Carried;
_carrier = carrier;
// Disable world presence
if (_collider != null) _collider.enabled = false;
if (meshRenderer != null) meshRenderer.enabled = false;
if (worldIndicator != null) worldIndicator.SetActive(false);
SetParticles(idleParticles, false);
// Burst pickup effect
if (pickupBurstEffect != null)
{
pickupBurstEffect.transform.position = transform.position;
pickupBurstEffect.Play();
}
// Play pickup sound effect (play at camera position so volume is consistent)
if (pickupSound != null)
{
Vector3 listenerPos = Camera.main != null ? Camera.main.transform.position : transform.position;
AudioSource.PlayClipAtPoint(pickupSound, listenerPos, pickupSoundVolume);
}
// Set team color
Color teamColor = carrier.TeamTag == "Player" ? playerTeamColor : enemyTeamColor;
SetColor(teamColor);
// Hide glow while carried
SetGlowActive(false);
// Fire events (replaces FindObjectOfType calls)
GameEvents.FireBagPickedUp(this, carrier);
GameEvents.FireBagCollected(this);
// Hide minimap marker while carried
var marker = GetComponent<MinimapObjectMarker>();
if (marker != null) marker.SetVisible(false);
}
/// <summary>Dropped at a world position (carrier died or lost it).</summary>
public void Drop(Vector3 dropPosition)
{
_state = BagState.Dropped;
_carrier = null;
_dropTime = Time.time;
_despawnTimer = 0f;
// Re-enable world presence
transform.position = dropPosition + Vector3.up * 0.5f;
if (_collider != null) _collider.enabled = true;
if (meshRenderer != null) meshRenderer.enabled = true;
SetColor(droppedPulseColor);
SetParticles(idleParticles, true);
// Re-enable glow when dropped
SetGlowActive(true);
if (worldIndicator != null) worldIndicator.SetActive(true);
// Impact effect
if (dropImpactEffect != null)
{
dropImpactEffect.transform.position = dropPosition;
dropImpactEffect.Play();
}
GameEvents.FireBagDropped(this, dropPosition);
// Show minimap marker when dropped in world
var marker = GetComponent<MinimapObjectMarker>();
if (marker != null) marker.SetVisible(true);
}
/// <summary>Remove from play (despawn or deposited).</summary>
public void Despawn()
{
_state = BagState.Available;
_carrier = null;
SetParticles(idleParticles, false);
SetGlowActive(false);
// Hide from world
var marker = GetComponent<MinimapObjectMarker>();
if (marker != null) marker.SetVisible(false);
gameObject.SetActive(false);
GameEvents.FireBagDespawned(this);
// Return to pool if pooled, otherwise destroy
var pooled = GetComponent<PooledObject>();
if (pooled != null)
pooled.ReturnToPool();
else
Destroy(gameObject, 0.1f);
}
// ─── Trigger Detection ───────────────────────────────────
private void OnTriggerEnter(Collider other)
{
if (_state == BagState.Carried) return;
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected) return;
// Grace period: prevent insta-pickup when bag spawns on top of player
if (Time.time - _enabledTime < pickupGracePeriod) return;
// Use GetComponentInParent because collider may be on child (hand/foot)
CashCarrier carrier = other.GetComponentInParent<CashCarrier>();
if (carrier != null && carrier.CanPickUp())
{
PickUp(carrier);
carrier.OnBagPickedUp(this);
}
}
// ─── Glow ─────────────────────────────────────────────────
private void CreateGlow()
{
// Use existing glowObject light if assigned in inspector
if (glowObject != null)
{
_glowLight = glowObject.GetComponent<Light>();
if (_glowLight != null) return;
}
// Create a point light child for long-range visibility
GameObject lightObj = new GameObject("BagGlow");
lightObj.transform.SetParent(transform, false);
lightObj.transform.localPosition = Vector3.up * 0.3f;
_glowLight = lightObj.AddComponent<Light>();
_glowLight.type = LightType.Point;
_glowLight.color = glowLightColor;
_glowLight.range = glowLightRange;
_glowLight.intensity = glowLightIntensity;
_glowLight.shadows = LightShadows.None;
_glowLight.renderMode = LightRenderMode.Auto;
// Store as glowObject for other methods to reference
if (glowObject == null)
glowObject = lightObj;
}
private void SetGlowActive(bool active)
{
if (_glowLight != null) _glowLight.enabled = active;
if (glowObject != null) glowObject.SetActive(active);
}
private void PulseGlow()
{
if (_glowLight == null) return;
float pulse = Mathf.Lerp(glowPulseMin, glowPulseMax, (Mathf.Sin(Time.time * glowPulseSpeed + _bobPhase) + 1f) * 0.5f);
_glowLight.intensity = glowLightIntensity * pulse;
}
// ─── Animations ──────────────────────────────────────────
private void AnimateIdle()
{
// Rotate
transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime, Space.World);
// Bob
float y = _spawnPosition.y + floatOffset + Mathf.Sin((Time.time + _bobPhase) * bobSpeed) * bobHeight;
transform.position = new Vector3(transform.position.x, y, transform.position.z);
// Pulse glow
PulseGlow();
}
private void AnimateDropped()
{
// Faster pulse for hot drops
float pulseSpeed = IsHotDrop ? bobSpeed * 2f : bobSpeed;
float y = transform.position.y;
float targetY = _spawnPosition.y + floatOffset + Mathf.Sin((Time.time + _bobPhase) * pulseSpeed) * bobHeight * 0.5f;
// Smooth lerp for drop→bob transition
transform.position = new Vector3(transform.position.x, Mathf.Lerp(y, targetY, Time.deltaTime * 3f), transform.position.z);
// Pulse glow for hot drops
if (IsHotDrop && glowObject != null)
{
float pulse = Mathf.PingPong(Time.time * 3f, 1f);
glowObject.transform.localScale = Vector3.one * (1f + pulse * 0.3f);
}
}
// ─── Visual Helpers ──────────────────────────────────────
private void SetColor(Color color)
{
if (meshRenderer != null)
{
// MaterialPropertyBlock = zero allocation, no material instance created
var mpb = new MaterialPropertyBlock();
meshRenderer.GetPropertyBlock(mpb);
mpb.SetColor("_BaseColor", color);
mpb.SetColor("_EmissionColor", color * 1.2f); // Bright emission for visibility
meshRenderer.SetPropertyBlock(mpb);
// Enable emission keyword if possible
if (meshRenderer.sharedMaterial != null)
meshRenderer.sharedMaterial.EnableKeyword("_EMISSION");
}
if (glowObject != null)
{
Light light = glowObject.GetComponent<Light>();
if (light != null) light.color = color;
}
}
private void SetParticles(ParticleSystem ps, bool play)
{
if (ps == null) return;
if (play && !ps.isPlaying) ps.Play();
else if (!play && ps.isPlaying) ps.Stop(true, ParticleSystemStopBehavior.StopEmitting);
}
// ─── API ─────────────────────────────────────────────────
/// <summary>Set the cash value (used by spawner).</summary>
public void SetCashValue(float value) => cashValue = value;
/// <summary>Get effective value including hot drop bonus.</summary>
public float GetEffectiveValue()
{
if (IsHotDrop)
return cashValue * (1f + hotDropBonusPercent / 100f);
return cashValue;
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, 1.5f);
UnityEditor.Handles.Label(transform.position + Vector3.up * 1.5f,
$"CashBag ${cashValue}\n{_state}");
}
#endif
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7ba5b44417d2bdcb898326c6c046307c

View File

@@ -0,0 +1,685 @@
using UnityEngine;
using UnityEngine.AI;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// Spawn zone classification for cash bags.
/// Contested zones are between team vaults (center of map) — forces team encounters.
/// </summary>
public enum SpawnZone
{
Safe, // Near a vault — low risk, low reward
Neutral, // Between center and vaults — medium risk
Contested // Center of map between vaults — high risk, high reward
}
/// <summary>
/// CashBagSpawner — Wave-based spawning of cash bags with object pooling.
/// Uses zone-weighted spawning to force team encounters in contested areas.
/// </summary>
public class CashBagSpawner : MonoBehaviour
{
[Header("Cash Bag Settings")]
[SerializeField] private GameObject cashBagPrefab;
[SerializeField] private float cashPerBag = 100f;
[Header("Spawn Limits")]
[SerializeField] private int maxBagCount = 15;
[SerializeField] private float initialSpawnPercent = 0.60f; // 60% on start
[Header("Wave Settings")]
[SerializeField] private float waveInterval = 30f;
[SerializeField] private int bagsPerWave = 3;
[Header("Spawn Area")]
[SerializeField] private Transform[] spawnPoints;
[SerializeField] private bool useRandomSpawning = false;
[SerializeField] private Vector3 spawnAreaCenter = Vector3.zero;
[SerializeField] private Vector3 spawnAreaSize = new Vector3(20f, 0f, 20f);
[SerializeField] private float minSpawnDistance = 5f; // Min distance between bags
[SerializeField] private float playerAvoidDistance = 5f; // Distance from players
[SerializeField] private float vaultAvoidDistance = 6f; // Distance from vaults
[Header("Zone-Weighted Spawning")]
[Tooltip("Chance to spawn in contested center zone (forces team encounters)")]
[SerializeField, Range(0f, 1f)] private float contestedZoneWeight = 0.60f;
[Tooltip("Chance to spawn in neutral middle zone")]
[SerializeField, Range(0f, 1f)] private float neutralZoneWeight = 0.30f;
[Tooltip("Chance to spawn in safe zone near vaults")]
[SerializeField, Range(0f, 1f)] private float safeZoneWeight = 0.10f;
[Tooltip("Value multiplier for contested zone bags")]
[SerializeField] private float contestedValueMultiplier = 2.0f;
[Tooltip("Value multiplier for neutral zone bags")]
[SerializeField] private float neutralValueMultiplier = 1.0f;
[Tooltip("Value multiplier for safe zone bags")]
[SerializeField] private float safeValueMultiplier = 0.5f;
[Header("Debug")]
[SerializeField] private bool showSpawnGizmos = true;
// ─── Runtime ─────────────────────────────────────────────
private ObjectPool _pool;
private readonly List<CashBag> _activeBags = new List<CashBag>();
private readonly HashSet<int> _occupiedSpawnPoints = new HashSet<int>(); // tracks which spawn points have an active bag
private readonly Dictionary<int, float> _spawnPointCooldowns = new Dictionary<int, float>(); // prevents same-point reuse
private const float SPAWN_POINT_COOLDOWN = 8f; // seconds before a freed point can be reused
private int _totalSpawned;
private bool _isSpawning;
private Coroutine _waveCoroutine;
// ─── Zone Classification ─────────────────────────────────
private readonly List<int> _contestedPoints = new List<int>();
private readonly List<int> _neutralPoints = new List<int>();
private readonly List<int> _safePoints = new List<int>();
private bool _zonesClassified;
private Vector3 _mapCenter; // midpoint between vaults — the contested zone center
public int ActiveBagCount => _activeBags.Count;
public int TotalSpawned => _totalSpawned;
// ─── Lifecycle ───────────────────────────────────────────
private void Start()
{
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
{
enabled = false;
return;
}
// Find spawn points by tag if none assigned
if ((spawnPoints == null || spawnPoints.Length == 0) && !useRandomSpawning)
{
GameObject[] spawnPointObjects = GameObject.FindGameObjectsWithTag("CashBagSpawn");
if (spawnPointObjects.Length > 0)
{
spawnPoints = new Transform[spawnPointObjects.Length];
for (int i = 0; i < spawnPointObjects.Length; i++)
spawnPoints[i] = spawnPointObjects[i].transform;
}
else
{
useRandomSpawning = true;
}
}
// Initialize object pool (pre-warm up to maxBagCount and parent under this transform)
if (cashBagPrefab != null)
{
_pool = new ObjectPool(cashBagPrefab, initialSize: maxBagCount, maxSize: maxBagCount, parent: transform);
}
// Classify spawn points into zones for weighted spawning
ClassifySpawnPoints();
StartSpawning();
}
private void OnEnable()
{
GameEvents.OnBagDespawned += OnBagDespawned;
// NOTE: We intentionally do NOT subscribe to OnBagCollected here.
// A bag being picked up is NOT the same as a bag leaving the field —
// the carrier is still running around with it. We only remove from
// _activeBags when the bag is truly gone (despawned/deposited).
}
private void OnDisable()
{
GameEvents.OnBagDespawned -= OnBagDespawned;
}
// ─── Spawning ────────────────────────────────────────────
public void StartSpawning()
{
if (_isSpawning) return;
_isSpawning = true;
int initialCount = Mathf.RoundToInt(maxBagCount * initialSpawnPercent);
for (int i = 0; i < initialCount; i++)
SpawnBag();
GameEvents.FireActiveBagCountChanged(_activeBags.Count);
GameEvents.FirePopupMessage("CASH DROPPED!", 1.5f);
// Start wave timer
_waveCoroutine = StartCoroutine(WaveSpawnRoutine());
}
private IEnumerator WaveSpawnRoutine()
{
while (_isSpawning)
{
yield return new WaitForSeconds(waveInterval);
if (!_isSpawning) break;
// Clean up null references
_activeBags.RemoveAll(b => b == null || !b.gameObject.activeInHierarchy);
int canSpawn = maxBagCount - _activeBags.Count;
int toSpawn = Mathf.Min(bagsPerWave, canSpawn);
if (toSpawn <= 0) continue;
for (int i = 0; i < toSpawn; i++)
{
SpawnBag();
yield return new WaitForSeconds(0.15f); // Stagger
}
GameEvents.FireActiveBagCountChanged(_activeBags.Count);
GameEvents.FirePopupMessage($"+{toSpawn} CASH!", 1f);
}
}
private void SpawnBag()
{
if (cashBagPrefab == null) return;
// Determine spawn zone for value multiplier
SpawnZone zone = _zonesClassified ? PickWeightedZone() : SpawnZone.Neutral;
Vector3 spawnPos = _zonesClassified ? GetZoneSpawnPosition(zone) : GetValidSpawnPosition();
if (spawnPos == Vector3.zero && !useRandomSpawning) return;
// Snap to NavMesh
if (NavMesh.SamplePosition(spawnPos, out NavMeshHit navHit, 5f, NavMesh.AllAreas))
spawnPos = navHit.position + Vector3.up * 0.7f;
// Use pool if available, fallback to Instantiate
GameObject bagObj;
if (_pool != null)
{
bagObj = _pool.Get(spawnPos, Quaternion.identity);
if (bagObj == null)
{
bagObj = Instantiate(cashBagPrefab, spawnPos, Quaternion.identity);
}
}
else
{
bagObj = Instantiate(cashBagPrefab, spawnPos, Quaternion.identity);
}
CashBag bag = bagObj.GetComponent<CashBag>();
if (bag == null) bag = bagObj.AddComponent<CashBag>();
// Apply zone-based value multiplier — contested bags are worth MORE
float zoneMultiplier = GetZoneValueMultiplier(zone);
bag.SetCashValue(cashPerBag * zoneMultiplier);
_activeBags.Add(bag);
_totalSpawned++;
// Ensure the bag gets a minimap marker
if (bag.GetComponent<MinimapObjectMarker>() == null)
bag.gameObject.AddComponent<MinimapObjectMarker>();
GameEvents.FireBagSpawned(bag);
}
// ─── Zone Classification ─────────────────────────────────
/// <summary>
/// Classifies each spawn point as Contested, Neutral, or Safe based on
/// its distance from the map center (midpoint between team vaults).
/// This ensures bags spawn more often in areas where teams will clash.
/// </summary>
private void ClassifySpawnPoints()
{
if (spawnPoints == null || spawnPoints.Length == 0 || useRandomSpawning)
{
_zonesClassified = false;
return;
}
_contestedPoints.Clear();
_neutralPoints.Clear();
_safePoints.Clear();
// Find vault positions to determine map layout
var vaults = EntityRegistry<TeamVault>.GetAll();
if (vaults == null || vaults.Count < 2)
{
// Can't classify without vaults — treat all as neutral
for (int i = 0; i < spawnPoints.Length; i++)
if (spawnPoints[i] != null)
_neutralPoints.Add(i);
_zonesClassified = true;
_mapCenter = spawnAreaCenter;
DevLog.Log("[CashBagSpawner] <2 vaults found, all spawn points classified as Neutral.");
return;
}
// Calculate map center (midpoint between vaults — XZ plane)
Vector3 v0 = vaults[0].transform.position;
Vector3 v1 = vaults[1].transform.position;
_mapCenter = (v0 + v1) * 0.5f;
_mapCenter.y = 0f; // Normalize to ground
// Find the maximum distance from center to any spawn point (for normalization)
float maxDist = 0f;
for (int i = 0; i < spawnPoints.Length; i++)
{
if (spawnPoints[i] == null) continue;
Vector3 flatPos = spawnPoints[i].position;
flatPos.y = 0f;
float d = Vector3.Distance(flatPos, _mapCenter);
if (d > maxDist) maxDist = d;
}
if (maxDist < 1f) maxDist = 1f; // Prevent division by zero
// Classify: inner 40% = contested, 40-70% = neutral, outer 70%+ = safe
for (int i = 0; i < spawnPoints.Length; i++)
{
if (spawnPoints[i] == null) continue;
Vector3 flatPos = spawnPoints[i].position;
flatPos.y = 0f;
float normalizedDist = Vector3.Distance(flatPos, _mapCenter) / maxDist;
if (normalizedDist < 0.40f)
_contestedPoints.Add(i);
else if (normalizedDist < 0.70f)
_neutralPoints.Add(i);
else
_safePoints.Add(i);
}
_zonesClassified = true;
DevLog.Log($"[CashBagSpawner] Zone classification: " +
$"Contested={_contestedPoints.Count}, " +
$"Neutral={_neutralPoints.Count}, " +
$"Safe={_safePoints.Count}");
}
/// <summary>
/// Selects a spawn zone based on weighted random distribution.
/// 60% contested, 30% neutral, 10% safe by default.
/// Falls back to any available zone if the chosen zone is empty.
/// </summary>
private SpawnZone PickWeightedZone()
{
float r = Random.value;
float cumulative = 0f;
cumulative += contestedZoneWeight;
if (r < cumulative && _contestedPoints.Count > 0)
return SpawnZone.Contested;
cumulative += neutralZoneWeight;
if (r < cumulative && _neutralPoints.Count > 0)
return SpawnZone.Neutral;
if (_safePoints.Count > 0)
return SpawnZone.Safe;
// Fallback: pick whichever zone has points
if (_contestedPoints.Count > 0) return SpawnZone.Contested;
if (_neutralPoints.Count > 0) return SpawnZone.Neutral;
if (_safePoints.Count > 0) return SpawnZone.Safe;
return SpawnZone.Neutral; // absolute fallback
}
/// <summary>Returns the value multiplier for a given spawn zone.</summary>
private float GetZoneValueMultiplier(SpawnZone zone)
{
switch (zone)
{
case SpawnZone.Contested: return contestedValueMultiplier;
case SpawnZone.Neutral: return neutralValueMultiplier;
case SpawnZone.Safe: return safeValueMultiplier;
default: return 1f;
}
}
/// <summary>Returns the spawn point list for a given zone.</summary>
private List<int> GetZonePoints(SpawnZone zone)
{
switch (zone)
{
case SpawnZone.Contested: return _contestedPoints;
case SpawnZone.Neutral: return _neutralPoints;
case SpawnZone.Safe: return _safePoints;
default: return _neutralPoints;
}
}
// ─── Position Validation ─────────────────────────────────
private Vector3 GetValidSpawnPosition()
{
return useRandomSpawning ? GetRandomSpawnPosition() : GetSpawnPointPosition();
}
/// <summary>
/// Zone-aware spawn position selection. Picks from the specified zone's spawn points,
/// falling back to other zones if all points in the chosen zone are occupied.
/// </summary>
private Vector3 GetZoneSpawnPosition(SpawnZone preferredZone)
{
if (!_zonesClassified || useRandomSpawning)
return GetValidSpawnPosition();
// Try preferred zone first
Vector3 pos = GetSpawnPointFromZone(GetZonePoints(preferredZone));
if (pos != Vector3.zero) return pos;
// Fallback: try other zones (contested > neutral > safe priority)
if (preferredZone != SpawnZone.Contested)
{
pos = GetSpawnPointFromZone(_contestedPoints);
if (pos != Vector3.zero) return pos;
}
if (preferredZone != SpawnZone.Neutral)
{
pos = GetSpawnPointFromZone(_neutralPoints);
if (pos != Vector3.zero) return pos;
}
if (preferredZone != SpawnZone.Safe)
{
pos = GetSpawnPointFromZone(_safePoints);
if (pos != Vector3.zero) return pos;
}
// Absolute fallback: original unfiltered method
return GetSpawnPointPosition();
}
/// <summary>
/// Picks a valid spawn position from a specific set of spawn point indices.
/// </summary>
private Vector3 GetSpawnPointFromZone(List<int> zoneIndices)
{
if (zoneIndices == null || zoneIndices.Count == 0)
return Vector3.zero;
// Clean up expired cooldowns
List<int> expired = null;
foreach (var kvp in _spawnPointCooldowns)
{
if (Time.time >= kvp.Value)
{
if (expired == null) expired = new List<int>(4);
expired.Add(kvp.Key);
}
}
if (expired != null)
foreach (int idx in expired)
_spawnPointCooldowns.Remove(idx);
// Shuffle to randomize within zone
List<int> shuffled = new List<int>(zoneIndices);
for (int i = shuffled.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]);
}
// Pick first unoccupied, valid, non-cooldown point
foreach (int idx in shuffled)
{
if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
if (_occupiedSpawnPoints.Contains(idx)) continue;
if (_spawnPointCooldowns.ContainsKey(idx)) continue;
if (IsPositionValid(spawnPoints[idx].position))
{
_occupiedSpawnPoints.Add(idx);
return spawnPoints[idx].position;
}
}
// Fallback: allow cooldown points in this zone
foreach (int idx in shuffled)
{
if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
if (_occupiedSpawnPoints.Contains(idx)) continue;
if (IsPositionValid(spawnPoints[idx].position))
{
_occupiedSpawnPoints.Add(idx);
return spawnPoints[idx].position;
}
}
return Vector3.zero;
}
private Vector3 GetSpawnPointPosition()
{
if (spawnPoints == null || spawnPoints.Length == 0)
return Vector3.zero;
// Clean up expired cooldowns
List<int> expired = null;
foreach (var kvp in _spawnPointCooldowns)
{
if (Time.time >= kvp.Value)
{
if (expired == null) expired = new List<int>(4);
expired.Add(kvp.Key);
}
}
if (expired != null)
foreach (int idx in expired)
_spawnPointCooldowns.Remove(idx);
// Build a list of indices, then shuffle to randomize
List<int> indices = new List<int>(spawnPoints.Length);
for (int i = 0; i < spawnPoints.Length; i++)
indices.Add(i);
for (int i = indices.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(indices[i], indices[j]) = (indices[j], indices[i]);
}
// Pick the first unoccupied, valid, non-cooldown spawn point
foreach (int idx in indices)
{
if (spawnPoints[idx] == null) continue;
if (_occupiedSpawnPoints.Contains(idx)) continue; // skip occupied
if (_spawnPointCooldowns.ContainsKey(idx)) continue; // skip recently used
if (IsPositionValid(spawnPoints[idx].position))
{
_occupiedSpawnPoints.Add(idx);
return spawnPoints[idx].position;
}
}
// Fallback: allow cooldown points if nothing else available
foreach (int idx in indices)
{
if (spawnPoints[idx] == null) continue;
if (_occupiedSpawnPoints.Contains(idx)) continue;
if (IsPositionValid(spawnPoints[idx].position))
{
_occupiedSpawnPoints.Add(idx);
return spawnPoints[idx].position;
}
}
return Vector3.zero;
}
private Vector3 GetRandomSpawnPosition()
{
for (int i = 0; i < 30; i++)
{
Vector3 randomPos = spawnAreaCenter + new Vector3(
Random.Range(-spawnAreaSize.x / 2, spawnAreaSize.x / 2),
0.5f,
Random.Range(-spawnAreaSize.z / 2, spawnAreaSize.z / 2)
);
if (IsPositionValid(randomPos))
return randomPos;
}
return spawnAreaCenter + new Vector3(Random.Range(-5f, 5f), 0.5f, Random.Range(-5f, 5f));
}
private bool IsPositionValid(Vector3 pos)
{
// Check distance from active bags (using EntityRegistry — zero alloc)
var bags = EntityRegistry<CashBag>.GetAll();
for (int i = 0; i < bags.Count; i++)
{
if (bags[i] != null && Vector3.Distance(pos, bags[i].transform.position) < minSpawnDistance)
return false;
}
// Check distance from all characters (using TeamMember static registry)
foreach (TeamMember member in TeamMember.AllMembers)
{
if (member != null && Vector3.Distance(pos, member.transform.position) < playerAvoidDistance)
return false;
}
// Check distance from vaults
var vaults = EntityRegistry<TeamVault>.GetAll();
for (int i = 0; i < vaults.Count; i++)
{
if (vaults[i] != null && Vector3.Distance(pos, vaults[i].transform.position) < vaultAvoidDistance)
return false;
}
return true;
}
// ─── Event Handlers ──────────────────────────────────────
private void OnBagDespawned(CashBag bag)
{
_activeBags.Remove(bag);
ReleaseSpawnPoint(bag);
GameEvents.FireActiveBagCountChanged(_activeBags.Count);
// Immediately spawn a replacement at a DIFFERENT position
// so the field always has bags and they rotate across spawn points
if (_isSpawning && _activeBags.Count < maxBagCount)
{
StartCoroutine(DelayedRespawn(0.5f));
}
}
/// <summary>Respawn a bag after a short delay at a different position.</summary>
private IEnumerator DelayedRespawn(float delay)
{
yield return new WaitForSeconds(delay);
if (!_isSpawning) yield break;
// Clean up null references
_activeBags.RemoveAll(b => b == null || !b.gameObject.activeInHierarchy);
if (_activeBags.Count < maxBagCount)
{
SpawnBag();
GameEvents.FireActiveBagCountChanged(_activeBags.Count);
}
}
/// <summary>Called when a bag is collected (legacy API for compatibility).</summary>
public void OnBagCollected(CashBag bag)
{
// Legacy API: only remove if bag is truly gone (inactive).
// Don't remove carried bags — they're still in play.
if (bag != null && !bag.gameObject.activeInHierarchy)
{
if (_activeBags.Contains(bag))
_activeBags.Remove(bag);
ReleaseSpawnPoint(bag);
}
}
/// <summary>
/// Free the spawn point closest to a removed bag so it can be reused.
/// Applies a cooldown so the same point isn't immediately reused.
/// </summary>
private void ReleaseSpawnPoint(CashBag bag)
{
if (bag == null || spawnPoints == null) return;
float bestDist = float.MaxValue;
int bestIdx = -1;
foreach (int idx in _occupiedSpawnPoints)
{
if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
float dist = Vector3.Distance(bag.SpawnPosition, spawnPoints[idx].position);
if (dist < bestDist)
{
bestDist = dist;
bestIdx = idx;
}
}
// Only release if the bag was reasonably close to the spawn point
if (bestIdx >= 0 && bestDist < minSpawnDistance * 2f)
{
_occupiedSpawnPoints.Remove(bestIdx);
// Add cooldown so next spawn goes to a DIFFERENT position
_spawnPointCooldowns[bestIdx] = Time.time + SPAWN_POINT_COOLDOWN;
}
}
public void StopSpawning()
{
_isSpawning = false;
if (_waveCoroutine != null)
StopCoroutine(_waveCoroutine);
}
private void OnDestroy()
{
StopSpawning();
}
private void OnDrawGizmos()
{
if (!showSpawnGizmos) return;
Gizmos.color = new Color(0, 1, 0, 0.3f);
Gizmos.DrawCube(spawnAreaCenter, spawnAreaSize);
Gizmos.color = Color.green;
Gizmos.DrawWireCube(spawnAreaCenter, spawnAreaSize);
if (spawnPoints != null)
{
for (int i = 0; i < spawnPoints.Length; i++)
{
if (spawnPoints[i] == null) continue;
// Color by zone classification
if (_zonesClassified)
{
if (_contestedPoints.Contains(i))
Gizmos.color = Color.red; // Contested = RED (danger zone)
else if (_neutralPoints.Contains(i))
Gizmos.color = Color.yellow; // Neutral = YELLOW
else if (_safePoints.Contains(i))
Gizmos.color = Color.green; // Safe = GREEN
else
Gizmos.color = Color.white;
}
else
{
Gizmos.color = Color.yellow;
}
Gizmos.DrawSphere(spawnPoints[i].position, 0.5f);
}
// Draw map center (contested zone center)
if (_zonesClassified)
{
Gizmos.color = new Color(1f, 0f, 0f, 0.5f);
Gizmos.DrawWireSphere(_mapCenter, 3f);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4a1fa35c76b8d04cfb343b59d172b2ab

View File

@@ -0,0 +1,679 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections;
/// <summary>
/// CashBoxImportController — Manages cash import to a TeamVault's CashBox.
/// Features:
/// - Spawns CashBox prefab at vault position with correct transform
/// - Countdown window based on import amount
/// - Reverse-plays "close" animation to open, then plays forward to close
/// - Camera focuses on player and cashbox during import
/// - Freezes player movement during import
/// - Sound effects for open/close/counting
///
/// Attach to the TeamVault or a manager object.
/// </summary>
public class CashBoxImportController : MonoBehaviour
{
[Header("CashBox Prefab")]
[Tooltip("CashBox prefab to spawn at the vault")]
[SerializeField] private GameObject cashBoxPrefab;
[Header("Spawn Transform (from prefab inspector)")]
[SerializeField] private Vector3 spawnOffset = Vector3.zero;
[SerializeField] private Vector3 spawnRotation = new Vector3(-90f, 0f, 0f);
[SerializeField] private Vector3 spawnScale = new Vector3(60f, 60f, 60f);
[Header("Animation Settings")]
[Tooltip("Name of the close animation state")]
[SerializeField] private string closeAnimationName = "close";
[SerializeField] private float openAnimationDuration = 1f;
[SerializeField] private float closeAnimationDuration = 1f;
[Header("Import Settings")]
[Tooltip("Seconds per $100 imported")]
[SerializeField] private float countdownPerHundred = 0.5f;
[SerializeField] private float minimumCountdown = 1f;
[SerializeField] private float maximumCountdown = 10f;
[Header("Camera Settings")]
[SerializeField] private float cameraFocusDistance = 5f;
[SerializeField] private float cameraFocusHeight = 2f;
[SerializeField] private float cameraSmoothSpeed = 5f;
[Header("Audio")]
[Tooltip("Sound when CashBox opens")]
[SerializeField] private AudioClip openSound;
[Tooltip("Sound when CashBox closes")]
[SerializeField] private AudioClip closeSound;
[Tooltip("Looping sound during cash counting")]
[SerializeField] private AudioClip countingSound;
[Tooltip("Sound when import completes")]
[SerializeField] private AudioClip completeSound;
[SerializeField] private float soundVolume = 1f;
[Header("UI References")]
[SerializeField] private GameObject importUIPanel;
[SerializeField] private TMP_Text countdownText;
[Tooltip("Separate TMP for status label (e.g. 'IMPORTING'). Assign a TextMeshPro in your scene.")]
[SerializeField] private TMP_Text statusText;
[SerializeField] private TMP_Text amountText;
[SerializeField] private Image progressDial;
[Tooltip("Optional sprite for the circular dial (e.g. Thick_Dial)")]
[SerializeField] private Sprite dialSprite;
[Header("Scene References")]
[Tooltip("Drag the TimeDial GameObject from the scene here (will use its Image component)")]
[SerializeField] private GameObject progressDialObject;
// Runtime
private GameObject _spawnedCashBox;
private Animator _cashBoxAnimator;
private AudioSource _audioSource;
private TeamVault _vault;
private Camera _mainCamera;
private Vector3 _originalCameraPosition;
private Quaternion _originalCameraRotation;
private Transform _originalCameraParent;
private bool _isImporting;
private CashCarrier _currentCarrier;
public bool IsImporting => _isImporting;
private void Awake()
{
_vault = GetComponent<TeamVault>();
_audioSource = GetComponent<AudioSource>();
if (_audioSource == null)
_audioSource = gameObject.AddComponent<AudioSource>();
_audioSource.playOnAwake = false;
_audioSource.spatialBlend = 0f; // 2D sound for UI feedback
_mainCamera = Camera.main;
// If a GameObject was provided (e.g. your TimeDial), try to grab its Image component
if (progressDial == null && progressDialObject != null)
{
var img = progressDialObject.GetComponent<Image>();
if (img == null) img = progressDialObject.GetComponentInChildren<Image>();
if (img != null)
{
progressDial = img;
Debug.Log("[CashBoxImportController] Assigned progressDial from progressDialObject");
}
else
{
Debug.LogWarning("[CashBoxImportController] progressDialObject does not contain an Image component");
}
}
// If a dial sprite was provided and progressDial exists, apply it
if (progressDial != null && dialSprite != null)
{
progressDial.sprite = dialSprite;
progressDial.type = Image.Type.Filled;
progressDial.fillMethod = Image.FillMethod.Radial360;
progressDial.fillOrigin = (int)Image.Origin360.Top;
progressDial.fillClockwise = true;
progressDial.fillAmount = 0f;
}
}
private void Start()
{
// Auto-setup UI if not assigned
if (importUIPanel == null)
CreateImportUI();
else
importUIPanel.SetActive(false);
// Auto-spawn CashBox on start if prefab is assigned
if (cashBoxPrefab != null && _spawnedCashBox == null)
SpawnCashBox();
}
/// <summary>
/// Configure the controller from TeamVault at runtime (when added dynamically).
/// </summary>
public void Configure(GameObject prefab, Vector3 offset, Vector3 rotation, Vector3 scale)
{
cashBoxPrefab = prefab;
spawnOffset = offset;
spawnRotation = rotation;
spawnScale = scale;
}
/// <summary>
/// Spawn the CashBox at vault position. Call this on game start or when needed.
/// </summary>
public void SpawnCashBox()
{
if (cashBoxPrefab == null)
{
Debug.LogWarning("[CashBoxImportController] No CashBox prefab assigned!");
return;
}
if (_spawnedCashBox != null)
{
Debug.Log("[CashBoxImportController] CashBox already spawned");
return;
}
// Hide the vault's own visual mesh/renderers so only the CashBox is visible
HideVaultVisuals();
// Spawn at vault position with specified transform
Vector3 spawnPos = transform.position + spawnOffset;
Quaternion spawnRot = Quaternion.Euler(spawnRotation);
_spawnedCashBox = Instantiate(cashBoxPrefab, spawnPos, spawnRot, transform);
_spawnedCashBox.transform.localScale = spawnScale;
_spawnedCashBox.name = "CashBox_" + (_vault != null ? _vault.TeamTag : "Vault");
_cashBoxAnimator = _spawnedCashBox.GetComponent<Animator>();
if (_cashBoxAnimator == null)
_cashBoxAnimator = _spawnedCashBox.GetComponentInChildren<Animator>();
Debug.Log($"[CashBoxImportController] Spawned CashBox at {spawnPos}, rotation {spawnRotation}, scale {spawnScale}");
}
/// <summary>
/// Disable the vault's own mesh renderers so only the CashBox prefab shows.
/// Skips any renderers on the spawned CashBox itself.
/// </summary>
private void HideVaultVisuals()
{
// Disable all renderers on the vault GameObject (not children that are the CashBox)
foreach (var renderer in GetComponents<Renderer>())
{
renderer.enabled = false;
}
// Also disable renderers on existing vault children (the old "vault box")
// but skip any previously spawned CashBox
for (int i = 0; i < transform.childCount; i++)
{
Transform child = transform.GetChild(i);
if (_spawnedCashBox != null && child.gameObject == _spawnedCashBox) continue;
if (child.name.StartsWith("CashBox_")) continue; // Skip our CashBox
foreach (var renderer in child.GetComponentsInChildren<Renderer>())
{
renderer.enabled = false;
Debug.Log($"[CashBoxImportController] Disabled vault visual: {renderer.gameObject.name}");
}
}
}
/// <summary>
/// Start the import process for a carrier depositing cash.
/// </summary>
public void StartImport(CashCarrier carrier)
{
if (_isImporting)
{
Debug.LogWarning("[CashBoxImportController] Import already in progress!");
return;
}
if (carrier == null || carrier.CarriedCash <= 0)
{
Debug.LogWarning("[CashBoxImportController] No cash to import!");
return;
}
// Spawn CashBox if not already
if (_spawnedCashBox == null)
SpawnCashBox();
if (_spawnedCashBox == null)
{
Debug.LogError("[CashBoxImportController] Failed to spawn CashBox!");
return;
}
_currentCarrier = carrier;
StartCoroutine(ImportRoutine(carrier));
}
private IEnumerator ImportRoutine(CashCarrier carrier)
{
_isImporting = true;
float cashAmount = carrier.CarriedCash;
// Calculate countdown based on amount
float countdownDuration = Mathf.Clamp(
(cashAmount / 100f) * countdownPerHundred,
minimumCountdown,
maximumCountdown
);
Debug.Log($"[CashBoxImportController] Starting import of ${cashAmount:F0}, duration {countdownDuration:F1}s");
// 1. Freeze player movement
FreezeCarrier(carrier, true);
// 2. Focus camera
yield return StartCoroutine(FocusCamera(carrier.transform, _spawnedCashBox.transform));
// 3. Show UI
ShowImportUI(cashAmount, countdownDuration);
// 4. Play open animation (reverse of close)
yield return StartCoroutine(PlayAnimationReverse(closeAnimationName, openAnimationDuration));
PlaySound(openSound);
// 5. Countdown with cash counting sound
yield return StartCoroutine(CountdownRoutine(cashAmount, countdownDuration));
// 6. Play close animation (forward)
yield return StartCoroutine(PlayAnimationForward(closeAnimationName, closeAnimationDuration));
PlaySound(closeSound);
// 7. Deposit the cash to vault
if (_vault != null && carrier != null)
{
float deposited = carrier.DepositToVault(_vault);
Debug.Log($"[CashBoxImportController] Deposited ${deposited:F0} to vault. Carrier now has ${carrier.CarriedCash:F0}");
}
else
{
Debug.LogError($"[CashBoxImportController] Deposit FAILED — vault: {_vault}, carrier: {carrier}");
}
// Force-clear any remaining carried cash display
// DepositToVault clears bags, but fire event again as safety
GameEvents.FireCashDeposited(
carrier != null ? carrier.TeamTag : "Player",
cashAmount, 1);
PlaySound(completeSound);
// 8. Hide UI
HideImportUI();
// 9. Restore camera
yield return StartCoroutine(RestoreCamera());
// 10. Unfreeze player
FreezeCarrier(carrier, false);
_isImporting = false;
_currentCarrier = null;
Debug.Log($"[CashBoxImportController] Import complete!");
}
/// <summary>
/// Play animation in reverse by stepping normalizedTime from 1 to 0.
/// </summary>
private IEnumerator PlayAnimationReverse(string animName, float duration)
{
if (_cashBoxAnimator == null) yield break;
float elapsed = 0f;
// Start at end of animation (normalizedTime = 1)
_cashBoxAnimator.Play(animName, 0, 1f);
_cashBoxAnimator.speed = 0f; // Pause normal playback
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float normalizedTime = 1f - (elapsed / duration); // Go from 1 to 0
normalizedTime = Mathf.Clamp01(normalizedTime);
// Continuously set the animation time
_cashBoxAnimator.Play(animName, 0, normalizedTime);
yield return null;
}
// Ensure we end at time 0 (fully open)
_cashBoxAnimator.Play(animName, 0, 0f);
_cashBoxAnimator.speed = 1f; // Restore normal speed
}
/// <summary>
/// Play animation forward normally.
/// </summary>
private IEnumerator PlayAnimationForward(string animName, float duration)
{
if (_cashBoxAnimator == null) yield break;
// Play from start
_cashBoxAnimator.speed = 1f;
_cashBoxAnimator.Play(animName, 0, 0f);
yield return new WaitForSeconds(duration);
}
private IEnumerator FocusCamera(Transform playerTransform, Transform boxTransform)
{
if (_mainCamera == null) yield break;
// Store original camera state
_originalCameraPosition = _mainCamera.transform.position;
_originalCameraRotation = _mainCamera.transform.rotation;
_originalCameraParent = _mainCamera.transform.parent;
// Detach camera temporarily
_mainCamera.transform.SetParent(null);
// Calculate target position (between player and box, offset back)
Vector3 midPoint = (playerTransform.position + boxTransform.position) / 2f;
Vector3 dirToBox = (boxTransform.position - playerTransform.position).normalized;
Vector3 sideDir = Vector3.Cross(dirToBox, Vector3.up).normalized;
Vector3 targetPos = midPoint + sideDir * cameraFocusDistance + Vector3.up * cameraFocusHeight;
Quaternion targetRot = Quaternion.LookRotation(midPoint - targetPos);
// Smooth move camera
float elapsed = 0f;
float focusDuration = 0.5f;
while (elapsed < focusDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / focusDuration;
t = t * t * (3f - 2f * t); // Smoothstep
_mainCamera.transform.position = Vector3.Lerp(_originalCameraPosition, targetPos, t);
_mainCamera.transform.rotation = Quaternion.Slerp(_originalCameraRotation, targetRot, t);
yield return null;
}
}
private IEnumerator RestoreCamera()
{
if (_mainCamera == null) yield break;
Vector3 currentPos = _mainCamera.transform.position;
Quaternion currentRot = _mainCamera.transform.rotation;
float elapsed = 0f;
float restoreDuration = 0.5f;
while (elapsed < restoreDuration)
{
elapsed += Time.deltaTime;
float t = elapsed / restoreDuration;
t = t * t * (3f - 2f * t); // Smoothstep
_mainCamera.transform.position = Vector3.Lerp(currentPos, _originalCameraPosition, t);
_mainCamera.transform.rotation = Quaternion.Slerp(currentRot, _originalCameraRotation, t);
yield return null;
}
// Re-parent camera
if (_originalCameraParent != null)
_mainCamera.transform.SetParent(_originalCameraParent);
}
private IEnumerator CountdownRoutine(float totalAmount, float duration)
{
float elapsed = 0f;
// Start counting sound loop
if (countingSound != null)
{
_audioSource.clip = countingSound;
_audioSource.loop = true;
_audioSource.volume = soundVolume * 0.7f;
_audioSource.Play();
}
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float progress = elapsed / duration;
float remaining = duration - elapsed;
float countedAmount = totalAmount * progress;
// Update UI
UpdateImportUI(remaining, countedAmount, totalAmount, progress);
yield return null;
}
// Stop counting sound
if (_audioSource.isPlaying && _audioSource.clip == countingSound)
_audioSource.Stop();
// Final UI update
UpdateImportUI(0f, totalAmount, totalAmount, 1f);
}
// Saved component states before freeze (restore exactly on unfreeze)
private bool _carrierWasKinematic;
private bool _aiWasEnabled;
private bool _playerScriptWasEnabled;
private bool _navAgentWasEnabled;
private bool _navAgentWasStopped;
private bool _cashAIWasEnabled;
private bool _playerMovementWasEnabled;
private void FreezeCarrier(CashCarrier carrier, bool freeze)
{
if (carrier == null) return;
// CharacterAIController
var aiController = carrier.GetComponent<CharacterAIController>();
// PlayerScript
var playerScript = carrier.GetComponent<PlayerScript>();
// PlayerMovementBehaviour
var playerMovement = carrier.GetComponent<PlayerMovementBehaviour>();
// NavMeshAgent
var navAgent = carrier.GetComponent<UnityEngine.AI.NavMeshAgent>();
// CashSystemAI
var cashAI = carrier.GetComponent<CashSystemAI>();
// Rigidbody
var rb = carrier.GetComponent<Rigidbody>();
if (freeze)
{
// ─── SAVE original states BEFORE disabling ───
_aiWasEnabled = aiController != null && aiController.enabled;
_playerScriptWasEnabled = playerScript != null && playerScript.enabled;
_playerMovementWasEnabled = playerMovement != null && playerMovement.enabled;
_navAgentWasEnabled = navAgent != null && navAgent.enabled;
_navAgentWasStopped = navAgent != null && navAgent.enabled && navAgent.isStopped;
_cashAIWasEnabled = cashAI != null && cashAI.enabled;
// Disable everything
if (aiController != null) aiController.enabled = false;
if (playerScript != null) playerScript.enabled = false;
if (playerMovement != null) playerMovement.enabled = false;
if (cashAI != null) cashAI.enabled = false;
if (navAgent != null && navAgent.enabled) navAgent.isStopped = true;
if (rb != null)
{
_carrierWasKinematic = rb.isKinematic;
rb.isKinematic = true;
rb.linearVelocity = Vector3.zero;
rb.angularVelocity = Vector3.zero;
}
}
else
{
// ─── RESTORE exactly the original states ───
// Only re-enable components that were enabled BEFORE freeze
if (aiController != null) aiController.enabled = _aiWasEnabled;
if (playerScript != null) playerScript.enabled = _playerScriptWasEnabled;
if (playerMovement != null) playerMovement.enabled = _playerMovementWasEnabled;
if (cashAI != null) cashAI.enabled = _cashAIWasEnabled;
if (navAgent != null)
{
// Only touch the agent if it was enabled before freeze
if (_navAgentWasEnabled && navAgent.enabled)
navAgent.isStopped = _navAgentWasStopped;
}
if (rb != null)
{
rb.isKinematic = _carrierWasKinematic;
}
}
Debug.Log($"[CashBoxImportController] Carrier movement {(freeze ? "frozen" : "restored")}");
}
private void PlaySound(AudioClip clip)
{
if (clip == null || _audioSource == null) return;
_audioSource.PlayOneShot(clip, soundVolume);
}
// ─── UI Management ───────────────────────────────────────
private void CreateImportUI()
{
// Create a simple runtime UI panel
Canvas canvas = FindObjectOfType<Canvas>();
if (canvas == null)
{
GameObject canvasObj = new GameObject("ImportUICanvas");
canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasObj.AddComponent<CanvasScaler>();
canvasObj.AddComponent<GraphicRaycaster>();
}
// Create panel
importUIPanel = new GameObject("ImportUIPanel");
importUIPanel.transform.SetParent(canvas.transform, false);
RectTransform panelRect = importUIPanel.AddComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.sizeDelta = new Vector2(400f, 150f);
panelRect.anchoredPosition = new Vector2(0f, 100f);
Image panelBg = importUIPanel.AddComponent<Image>();
panelBg.color = new Color(0f, 0f, 0f, 0.8f);
// Create countdown text
GameObject countdownObj = new GameObject("CountdownText");
countdownObj.transform.SetParent(importUIPanel.transform, false);
RectTransform countdownRect = countdownObj.AddComponent<RectTransform>();
countdownRect.anchorMin = new Vector2(0, 0.6f);
countdownRect.anchorMax = new Vector2(1, 1f);
countdownRect.offsetMin = Vector2.zero;
countdownRect.offsetMax = Vector2.zero;
countdownText = countdownObj.AddComponent<TextMeshProUGUI>();
countdownText.text = "IMPORTING...";
countdownText.fontSize = 36;
countdownText.alignment = TextAlignmentOptions.Center;
countdownText.color = Color.white;
// Create amount text
GameObject amountObj = new GameObject("AmountText");
amountObj.transform.SetParent(importUIPanel.transform, false);
RectTransform amountRect = amountObj.AddComponent<RectTransform>();
amountRect.anchorMin = new Vector2(0, 0.3f);
amountRect.anchorMax = new Vector2(1, 0.6f);
amountRect.offsetMin = Vector2.zero;
amountRect.offsetMax = Vector2.zero;
amountText = amountObj.AddComponent<TextMeshProUGUI>();
amountText.text = "$0";
amountText.fontSize = 28;
amountText.alignment = TextAlignmentOptions.Center;
amountText.color = new Color(0.2f, 1f, 0.2f);
// Create circular progress dial (radial Image)
GameObject dialObj = new GameObject("ProgressDial");
dialObj.transform.SetParent(importUIPanel.transform, false);
RectTransform dialRect = dialObj.AddComponent<RectTransform>();
dialRect.anchorMin = new Vector2(0.35f, 0.05f);
dialRect.anchorMax = new Vector2(0.65f, 0.35f);
dialRect.offsetMin = Vector2.zero;
dialRect.offsetMax = Vector2.zero;
progressDial = dialObj.AddComponent<Image>();
if (dialSprite != null)
progressDial.sprite = dialSprite;
progressDial.type = Image.Type.Filled;
progressDial.fillMethod = Image.FillMethod.Radial360;
progressDial.fillOrigin = (int)Image.Origin360.Top;
progressDial.fillClockwise = true;
progressDial.fillAmount = 0f;
progressDial.preserveAspect = true;
importUIPanel.SetActive(false);
}
private void ShowImportUI(float totalAmount, float duration)
{
if (importUIPanel != null)
importUIPanel.SetActive(true);
UpdateImportUI(duration, 0f, totalAmount, 0f);
}
private void HideImportUI()
{
if (importUIPanel != null)
importUIPanel.SetActive(false);
}
private void UpdateImportUI(float remaining, float counted, float total, float progress)
{
// Timer text: only the seconds number
if (countdownText != null)
countdownText.text = remaining > 0 ? $"{remaining:F1}" : "0";
// Status text: label on a separate TMP
if (statusText != null)
statusText.text = remaining > 0 ? "IMPORTING" : "COMPLETE!";
if (amountText != null)
amountText.text = $"${counted:F0} / ${total:F0}";
if (progressDial != null)
progressDial.fillAmount = Mathf.Clamp01(progress);
}
// ─── Public API ──────────────────────────────────────────
/// <summary>
/// Check if a carrier can start importing (not already importing).
/// </summary>
public bool CanImport(CashCarrier carrier)
{
return !_isImporting && carrier != null && carrier.CarriedCash > 0;
}
/// <summary>
/// Cancel current import (emergency use only).
/// </summary>
public void CancelImport()
{
if (!_isImporting) return;
StopAllCoroutines();
if (_currentCarrier != null)
FreezeCarrier(_currentCarrier, false);
HideImportUI();
_isImporting = false;
_currentCarrier = null;
// Restore camera immediately
if (_mainCamera != null && _originalCameraParent != null)
{
_mainCamera.transform.SetParent(_originalCameraParent);
_mainCamera.transform.position = _originalCameraPosition;
_mainCamera.transform.rotation = _originalCameraRotation;
}
Debug.Log("[CashBoxImportController] Import cancelled");
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2bc8982e2d3d6af29a9b51c9542690bd

View File

@@ -0,0 +1,390 @@
using UnityEngine;
using UnityEngine.Events;
using System.Collections.Generic;
/// <summary>
/// CashCarrier — Attach to characters to track carried cash bags.
/// Tracks actual CashBag references (not just float), handles stacking
/// speed penalty, combo multiplier on deposit, death drops.
/// Uses GameEvents instead of FindObjectOfType.
/// </summary>
public class CashCarrier : MonoBehaviour
{
[Header("Carry Settings")]
[SerializeField] private int maxBags = 3;
[Header("Movement Penalty")]
[SerializeField] private float singleBagPenalty = 0.15f; // -15% speed per bag
[SerializeField] private float multiBagExtraPenalty = 0.10f; // Additional -10% for 2+ bags
[SerializeField] private float maxSpeedReduction = 0.40f; // Never slower than 60% speed
[Header("Death Drop")]
[SerializeField] private float cashDropPercent = 1.0f; // Drop 100% on death (all bags scatter)
[SerializeField] private GameObject droppedCashPrefab; // Optional: legacy prefab for dropped cash
[Header("Visual Indicators")]
[SerializeField] private GameObject carryingIndicator;
[SerializeField] private ParticleSystem cashParticles;
[Header("Events")]
public UnityEvent<float> OnCashChanged;
public UnityEvent<float> OnCashDropped;
// ─── Runtime ─────────────────────────────────────────────
private readonly List<CashBag> _carriedBags = new List<CashBag>(4);
private float _legacyCash = 0f; // Backwards compat: cash from old-style AddCash(float)
private float _originalSpeed = -1f;
private CharacterAIController _aiController;
private HealthNew _health;
private Animator _animator;
private bool _isDead = false;
// ─── Combo Tracking ──────────────────────────────────────
private float _lastDepositTime;
private int _comboCount;
private const float COMBO_WINDOW = 10f; // Seconds between deposits to maintain combo
// ─── Properties ──────────────────────────────────────────
public int BagCount => _carriedBags.Count;
public int BoxCount => BagCount; // Legacy alias
public float TotalValue => CarriedCash; // Alias for AI compatibility
public float CarriedCash
{
get
{
float total = _legacyCash;
for (int i = 0; i < _carriedBags.Count; i++)
if (_carriedBags[i] != null) total += _carriedBags[i].GetEffectiveValue();
return total;
}
}
public bool IsCarrying => _carriedBags.Count > 0 || _legacyCash > 0;
public string TeamTag => gameObject.tag;
public float SpeedMultiplier { get; private set; } = 1f;
public int ComboCount => _comboCount;
public List<CashBag> CarriedBags => _carriedBags;
public List<CashBag> CarriedBoxes => _carriedBags; // Legacy alias
/// <summary>True when this character is carrying an ExtractionVault (managed by ExtractionVault, not CashCarrier).</summary>
public bool IsCarryingVault
{
get
{
var vaults = EntityRegistry<ExtractionVault>.GetAll();
for (int i = 0; i < vaults.Count; i++)
if (vaults[i] != null && vaults[i].IsCarried && vaults[i].Carrier == gameObject)
return true;
return false;
}
}
// ─── Lifecycle ───────────────────────────────────────────
private void OnEnable()
{
EntityRegistry<CashCarrier>.Register(this);
}
private void OnDisable()
{
EntityRegistry<CashCarrier>.Unregister(this);
}
private void Start()
{
if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
{
enabled = false;
return;
}
// ── Explicit reset — guarantees 0 cash on every spawn/reuse ──
_carriedBags.Clear();
_legacyCash = 0f;
_isDead = false;
_aiController = GetComponent<CharacterAIController>();
_health = GetComponent<HealthNew>();
_animator = GetComponent<Animator>();
if (_aiController != null)
_originalSpeed = _aiController.GetSpeed();
UpdateVisuals();
OnCashChanged?.Invoke(0f); // Tell listeners we start at zero
}
private void Update()
{
// Death check
if (_health != null && _health.isDead && !_isDead)
{
_isDead = true;
OnDeath();
}
else if (_health != null && !_health.isDead)
{
_isDead = false;
}
ApplyMovementPenalty();
}
// ─── Bag Management ─────────────────────────────────────
/// <summary>Can this carrier pick up more bags?</summary>
public bool CanPickUp() => _carriedBags.Count < maxBags && !_isDead;
/// <summary>Called by CashBag when picked up.</summary>
public void OnBagPickedUp(CashBag bag)
{
if (bag == null || _carriedBags.Contains(bag)) return;
_carriedBags.Add(bag);
UpdateVisuals();
OnCashChanged?.Invoke(CarriedCash);
GameEvents.FireKillFeedEntry(gameObject.name, "picked up", $"Cash (${bag.CashValue})");
}
/// <summary>Legacy alias for OnBagPickedUp.</summary>
public void OnBoxPickedUp(CashBag bag) => OnBagPickedUp(bag);
/// <summary>Drop all carried bags.</summary>
public void DropAllBags()
{
if (_carriedBags.Count == 0) return;
Vector3 dropPos = transform.position;
float totalDropped = 0f;
for (int i = _carriedBags.Count - 1; i >= 0; i--)
{
CashBag bag = _carriedBags[i];
if (bag != null)
{
// Scatter bags slightly around drop point
Vector3 offset = Random.insideUnitSphere * 1.5f;
offset.y = 0;
bag.Drop(dropPos + offset);
totalDropped += bag.CashValue;
}
}
_carriedBags.Clear();
UpdateVisuals();
OnCashDropped?.Invoke(totalDropped);
OnCashChanged?.Invoke(CarriedCash);
}
/// <summary>Legacy alias for DropAllBags.</summary>
public void DropAllBoxes() => DropAllBags();
/// <summary>Deposit all to vault. Returns total value deposited.</summary>
public float DepositToVault(TeamVault vault)
{
if (vault == null) return 0f;
if (_carriedBags.Count == 0 && _legacyCash <= 0) return 0f;
// Calculate combo
if (Time.time - _lastDepositTime <= COMBO_WINDOW)
_comboCount++;
else
_comboCount = 1;
_lastDepositTime = Time.time;
float totalValue = 0f;
int comboMultiplier = Mathf.Min(_comboCount, 3); // Cap at 3x
// Deposit tracked bags
for (int i = _carriedBags.Count - 1; i >= 0; i--)
{
CashBag bag = _carriedBags[i];
if (bag != null)
{
float value = bag.GetEffectiveValue();
switch (comboMultiplier)
{
case 2: value *= 1.5f; break;
case 3: value *= 2.0f; break;
}
totalValue += value;
bag.Despawn();
}
}
_carriedBags.Clear();
// Deposit any legacy cash too
if (_legacyCash > 0)
{
float legacyValue = _legacyCash;
switch (comboMultiplier)
{
case 2: legacyValue *= 1.5f; break;
case 3: legacyValue *= 2.0f; break;
}
totalValue += legacyValue;
_legacyCash = 0f;
}
vault.DepositCash(totalValue);
UpdateVisuals();
OnCashChanged?.Invoke(0f);
// Fire events
GameEvents.FireCashDeposited(TeamTag, totalValue, comboMultiplier);
if (comboMultiplier > 1)
GameEvents.FireComboMultiplier(comboMultiplier);
GameEvents.FireKillFeedEntry(
gameObject.name,
comboMultiplier > 1 ? $"SCORED {comboMultiplier}x" : "scored",
$"${totalValue:F0}"
);
return totalValue;
}
// ─── Legacy API (backwards compat with old CashBag/CashSystemAI) ─
/// <summary>Add cash directly (legacy float-based API).</summary>
public void AddCash(float amount)
{
_legacyCash += amount;
UpdateVisuals();
OnCashChanged?.Invoke(CarriedCash);
}
/// <summary>Remove cash amount (legacy API).</summary>
public float RemoveCash(float amount)
{
float removed = Mathf.Min(amount, _legacyCash);
_legacyCash -= removed;
UpdateVisuals();
OnCashChanged?.Invoke(CarriedCash);
return removed;
}
/// <summary>Remove all cash and return amount (legacy API).</summary>
public float RemoveAllCash()
{
float removed = CarriedCash;
_legacyCash = 0f;
_carriedBags.Clear();
UpdateVisuals();
OnCashChanged?.Invoke(0f);
return removed;
}
// ─── Death ───────────────────────────────────────────────
private void OnDeath()
{
// Fire character died for respawn system (always, even if not carrying)
GameEvents.FireCharacterDied(gameObject);
if (!IsCarrying) return;
GameEvents.FireCarrierKilled(this);
// Drop tracked bags (scatter around)
DropAllBags();
// Drop legacy cash as a physical pickup
if (_legacyCash > 0 && droppedCashPrefab != null)
{
float dropAmount = _legacyCash * cashDropPercent;
GameObject dropped = Instantiate(droppedCashPrefab, transform.position + Vector3.up, Quaternion.identity);
CashBag droppedBag = dropped.GetComponent<CashBag>();
if (droppedBag != null)
droppedBag.SetCashValue(dropAmount);
_legacyCash -= dropAmount;
OnCashDropped?.Invoke(dropAmount);
}
}
// ─── Movement Penalty ────────────────────────────────────
private void ApplyMovementPenalty()
{
int count = _carriedBags.Count;
if (count == 0 && _legacyCash <= 0)
{
SpeedMultiplier = 1f;
}
else if (count == 0)
{
// Legacy cash — simple penalty
float penaltyFactor = Mathf.Clamp01(_legacyCash / 500f);
SpeedMultiplier = 1f - (singleBagPenalty * penaltyFactor);
}
else
{
// Stacking penalty: -15% per bag, -10% extra for 2+
float penalty = singleBagPenalty * count;
if (count >= 2)
penalty += multiBagExtraPenalty * (count - 1);
penalty = Mathf.Min(penalty, maxSpeedReduction);
SpeedMultiplier = 1f - penalty;
}
// Apply to AI controller (skip if carrying ExtractionVault — vault manages speed directly)
if (_aiController != null && _originalSpeed > 0 && !IsCarryingVault)
_aiController.SetSpeed(_originalSpeed * SpeedMultiplier);
// Apply to animator
if (_animator != null)
{
if (IsCarrying)
{
float currentSpeed = _animator.GetFloat("Speed");
if (currentSpeed > 0) _animator.speed = SpeedMultiplier;
}
else
{
_animator.speed = 1f;
}
}
}
// ─── Visuals ─────────────────────────────────────────────
private void UpdateVisuals()
{
bool carrying = IsCarrying;
if (carryingIndicator != null)
carryingIndicator.SetActive(carrying);
if (cashParticles != null)
{
if (carrying && !cashParticles.isPlaying)
cashParticles.Play();
else if (!carrying && cashParticles.isPlaying)
cashParticles.Stop(true, ParticleSystemStopBehavior.StopEmitting);
}
}
// ─── Queries ─────────────────────────────────────────────
/// <summary>Should AI deposit? (has bags or enough cash)</summary>
public bool ShouldDeposit(float threshold = 200f)
{
return _carriedBags.Count >= 1 || _legacyCash >= threshold;
}
/// <summary>Check if carrying a specific bag.</summary>
public bool IsCarryingBag(CashBag bag) => _carriedBags.Contains(bag);
/// <summary>Legacy alias for IsCarryingBag.</summary>
public bool IsCarryingBox(CashBag bag) => IsCarryingBag(bag);
// ─── Cleanup ─────────────────────────────────────────────
private void OnDestroy()
{
if (_originalSpeed >= 0 && _aiController != null)
_aiController.SetSpeed(_originalSpeed);
if (_animator != null) _animator.speed = 1f;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cf4ee3cf212672b8d96990e29bcb472c

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More