Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user