asset modified

This commit is contained in:
2026-09-25 06:27:30 +09:00
parent 0aba40db2b
commit 84dd12414a
626 changed files with 86385 additions and 112 deletions
@@ -42,6 +42,120 @@ namespace BumpCombat.PlayModeTests
private bool originalForceProductionModeForTests;
#endif
[Test]
public void DefenseTowerArt_ResourcesHaveExpectedFramesAndUnityPivots()
{
int keyCount = 0;
foreach (string key in DefenseTowerArt.Keys)
{
Sprite[] frames = DefenseTowerArt.Load(key);
Assert.That(frames.Length, Is.EqualTo(DefenseTowerArt.ExpectedFrameCount(key)), key);
Assert.That(frames.Length, Is.GreaterThan(0), key);
for (int i = 0; i < frames.Length; i++)
{
Assert.That(frames[i], Is.Not.Null, $"{key} frame {i}");
Assert.That(frames[i].texture.filterMode, Is.EqualTo(FilterMode.Point), key);
Assert.That(frames[i].texture.mipmapCount, Is.EqualTo(1), key);
}
keyCount++;
}
Assert.That(keyCount, Is.EqualTo(132));
AssertSpritePivot("Effects/Fire/Meteor-Flight", 0.625f, 0.375f);
AssertSpritePivot("Effects/Fire/Flamethrower", 0.09375f, 0.5f);
AssertSpritePivot("Effects/Lightning/T1-Bolt", 0.08f, 0.57f);
AssertSpritePivot("Effects/Control/T1-Aura", 0.5f, 0.4f);
AssertSpritePivot("Summons/LavaSlime/GroundSpike-Segment", 0.5f, 0.4f);
}
[UnityTest]
public IEnumerator DefenseTowerLightningBeamFitsMeasuredBoundsAndCollapsesAtZeroDistance()
{
const string key = "Effects/Lightning/T1-Bolt";
Sprite[] frames = DefenseTowerArt.Load(key);
GameObject beamObject = new("Lightning Beam Fit Test");
SpriteRenderer renderer = beamObject.AddComponent<SpriteRenderer>();
DefenseSpriteSequence sequence = beamObject.AddComponent<DefenseSpriteSequence>();
sequence.Play(renderer, key, frames, false);
Vector2 start = new(1.25f, -0.75f);
Vector2 end = new(3.5f, 0.625f);
sequence.FitLightningBeam(start, end, 1f);
HashSet<int> verifiedFrames = new();
float deadline = Time.realtimeSinceStartup + 2f;
while (verifiedFrames.Count < frames.Length && Time.realtimeSinceStartup < deadline)
{
int separator = renderer.sprite.name.LastIndexOf('_');
int frame = int.Parse(renderer.sprite.name.Substring(separator + 1));
if (verifiedFrames.Add(frame))
{
Vector2Int bounds = DefenseTowerArtBounds.LightningBeamXBounds(frame);
float ppu = renderer.sprite.pixelsPerUnit;
Vector3 localStart = new((bounds.x - renderer.sprite.pivot.x) / ppu, 0f, 0f);
Vector3 localEnd = new((bounds.y - renderer.sprite.pivot.x) / ppu, 0f, 0f);
Assert.That(Vector2.Distance(renderer.transform.TransformPoint(localStart), start),
Is.LessThan(0.0001f), $"frame {frame} start");
Assert.That(Vector2.Distance(renderer.transform.TransformPoint(localEnd), end),
Is.LessThan(0.0001f), $"frame {frame} end");
}
yield return null;
}
Assert.That(verifiedFrames.Count, Is.EqualTo(frames.Length),
"Every lightning frame should be checked before the sequence completes.");
sequence.FitLightningBeam(start, start, 1f);
Assert.That(renderer.transform.localScale.x, Is.EqualTo(0f));
Vector2 collapsedStart = renderer.transform.TransformPoint(new Vector3(
(DefenseTowerArtBounds.LightningBeamXBounds(7).x - renderer.sprite.pivot.x)
/ renderer.sprite.pixelsPerUnit, 0f, 0f));
Vector2 collapsedEnd = renderer.transform.TransformPoint(new Vector3(
(DefenseTowerArtBounds.LightningBeamXBounds(7).y - renderer.sprite.pivot.x)
/ renderer.sprite.pixelsPerUnit, 0f, 0f));
Assert.That((collapsedStart - collapsedEnd).sqrMagnitude, Is.EqualTo(0f));
UnityEngine.Object.Destroy(beamObject);
}
[UnityTest]
public IEnumerator DefenseTowerLightningImpactWaveCentersEveryFrameAtTargetShadow()
{
const string key = "Effects/Lightning/ImpactWave";
Sprite[] frames = DefenseTowerArt.Load(key);
GameObject waveObject = new("Lightning Impact Wave Center Test");
SpriteRenderer renderer = waveObject.AddComponent<SpriteRenderer>();
DefenseSpriteSequence sequence = waveObject.AddComponent<DefenseSpriteSequence>();
sequence.Play(renderer, key, frames, false);
Vector2 expectedCenter = new(4.25f, -1.75f);
const float scale = 1.35f;
sequence.CenterImpactWave(expectedCenter, scale);
HashSet<int> verifiedFrames = new();
float deadline = Time.realtimeSinceStartup + 2f;
while (verifiedFrames.Count < frames.Length && Time.realtimeSinceStartup < deadline)
{
int frame = Array.IndexOf(frames, renderer.sprite);
Assert.That(frame, Is.GreaterThanOrEqualTo(0), "The wave must display one of its loaded frames.");
if (verifiedFrames.Add(frame))
{
Vector2 localCenter = (DefenseTowerArtBounds.ImpactWaveCenterPixels(frame, renderer.sprite)
- renderer.sprite.pivot) / renderer.sprite.pixelsPerUnit;
Assert.That(Vector2.Distance(renderer.transform.TransformPoint(localCenter), expectedCenter),
Is.LessThan(0.0001f), $"frame {frame} wave center");
}
yield return null;
}
UnityEngine.Object.Destroy(waveObject);
Assert.That(verifiedFrames.Count, Is.EqualTo(frames.Length),
"Every visible and trailing transparent wave frame should be checked.");
}
private static void AssertSpritePivot(string key, float x, float y)
{
Sprite sprite = DefenseTowerArt.Load(key)[0];
Vector2 normalized = new(
sprite.pivot.x / sprite.rect.width,
sprite.pivot.y / sprite.rect.height);
Assert.That(normalized.x, Is.EqualTo(x).Within(0.001f), key);
Assert.That(normalized.y, Is.EqualTo(y).Within(0.001f), key);
}
[SetUp]
public void SetUp()
{
@@ -232,6 +346,13 @@ namespace BumpCombat.PlayModeTests
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(200), Is.True);
Color[] tierColors = controller.Tuning.SupportTierColors;
Assert.That(tierColors, Has.Length.EqualTo(4));
Assert.That((Color32)tierColors[0], Is.EqualTo(new Color32(151, 169, 119, 102)));
Assert.That((Color32)tierColors[1], Is.EqualTo(new Color32(153, 183, 108, 148)));
Assert.That((Color32)tierColors[2], Is.EqualTo(new Color32(158, 201, 96, 194)));
Assert.That((Color32)tierColors[3], Is.EqualTo(new Color32(170, 218, 103, 242)));
Assert.That(tierColors[0].a, Is.EqualTo(0.4f).Within(0.002f));
DefenseTowerKind[] kinds =
{
DefenseTowerKind.Direct, DefenseTowerKind.Summon,
@@ -243,15 +364,32 @@ namespace BumpCombat.PlayModeTests
DefenseTower tower = BuildTower(controller, player, pad, kinds[pad]);
Transform range = tower.transform.Find("Effect Range");
Assert.That(range, Is.Not.Null, $"{kinds[pad]} range is missing.");
LineRenderer line = range.GetComponent<LineRenderer>();
Assert.That(line, Is.Not.Null);
Assert.That(line.positionCount, Is.GreaterThan(16));
float expected = kinds[pad] == DefenseTowerKind.Summon
? controller.Tuning.SummonAreaRadius
: kinds[pad] == DefenseTowerKind.Empowering
? controller.Tuning.SupportRadius : controller.Tuning.TowerRange;
Assert.That(line.GetPosition(0).x * tower.transform.lossyScale.x,
Is.EqualTo(expected).Within(0.02f));
if (kinds[pad] == DefenseTowerKind.Empowering)
{
SpriteRenderer aura = range.GetComponent<SpriteRenderer>();
Assert.That(aura, Is.Not.Null);
Assert.That(DefenseTowerArt.Load("SupportRange/T1-Aura"), Has.Length.EqualTo(6));
Color expectedRangeColor = controller.Tuning.SupportTierColors[0];
expectedRangeColor.a *= controller.Tuning.SupportRangeAlpha;
Assert.That(aura.color, Is.EqualTo(expectedRangeColor));
RectInt bounds = DefenseConstants.SupportRangeAlphaBounds[0];
float visibleWidth = bounds.width / 32f
* aura.transform.localScale.x * tower.transform.lossyScale.x;
float visibleHeight = bounds.height / 32f
* aura.transform.localScale.y * tower.transform.lossyScale.y;
Assert.That(visibleWidth, Is.EqualTo(controller.Tuning.SupportRadius * 2f).Within(0.02f));
Assert.That(visibleHeight, Is.EqualTo(controller.Tuning.SupportRadius * 2f).Within(0.02f));
}
else
{
LineRenderer line = range.GetComponent<LineRenderer>();
Assert.That(line, Is.Not.Null);
Assert.That(line.positionCount, Is.GreaterThan(16));
float expected = kinds[pad] == DefenseTowerKind.Summon
? controller.Tuning.SummonAreaRadius : controller.Tuning.TowerRange;
Assert.That(line.GetPosition(0).x * tower.transform.lossyScale.x,
Is.EqualTo(expected).Within(0.02f));
}
}
DefenseTower fire = FindTower(0, DefenseTowerKind.Direct);
@@ -285,11 +423,44 @@ namespace BumpCombat.PlayModeTests
for (int i = 0; i < kinds.Length; i++)
{
DefenseTower tower = BuildTower(controller, player, i, kinds[i]);
Assert.That(target.Enemy.TryReposition(tower.transform.position), Is.True);
Vector2 targetPosition = tower.transform.position;
if (kinds[i] == DefenseTowerKind.Lightning || kinds[i] == DefenseTowerKind.Seismic)
{
Vector2 casterShadow = tower.transform.TransformPoint(
DefenseTowerArtBounds.CharacterShadowOffset(kinds[i]));
targetPosition = casterShadow
- DefenseTowerArtBounds.EnemyShadowOffsetWorld(target.Enemy);
}
Assert.That(target.Enemy.TryReposition(targetPosition), Is.True);
Physics2D.SyncTransforms();
Assert.That(tower.TryPulse(), Is.True, $"{kinds[i]} should attack.");
GameObject effect = GameObject.Find($"{kinds[i]} Attack Effect");
Assert.That(effect, Is.Not.Null, $"{kinds[i]} attack effect is missing.");
if (kinds[i] == DefenseTowerKind.Seismic)
{
Vector2 expectedShadow = DefenseTowerArtBounds.EnemyShadowWorldPosition(
target.Enemy, target.transform.position);
Assert.That((Vector2)effect.transform.position, Is.EqualTo(expectedShadow),
"Seismic aura and its impact radius debug ring must use the pre-hit enemy shadow anchor.");
}
if (kinds[i] == DefenseTowerKind.Lightning)
{
Transform bolt = effect.transform.Find("Shot Sprite");
Assert.That(bolt, Is.Not.Null);
Assert.That(bolt.localScale.x, Is.EqualTo(0f),
"A zero-distance strike must collapse its visible bolt to zero length.");
Transform wave = effect.transform.Find("Impact Sprite");
SpriteRenderer waveRenderer = wave?.GetComponent<SpriteRenderer>();
Assert.That(waveRenderer, Is.Not.Null);
Vector2 expectedShadow = DefenseTowerArtBounds.EnemyShadowWorldPosition(
target.Enemy, target.transform.position);
Vector2 waveLocalCenter = (DefenseTowerArtBounds.ImpactWaveCenterPixels(
0, waveRenderer.sprite) - waveRenderer.sprite.pivot)
/ waveRenderer.sprite.pixelsPerUnit;
Assert.That(Vector2.Distance(wave.TransformPoint(waveLocalCenter), expectedShadow),
Is.LessThan(0.0001f),
"The visible impact wave center must land on the target's shadow anchor.");
}
SpriteRenderer impact = effect.transform.Find("Impact Sprite")
?.GetComponent<SpriteRenderer>();
Assert.That(impact, Is.Not.Null);
@@ -355,11 +526,11 @@ namespace BumpCombat.PlayModeTests
yield return null;
Sprite sprite = unit.GetComponent<SpriteRenderer>().sprite;
attackSpriteSeen |= sprite != null
&& sprite.name.Contains("BumpAttack");
&& sprite.name.Contains("Attack01");
effectSeen |= GameObject.Find("Summon Attack Effect") != null;
}
Assert.That(attackSpriteSeen, Is.True,
"The summoned swordsman must play the existing attack sheet.");
"The summoned slime must play its approved Attack01 sheet.");
Assert.That(effectSeen, Is.True,
"A successful summon hit must show its impact.");
}
@@ -439,6 +610,83 @@ namespace BumpCombat.PlayModeTests
Assert.That(scout.GetComponentsInChildren<DefenseSummonUnit>().Length,
Is.EqualTo(6));
}
[UnityTest]
public IEnumerator DefensePrototype_SupportAuraShowsOnlyOnActiveRecipientAndCapturesRuntime()
{
try
{
yield return StartDefenseRun();
DefensePrototypeController controller = DefensePrototypeController.Instance;
PlayerController player = UnityEngine.Object.FindAnyObjectByType<PlayerController>();
Assert.That(controller.TryAwardBondLedgerCurrency(200), Is.True);
DefenseTower support = BuildTower(
controller, player, 4, DefenseTowerKind.Empowering);
DefenseTower recipient = BuildTower(
controller, player, 0, DefenseTowerKind.Direct);
DefenseTower outside = BuildTower(
controller, player, 5, DefenseTowerKind.Lightning);
yield return null;
Assert.That(controller.GetActiveSupportFor(support), Is.Null,
"A support tower cannot select itself as its own source.");
Assert.That(controller.GetActiveSupportFor(recipient), Is.SameAs(support));
Assert.That(controller.GetActiveSupportFor(outside), Is.Null);
Assert.That(support.transform.Find("Support Aura Art"), Is.Null);
SpriteRenderer recipientAura = recipient.transform.Find("Support Aura Art")
?.GetComponent<SpriteRenderer>();
Assert.That(recipientAura, Is.Not.Null);
Assert.That(recipientAura.enabled, Is.True);
Assert.That((Vector2)recipientAura.transform.localPosition,
Is.EqualTo((Vector2)recipient.transform.Find("Tower Body").localPosition));
SpriteRenderer recipientBody = recipient.transform.Find("Tower Body").GetComponent<SpriteRenderer>();
Assert.That(recipientAura.sprite.name, Is.EqualTo(recipientBody.sprite.name));
Assert.That(recipientAura.sortingOrder, Is.EqualTo(recipientBody.sortingOrder + 1));
Assert.That(recipientAura.color, Is.EqualTo(controller.Tuning.SupportTierColors[0]));
Assert.That(recipientAura.sprite.texture.name, Does.Contain("T1-Idle"));
Assert.That(outside.transform.Find("Support Aura Art"), Is.Null);
MovePlayerNear(player, Vector2.zero);
PrepareCameraForDefenseCapture();
string capturePath = Environment.GetEnvironmentVariable(
"BUMPCOMBAT_SUPPORT_AURA_CAPTURE_PATH");
if (!string.IsNullOrWhiteSpace(capturePath))
{
string fullPath = Path.GetFullPath(capturePath);
CaptureCameraToPath(fullPath);
}
MovePlayerNear(player, support.transform.position);
for (int tier = 2; tier <= 4; tier++)
{
Assert.That(controller.TryUpgradeTowerAtPad(4, tier == 4
? DefenseTowerBranch.First : DefenseTowerBranch.None), Is.True);
yield return null;
Assert.That(recipientAura.enabled, Is.True);
Assert.That(recipientAura.color, Is.EqualTo(controller.Tuning.SupportTierColors[tier - 1]));
SpriteRenderer sourceRange = support.transform.Find("Effect Range").GetComponent<SpriteRenderer>();
Color expectedSourceColor = controller.Tuning.SupportTierColors[tier - 1];
expectedSourceColor.a *= controller.Tuning.SupportRangeAlpha;
Assert.That(sourceRange.color, Is.EqualTo(expectedSourceColor));
}
if (!string.IsNullOrWhiteSpace(capturePath))
{
string fullPath = Path.GetFullPath(capturePath);
string t4Path = Path.Combine(Path.GetDirectoryName(fullPath),
Path.GetFileNameWithoutExtension(fullPath) + "-t4" + Path.GetExtension(fullPath));
CaptureCameraToPath(t4Path);
}
support.enabled = false;
yield return null;
Assert.That(recipientAura.enabled, Is.False,
"Disabling the support source must hide its recipient aura.");
}
finally
{
RestoreCameraFixture();
}
}
[UnityTest]
public IEnumerator DefensePrototype_SmotheringBoostsTowerDamageInsideSupportAura()
{
@@ -2307,6 +2555,33 @@ namespace BumpCombat.PlayModeTests
return false;
}
private void CaptureCameraToPath(string fullPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
RenderTexture target = new(1920, 1080, 24);
Assert.That(target.Create(), Is.True, "The PlayMode runtime capture target must be created.");
Texture2D capture = new(1920, 1080, TextureFormat.RGB24, false);
RenderTexture previousTarget = captureCamera.targetTexture;
RenderTexture previousActive = RenderTexture.active;
try
{
captureCamera.targetTexture = target;
captureCamera.Render();
RenderTexture.active = target;
capture.ReadPixels(new Rect(0, 0, 1920, 1080), 0, 0);
capture.Apply();
File.WriteAllBytes(fullPath, capture.EncodeToPNG());
}
finally
{
captureCamera.targetTexture = previousTarget;
RenderTexture.active = previousActive;
UnityEngine.Object.Destroy(capture);
target.Release();
UnityEngine.Object.Destroy(target);
}
}
private void PrepareCameraForDefenseCapture()
{
captureCameraFollow = UnityEngine.Object.FindAnyObjectByType<ArenaCameraFollow>();
@@ -2322,20 +2597,22 @@ namespace BumpCombat.PlayModeTests
if (captureCameraFollow != null)
{
captureCameraFollow.enabled = false;
captureCameraFollow.SetOrthographicSize(6f);
captureCameraFollow.SetOrthographicSize(9f);
}
captureCamera.transform.position = new Vector3(
0f, 0f, captureCamera.transform.position.z);
captureCamera.orthographicSize = 6f;
captureCamera.orthographicSize = 9f;
}
private void AssertTowerVisible(DefenseTower tower)
{
Assert.That(tower, Is.Not.Null);
AssertWorldPointVisible(tower.transform.position);
SpriteRenderer renderer = tower.GetComponent<SpriteRenderer>();
SpriteRenderer renderer = tower.transform.Find("Tower Body")
?.GetComponent<SpriteRenderer>();
Assert.That(renderer, Is.Not.Null);
Assert.That(renderer.enabled, Is.True);
Assert.That(renderer.sprite, Is.Not.Null);
}