chunk 1: core gameplay scripts scenes runtime assets
This commit is contained in:
8
Assets/Scripts/AI.meta
Normal file
8
Assets/Scripts/AI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9cc74237c7d2433eb1585fde95783ef
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/AI/Actions.meta
Normal file
8
Assets/Scripts/AI/Actions.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbb831ed5741e5286af289e75349418b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
72
Assets/Scripts/AI/Actions/AIAction.cs
Normal file
72
Assets/Scripts/AI/Actions/AIAction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AI/Actions/AIAction.cs.meta
Normal file
2
Assets/Scripts/AI/Actions/AIAction.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fde981af550c7988ebf29c73e80a06b2
|
||||
471
Assets/Scripts/AI/Actions/AIActions.cs
Normal file
471
Assets/Scripts/AI/Actions/AIActions.cs
Normal file
@@ -0,0 +1,471 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// DepositBagAction — Navigate to own vault and deposit carried bags.
|
||||
/// Score increases with more bags carried and when vault is nearby.
|
||||
/// </summary>
|
||||
public class DepositBagAction : AIAction
|
||||
{
|
||||
public override string Name => "DepositBag";
|
||||
|
||||
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.isDead || !bb.isCarrying) return 0f;
|
||||
if (bb.myVault == null) return 0f;
|
||||
|
||||
float score = 0.4f; // Base desire to deposit
|
||||
|
||||
// More bags = stronger urge to deposit (risk increases)
|
||||
score += bb.carriedBagCount * 0.2f;
|
||||
|
||||
// High value = deposit urgently
|
||||
if (bb.totalCarriedValue >= 200f)
|
||||
score += 0.15f;
|
||||
|
||||
// Enemies nearby while carrying = deposit NOW
|
||||
if (bb.nearbyEnemyCount > 0 && bb.nearestEnemyDistance < 12f)
|
||||
score += 0.25f;
|
||||
|
||||
// Low health while carrying = deposit ASAP
|
||||
if (bb.healthPercent < 0.4f)
|
||||
score += 0.2f;
|
||||
|
||||
// Close to vault = just do it
|
||||
float vaultDist = Vector3.Distance(
|
||||
bb.myVault.transform.position,
|
||||
bb.myVault.transform.position // Will be overridden by brain position
|
||||
);
|
||||
|
||||
// Bonus if losing (need to score to catch up)
|
||||
if (bb.scoreDifference < -200f)
|
||||
score += 0.1f;
|
||||
|
||||
return Mathf.Clamp01(score);
|
||||
}
|
||||
|
||||
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.myVault == null) return;
|
||||
|
||||
brain.NavigateTo(bb.myVault.transform.position);
|
||||
|
||||
// Check if close enough to deposit
|
||||
float dist = Vector3.Distance(brain.transform.position, bb.myVault.transform.position);
|
||||
if (dist < 3f)
|
||||
{
|
||||
CashCarrier carrier = brain.GetComponent<CashCarrier>();
|
||||
if (carrier != null && carrier.IsCarrying)
|
||||
{
|
||||
carrier.DepositToVault(bb.myVault);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AttackEnemyAction — Engage nearest enemy. Prioritizes bag carriers and low-HP targets.
|
||||
/// Respects team engagement limits (max 2 AI per enemy).
|
||||
/// </summary>
|
||||
public class AttackEnemyAction : AIAction
|
||||
{
|
||||
public override string Name => "AttackEnemy";
|
||||
public override bool CanInterrupt => true;
|
||||
|
||||
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.isDead) return 0f;
|
||||
if (bb.nearbyEnemyCount == 0) return 0f;
|
||||
|
||||
float score = 0.35f; // Base aggression
|
||||
|
||||
// Enemy carrier spotted — HIGH priority
|
||||
if (bb.nearestEnemyCarrier != null)
|
||||
{
|
||||
float carrierDist = Vector3.Distance(bb.nearestEnemyCarrier.transform.position,
|
||||
bb.myVault != null ? bb.myVault.transform.position : Vector3.zero);
|
||||
score += 0.35f;
|
||||
|
||||
// More bags on carrier = more urgent
|
||||
score += bb.nearestEnemyCarrier.BagCount * 0.1f;
|
||||
}
|
||||
|
||||
// Close enemy = react aggressively
|
||||
float proximityFactor = InverseCurve(bb.nearestEnemyDistance, 15f);
|
||||
score *= (0.5f + proximityFactor * 0.5f);
|
||||
|
||||
// Outnumber advantage
|
||||
if (bb.nearbyAllyCount > bb.nearbyEnemyCount)
|
||||
score += 0.1f;
|
||||
|
||||
// Don't be aggressive when low health (unless enemy carrier)
|
||||
if (bb.healthPercent < 0.3f && bb.nearestEnemyCarrier == null)
|
||||
score *= 0.3f;
|
||||
|
||||
// Carrying bags = less aggressive (protect the payload)
|
||||
if (bb.carriedBagCount > 0)
|
||||
score *= 0.5f;
|
||||
|
||||
return Mathf.Clamp01(score);
|
||||
}
|
||||
|
||||
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
// Choose target: carrier > low HP > nearest
|
||||
Transform target = null;
|
||||
float bestPriority = float.MinValue;
|
||||
|
||||
for (int i = 0; i < bb.perceivedEnemies.Count; i++)
|
||||
{
|
||||
PerceivedEntity enemy = bb.perceivedEnemies[i];
|
||||
if (enemy.gameObject == null) continue;
|
||||
|
||||
// Skip if too many teammates already targeting
|
||||
int engagedCount = tb.GetEnemyTargetCount(enemy.transform);
|
||||
if (engagedCount >= 2) continue;
|
||||
|
||||
float priority = 0f;
|
||||
|
||||
// Carrier priority
|
||||
if (enemy.isCarryingBag)
|
||||
priority += 50f + enemy.carriedBagCount * 20f;
|
||||
|
||||
// Low HP priority
|
||||
priority += (1f - enemy.healthPercent) * 30f;
|
||||
|
||||
// Distance priority (closer = higher)
|
||||
priority += InverseCurve(enemy.distance, 15f) * 20f;
|
||||
|
||||
// Visible bonus
|
||||
if (enemy.isVisible) priority += 10f;
|
||||
|
||||
if (priority > bestPriority)
|
||||
{
|
||||
bestPriority = priority;
|
||||
target = enemy.transform;
|
||||
}
|
||||
}
|
||||
|
||||
if (target == null) return;
|
||||
|
||||
// Register targeting
|
||||
bb.currentTarget = target;
|
||||
tb.AddEnemyTarget(target);
|
||||
|
||||
// Delegate combat to CharacterAIController
|
||||
brain.EngageTarget(target);
|
||||
}
|
||||
|
||||
public override void OnExit(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.currentTarget != null)
|
||||
tb.RemoveEnemyTarget(bb.currentTarget);
|
||||
bb.currentTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DefendVaultAction — Rush to own vault when under attack.
|
||||
/// Highest priority interrupt action.
|
||||
/// </summary>
|
||||
public class DefendVaultAction : AIAction
|
||||
{
|
||||
public override string Name => "DefendVault";
|
||||
public override bool CanInterrupt => true;
|
||||
|
||||
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.isDead) return 0f;
|
||||
if (!bb.myVaultUnderAttack) return 0f;
|
||||
|
||||
float score = 0.9f; // Very high base — vault defense is critical
|
||||
|
||||
// Closer to vault = we should definitely defend
|
||||
if (bb.myVault != null)
|
||||
{
|
||||
float dist = Vector3.Distance(bb.myVault.transform.position, bb.myVault.transform.position);
|
||||
score += InverseCurve(dist, 30f) * 0.1f;
|
||||
}
|
||||
|
||||
return Mathf.Clamp01(score);
|
||||
}
|
||||
|
||||
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.myVault == null) return;
|
||||
|
||||
// If carrying, deposit first
|
||||
CashCarrier carrier = brain.GetComponent<CashCarrier>();
|
||||
float vaultDist = Vector3.Distance(brain.transform.position, bb.myVault.transform.position);
|
||||
|
||||
if (carrier != null && carrier.IsCarrying && vaultDist < 3f)
|
||||
{
|
||||
carrier.DepositToVault(bb.myVault);
|
||||
}
|
||||
|
||||
// Check for enemies near vault
|
||||
Transform nearestEnemyAtVault = null;
|
||||
float nearestDist = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < bb.perceivedEnemies.Count; i++)
|
||||
{
|
||||
PerceivedEntity enemy = bb.perceivedEnemies[i];
|
||||
if (enemy.gameObject == null) continue;
|
||||
|
||||
float enemyVaultDist = Vector3.Distance(enemy.transform.position, bb.myVault.transform.position);
|
||||
if (enemyVaultDist < 10f && enemyVaultDist < nearestDist)
|
||||
{
|
||||
nearestDist = enemyVaultDist;
|
||||
nearestEnemyAtVault = enemy.transform;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestEnemyAtVault != null)
|
||||
{
|
||||
brain.EngageTarget(nearestEnemyAtVault);
|
||||
}
|
||||
else
|
||||
{
|
||||
brain.NavigateTo(bb.myVault.transform.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PatrolAction — Wander randomly via NavMesh when no other objective.
|
||||
/// Lowest-priority fallback action.
|
||||
/// </summary>
|
||||
public class PatrolAction : AIAction
|
||||
{
|
||||
public override string Name => "Patrol";
|
||||
|
||||
private float _pauseEndTime;
|
||||
|
||||
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.isDead) return 0f;
|
||||
|
||||
// Patrol is the default — returns low score so anything else takes priority
|
||||
return 0.1f;
|
||||
}
|
||||
|
||||
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (Time.time < _pauseEndTime) return;
|
||||
|
||||
if (!bb.hasPatrolDestination || brain.HasReachedDestination())
|
||||
{
|
||||
if (bb.hasPatrolDestination)
|
||||
{
|
||||
// Pause at patrol point
|
||||
_pauseEndTime = Time.time + Random.Range(1f, 3f);
|
||||
bb.hasPatrolDestination = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate new patrol destination
|
||||
Vector3 randomPoint = brain.transform.position + Random.insideUnitSphere * 12f;
|
||||
randomPoint.y = brain.transform.position.y;
|
||||
|
||||
UnityEngine.AI.NavMeshHit hit;
|
||||
if (UnityEngine.AI.NavMesh.SamplePosition(randomPoint, out hit, 12f, UnityEngine.AI.NavMesh.AllAreas))
|
||||
{
|
||||
bb.patrolDestination = hit.position;
|
||||
bb.hasPatrolDestination = true;
|
||||
brain.NavigateTo(bb.patrolDestination);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
brain.NavigateTo(bb.patrolDestination);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnExit(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
bb.hasPatrolDestination = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RetreatAction — Fall back to vault when low health.
|
||||
/// Deposits any carried bags along the way.
|
||||
/// </summary>
|
||||
public class RetreatAction : AIAction
|
||||
{
|
||||
public override string Name => "Retreat";
|
||||
public override bool CanInterrupt => true;
|
||||
|
||||
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.isDead) return 0f;
|
||||
|
||||
// Only retreat when health is low
|
||||
if (bb.healthPercent > 0.3f) return 0f;
|
||||
|
||||
float score = 0.7f; // High priority when triggered
|
||||
|
||||
// Lower health = more urgent
|
||||
score += InverseCurve(bb.healthPercent, 0.3f) * 0.3f;
|
||||
|
||||
// Even more urgent if carrying bags
|
||||
if (bb.isCarrying)
|
||||
score += 0.1f;
|
||||
|
||||
// Less urgent if no enemies nearby
|
||||
if (bb.nearbyEnemyCount == 0)
|
||||
score *= 0.6f;
|
||||
|
||||
return Mathf.Clamp01(score);
|
||||
}
|
||||
|
||||
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.myVault == null) return;
|
||||
|
||||
brain.NavigateTo(bb.myVault.transform.position);
|
||||
|
||||
// Deposit if close to vault
|
||||
float vaultDist = Vector3.Distance(brain.transform.position, bb.myVault.transform.position);
|
||||
if (vaultDist < 3f)
|
||||
{
|
||||
CashCarrier carrier = brain.GetComponent<CashCarrier>();
|
||||
if (carrier != null && carrier.IsCarrying)
|
||||
carrier.DepositToVault(bb.myVault);
|
||||
}
|
||||
|
||||
// If health recovered, exit retreat
|
||||
if (bb.healthPercent > 0.5f)
|
||||
{
|
||||
// Score will drop, brain will switch naturally
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EscortCarrierAction — Follow and protect an ally who is carrying bags.
|
||||
/// Maintains distance, engages threats approaching the carrier.
|
||||
/// </summary>
|
||||
public class EscortCarrierAction : AIAction
|
||||
{
|
||||
public override string Name => "EscortCarrier";
|
||||
|
||||
private const float ESCORT_DISTANCE = 4f;
|
||||
private const float THREAT_RANGE = 10f;
|
||||
|
||||
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.isDead) return 0f;
|
||||
if (bb.isCarrying) return 0f; // Don't escort if carrying ourselves
|
||||
|
||||
// Need an ally carrier to escort
|
||||
if (bb.nearestAllyCarrier == null) return 0f;
|
||||
|
||||
float score = 0.45f;
|
||||
|
||||
// More bags on ally = more valuable to escort
|
||||
score += bb.nearestAllyCarrier.BagCount * 0.1f;
|
||||
|
||||
// Enemies near the carrier = escort more important
|
||||
float carrierPos = 0;
|
||||
for (int i = 0; i < bb.perceivedEnemies.Count; i++)
|
||||
{
|
||||
float enemyDist = Vector3.Distance(
|
||||
bb.perceivedEnemies[i].transform.position,
|
||||
bb.nearestAllyCarrier.transform.position);
|
||||
if (enemyDist < THREAT_RANGE)
|
||||
score += 0.15f;
|
||||
}
|
||||
|
||||
// Already close to carrier = maintain escort
|
||||
float distToCarrier = Vector3.Distance(bb.nearestAllyCarrier.transform.position, bb.myVault.transform.position);
|
||||
if (distToCarrier < ESCORT_DISTANCE * 2f)
|
||||
score += 0.1f;
|
||||
|
||||
return Mathf.Clamp01(score);
|
||||
}
|
||||
|
||||
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.nearestAllyCarrier == null) return;
|
||||
|
||||
Vector3 carrierPos = bb.nearestAllyCarrier.transform.position;
|
||||
|
||||
// Check for threats near carrier
|
||||
Transform nearestThreat = null;
|
||||
float nearestThreatDist = float.MaxValue;
|
||||
|
||||
for (int i = 0; i < bb.perceivedEnemies.Count; i++)
|
||||
{
|
||||
PerceivedEntity enemy = bb.perceivedEnemies[i];
|
||||
float distToCarrier = Vector3.Distance(enemy.transform.position, carrierPos);
|
||||
if (distToCarrier < THREAT_RANGE && distToCarrier < nearestThreatDist)
|
||||
{
|
||||
nearestThreatDist = distToCarrier;
|
||||
nearestThreat = enemy.transform;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestThreat != null)
|
||||
{
|
||||
// Engage threat
|
||||
brain.EngageTarget(nearestThreat);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Follow carrier at distance
|
||||
float distToCarrier = Vector3.Distance(brain.transform.position, carrierPos);
|
||||
if (distToCarrier > ESCORT_DISTANCE)
|
||||
{
|
||||
brain.NavigateTo(carrierPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
brain.StopNavigation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InterceptCarrierAction — Chase and attack an enemy who is carrying bags.
|
||||
/// Highest combat priority.
|
||||
/// </summary>
|
||||
public class InterceptCarrierAction : AIAction
|
||||
{
|
||||
public override string Name => "InterceptCarrier";
|
||||
public override bool CanInterrupt => true;
|
||||
|
||||
public override float Score(AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.isDead) return 0f;
|
||||
if (bb.nearestEnemyCarrier == null) return 0f;
|
||||
|
||||
float score = 0.75f; // Very high base priority
|
||||
|
||||
// More bags = more vital to intercept
|
||||
score += bb.nearestEnemyCarrier.BagCount * 0.08f;
|
||||
|
||||
// Close to our vault = CRITICAL
|
||||
if (bb.myVault != null)
|
||||
{
|
||||
float carrierToVault = Vector3.Distance(
|
||||
bb.nearestEnemyCarrier.transform.position,
|
||||
bb.myVault.transform.position);
|
||||
if (carrierToVault < 15f)
|
||||
score += 0.15f;
|
||||
}
|
||||
|
||||
// Low health = less interception
|
||||
if (bb.healthPercent < 0.25f)
|
||||
score *= 0.4f;
|
||||
|
||||
return Mathf.Clamp01(score);
|
||||
}
|
||||
|
||||
public override void Execute(AIBrain brain, AIBlackboard bb, TeamBlackboard tb)
|
||||
{
|
||||
if (bb.nearestEnemyCarrier == null) return;
|
||||
|
||||
brain.EngageTarget(bb.nearestEnemyCarrier.transform);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AI/Actions/AIActions.cs.meta
Normal file
2
Assets/Scripts/AI/Actions/AIActions.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba24431079dff36f3822b97125261be6
|
||||
107
Assets/Scripts/AI/Actions/CollectBagAction.cs
Normal file
107
Assets/Scripts/AI/Actions/CollectBagAction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AI/Actions/CollectBagAction.cs.meta
Normal file
2
Assets/Scripts/AI/Actions/CollectBagAction.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12d6d7471a2b608a38e68d2073778452
|
||||
8
Assets/Scripts/AI/Core.meta
Normal file
8
Assets/Scripts/AI/Core.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf57712f1987ec7d7b6160acc2e1fa7f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
274
Assets/Scripts/AI/Core/AIBlackboard.cs
Normal file
274
Assets/Scripts/AI/Core/AIBlackboard.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AI/Core/AIBlackboard.cs.meta
Normal file
2
Assets/Scripts/AI/Core/AIBlackboard.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f851e5eb528f0fa12a0efb0e7ecde6f6
|
||||
352
Assets/Scripts/AI/Core/AIBrain.cs
Normal file
352
Assets/Scripts/AI/Core/AIBrain.cs
Normal 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
|
||||
}
|
||||
2
Assets/Scripts/AI/Core/AIBrain.cs.meta
Normal file
2
Assets/Scripts/AI/Core/AIBrain.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5557a60f10e7a97e79823d421b552c2d
|
||||
241
Assets/Scripts/AI/Core/AIPerceptionSystem.cs
Normal file
241
Assets/Scripts/AI/Core/AIPerceptionSystem.cs
Normal 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
|
||||
}
|
||||
2
Assets/Scripts/AI/Core/AIPerceptionSystem.cs.meta
Normal file
2
Assets/Scripts/AI/Core/AIPerceptionSystem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09984091d0b1426349f45490e2b656e7
|
||||
8
Assets/Scripts/AI/Navigation.meta
Normal file
8
Assets/Scripts/AI/Navigation.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1513669be5125c8c2bee7148a001ff8d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
145
Assets/Scripts/AI/Navigation/AINavigationHelper.cs
Normal file
145
Assets/Scripts/AI/Navigation/AINavigationHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AI/Navigation/AINavigationHelper.cs.meta
Normal file
2
Assets/Scripts/AI/Navigation/AINavigationHelper.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfe5b90516a45b1bb9448ea8c0a67baa
|
||||
8
Assets/Scripts/AI/Scoring.meta
Normal file
8
Assets/Scripts/AI/Scoring.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4bc8a91e3ceecf97816ea2d740ae97f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
216
Assets/Scripts/AI/Scoring/UtilityScorer.cs
Normal file
216
Assets/Scripts/AI/Scoring/UtilityScorer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AI/Scoring/UtilityScorer.cs.meta
Normal file
2
Assets/Scripts/AI/Scoring/UtilityScorer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84ff338ac6954704cbaa771ed42b9e3e
|
||||
240
Assets/Scripts/AdsManager.cs
Normal file
240
Assets/Scripts/AdsManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/AdsManager.cs.meta
Normal file
11
Assets/Scripts/AdsManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea6ba061c4b33e741b3de0fa3565dc76
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
226
Assets/Scripts/AnimationNames.cs
Normal file
226
Assets/Scripts/AnimationNames.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/AnimationNames.cs.meta
Normal file
2
Assets/Scripts/AnimationNames.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f05e5247740dc4a5a9577cf064b81494
|
||||
8
Assets/Scripts/Announcement.meta
Normal file
8
Assets/Scripts/Announcement.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d813156f460e49243b205b9d1425da84
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
170
Assets/Scripts/Announcement/Announcement.cs
Normal file
170
Assets/Scripts/Announcement/Announcement.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Announcement/Announcement.cs.meta
Normal file
11
Assets/Scripts/Announcement/Announcement.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 814b47897efd4d14fa880dc57b2e2ccb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
548
Assets/Scripts/Announcement/LoadingScreen.cs
Normal file
548
Assets/Scripts/Announcement/LoadingScreen.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Announcement/LoadingScreen.cs.meta
Normal file
11
Assets/Scripts/Announcement/LoadingScreen.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 006b3e96af949e44aaa312f9acd935b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/AssetBundleSystem.meta
Normal file
8
Assets/Scripts/AssetBundleSystem.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e292ac785a1575376b06013ba96ee85b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
347
Assets/Scripts/AssetBundleSystem/AssetBundleDownloadManager.cs
Normal file
347
Assets/Scripts/AssetBundleSystem/AssetBundleDownloadManager.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89228db1021bc63ddaafaef772de5318
|
||||
181
Assets/Scripts/AssetBundleSystem/CustomAssetBundleSceneLoader.cs
Normal file
181
Assets/Scripts/AssetBundleSystem/CustomAssetBundleSceneLoader.cs
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5a2bd36bf588caa4835bea95fcc3985
|
||||
663
Assets/Scripts/AssetBundleSystem/RemoteAssetBundleManager.cs
Normal file
663
Assets/Scripts/AssetBundleSystem/RemoteAssetBundleManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17480a2e38229ac01a32a1cc37aa82a0
|
||||
85
Assets/Scripts/AssetBundleSystem/RemoteAssetCatalog.cs
Normal file
85
Assets/Scripts/AssetBundleSystem/RemoteAssetCatalog.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b442d4ceb7eb5b9a78189a4b3dedd9bf
|
||||
8
Assets/Scripts/Assets.meta
Normal file
8
Assets/Scripts/Assets.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e1487159000ff9b40a0c860d9055714a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Aunthetication.meta
Normal file
8
Assets/Scripts/Aunthetication.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c47f8738dc6b2014f82a6949aac64682
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
215
Assets/Scripts/Aunthetication/UserLogin.cs
Normal file
215
Assets/Scripts/Aunthetication/UserLogin.cs
Normal 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; }
|
||||
}
|
||||
2
Assets/Scripts/Aunthetication/UserLogin.cs.meta
Normal file
2
Assets/Scripts/Aunthetication/UserLogin.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 430bcebc27f4a6c4da036ebc42b18ee2
|
||||
115
Assets/Scripts/Aunthetication/UserRegistration.cs
Normal file
115
Assets/Scripts/Aunthetication/UserRegistration.cs
Normal 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;
|
||||
}
|
||||
2
Assets/Scripts/Aunthetication/UserRegistration.cs.meta
Normal file
2
Assets/Scripts/Aunthetication/UserRegistration.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43b25d92836f2c649ba0e90035bf3b12
|
||||
8
Assets/Scripts/Behaviours.meta
Normal file
8
Assets/Scripts/Behaviours.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f469b33c1b32264fa1a89726a11b981
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Behaviours/Player.meta
Normal file
8
Assets/Scripts/Behaviours/Player.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8fc13f43eb65864db04c81601dcf0cc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
Assets/Scripts/Behaviours/Player/PlayerAnimationBehaviour.cs
Normal file
39
Assets/Scripts/Behaviours/Player/PlayerAnimationBehaviour.cs
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 733c62aa19a5f384ea007d48fd472e50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
209
Assets/Scripts/Behaviours/Player/PlayerController.cs
Normal file
209
Assets/Scripts/Behaviours/Player/PlayerController.cs
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
11
Assets/Scripts/Behaviours/Player/PlayerController.cs.meta
Normal file
11
Assets/Scripts/Behaviours/Player/PlayerController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e20fb9a3b0e7eae4e9e630fc6e8b1a3f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
75
Assets/Scripts/Behaviours/Player/PlayerMovementBehaviour.cs
Normal file
75
Assets/Scripts/Behaviours/Player/PlayerMovementBehaviour.cs
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63b2534bfb74738418425a76f03357fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Assets/Scripts/Behaviours/Player/PlayerUIDisplayBehaviour.cs
Normal file
29
Assets/Scripts/Behaviours/Player/PlayerUIDisplayBehaviour.cs
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90ce320e2fa6597478b575fc077d0c27
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
72
Assets/Scripts/Behaviours/Player/PlayerVisualsBehaviour.cs
Normal file
72
Assets/Scripts/Behaviours/Player/PlayerVisualsBehaviour.cs
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b9c7226a9b7db047a442efcb4f263ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Behaviours/UI.meta
Normal file
8
Assets/Scripts/Behaviours/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 045edeee3a7092d418dde20ef8ae339b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Assets/Scripts/Behaviours/UI/UIBillboardBehaviour.cs
Normal file
29
Assets/Scripts/Behaviours/UI/UIBillboardBehaviour.cs
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Behaviours/UI/UIBillboardBehaviour.cs.meta
Normal file
11
Assets/Scripts/Behaviours/UI/UIBillboardBehaviour.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d237ecbece995f4b99ac5ebab0f1baf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Assets/Scripts/Behaviours/UI/UIContextPanelsBehaviour.cs
Normal file
29
Assets/Scripts/Behaviours/UI/UIContextPanelsBehaviour.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a55701e00d9d61e4d857a2040c6c872d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
100
Assets/Scripts/Behaviours/UI/UIMenuBehaviour.cs
Normal file
100
Assets/Scripts/Behaviours/UI/UIMenuBehaviour.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
11
Assets/Scripts/Behaviours/UI/UIMenuBehaviour.cs.meta
Normal file
11
Assets/Scripts/Behaviours/UI/UIMenuBehaviour.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4cf0d34234797b4f8e05fdf8db216f7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
13
Assets/Scripts/Behaviours/UI/UIMenuOptionBehaviour.cs
Normal file
13
Assets/Scripts/Behaviours/UI/UIMenuOptionBehaviour.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Behaviours/UI/UIMenuOptionBehaviour.cs.meta
Normal file
11
Assets/Scripts/Behaviours/UI/UIMenuOptionBehaviour.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab404cb73cfd59041ae6fa39d672efef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb9fa96d7e2cd5d429bb276c06a46c72
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Assets/Scripts/Behaviours/UI/UIPanelQuitBehaviour.cs
Normal file
20
Assets/Scripts/Behaviours/UI/UIPanelQuitBehaviour.cs
Normal 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
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Behaviours/UI/UIPanelQuitBehaviour.cs.meta
Normal file
11
Assets/Scripts/Behaviours/UI/UIPanelQuitBehaviour.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dda3a4598827131489bee68ae015c43e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/Scripts/Behaviours/UI/UIPanelRebindBehaviour.cs
Normal file
16
Assets/Scripts/Behaviours/UI/UIPanelRebindBehaviour.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Behaviours/UI/UIPanelRebindBehaviour.cs.meta
Normal file
11
Assets/Scripts/Behaviours/UI/UIPanelRebindBehaviour.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d0ee8987180cc640b92a50bdf409025
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
144
Assets/Scripts/Behaviours/UI/UIRebindActionBehaviour.cs
Normal file
144
Assets/Scripts/Behaviours/UI/UIRebindActionBehaviour.cs
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
11
Assets/Scripts/Behaviours/UI/UIRebindActionBehaviour.cs.meta
Normal file
11
Assets/Scripts/Behaviours/UI/UIRebindActionBehaviour.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6005e3b6c6ebbd4a98d34a9488cf066
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Button Click.meta
Normal file
8
Assets/Scripts/Button Click.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f7a99d17f33abe4ab6dc9c57bb6bcc2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/Scripts/Button Click/ButtonClickDetector.cs
Normal file
16
Assets/Scripts/Button Click/ButtonClickDetector.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Button Click/ButtonClickDetector.cs.meta
Normal file
11
Assets/Scripts/Button Click/ButtonClickDetector.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bee16f2af4e522b4c82a66e80d442828
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
40
Assets/Scripts/Button Click/ButtonClickSound.cs
Normal file
40
Assets/Scripts/Button Click/ButtonClickSound.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
12
Assets/Scripts/Button Click/ButtonClickSound.cs.meta
Normal file
12
Assets/Scripts/Button Click/ButtonClickSound.cs.meta
Normal 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:
|
||||
8
Assets/Scripts/Camera.meta
Normal file
8
Assets/Scripts/Camera.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d82fc89bd2e16343bf0f53bd58b742b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
182
Assets/Scripts/Camera/AutoAddCinemachineCamera.cs
Normal file
182
Assets/Scripts/Camera/AutoAddCinemachineCamera.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/AutoAddCinemachineCamera.cs.meta
Normal file
11
Assets/Scripts/Camera/AutoAddCinemachineCamera.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0638b26c337ab6d459ed0f3d377f69f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
69
Assets/Scripts/Camera/CameraFollow.cs
Normal file
69
Assets/Scripts/Camera/CameraFollow.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Camera/CameraFollow.cs.meta
Normal file
11
Assets/Scripts/Camera/CameraFollow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5946991bb03d5b341872abb3e254971d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
188
Assets/Scripts/Camera/CameraInitializer.cs
Normal file
188
Assets/Scripts/Camera/CameraInitializer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Camera/CameraInitializer.cs.meta
Normal file
2
Assets/Scripts/Camera/CameraInitializer.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16d9747b21ad6547384cf9a5e0f0abab
|
||||
50
Assets/Scripts/Camera/CameraShake.cs
Normal file
50
Assets/Scripts/Camera/CameraShake.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Camera/CameraShake.cs.meta
Normal file
2
Assets/Scripts/Camera/CameraShake.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e80f5ecd3b46f8c448c32ad78d00ce04
|
||||
203
Assets/Scripts/Camera/CinemachineCameraInput.cs
Normal file
203
Assets/Scripts/Camera/CinemachineCameraInput.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Camera/CinemachineCameraInput.cs.meta
Normal file
2
Assets/Scripts/Camera/CinemachineCameraInput.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85b5c64e9ac2e5829b1e858392b5955c
|
||||
8
Assets/Scripts/CashSystem.meta
Normal file
8
Assets/Scripts/CashSystem.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a1d7453d1c6c1304b8c80446b3214fe
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
415
Assets/Scripts/CashSystem/CashBag.cs
Normal file
415
Assets/Scripts/CashSystem/CashBag.cs
Normal 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
|
||||
}
|
||||
2
Assets/Scripts/CashSystem/CashBag.cs.meta
Normal file
2
Assets/Scripts/CashSystem/CashBag.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ba5b44417d2bdcb898326c6c046307c
|
||||
685
Assets/Scripts/CashSystem/CashBagSpawner.cs
Normal file
685
Assets/Scripts/CashSystem/CashBagSpawner.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/CashSystem/CashBagSpawner.cs.meta
Normal file
2
Assets/Scripts/CashSystem/CashBagSpawner.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a1fa35c76b8d04cfb343b59d172b2ab
|
||||
679
Assets/Scripts/CashSystem/CashBoxImportController.cs
Normal file
679
Assets/Scripts/CashSystem/CashBoxImportController.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2bc8982e2d3d6af29a9b51c9542690bd
|
||||
390
Assets/Scripts/CashSystem/CashCarrier.cs
Normal file
390
Assets/Scripts/CashSystem/CashCarrier.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/CashSystem/CashCarrier.cs.meta
Normal file
2
Assets/Scripts/CashSystem/CashCarrier.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf4ee3cf212672b8d96990e29bcb472c
|
||||
2167
Assets/Scripts/CashSystem/CashSystemAI.cs
Normal file
2167
Assets/Scripts/CashSystem/CashSystemAI.cs
Normal file
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
Reference in New Issue
Block a user