508 lines
16 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|