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,333 @@
using BumpCombat.Combat;
using BumpCombat.Enemies;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace BumpCombat.Tests.EditMode
{
public sealed class InteractionFeedbackTests
{
[TestCase("Slime/Slime_Idle", 46)]
[TestCase("Bat/Bat_Idle-shadow-v2", 40)]
[TestCase("ArmoredSkeleton/ArmoredSkeleton_Idle-shadow-v2", 39)]
[TestCase("NecroGolem/NecroGolem_Idle-shadow-v2", 29)]
public void CrowdControlHeadAnchor_IgnoresHiddenRendererAndTracksScaledHead(string asset, int top)
{
var texture = AssetDatabase.LoadAssetAtPath<Texture2D>("Assets/_Project/Art/Characters/" + asset + ".png");
Assert.That(texture, Is.Not.Null);
var sprite = Sprite.Create(texture, new Rect(0, 0, 100, 100), new Vector2(.5f, .5f), 32f);
var target = new GameObject("CC Head Test");
var renderer = target.AddComponent<SpriteRenderer>();
renderer.sprite = sprite;
renderer.enabled = false;
foreach (float scale in new[] { 1f, 1.25f, 2f })
{
target.transform.position = new Vector3(3f, -2f, 0f);
target.transform.localScale = Vector3.one * scale;
Vector3 actual = target.transform.TransformPoint(CombatFeedback.CalculateStunMarkerLocalPosition(target.transform, renderer));
Assert.That(actual.x, Is.EqualTo(3f).Within(.0001f));
Assert.That(actual.y, Is.EqualTo(-2f + (50f - top) / 32f * scale + (4f + 5f * scale) / 32f).Within(.0001f));
}
Object.DestroyImmediate(target);
Object.DestroyImmediate(sprite);
}
[Test]
public void CrowdControlSpiral_UsesDistinctPalettesAndLoopsEightFrames()
{
Sprite first = CrowdControlMarkerArt.Frame(0f, false);
Assert.That(first, Is.Not.Null);
Assert.That(first.rect.size, Is.EqualTo(new Vector2(32f, 24f)));
Assert.That(first.texture.filterMode, Is.EqualTo(FilterMode.Point));
Assert.That(CrowdControlMarkerArt.Frame(.07f, false).rect.x, Is.EqualTo(32f));
Assert.That(CrowdControlMarkerArt.Frame(.481f, false), Is.SameAs(first));
Assert.That(CrowdControlMarkerArt.Frame(0f, true).texture, Is.Not.SameAs(first.texture));
}
[Test]
public void StrongDashLaunchResult_IsDistinctFromStrongDashIntent()
{
CombatHitResult resisted = new(
null,
null,
true,
HitSide.Front,
1f,
Vector2.right,
1f,
Vector2.zero,
false,
true,
DamageTag.Collision,
"Dash",
true,
true,
false);
CombatHitResult launched = new(
null,
null,
true,
HitSide.Front,
1f,
Vector2.right,
1f,
Vector2.zero,
false,
true,
DamageTag.Collision,
"Dash",
true,
true,
true);
Assert.That(resisted.IsStrongDash, Is.True);
Assert.That(resisted.LaunchSucceeded, Is.False);
Assert.That(launched.IsStrongDash, Is.True);
Assert.That(launched.LaunchSucceeded, Is.True);
}
[TestCase(0.09f, false)]
[TestCase(0.1f, true)]
[TestCase(0.5f, true)]
public void PulseGatherDistance_UsesMinimumStreakThreshold(float distance, bool expectedStreak)
{
Assert.That(CombatFeedback.ShouldShowPulseStreak(distance), Is.EqualTo(expectedStreak));
}
[TestCase(0.49f, false)]
[TestCase(0.5f, true)]
public void KnockbackDistance_UsesMinimumDustThreshold(float distance, bool expectedDust)
{
Assert.That(CombatFeedback.ShouldShowKnockbackDust(distance), Is.EqualTo(expectedDust));
}
[TestCase(true, false, true)]
[TestCase(true, true, false)]
[TestCase(false, false, false)]
public void LaunchResult_SeparatesResistFromDeath(
bool canBeLaunched,
bool isDead,
bool expectedLaunch)
{
Assert.That(
BumpCombatResolver.CanLaunchAfterHit(canBeLaunched, isDead),
Is.EqualTo(expectedLaunch));
Assert.That(
BumpCombatResolver.ShouldShowLaunchResist(canBeLaunched),
Is.EqualTo(!canBeLaunched));
}
[TestCase(false, true, false, true)]
[TestCase(true, true, false, false)]
[TestCase(false, false, false, false)]
[TestCase(false, true, true, false)]
public void LineTransientRecycle_RejectsPendingOrDuplicateEntries(
bool destroyPending,
bool activeRegistrationRemoved,
bool alreadyPooled,
bool expected)
{
Assert.That(
CombatFeedback.CanRecycleLineTransient(
destroyPending,
activeRegistrationRemoved,
alreadyPooled),
Is.EqualTo(expected));
Assert.That(
CombatFeedback.MaxConcurrentHeavyEffects,
Is.EqualTo(12));
}
[Test]
public void WorldStreakRenderer_ResetsRingLoopState()
{
GameObject host = new("Line Renderer State Test");
LineRenderer line = host.AddComponent<LineRenderer>();
line.loop = true;
line.useWorldSpace = false;
CombatFeedback.ConfigureWorldStreakRenderer(
line,
null,
Color.white,
0.04f,
Vector2.zero,
Vector2.right);
Assert.That(line.loop, Is.False);
Assert.That(line.useWorldSpace, Is.True);
Assert.That(line.positionCount, Is.EqualTo(2));
Object.DestroyImmediate(host);
}
[TestCase(false, false, true, true)]
[TestCase(false, true, true, false)]
[TestCase(false, false, false, false)]
[TestCase(true, false, true, false)]
public void LineTransientAcquire_RejectsPendingOrInvalidEntries(
bool unityNull,
bool destroyPending,
bool hasLineRenderer,
bool expected)
{
Assert.That(
CombatFeedback.CanAcquireLineTransient(
unityNull,
destroyPending,
hasLineRenderer),
Is.EqualTo(expected));
}
[Test]
public void StunMarkerAnchor_FallbackKeepsClearanceAndScalesWithCharacter()
{
GameObject target = new("Stun Marker Anchor Test");
SpriteRenderer targetRenderer = target.AddComponent<SpriteRenderer>();
Sprite sprite = Sprite.Create(
Texture2D.whiteTexture,
new Rect(0f, 0f, 1f, 1f),
new Vector2(0.5f, 0.5f),
32f);
targetRenderer.sprite = sprite;
Vector3 runeLocalScale = new(0.11f, 0.035f, 1f);
foreach (float targetScale in new[] { 0.8f, 1f, 1.25f, 1.6f, 2f, 2.5f, 3.5f })
{
Vector3 expectedWorldScale = Vector3.Scale(new Vector3(.5f * targetScale, .5f * targetScale, 1f), runeLocalScale);
target.transform.localScale = Vector3.one * targetScale;
Vector3 localAnchor = CombatFeedback.CalculateStunMarkerLocalPosition(
target.transform,
targetRenderer);
Vector3 worldAnchor = target.transform.TransformPoint(localAnchor);
Assert.That(worldAnchor.x, Is.EqualTo(target.transform.position.x).Within(0.0001f));
Assert.That(
worldAnchor.y,
Is.EqualTo(targetRenderer.bounds.max.y + (4f + 5f * targetScale) / 32f).Within(0.0001f));
Vector3 baseLocalScale = CombatFeedback.CalculateStunMarkerBaseLocalScale(
target.transform.lossyScale);
Vector3 worldScale = Vector3.Scale(
target.transform.lossyScale,
Vector3.Scale(baseLocalScale, runeLocalScale));
Assert.That(worldScale.x, Is.EqualTo(expectedWorldScale.x).Within(0.0001f));
Assert.That(worldScale.y, Is.EqualTo(expectedWorldScale.y).Within(0.0001f));
}
Object.DestroyImmediate(sprite);
Object.DestroyImmediate(target);
}
[TestCase(485)]
[TestCase(1000)]
[TestCase(1515)]
public void StunMarkerSortingOrder_StaysAheadOfTarget(int targetSortingOrder)
{
Assert.That(
CombatFeedback.CalculateStunMarkerSortingOrder(targetSortingOrder),
Is.GreaterThan(targetSortingOrder));
}
[Test]
public void BumpImpactGrades_UseHitSideAndFixedFrameTiming()
{
Assert.That(
CombatFeedback.GetBumpImpactResourcePath(HitSide.Front),
Is.EqualTo("Combat/BumpImpact-Weak-v1"));
Assert.That(
CombatFeedback.GetBumpImpactResourcePath(HitSide.Side),
Is.EqualTo("Combat/BumpImpact-Medium-v1"));
Assert.That(
CombatFeedback.GetBumpImpactResourcePath(HitSide.Back),
Is.EqualTo("Combat/BumpImpact-Strong-v1"));
Assert.That(
CombatFeedback.GetBumpImpactFrameDuration(0),
Is.EqualTo(0.03f));
Assert.That(
CombatFeedback.GetBumpImpactFrameDuration(1),
Is.EqualTo(0.04f));
Assert.That(
CombatFeedback.GetBumpImpactFrameDuration(2),
Is.EqualTo(0.04f));
Assert.That(
CombatFeedback.GetBumpImpactFrameDuration(3),
Is.EqualTo(0.03f));
}
[Test]
public void BumpImpactResources_ContainFourCenteredWhitePixelFrames()
{
foreach (HitSide side in System.Enum.GetValues(typeof(HitSide)))
{
Sprite[] frames = Resources.LoadAll<Sprite>(
CombatFeedback.GetBumpImpactResourcePath(side));
System.Array.Sort(
frames,
(left, right) => System.String.CompareOrdinal(left.name, right.name));
Assert.That(frames, Has.Length.EqualTo(CombatFeedback.BumpImpactFrameCount));
for (int i = 0; i < frames.Length; i++)
{
Assert.That(frames[i].name, Does.EndWith($"_{i}"));
Assert.That(frames[i].rect.width, Is.EqualTo(64f));
Assert.That(frames[i].rect.height, Is.EqualTo(64f));
Assert.That(frames[i].pixelsPerUnit, Is.EqualTo(32f));
Assert.That(frames[i].pivot.x, Is.EqualTo(32f));
Assert.That(frames[i].pivot.y, Is.EqualTo(32f));
Assert.That(frames[i].texture, Is.Not.Null);
}
}
}
[Test]
public void SwordsmanController_WiresCombatStatesToExpectedClipLengths()
{
const string controllerPath =
"Assets/_Project/Art/Characters/Swordsman/Animations/Swordsman.controller";
AnimatorController controller =
AssetDatabase.LoadAssetAtPath<AnimatorController>(controllerPath);
Assert.That(controller, Is.Not.Null);
AnimatorState bumpAttack = FindState(controller, "BumpAttack");
AnimatorState artifactUse = FindState(controller, "ArtifactUse");
Assert.That(bumpAttack, Is.Not.Null);
Assert.That(artifactUse, Is.Not.Null);
Assert.That((bumpAttack.motion as AnimationClip).length, Is.EqualTo(0.24f).Within(0.0001f));
Assert.That((artifactUse.motion as AnimationClip).length, Is.EqualTo(0.4f).Within(0.0001f));
Assert.That(bumpAttack.transitions, Has.Length.EqualTo(2));
Assert.That(artifactUse.transitions, Has.Length.EqualTo(2));
foreach (AnimatorStateTransition transition in bumpAttack.transitions)
{
Assert.That(transition.hasExitTime, Is.True);
Assert.That(transition.exitTime, Is.EqualTo(1f));
}
foreach (AnimatorStateTransition transition in artifactUse.transitions)
{
Assert.That(transition.hasExitTime, Is.True);
Assert.That(transition.exitTime, Is.EqualTo(1f));
}
foreach (AnimatorControllerParameter parameter in controller.parameters)
{
Assert.That(parameter.name, Is.Not.EqualTo("BumpAttack"));
Assert.That(parameter.name, Is.Not.EqualTo("ArtifactUse"));
}
}
private static AnimatorState FindState(
AnimatorController controller,
string stateName)
{
foreach (ChildAnimatorState child in controller.layers[0].stateMachine.states)
{
if (child.state.name == stateName)
{
return child.state;
}
}
return null;
}
}
}