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
@@ -508,6 +508,38 @@ namespace BumpCombat.Defense
return targets;
}
public DefenseTower GetActiveSupportFor(DefenseTower tower)
{
if (!CanOperateTowers || tower == null || !tower.IsOperational
|| tower.Kind == DefenseTowerKind.Empowering)
{
return null;
}
DefenseTower strongestSupport = null;
float strongestBonus = 0f;
foreach (DefenseTower support in towers)
{
if (support == null || support == tower || !support.IsOperational
|| support.Kind != DefenseTowerKind.Empowering
|| Vector2.Distance(support.transform.position,
tower.transform.position) > tuning.SupportRadius)
{
continue;
}
float strength = support.Tier * GetSupportSightEfficiency(support);
if (strength > strongestBonus
&& tuning.SupportDamageBonusPerTier * strength > 0f)
{
strongestSupport = support;
strongestBonus = strength;
}
}
return strongestSupport;
}
public float GetTowerAttackIntervalMultiplier(DefenseTower tower)
{
if (!CanOperateTowers || tower == null)
@@ -0,0 +1,172 @@
using UnityEngine;
using BumpCombat.Constants;
namespace BumpCombat.Defense
{
[DisallowMultipleComponent]
public sealed class DefenseSpriteSequence : MonoBehaviour
{
private SpriteRenderer target;
private Sprite[] frames;
private string key;
private float frameRemaining;
private int frame;
private Sprite[] nextFrames;
private string nextKey;
private bool destroyWhenFinished;
private float delayRemaining;
private bool animationFinished;
private Vector2 travelStart;
private Vector2 travelEnd;
private float travelDuration;
private float travelElapsed;
private bool fitsLightningBeam;
private Vector2 beamStart;
private Vector2 beamEnd;
private float beamThickness;
private bool centersImpactWave;
private Vector2 impactWaveCenter;
private float impactWaveScale;
public void Play(SpriteRenderer renderer, string sequenceKey, Sprite[] sequenceFrames,
bool destroyOnFinish, string followingKey = null,
Sprite[] followingFrames = null, float startDelay = 0f)
{
target = renderer;
key = sequenceKey;
frames = sequenceFrames;
destroyWhenFinished = destroyOnFinish;
nextKey = followingKey;
nextFrames = followingFrames;
delayRemaining = Mathf.Max(0f, startDelay);
if (target == null || frames == null || frames.Length == 0)
{
enabled = false;
if (destroyWhenFinished) Destroy(gameObject);
return;
}
frame = 0;
target.sprite = frames[frame];
target.enabled = delayRemaining <= 0f;
frameRemaining = DefenseTowerArt.FrameSeconds(key, frame);
UpdateVisualAlignment();
}
public void FitLightningBeam(Vector2 start, Vector2 end, float thickness)
{
fitsLightningBeam = true;
beamStart = start;
beamEnd = end;
beamThickness = thickness;
UpdateVisualAlignment();
}
public void CenterImpactWave(Vector2 center, float uniformScale)
{
centersImpactWave = true;
impactWaveCenter = center;
impactWaveScale = uniformScale;
UpdateVisualAlignment();
}
public void Travel(Vector2 start, Vector2 end, float duration)
{
travelStart = start;
travelEnd = end;
travelDuration = Mathf.Max(0.001f, duration);
travelElapsed = 0f;
transform.position = start;
}
private void Update()
{
if (target == null || frames == null || frames.Length == 0)
{
FinishSequence();
return;
}
if (delayRemaining > 0f)
{
delayRemaining -= Time.deltaTime;
if (delayRemaining > 0f) return;
target.enabled = true;
}
if (travelDuration > 0f && travelElapsed < travelDuration)
{
travelElapsed += Time.deltaTime;
transform.position = Vector2.Lerp(
travelStart, travelEnd, Mathf.Clamp01(travelElapsed / travelDuration));
}
if (animationFinished)
{
if (travelDuration <= 0f || travelElapsed >= travelDuration) FinishSequence();
return;
}
frameRemaining -= Time.deltaTime;
while (frameRemaining <= 0f)
{
frame++;
if (frame >= frames.Length)
{
if (nextFrames != null && nextFrames.Length > 0)
{
frames = nextFrames;
key = nextKey;
nextFrames = null;
frame = 0;
}
else
{
animationFinished = true;
if (travelDuration <= 0f || travelElapsed >= travelDuration) FinishSequence();
return;
}
}
target.sprite = frames[frame];
UpdateVisualAlignment();
frameRemaining += DefenseTowerArt.FrameSeconds(key, frame);
}
}
private void UpdateVisualAlignment()
{
if (target == null || target.sprite == null) return;
if (fitsLightningBeam)
{
Vector2 direction = beamEnd - beamStart;
float distance = direction.magnitude;
Vector2Int bounds = DefenseTowerArtBounds.LightningBeamXBounds(frame);
float pixelsPerUnit = target.sprite.pixelsPerUnit;
float visibleWidth = (bounds.y - bounds.x) / pixelsPerUnit;
float scaleX = distance <= 0f || visibleWidth <= 0f
? 0f : distance / visibleWidth;
float localStart = (bounds.x - target.sprite.pivot.x) / pixelsPerUnit * scaleX;
transform.rotation = Quaternion.Euler(0f, 0f,
distance > 0f ? Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg : 0f);
transform.position = beamStart - (Vector2)(transform.right * localStart);
transform.localScale = new Vector3(scaleX, beamThickness, 1f);
}
if (centersImpactWave)
{
transform.localScale = Vector3.one * impactWaveScale;
Vector2 localCenter = (DefenseTowerArtBounds.ImpactWaveCenterPixels(frame, target.sprite)
- target.sprite.pivot) / target.sprite.pixelsPerUnit;
transform.position = impactWaveCenter - (Vector2)transform.TransformVector(localCenter);
}
}
private void FinishSequence()
{
if (destroyWhenFinished)
{
Destroy(gameObject);
}
else
{
if (target != null) target.enabled = false;
enabled = false;
}
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 54af8e9464f3423e9799dbb80339a7f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -5,15 +5,22 @@ namespace BumpCombat.Defense
[DisallowMultipleComponent]
public sealed class DefenseSummonUnit : MonoBehaviour
{
private static Sprite[] idleFrames;
private static Sprite[] attackFrames;
private Sprite[] idleFrames;
private Sprite[] attackFrames;
private string idleKey;
private string attackKey;
private float spriteFrameTime;
private int spriteFrame;
private bool playingAttack;
private bool isWalking;
private string walkKey;
private Sprite[] walkFrames;
private DefenseTower owner;
private DefensePrototypeController controller;
private Vector2 anchorLocalPosition;
private SpriteRenderer spriteRenderer;
private float attackCooldown;
private float attackAnimationTime = -1f;
public void Configure(
DefenseTower tower,
@@ -24,22 +31,42 @@ namespace BumpCombat.Defense
controller = prototypeController;
anchorLocalPosition = localAnchor;
spriteRenderer = gameObject.AddComponent<SpriteRenderer>();
idleFrames ??= Resources.LoadAll<Sprite>("Tutorial/Swordsman_Idle");
attackFrames ??= Resources.LoadAll<Sprite>("Tutorial/Swordsman_BumpAttack-v1");
spriteRenderer.color = Color.white;
spriteRenderer.sortingOrder = 11;
RefreshArt();
}
public void RefreshArt()
{
if (spriteRenderer == null || owner == null)
{
return;
}
string family = owner.Tier == 4 && owner.Branch == DefenseTowerBranch.First
? "GiantSlime"
: owner.Tier == 4 && owner.Branch == DefenseTowerBranch.Second
? "LavaSlime" : "Slime";
idleKey = $"Summons/{family}/Idle";
attackKey = $"Summons/{family}/Attack01";
walkKey = $"Summons/{family}/Walk";
idleFrames = DefenseTowerArt.Load(idleKey);
attackFrames = DefenseTowerArt.Load(attackKey);
walkFrames = DefenseTowerArt.Load(walkKey);
spriteFrame = 0;
spriteFrameTime = 0f;
playingAttack = false;
if (idleFrames.Length > 0)
{
spriteRenderer.sprite = idleFrames[0];
}
spriteRenderer.color = new Color32(255, 234, 137, 255);
spriteRenderer.sortingOrder = 11;
float scale = owner.Tier == 4 && owner.Branch == DefenseTowerBranch.First
? 1.35f : 0.9f;
float scale = family == "GiantSlime" ? 1.55f
: family == "LavaSlime" ? 1.15f : 1.1f;
transform.localScale = new Vector3(scale, scale, 1f);
}
private void Update()
{
UpdateAttackSprite();
UpdateSpriteAnimation();
if (owner == null
|| !owner.isActiveAndEnabled
|| controller == null
@@ -56,19 +83,20 @@ namespace BumpCombat.Defense
DefenseTowerKind.Summon);
if (target == null)
{
Vector2 previous = transform.position;
transform.position = Vector2.MoveTowards(
transform.position,
anchor,
controller.Tuning.SummonUnitMoveSpeed * Time.deltaTime);
previous, anchor, controller.Tuning.SummonUnitMoveSpeed * Time.deltaTime);
isWalking = ((Vector2)transform.position - previous).sqrMagnitude > 0.000001f;
return;
}
Vector2 targetPosition = target.transform.position;
spriteRenderer.flipX = targetPosition.x < transform.position.x;
Vector2 previousPosition = transform.position;
transform.position = Vector2.MoveTowards(
transform.position,
targetPosition,
previousPosition, targetPosition,
controller.Tuning.SummonUnitMoveSpeed * Time.deltaTime);
isWalking = ((Vector2)transform.position - previousPosition).sqrMagnitude > 0.000001f;
float attackRange = controller.Tuning.SummonAttackRange
* (owner.Tier == 4 && owner.Branch == DefenseTowerBranch.Second ? 2.5f : 1f);
if (attackCooldown <= 0f
@@ -86,34 +114,45 @@ namespace BumpCombat.Defense
private void PlayAttackSprite()
{
if (attackFrames.Length == 0)
if (attackFrames == null || attackFrames.Length == 0)
{
return;
}
attackAnimationTime = 0f;
playingAttack = true;
spriteFrameTime = 0f;
spriteFrame = 0;
spriteRenderer.sprite = attackFrames[0];
}
private void UpdateAttackSprite()
private void UpdateSpriteAnimation()
{
if (attackAnimationTime < 0f || attackFrames == null
|| attackFrames.Length == 0)
Sprite[] frames = playingAttack ? attackFrames : isWalking ? walkFrames : idleFrames;
string key = playingAttack ? attackKey : isWalking ? walkKey : idleKey;
if (spriteRenderer == null || frames == null || frames.Length == 0)
{
return;
}
attackAnimationTime += Time.deltaTime;
int frame = Mathf.FloorToInt(attackAnimationTime * 12f);
if (frame >= attackFrames.Length)
spriteFrameTime += Time.deltaTime;
while (spriteFrameTime >= DefenseTowerArt.FrameSeconds(key, spriteFrame))
{
attackAnimationTime = -1f;
if (idleFrames != null && idleFrames.Length > 0)
spriteFrameTime -= DefenseTowerArt.FrameSeconds(key, spriteFrame);
spriteFrame++;
if (spriteFrame >= frames.Length)
{
spriteRenderer.sprite = idleFrames[0];
if (playingAttack)
{
playingAttack = false;
spriteFrame = 0;
frames = idleFrames;
key = idleKey;
}
else
{
spriteFrame = 0;
}
}
return;
spriteRenderer.sprite = frames[spriteFrame];
}
spriteRenderer.sprite = attackFrames[frame];
}
}
}
+402 -67
View File
@@ -13,6 +13,21 @@ namespace BumpCombat.Defense
private LineRenderer effectRange;
private static Material effectMaterial;
private int summonCount;
private SpriteRenderer bodyRenderer;
private SpriteRenderer supportAuraRenderer;
private SpriteRenderer supportRangeRenderer;
private Sprite[] supportRangeFrames;
private float supportRangeFrameTime;
private int supportRangeFrame;
private Sprite[] bodyIdleFrames;
private Sprite[] bodyAttackFrames;
private string bodyIdleKey;
private string bodyAttackKey;
private float bodyFrameTime;
private int bodyFrame;
private bool bodyAttacking;
private string supportAuraKey;
private Sprite[] supportAuraFrames;
public DefenseTowerKind Kind { get; private set; }
public DefenseTowerBranch Branch { get; private set; }
@@ -77,11 +92,33 @@ namespace BumpCombat.Defense
UpdateLabel();
UpdateEffectRange();
UpdateBodyArt();
UpdateSupportAura();
foreach (DefenseSummonUnit unit in GetComponentsInChildren<DefenseSummonUnit>())
{
unit.RefreshArt();
}
return true;
}
private void OnDisable()
{
if (supportAuraRenderer != null)
{
supportAuraRenderer.enabled = false;
}
}
private void Update()
{
UpdateBodyAnimation();
if (Kind == DefenseTowerKind.Empowering)
UpdateEffectRange();
else if (effectRange != null)
effectRange.transform.localPosition = DefenseTowerArtBounds.CharacterShadowOffset(
Kind, bodyRenderer != null && bodyRenderer.flipX);
UpdateSupportAura();
UpdateSupportRangeAnimation();
if (!IsOperational || Kind == DefenseTowerKind.Summon
|| Kind == DefenseTowerKind.Empowering)
{
@@ -133,6 +170,8 @@ namespace BumpCombat.Defense
}
Vector2 center = target.transform.position;
Vector2 lightningVisualCenter = DefenseTowerArtBounds.EnemyShadowWorldPosition(
target.Enemy, target.transform.position);
float scale = 1f + tuning.TowerDamageBonusPerTier * (Tier - 1);
float impactRadius = 0f;
bool cone = false;
@@ -213,7 +252,13 @@ namespace BumpCombat.Defense
if (applied)
{
ShowAttackEffect(center, impactRadius, cone);
PlayBodyAttack(center.x < transform.position.x);
bool shadowCenteredCircle = Kind == DefenseTowerKind.Seismic
|| (Kind == DefenseTowerKind.Cold && Tier == 4
&& Branch == DefenseTowerBranch.Second);
ShowAttackEffect(center, impactRadius, cone,
visualCenter: Kind == DefenseTowerKind.Lightning || shadowCenteredCircle
? lightningVisualCenter : null);
}
return applied;
}
@@ -319,13 +364,26 @@ namespace BumpCombat.Defense
{
GameObject rangeObject = new("Effect Range");
rangeObject.transform.SetParent(transform, false);
if (Kind == DefenseTowerKind.Empowering)
{
supportRangeRenderer = rangeObject.AddComponent<SpriteRenderer>();
supportRangeRenderer.sortingOrder = 3;
supportRangeFrames = DefenseTowerArt.Load("SupportRange/T1-Aura");
if (supportRangeFrames.Length > 0)
{
supportRangeRenderer.sprite = supportRangeFrames[0];
supportRangeRenderer.color = new Color(1f, 1f, 1f, tuning.SupportRangeAlpha);
}
UpdateEffectRange();
return;
}
effectRange = rangeObject.AddComponent<LineRenderer>();
effectRange.useWorldSpace = false;
effectRange.loop = true;
effectRange.positionCount = 48;
effectRange.sharedMaterial = GetEffectMaterial();
effectRange.sortingOrder = 3;
Color color = GetComponent<SpriteRenderer>().color;
Color color = GetTowerColor();
color.a = Kind == DefenseTowerKind.Empowering ? 0.35f : 0.22f;
effectRange.startColor = color;
effectRange.endColor = color;
@@ -334,10 +392,38 @@ namespace BumpCombat.Defense
private void UpdateEffectRange()
{
if (Kind == DefenseTowerKind.Empowering && supportRangeRenderer != null)
{
Sprite sprite = supportRangeRenderer.sprite;
if (sprite == null) return;
RectInt bounds = DefenseConstants.SupportRangeAlphaBounds[Mathf.Clamp(supportRangeFrame, 0, DefenseConstants.SupportRangeAlphaBounds.Length - 1)];
Vector2 lossyScale = transform.lossyScale;
Vector2 localScale = new(
tuning.SupportRadius * 2f * sprite.pixelsPerUnit / (bounds.width * Mathf.Max(0.001f, lossyScale.x)),
tuning.SupportRadius * 2f * sprite.pixelsPerUnit / (bounds.height * Mathf.Max(0.001f, lossyScale.y)));
Color[] colors = tuning.SupportTierColors;
Color sourceColor = colors != null && colors.Length > 0
? colors[Mathf.Clamp(Tier - 1, 0, colors.Length - 1)]
: new Color32(151, 169, 119, 102);
sourceColor.a *= tuning.SupportRangeAlpha;
supportRangeRenderer.color = sourceColor;
Vector2 alphaCenterBottomLeft = new(
bounds.x + bounds.width * 0.5f,
100f - bounds.y - bounds.height * 0.5f);
Vector2 correctionLocal = Vector2.Scale(
(alphaCenterBottomLeft - sprite.pivot) / sprite.pixelsPerUnit, localScale);
Vector2 shadow = DefenseTowerArtBounds.CharacterShadowOffset(
Kind, bodyRenderer != null && bodyRenderer.flipX);
supportRangeRenderer.transform.localPosition = shadow - correctionLocal;
supportRangeRenderer.transform.localScale = new Vector3(localScale.x, localScale.y, 1f);
return;
}
if (effectRange == null)
{
return;
}
effectRange.transform.localPosition = DefenseTowerArtBounds.CharacterShadowOffset(
Kind, bodyRenderer != null && bodyRenderer.flipX);
float worldScale = Mathf.Max(0.001f, transform.lossyScale.x);
float radius = GetEffectRange() / worldScale;
effectRange.startWidth = 0.055f / worldScale;
@@ -352,46 +438,67 @@ namespace BumpCombat.Defense
}
private void ShowAttackEffect(
Vector2 center, float radius, bool cone, Vector2? source = null)
Vector2 center, float radius, bool cone, Vector2? source = null,
Vector2? visualCenter = null)
{
Vector2 origin = source ?? (Vector2)transform.position;
Vector2 direction = center - origin;
GameObject effect = new($"{Kind} Attack Effect");
effect.transform.SetParent(transform.parent, false);
effect.transform.position = center;
Color color = GetComponent<SpriteRenderer>().color;
bool shadowCenteredCircle = Kind == DefenseTowerKind.Seismic
|| (Kind == DefenseTowerKind.Cold && Tier == 4
&& Branch == DefenseTowerBranch.Second);
Vector2 effectCenter = shadowCenteredCircle ? visualCenter ?? center : center;
effect.transform.position = effectCenter;
Color color = GetTowerColor();
color.a = 0.9f;
Vector2 direction = center - origin;
if (Kind == DefenseTowerKind.Lightning && direction.sqrMagnitude > 0.001f)
switch (Kind)
{
LineRenderer bolt = CreateEffectLine(effect.transform, "Bolt", color, 0.13f);
bolt.positionCount = 5;
Vector2 side = new(-direction.y, direction.x);
side.Normalize();
for (int i = 0; i < 5; i++)
{
float fraction = i / 4f;
float offset = i == 1 ? 0.22f : i == 2 ? -0.16f
: i == 3 ? 0.18f : 0f;
bolt.SetPosition(i, origin - center
+ direction * fraction + side * offset);
}
}
else if (Kind != DefenseTowerKind.Seismic
&& direction.sqrMagnitude > 0.001f)
{
SpriteRenderer shot = CreateEffectSprite(
effect.transform, "Shot Sprite",
origin + direction * 0.5f,
new Vector2(direction.magnitude,
Kind == DefenseTowerKind.Direct ? 0.2f : 0.13f), color);
shot.transform.rotation = Quaternion.Euler(
0f, 0f, Mathf.Atan2(direction.y, direction.x)
* Mathf.Rad2Deg);
case DefenseTowerKind.Direct:
ShowFireArt(effect.transform, origin, center, direction);
break;
case DefenseTowerKind.Lightning:
Vector2 shadowOrigin = transform.TransformPoint(DefenseTowerArtBounds.CharacterShadowOffset(Kind, bodyRenderer != null && bodyRenderer.flipX));
Vector2 impactCenter = visualCenter ?? center;
ShowLightningArt(effect.transform, shadowOrigin, impactCenter,
impactCenter - shadowOrigin, radius);
break;
case DefenseTowerKind.Cold:
ShowColdArt(effect.transform, origin, center, direction,
shadowCenteredCircle ? effectCenter : (Vector2?)null);
break;
case DefenseTowerKind.Seismic:
string controlKey = Tier == 4
? (Branch == DefenseTowerBranch.First
? "Effects/Control/StoneGaze" : "Effects/Control/Temporal")
: $"Effects/Control/T{Tier}-Aura";
int visibleWidth = Tier == 4
? (Branch == DefenseTowerBranch.First ? 40 : 45)
: 32 + (Tier - 1) * 6;
float controlScale = ScaleToVisibleWidth(radius * 2f, visibleWidth);
CreateAnimatedArt(effect.transform, "Impact Sprite", controlKey,
effectCenter, Vector2.one * controlScale, 0f, 13);
break;
case DefenseTowerKind.Summon:
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
{
for (int i = 0; i < 5; i++)
{
Vector2 spikePosition = Vector2.Lerp(origin, center, (i + 1) / 5f);
CreateAnimatedArt(effect.transform, $"Shot Sprite {i + 1}",
"Summons/LavaSlime/GroundSpike-Segment", spikePosition,
Vector2.one, 0f, 14, startDelay: i * 0.12f);
}
}
else
{
CreateAnimatedArt(effect.transform, "Impact Sprite",
"Effects/Fire/Impact", center, Vector2.one, 0f, 13);
}
break;
}
CreateEffectSprite(effect.transform, "Impact Sprite",
center, new Vector2(0.42f, 0.42f), color);
if (cone)
{
LineRenderer outline = CreateEffectLine(
@@ -403,6 +510,7 @@ namespace BumpCombat.Defense
+ (Vector2)(Quaternion.Euler(0f, 0f, -60f) * forward));
outline.SetPosition(2, origin - center
+ (Vector2)(Quaternion.Euler(0f, 0f, 60f) * forward));
Destroy(outline.gameObject, 0.22f);
}
else if (radius > 0f)
{
@@ -417,26 +525,124 @@ namespace BumpCombat.Defense
Mathf.Cos(angle) * radius,
Mathf.Sin(angle) * radius, 0f));
}
Destroy(area.gameObject, 0.22f);
}
Destroy(effect, 0.22f);
Destroy(effect, 1.6f);
}
private static SpriteRenderer CreateEffectSprite(
Transform parent, string name, Vector2 position,
Vector2 size, Color color)
private void ShowFireArt(
Transform parent, Vector2 origin, Vector2 center, Vector2 direction)
{
GameObject shape = new(name);
shape.transform.SetParent(parent, false);
shape.transform.position = position;
shape.transform.localScale = new Vector3(size.x, size.y, 1f);
SpriteRenderer renderer = shape.AddComponent<SpriteRenderer>();
renderer.sprite = DefensePrototypeController.WhiteSprite;
renderer.color = color;
renderer.sortingOrder = 15;
if (Tier == 4 && Branch == DefenseTowerBranch.First)
{
SpriteRenderer meteor = CreateAnimatedArt(parent, "Impact Sprite",
"Effects/Fire/Meteor-Flight", center + new Vector2(-0.81f, 0.81f),
Vector2.one, 0f, 13, "Effects/Fire/Meteor-Impact");
DefenseSpriteSequence sequence = meteor.GetComponent<DefenseSpriteSequence>();
Vector2 start = center + new Vector2(-0.81f, 0.81f);
meteor.transform.position = start;
sequence.Travel(start, center, 0.32f);
return;
}
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
{
float visibleWidth = 88f / 32f;
float scaleX = Mathf.Max(0.1f, direction.magnitude / visibleWidth);
CreateAnimatedArt(parent, "Shot Sprite", "Effects/Fire/Flamethrower",
origin, new Vector2(scaleX, 1f),
Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg,
14);
}
else
{
string key = $"Effects/Fire/T{Tier}-Fireball";
SpriteRenderer shot = CreateAnimatedArt(parent, "Shot Sprite", key,
Vector2.Lerp(origin, center, 0.5f), Vector2.one * 0.85f,
Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14);
DefenseSpriteSequence sequence = shot.GetComponent<DefenseSpriteSequence>();
sequence.Travel(origin, center, 0.18f);
}
CreateAnimatedArt(parent, "Impact Sprite", "Effects/Fire/Impact",
center, Vector2.one, 0f, 13, startDelay: 0.18f);
}
private void ShowLightningArt(
Transform parent, Vector2 origin, Vector2 center,
Vector2 direction, float radius)
{
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
{
float scale = Mathf.Max(0.8f, radius / 1.2f);
CreateAnimatedArt(parent, "Shot Sprite", "Effects/Lightning/LightningStorm",
center, Vector2.one * scale, 0f, 13);
}
else
{
string key = Tier == 4
? "Effects/Lightning/Arc" : $"Effects/Lightning/T{Tier}-Bolt";
SpriteRenderer bolt = CreateAnimatedArt(parent, "Shot Sprite", key, origin,
Vector2.one, Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14);
bolt.GetComponent<DefenseSpriteSequence>().FitLightningBeam(origin, center, 1f);
}
float waveScale = Mathf.Max(0.8f, radius / 1.4f);
SpriteRenderer wave = CreateAnimatedArt(parent, "Impact Sprite", "Effects/Lightning/ImpactWave",
center, Vector2.one * waveScale, 0f, 13, startDelay: 0.15f);
wave.GetComponent<DefenseSpriteSequence>().CenterImpactWave(center, waveScale);
}
private void ShowColdArt(
Transform parent, Vector2 origin, Vector2 center, Vector2 direction,
Vector2? visualCenter = null)
{
if (Tier == 4 && Branch == DefenseTowerBranch.Second)
{
float cageScale = ScaleToVisibleWidth(3.3f, 48f);
Vector2 cageCenter = visualCenter ?? center;
CreateAnimatedArt(parent, "Shot Sprite", "Effects/Ice/GlacialCage-Back",
cageCenter, Vector2.one * cageScale, 0f, 12);
CreateAnimatedArt(parent, "Impact Sprite", "Effects/Ice/GlacialCage-Front",
cageCenter, Vector2.one * cageScale, 0f, 14);
return;
}
string key = Tier == 4
? "Effects/Ice/Freezebolt" : $"Effects/Ice/T{Tier}-Volley";
SpriteRenderer shot = CreateAnimatedArt(parent, "Shot Sprite", key,
Vector2.Lerp(origin, center, 0.5f), Vector2.one,
Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg, 14);
shot.GetComponent<DefenseSpriteSequence>().Travel(origin, center, 0.18f);
CreateAnimatedArt(parent, "Impact Sprite", "Effects/Ice/Impact",
center, Vector2.one, 0f, 13, startDelay: 0.18f);
}
private static SpriteRenderer CreateAnimatedArt(
Transform parent, string name, string key, Vector2 position,
Vector2 scale, float rotation, int sortingOrder,
string nextKey = null, float startDelay = 0f)
{
GameObject spriteObject = new(name);
spriteObject.transform.SetParent(parent, false);
spriteObject.transform.position = position;
spriteObject.transform.localScale = new Vector3(scale.x, scale.y, 1f);
spriteObject.transform.rotation = Quaternion.Euler(0f, 0f, rotation);
SpriteRenderer renderer = spriteObject.AddComponent<SpriteRenderer>();
renderer.color = Color.white;
renderer.sortingOrder = sortingOrder;
Sprite[] frames = DefenseTowerArt.Load(key);
Sprite[] next = string.IsNullOrEmpty(nextKey)
? null : DefenseTowerArt.Load(nextKey);
spriteObject.AddComponent<DefenseSpriteSequence>().Play(
renderer, key, frames, true, nextKey, next, startDelay);
return renderer;
}
private static float ScaleToVisibleWidth(float worldWidth, float visiblePixels)
{
return worldWidth / (visiblePixels / 32f);
}
private static LineRenderer CreateEffectLine(
Transform parent, string name, Color color, float width)
{
@@ -479,32 +685,23 @@ namespace BumpCombat.Defense
private void CreateVisuals()
{
SpriteRenderer renderer = gameObject.GetComponent<SpriteRenderer>();
if (renderer == null)
SpriteRenderer rootRenderer = gameObject.GetComponent<SpriteRenderer>();
if (rootRenderer == null)
{
renderer = gameObject.AddComponent<SpriteRenderer>();
rootRenderer = gameObject.AddComponent<SpriteRenderer>();
}
renderer.sprite = DefensePrototypeController.WhiteSprite;
renderer.color = Kind switch
{
DefenseTowerKind.Direct => new Color32(240, 96, 65, 255),
DefenseTowerKind.Summon => new Color32(244, 154, 74, 255),
DefenseTowerKind.Lightning => new Color32(238, 218, 83, 255),
DefenseTowerKind.Cold => new Color32(65, 211, 240, 255),
DefenseTowerKind.Seismic => new Color32(175, 141, 100, 255),
_ => new Color32(148, 228, 122, 255),
};
renderer.sortingOrder = 8;
rootRenderer.sprite = DefensePrototypeController.WhiteSprite;
rootRenderer.color = Color.white;
rootRenderer.enabled = false;
transform.localScale = new Vector3(0.72f, 0.72f, 1f);
GameObject crown = new("Tower Crown");
crown.transform.SetParent(transform, false);
crown.transform.localPosition = new Vector3(0f, 0.42f, 0f);
crown.transform.localScale = new Vector3(0.42f, 0.22f, 1f);
SpriteRenderer crownRenderer = crown.AddComponent<SpriteRenderer>();
crownRenderer.sprite = DefensePrototypeController.WhiteSprite;
crownRenderer.color = Color.white;
crownRenderer.sortingOrder = 9;
GameObject bodyObject = new("Tower Body");
bodyObject.transform.SetParent(transform, false);
bodyObject.transform.localScale = new Vector3(2.1f, 2.1f, 1f);
bodyRenderer = bodyObject.AddComponent<SpriteRenderer>();
bodyRenderer.color = Color.white;
bodyRenderer.sortingOrder = 8;
UpdateBodyArt();
GameObject labelObject = new("Tower Label");
labelObject.transform.SetParent(transform, false);
@@ -520,6 +717,144 @@ namespace BumpCombat.Defense
CreateEffectRange();
}
private void UpdateBodyArt()
{
bodyIdleKey = DefenseTowerArt.BodyKey(Kind, Tier, "Idle");
bodyIdleFrames = DefenseTowerArt.Load(bodyIdleKey);
bodyAttackKey = Kind switch
{
DefenseTowerKind.Direct or DefenseTowerKind.Lightning or DefenseTowerKind.Cold
=> DefenseTowerArt.BodyKey(Kind, Tier, "Attack01"),
DefenseTowerKind.Seismic => DefenseTowerArt.BodyKey(Kind, Tier, "Attack"),
DefenseTowerKind.Empowering => DefenseTowerArt.BodyKey(Kind, Tier, "Heal"),
_ => null,
};
bodyAttackFrames = bodyAttackKey == null
? System.Array.Empty<Sprite>() : DefenseTowerArt.Load(bodyAttackKey);
bodyAttacking = false;
bodyFrame = 0;
bodyFrameTime = 0f;
if (bodyRenderer != null && bodyIdleFrames.Length > 0)
{
bodyRenderer.sprite = bodyIdleFrames[0];
}
}
private void PlayBodyAttack(bool faceLeft)
{
if (bodyRenderer == null || bodyAttackFrames == null || bodyAttackFrames.Length == 0)
{
return;
}
bodyRenderer.flipX = faceLeft;
bodyAttacking = true;
bodyFrame = 0;
bodyFrameTime = 0f;
bodyRenderer.sprite = bodyAttackFrames[0];
}
private void UpdateBodyAnimation()
{
if (bodyRenderer == null || bodyIdleFrames == null || bodyIdleFrames.Length == 0)
{
return;
}
bodyFrameTime += Time.deltaTime;
Sprite[] frames = bodyAttacking ? bodyAttackFrames : bodyIdleFrames;
string key = bodyAttacking ? bodyAttackKey : bodyIdleKey;
if (frames == null || frames.Length == 0)
{
bodyAttacking = false;
return;
}
while (bodyFrameTime >= DefenseTowerArt.FrameSeconds(key, bodyFrame))
{
bodyFrameTime -= DefenseTowerArt.FrameSeconds(key, bodyFrame);
bodyFrame++;
if (bodyFrame >= frames.Length)
{
if (bodyAttacking)
{
bodyAttacking = false;
bodyFrame = 0;
frames = bodyIdleFrames;
key = bodyIdleKey;
}
else
{
bodyFrame = 0;
}
}
bodyRenderer.sprite = frames[bodyFrame];
}
}
private void UpdateSupportAura()
{
DefenseTower support = owner != null ? owner.GetActiveSupportFor(this) : null;
if (support == null)
{
if (supportAuraRenderer != null) supportAuraRenderer.enabled = false;
return;
}
if (supportAuraRenderer == null)
{
GameObject auraObject = new("Support Aura Art");
auraObject.transform.SetParent(transform, false);
supportAuraRenderer = auraObject.AddComponent<SpriteRenderer>();
}
string motionKey = bodyAttacking && !string.IsNullOrEmpty(bodyAttackKey) ? bodyAttackKey : bodyIdleKey;
string key = motionKey.Replace("Bodies/", "SupportMasks/");
if (supportAuraKey != key)
{
supportAuraKey = key;
supportAuraFrames = DefenseTowerArt.Load(key);
}
int maskFrame = 0;
if (bodyRenderer != null && supportAuraFrames != null)
{
for (int i = 0; i < supportAuraFrames.Length; i++)
if (supportAuraFrames[i].name == bodyRenderer.sprite.name) { maskFrame = i; break; }
}
supportAuraRenderer.sprite = supportAuraFrames != null && supportAuraFrames.Length > maskFrame
? supportAuraFrames[maskFrame] : null;
supportAuraRenderer.flipX = bodyRenderer != null && bodyRenderer.flipX;
supportAuraRenderer.transform.localPosition = bodyRenderer != null ? bodyRenderer.transform.localPosition : Vector3.zero;
supportAuraRenderer.transform.localScale = bodyRenderer != null ? bodyRenderer.transform.localScale : Vector3.one;
supportAuraRenderer.sortingOrder = bodyRenderer != null ? bodyRenderer.sortingOrder + 1 : 9;
Color[] colors = tuning.SupportTierColors;
supportAuraRenderer.color = colors != null && colors.Length > 0
? colors[Mathf.Clamp(support.Tier - 1, 0, colors.Length - 1)]
: new Color32(151, 169, 119, 102);
supportAuraRenderer.enabled = supportAuraFrames != null && supportAuraFrames.Length > 0 && supportAuraRenderer.sprite != null;
}
private void UpdateSupportRangeAnimation()
{
if (supportRangeRenderer == null || supportRangeFrames == null || supportRangeFrames.Length == 0) return;
supportRangeFrameTime += Time.deltaTime;
while (supportRangeFrameTime >= 0.14f)
{
supportRangeFrameTime -= 0.14f;
supportRangeFrame = (supportRangeFrame + 1) % supportRangeFrames.Length;
supportRangeRenderer.sprite = supportRangeFrames[supportRangeFrame];
UpdateEffectRange();
}
}
private Color GetTowerColor()
{
return Kind switch
{
DefenseTowerKind.Direct => new Color32(240, 96, 65, 255),
DefenseTowerKind.Summon => new Color32(244, 154, 74, 255),
DefenseTowerKind.Lightning => new Color32(238, 218, 83, 255),
DefenseTowerKind.Cold => new Color32(65, 211, 240, 255),
DefenseTowerKind.Seismic => new Color32(175, 141, 100, 255),
_ => new Color32(148, 228, 122, 255),
};
}
private void UpdateLabel()
{
if (label == null)