Files
tiny_tackle_heroes/Assets/_Project/Scripts/Progression/RunMissionTracker.cs
T

508 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using BumpCombat.Combat;
using BumpCombat.Constants;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using UnityEngine;
namespace BumpCombat.Progression
{
public readonly struct RunMissionDefinition
{
public RunMissionDefinition(
string id,
string name,
string condition,
int target,
int experienceReward,
bool requiresGuardUnlock = false,
bool requiresChargeUnlock = false)
{
Id = id;
Name = name;
Condition = condition;
Target = target;
ExperienceReward = experienceReward;
RequiresGuardUnlock = requiresGuardUnlock;
RequiresChargeUnlock = requiresChargeUnlock;
}
public string Id { get; }
public string Name { get; }
public string Condition { get; }
public int Target { get; }
public int ExperienceReward { get; }
public bool RequiresGuardUnlock { get; }
public bool RequiresChargeUnlock { get; }
}
public readonly struct RunMissionProgress
{
public RunMissionProgress(
RunMissionDefinition definition,
int progress,
bool completed,
bool locked)
{
Definition = definition;
Progress = progress;
Completed = completed;
Locked = locked;
}
public RunMissionDefinition Definition { get; }
public int Progress { get; }
public bool Completed { get; }
public bool Locked { get; }
}
/// <summary>Tracks the fixed run missions from confirmed gameplay results.</summary>
[DisallowMultipleComponent]
public sealed class RunMissionTracker : MonoBehaviour
{
public const int MissionCount = 11;
private static readonly string[] DefinitionIds =
{
MissionIds.FirstSweep,
MissionIds.GrowthFooting,
MissionIds.BodyBreakthrough,
MissionIds.ArtifactUse,
MissionIds.RearAttack,
MissionIds.CrowdControl,
MissionIds.CancelAttack,
MissionIds.GuardDefense,
MissionIds.MatchShield,
MissionIds.ChargedMoment,
MissionIds.Rampage,
};
private readonly int[] progress = new int[MissionCount];
private readonly bool[] completed = new bool[MissionCount];
private readonly HashSet<int> effectiveArtifactCastIds = new();
private readonly HashSet<int> chargedArtifactCastIds = new();
private readonly Dictionary<int, HashSet<int>> targetsPerArtifactCast = new();
private readonly HashSet<int> rearBumpedEnemyLifetimes = new();
private readonly HashSet<int> guardActivationsThatBlocked = new();
private readonly HashSet<int> processedEnemyDeaths = new();
private readonly Queue<float> recentNonsummonedKillTimes = new();
private RunManager runManager;
private ExperienceSystem experienceSystem;
private PlayerHealth playerHealth;
private UserProfileStore profileStore;
private bool runHasStarted;
public static RunMissionTracker Instance { get; private set; }
public static float RampageWindowSeconds =>
GameplayConstants.Current.LevelUp.RampageWindowSeconds;
public static int RampageKillTarget => GetDefinition(MissionCount - 1).Target;
public event Action OnProgressChanged;
public UserProfileStore ProfileStore => profileStore;
public int MissionsCompletedThisRun
{
get
{
int result = 0;
for (int i = 0; i < completed.Length; i++)
{
if (completed[i]) result++;
}
return result;
}
}
public static RunMissionDefinition GetDefinition(int index)
{
index = Mathf.Clamp(index, 0, MissionCount - 1);
LevelUpConstants constants = GameplayConstants.Current.LevelUp;
MissionTuning tuning = constants.GetMission(index, default);
string condition = string.Format(
tuning.ConditionFormat ?? string.Empty,
tuning.Target,
constants.RampageWindowSeconds);
return new RunMissionDefinition(
DefinitionIds[index],
tuning.DisplayName ?? string.Empty,
condition,
tuning.Target,
tuning.ExperienceReward,
tuning.RequiresGuardUnlock,
tuning.RequiresChargeUnlock);
}
private void Awake()
{
if (Instance == null || Instance == this)
{
Instance = this;
}
profileStore = new UserProfileStore();
}
private void Start()
{
runManager = RunManager.Instance;
experienceSystem = FindAnyObjectByType<ExperienceSystem>();
playerHealth = FindAnyObjectByType<PlayerHealth>();
if (runManager != null)
{
runManager.OnRunStarted += HandleRunStarted;
runManager.OnGameOver += HandleGameOver;
}
if (experienceSystem != null)
{
experienceSystem.OnExperienceOrbPickedUp += HandleExperienceOrbPickedUp;
}
if (playerHealth != null)
{
playerHealth.OnGuardAttackBlocked += HandleGuardAttackBlocked;
}
CombatEvents.OnEnemyDied += HandleEnemyDied;
CombatEvents.OnValidHit += HandleValidHit;
CombatEvents.OnArtifactEffectiveHit += HandleArtifactEffectiveHit;
CombatEvents.OnRearAttackCancelled += HandleRearAttackCancelled;
if (runManager != null
&& !runManager.IsTitleScreen
&& !runManager.IsGameOver)
{
HandleRunStarted();
}
}
private void Update()
{
if (!runHasStarted || completed[MissionCount - 1])
{
return;
}
RefreshRampageProgress(GetCombatTime());
}
private void OnDestroy()
{
if (runManager != null)
{
runManager.OnRunStarted -= HandleRunStarted;
runManager.OnGameOver -= HandleGameOver;
}
if (experienceSystem != null)
{
experienceSystem.OnExperienceOrbPickedUp -= HandleExperienceOrbPickedUp;
}
if (playerHealth != null)
{
playerHealth.OnGuardAttackBlocked -= HandleGuardAttackBlocked;
}
CombatEvents.OnEnemyDied -= HandleEnemyDied;
CombatEvents.OnValidHit -= HandleValidHit;
CombatEvents.OnArtifactEffectiveHit -= HandleArtifactEffectiveHit;
CombatEvents.OnRearAttackCancelled -= HandleRearAttackCancelled;
if (Instance == this)
{
Instance = null;
}
}
public RunMissionProgress GetProgress(int index)
{
index = Mathf.Clamp(index, 0, MissionCount - 1);
RunMissionDefinition definition = GetDefinition(index);
bool locked = IsLocked(definition);
return new RunMissionProgress(
definition,
progress[index],
completed[index],
locked);
}
public int GetLifetimeMissionCompletions()
{
return profileStore?.GetTotalMissionCompletions() ?? 0;
}
public int GetLifetimeRampageCompletions()
{
return profileStore?.GetMissionCompletions(MissionIds.Rampage) ?? 0;
}
public void UseProfileStoreForTests(UserProfileStore isolatedStore)
{
if (isolatedStore != null)
{
profileStore = isolatedStore;
}
}
private void HandleRunStarted()
{
Array.Clear(progress, 0, progress.Length);
Array.Clear(completed, 0, completed.Length);
effectiveArtifactCastIds.Clear();
chargedArtifactCastIds.Clear();
targetsPerArtifactCast.Clear();
rearBumpedEnemyLifetimes.Clear();
guardActivationsThatBlocked.Clear();
processedEnemyDeaths.Clear();
recentNonsummonedKillTimes.Clear();
runHasStarted = true;
OnProgressChanged?.Invoke();
}
private void HandleGameOver()
{
runHasStarted = false;
}
private void HandleEnemyDied(GameObject target)
{
if (!runHasStarted || target == null)
{
return;
}
EnemyController enemy = target.GetComponent<EnemyController>();
if (enemy == null || enemy.IsSummoned
|| (completed[0] && completed[MissionCount - 1])
|| !processedEnemyDeaths.Add(enemy.SpawnLifetimeIdentity))
{
return;
}
if (!enemy.IsEventEnemy)
{
AddProgress(0, 1);
}
if (!completed[MissionCount - 1])
{
float now = GetCombatTime();
recentNonsummonedKillTimes.Enqueue(now);
RefreshRampageProgress(now);
}
}
private void HandleExperienceOrbPickedUp()
{
if (runHasStarted)
{
AddProgress(1, 1);
}
}
private void HandleValidHit(CombatHitResult result)
{
if (!runHasStarted
|| result.IsArtifactHit
|| result.IsDash
|| result.Damage <= 0f
|| result.Target == null)
{
return;
}
AddProgress(2, 1);
if (result.Side != HitSide.Back)
{
return;
}
EnemyController enemy = result.Target.GetComponent<EnemyController>();
if (!completed[4]
&& enemy != null
&& rearBumpedEnemyLifetimes.Add(enemy.SpawnLifetimeIdentity))
{
AddProgress(4, 1);
}
}
private void HandleArtifactEffectiveHit(ArtifactEffectiveHitResult result)
{
if (!runHasStarted || result.Target == null || result.CastIdentity <= 0)
{
return;
}
if (!completed[3] && effectiveArtifactCastIds.Add(result.CastIdentity))
{
AddProgress(3, 1);
}
if (result.IsCharged
&& !completed[9]
&& chargedArtifactCastIds.Add(result.CastIdentity))
{
AddProgress(9, 1);
}
if (result.MatchedShieldRequirement && !completed[8])
{
AddProgress(8, 1);
}
EnemyController enemy = result.Target.GetComponent<EnemyController>();
if (enemy == null)
{
return;
}
if (completed[5])
{
return;
}
if (!targetsPerArtifactCast.TryGetValue(
result.CastIdentity,
out HashSet<int> targets))
{
targets = new HashSet<int>();
targetsPerArtifactCast.Add(result.CastIdentity, targets);
}
if (targets.Add(enemy.SpawnLifetimeIdentity)
&& targets.Count > progress[5])
{
progress[5] = Mathf.Min(
GetDefinition(5).Target,
targets.Count);
OnProgressChanged?.Invoke();
if (progress[5] >= GetDefinition(5).Target)
{
targetsPerArtifactCast.Clear();
CompleteMission(5);
}
}
}
private void HandleRearAttackCancelled(GameObject target)
{
if (!runHasStarted || target == null)
{
return;
}
EnemyController enemy = target.GetComponent<EnemyController>();
if (!completed[6]
&& enemy != null
&& !enemy.IsEventEnemy
&& !enemy.IsSummoned)
{
AddProgress(6, 1);
}
}
private void HandleGuardAttackBlocked(int guardActivationIdentity)
{
if (!runHasStarted || guardActivationIdentity <= 0)
{
return;
}
if (!completed[7]
&& guardActivationsThatBlocked.Add(guardActivationIdentity))
{
AddProgress(7, 1);
}
}
private void RefreshRampageProgress(float now)
{
if (completed[MissionCount - 1])
{
return;
}
float cutoff = now - RampageWindowSeconds;
while (recentNonsummonedKillTimes.Count > 0
&& recentNonsummonedKillTimes.Peek() < cutoff)
{
recentNonsummonedKillTimes.Dequeue();
}
int recentKills = Mathf.Min(
RampageKillTarget,
recentNonsummonedKillTimes.Count);
int index = MissionCount - 1;
if (progress[index] != recentKills)
{
progress[index] = recentKills;
OnProgressChanged?.Invoke();
}
if (recentKills >= RampageKillTarget)
{
CompleteMission(index);
}
}
private void AddProgress(int index, int amount)
{
if (index < 0 || index >= MissionCount || amount <= 0
|| completed[index] || IsLocked(GetDefinition(index)))
{
return;
}
RunMissionDefinition definition = GetDefinition(index);
int next = Mathf.Min(definition.Target, progress[index] + amount);
if (next == progress[index])
{
return;
}
progress[index] = next;
OnProgressChanged?.Invoke();
if (next >= definition.Target)
{
CompleteMission(index);
}
}
private void CompleteMission(int index)
{
if (completed[index])
{
return;
}
completed[index] = true;
RunMissionDefinition definition = GetDefinition(index);
if (index == MissionCount - 1)
{
recentNonsummonedKillTimes.Clear();
}
if (runManager != null && runManager.IsProductionRun)
{
profileStore?.IncrementMissionCompletion(definition.Id);
}
OnProgressChanged?.Invoke();
// Keep accepting the remaining contacts from an action even if this
// award opens a level-up selection and pauses the same frame.
experienceSystem?.AddMissionExperience(definition.ExperienceReward);
}
private bool IsLocked(RunMissionDefinition definition)
{
if (runManager == null)
{
return false;
}
return (definition.RequiresGuardUnlock && !runManager.IsGuardUnlocked)
|| (definition.RequiresChargeUnlock && !runManager.IsArtifactChargeUnlocked);
}
private float GetCombatTime()
{
return runManager != null ? runManager.ElapsedTime : Time.time;
}
}
}