Initial commit: Tiny Tackle Heroes Unity project

This commit is contained in:
2026-09-15 17:37:42 +09:00
commit 591fa9a826
1623 changed files with 182420 additions and 0 deletions
@@ -0,0 +1,394 @@
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using BumpCombat.Combat;
using BumpCombat.Core;
using BumpCombat.Enemies;
using BumpCombat.Player;
using BumpCombat.Progression;
using BumpCombat.Spawning;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
namespace BumpCombat.PlayModeTests
{
public sealed class PressureStreakPlayModeTests
{
private const BindingFlags NonPublicInstance =
BindingFlags.Instance | BindingFlags.NonPublic;
private readonly List<GameObject> testTargets = new();
private bool previousGrantCatalogForTests;
private bool previousProductionModeForTests;
private float previousTimeScale;
private PlayerStats playerStats;
private RunManager runManager;
private SpawnDirector spawnDirector;
private EnemyController ordinarySceneTarget;
[SetUp]
public void SetUp()
{
previousGrantCatalogForTests = ArtifactRewardController.GrantCatalogForTests;
previousProductionModeForTests = RunManager.ForceProductionModeForTests;
previousTimeScale = Time.timeScale;
ArtifactRewardController.GrantCatalogForTests = true;
RunManager.ForceProductionModeForTests = false;
Time.timeScale = 1f;
}
[TearDown]
public void TearDown()
{
foreach (GameObject target in testTargets)
{
if (target != null)
{
Object.Destroy(target);
}
}
testTargets.Clear();
ArtifactRewardController.GrantCatalogForTests = previousGrantCatalogForTests;
RunManager.ForceProductionModeForTests = previousProductionModeForTests;
Time.timeScale = previousTimeScale;
}
[UnityTest]
public IEnumerator TwoKillsDoNotApply_ThirdKillAppliesTwentyPercent()
{
yield return LoadCombatPrototype();
SetRunTime(100f);
EnemyController first = CreateDeadEnemy();
yield return null;
RaiseOrdinaryHit(first);
Assert.That(playerStats.PressureStreakRemaining, Is.GreaterThan(0f));
Assert.That(playerStats.IsPressureStreakActive, Is.False);
SetRunTime(101f);
EnemyController second = CreateDeadEnemy();
RaiseOrdinaryHit(second);
Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(2));
Assert.That(playerStats.PressureStreakRemaining, Is.GreaterThan(0f));
Assert.That(playerStats.IsPressureStreakActive, Is.False);
EnemyController third = CreateDeadEnemy();
yield return null;
RaiseOrdinaryHit(third);
Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(3));
Assert.That(playerStats.IsPressureStreakActive, Is.True);
Assert.That(playerStats.MoveSpeed, Is.EqualTo(3.6f).Within(0.001f));
}
[UnityTest]
public IEnumerator ConsecutiveWindow_UsesKillToKillGap_AndFourthRefreshesWithoutStacking()
{
yield return LoadCombatPrototype();
SetRunTime(200f);
RaiseOrdinaryHit(CreateDeadEnemy());
SetRunTime(202f);
RaiseOrdinaryHit(CreateDeadEnemy());
SetRunTime(204f);
RaiseOrdinaryHit(CreateDeadEnemy());
Assert.That(playerStats.IsPressureStreakActive, Is.True);
SetRunTime(205f);
playerStats.RefreshPressureStreak();
float beforeRefresh = playerStats.PressureStreakRemaining;
RaiseOrdinaryHit(CreateDeadEnemy());
Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(4));
Assert.That(
playerStats.GetStackCount("player.pressure-streak", CharacterStat.MoveSpeed),
Is.EqualTo(1));
Assert.That(playerStats.MoveSpeed, Is.EqualTo(3.6f).Within(0.001f));
Assert.That(playerStats.PressureStreakRemaining, Is.GreaterThan(beforeRefresh));
SetRunTime(208.01f);
playerStats.RefreshPressureStreak();
Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(0));
Assert.That(playerStats.IsPressureStreakActive, Is.False);
SetRunTime(220f);
RaiseOrdinaryHit(CreateDeadEnemy());
SetRunTime(223f);
RaiseOrdinaryHit(CreateDeadEnemy());
SetRunTime(226f);
RaiseOrdinaryHit(CreateDeadEnemy());
Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(3));
Assert.That(playerStats.IsPressureStreakActive, Is.True);
SetRunTime(230.01f);
playerStats.RefreshPressureStreak();
SetRunTime(240f);
RaiseOrdinaryHit(CreateDeadEnemy());
SetRunTime(243.01f);
RaiseOrdinaryHit(CreateDeadEnemy());
SetRunTime(246.02f);
RaiseOrdinaryHit(CreateDeadEnemy());
Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(1));
Assert.That(playerStats.IsPressureStreakActive, Is.False);
}
[UnityTest]
public IEnumerator ArtifactDashNonlethalAndRetiredEvents_DoNotCount()
{
yield return LoadCombatPrototype();
EnemyController artifact = CreateDeadEnemy();
EnemyController dash = CreateDeadEnemy();
EnemyController nonlethal = CreateLivingEnemy();
EnemyController retired = CreateDeadEnemy();
yield return null;
CombatEvents.RaiseValidHit(CreateHit(
artifact.gameObject,
isArtifactHit: true,
sourceId: "artifact.pulse"));
CombatEvents.RaiseValidHit(CreateHit(
dash.gameObject,
isDash: true));
RaiseOrdinaryHit(nonlethal);
CombatEvents.RaiseEnemyRetired(retired.gameObject);
Assert.That(playerStats.PressureStreakKillCount, Is.Zero);
Assert.That(playerStats.IsPressureStreakActive, Is.False);
}
[UnityTest]
public IEnumerator SameEnemyLifetime_IsCountedOnce_AndOtherModifiersRemainOwned()
{
yield return LoadCombatPrototype();
playerStats.AddModifier(new StatModifier(
"level-up.move-speed",
CharacterStat.MoveSpeed,
ModifierOperation.Increased,
0.1f));
SetRunTime(300f);
EnemyController target = CreateDeadEnemy();
yield return null;
RaiseOrdinaryHit(target);
RaiseOrdinaryHit(target);
Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(1));
RaiseOrdinaryHit(CreateDeadEnemy());
RaiseOrdinaryHit(CreateDeadEnemy());
Assert.That(playerStats.MoveSpeed, Is.EqualTo(3.9f).Within(0.001f));
SetRunTime(304.01f);
playerStats.RefreshPressureStreak();
Assert.That(playerStats.GetStackCount(
"level-up.move-speed",
CharacterStat.MoveSpeed), Is.EqualTo(1));
Assert.That(playerStats.GetStackCount(
"player.pressure-streak",
CharacterStat.MoveSpeed), Is.Zero);
Assert.That(playerStats.MoveSpeed, Is.EqualTo(3.3f).Within(0.001f));
}
[UnityTest]
public IEnumerator PauseAndDisable_DoNotConsumeOrRetainPressureStreak()
{
yield return LoadCombatPrototype();
SetRunTime(400f);
RaiseOrdinaryHit(CreateDeadEnemy());
RaiseOrdinaryHit(CreateDeadEnemy());
RaiseOrdinaryHit(CreateDeadEnemy());
float remainingBeforePause = playerStats.PressureStreakRemaining;
runManager.SetSelectionOpen(true);
yield return new WaitForSecondsRealtime(0.2f);
playerStats.RefreshPressureStreak();
Assert.That(
playerStats.PressureStreakRemaining,
Is.EqualTo(remainingBeforePause).Within(0.001f));
runManager.SetSelectionOpen(false);
playerStats.enabled = false;
Assert.That(playerStats.GetStackCount(
"player.pressure-streak",
CharacterStat.MoveSpeed), Is.Zero);
playerStats.enabled = true;
Assert.That(playerStats.PressureStreakKillCount, Is.Zero);
Assert.That(playerStats.IsPressureStreakActive, Is.False);
}
[UnityTest]
public IEnumerator LethalResolverBump_RaisesOrdinaryDeathHitForTheStreak()
{
yield return LoadCombatPrototype();
if (ordinarySceneTarget == null)
{
Assert.That(spawnDirector, Is.Not.Null);
spawnDirector.DebugSpawnImmediate(1);
yield return null;
ordinarySceneTarget = FindOrdinarySceneTarget();
}
Assert.That(ordinarySceneTarget, Is.Not.Null);
ordinarySceneTarget.gameObject.SetActive(true);
yield return null;
SetEnemyState(ordinarySceneTarget, EnemyState.Chase);
FieldInfo healthBackingField = typeof(EnemyController).GetField(
"<CurrentHealth>k__BackingField",
NonPublicInstance);
Assert.That(healthBackingField, Is.Not.Null);
healthBackingField.SetValue(ordinarySceneTarget, 1f);
BumpCombatResolver resolver =
playerStats.GetComponent<BumpCombatResolver>();
Assert.That(resolver, Is.Not.Null);
MethodInfo resolveEnemy = typeof(BumpCombatResolver).GetMethod(
"ResolveEnemy",
NonPublicInstance);
Assert.That(resolveEnemy, Is.Not.Null);
object result = resolveEnemy.Invoke(
resolver,
new object[]
{
ordinarySceneTarget,
false,
(Vector2)ordinarySceneTarget.transform.position,
(Vector2)playerStats.transform.position,
Vector2.right,
HitSide.Front,
false,
100f,
null,
null,
DamageTag.Collision,
null,
0,
null,
});
Assert.That(result, Is.EqualTo(true));
Assert.That(ordinarySceneTarget.IsDead, Is.True);
Assert.That(playerStats.PressureStreakKillCount, Is.EqualTo(1));
}
private IEnumerator LoadCombatPrototype()
{
AsyncOperation load = SceneManager.LoadSceneAsync("CombatPrototype");
while (!load.isDone)
{
yield return null;
}
yield return null;
yield return null;
yield return null;
runManager = RunManager.Instance;
playerStats = Object.FindAnyObjectByType<PlayerStats>();
Assert.That(runManager, Is.Not.Null);
Assert.That(playerStats, Is.Not.Null);
spawnDirector = Object.FindAnyObjectByType<SpawnDirector>();
if (spawnDirector != null)
{
spawnDirector.enabled = false;
}
foreach (EnemyController enemy in Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.None))
{
if (ordinarySceneTarget == null
&& !enemy.IsEventEnemy
&& !enemy.IsDead)
{
ordinarySceneTarget = enemy;
}
enemy.gameObject.SetActive(false);
}
SetRunTime(100f);
playerStats.RefreshPressureStreak();
}
private EnemyController CreateDeadEnemy()
{
GameObject target = new(
"Pressure Streak Dead Target",
typeof(BoxCollider2D));
testTargets.Add(target);
EnemyController enemy = target.AddComponent<EnemyController>();
SetEnemyState(enemy, EnemyState.Dead);
return enemy;
}
private EnemyController CreateLivingEnemy()
{
GameObject target = new(
"Pressure Streak Living Target",
typeof(BoxCollider2D));
testTargets.Add(target);
EnemyController enemy = target.AddComponent<EnemyController>();
SetEnemyState(enemy, EnemyState.Chase);
return enemy;
}
private void RaiseOrdinaryHit(EnemyController enemy)
{
CombatEvents.RaiseValidHit(CreateHit(enemy.gameObject));
}
private CombatHitResult CreateHit(
GameObject target,
bool isDash = false,
bool isArtifactHit = false,
string sourceId = null)
{
return new CombatHitResult(
playerStats.gameObject,
target,
isDash,
HitSide.Front,
1f,
Vector2.right,
0f,
target.transform.position,
sourceId: sourceId,
isArtifactHit: isArtifactHit);
}
private void SetRunTime(float value)
{
FieldInfo elapsedTime = typeof(RunManager).GetField(
"<ElapsedTime>k__BackingField",
NonPublicInstance);
Assert.That(elapsedTime, Is.Not.Null);
elapsedTime.SetValue(runManager, value);
}
private static void SetEnemyState(EnemyController enemy, EnemyState state)
{
FieldInfo stateBackingField = typeof(EnemyController).GetField(
"<State>k__BackingField",
NonPublicInstance);
Assert.That(stateBackingField, Is.Not.Null);
stateBackingField.SetValue(enemy, state);
}
private static EnemyController FindOrdinarySceneTarget()
{
foreach (EnemyController enemy in Object.FindObjectsByType<EnemyController>(
FindObjectsSortMode.None))
{
if (enemy != null
&& !enemy.IsDead
&& !enemy.IsEventEnemy
&& !enemy.IsSummoned)
{
return enemy;
}
}
return null;
}
}
}