413 lines
14 KiB
C#

using OctoberStudio.Easing;
using OctoberStudio.Enemy;
using OctoberStudio.Extensions;
using OctoberStudio.Timeline;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
namespace OctoberStudio
{
public class EnemyBehavior : MonoBehaviour
{
protected static readonly int _Overlay = Shader.PropertyToID("_Overlay");
protected static readonly int _Disolve = Shader.PropertyToID("_Disolve");
private static readonly int HIT_HASH = "Hit".GetHashCode();
[Header("Settings")]
[Tooltip("The speed of the enemy")]
[SerializeField] protected float speed;
public float Speed { get; protected set; }
[Tooltip("The LevelData's 'Enemy Damage' is multiplied by this value to determine the damage of the enemy on each level")]
[SerializeField] float damage = 1f;
[Tooltip("The LevelData's 'Enemy HP' is multiplied by this value to determine the HP of the enemy on each level")]
[SerializeField] float hp;
[FormerlySerializedAs("canBekickedBack")]
[SerializeField] bool canBeKickedBack = true;
[SerializeField] bool shouldFadeIn;
[Header("References")]
[SerializeField] Rigidbody2D rb;
[SerializeField] SpriteRenderer spriteRenderer;
[SerializeField] DissolveSettings dissolveSettings;
[SerializeField] SpriteRenderer shadowSprite;
[SerializeField] Collider2D enemyCollider;
public Vector2 Center => enemyCollider.bounds.center;
[Header("Hit")]
[SerializeField] float hitScaleAmount = 0.2f;
[SerializeField] Color hitColor = Color.white;
public EnemyData Data { get; private set; }
public WaveOverride WaveOverride { get; protected set; }
public bool IsVisible => spriteRenderer.isVisible;
public bool IsAlive => HP > 0;
// The enemy does not receive damage when this property is true
public bool IsInvulnerable { get; protected set; }
public float HP { get; private set; }
public float MaxHP { get; private set; }
public bool ShouldSpawnChestOnDeath { get; set; }
IEasingCoroutine fallBackCoroutine;
private Dictionary<EffectType, List<Effect>> appliedEffects = new Dictionary<EffectType, List<Effect>>();
protected bool IsMoving { get; set; }
public bool IsMovingToCustomPoint { get; protected set; }
public Vector2 CustomPoint { get; protected set; }
public float LastTimeDamagedPlayer { get; set; }
private Material sharedMaterial;
private Material effectsMaterial;
private float shadowAlpha;
public event UnityAction<EnemyBehavior> onEnemyDied;
public event UnityAction<float, float> onHealthChanged;
// VAT 适配器(如 EnemyVATAttachment)挂载时,接管朝向 → EnemyBehavior 不再写 transform.localScale.x,
// sign 通过 event 推给适配器,适配器走 VATAnimationController.SetFacing 用 Y 旋转转身。
// +1 = 朝右,-1 = 朝左。
public event System.Action<int> OnFacingSignChanged;
private int currentFacingSign = 1;
// 接管开关(EnemyVATAttachment.Awake 中置 true 即关闭 localScale 翻转)
public bool ExternalFacingOverride { get; set; }
private float lastTimeSwitchedDirection = 0;
IEasingCoroutine damageCoroutine;
protected IEasingCoroutine scaleCoroutine;
IEasingCoroutine fadeInCoroutine;
private float damageTextValue;
private float lastTimeDamageText;
private static int lastFrameHitSound;
private float lastTimeHitSound;
protected virtual void Awake()
{
sharedMaterial = spriteRenderer.sharedMaterial;
effectsMaterial = Instantiate(sharedMaterial);
shadowAlpha = shadowSprite.color.a;
}
public void SetData(EnemyData data)
{
Data = data;
}
public void SetWaveOverride(WaveOverride waveOverride)
{
WaveOverride = waveOverride;
}
public virtual void Play()
{
MaxHP = StageController.Stage.EnemyHP * hp;
Speed = speed;
if (WaveOverride != null)
{
MaxHP = WaveOverride.ApplyHPOverride(MaxHP);
Speed = WaveOverride.ApplySpeedOverride(Speed);
}
HP = MaxHP;
IsMoving = true;
shadowSprite.SetAlpha(shadowAlpha);
enemyCollider.enabled = true;
if (shouldFadeIn)
{
spriteRenderer.SetAlpha(0);
fadeInCoroutine = spriteRenderer.DoAlpha(1, 0.2f);
}
}
protected virtual void Update()
{
if(!IsAlive || !IsMoving || PlayerBehavior.Player == null) return;
Vector3 target = IsMovingToCustomPoint ? CustomPoint : PlayerBehavior.Player.transform.position;
Vector3 direction = (target - transform.position).normalized;
float speed = Speed;
if (appliedEffects.TryGetValue(EffectType.Speed, out var speedEffects))
{
for(int i = 0; i < speedEffects.Count; i++)
{
Effect effect = speedEffects[i];
speed *= effect.Modifier;
}
}
transform.position += direction * Time.deltaTime * speed;
// Enemy looks in the direction it's moving
if (!scaleCoroutine.ExistsAndActive())
{
int wantSign = direction.x >= 0 ? 1 : -1;
if (wantSign != currentFacingSign &&
Time.unscaledTime - lastTimeSwitchedDirection > 0.1f)
{
currentFacingSign = wantSign;
if (ExternalFacingOverride)
{
// VAT 模式: 不写 localScale,把符号事件推给适配器(EnemyVATAttachment → VATAnimationController.SetFacing)
OnFacingSignChanged?.Invoke(currentFacingSign);
}
else
{
// 原 2D 行为: 直接翻 scale.x
var scale = transform.localScale;
scale.x = Mathf.Abs(scale.x) * wantSign;
transform.localScale = scale;
}
lastTimeSwitchedDirection = Time.unscaledTime;
}
}
}
private void OnTriggerEnter2D(Collider2D other)
{
ProjectileBehavior projectile = other.GetComponent<ProjectileBehavior>();
if(projectile != null)
{
TakeDamage(PlayerBehavior.Player.Damage * projectile.DamageMultiplier);
if(HP > 0)
{
if (projectile.KickBack && canBeKickedBack)
{
KickBack(PlayerBehavior.CenterPosition);
}
if(projectile.Effects != null && projectile.Effects.Count > 0)
{
AddEffects(projectile.Effects);
}
}
}
}
public float GetDamage()
{
var damage = this.damage;
if(WaveOverride != null) damage = WaveOverride.ApplyDamageOverride(damage);
var baseDamage = StageController.Stage.EnemyDamage * damage;
if (appliedEffects.ContainsKey(EffectType.Damage))
{
var damageEffects = appliedEffects[EffectType.Damage];
for (int i = 0; i < damageEffects.Count; i++)
{
var effect = damageEffects[i];
baseDamage *= effect.Modifier;
}
}
return baseDamage;
}
public List<EnemyDropData> GetDropData()
{
if (WaveOverride != null) return WaveOverride.ApplyDropOverride(Data.EnemyDrop);
return Data.EnemyDrop;
}
public void TakeDamage(float damage)
{
if (!IsAlive) return;
if (IsInvulnerable) return;
HP -= damage;
onHealthChanged?.Invoke(HP, MaxHP);
// Showing Damage Text
damageTextValue += damage;
if (Time.unscaledTime - lastTimeDamageText > 0.2f && damageTextValue >= 1)
{
var damageText = Mathf.RoundToInt(damageTextValue).ToString();
StageController.WorldSpaceTextManager.SpawnText(transform.position + new Vector3(Random.Range(-0.1f, 0.1f), Random.value * 0.1f), damageText);
damageTextValue = 0;
lastTimeDamageText = Time.unscaledTime;
} else
{
damageTextValue += damage;
}
// Playing Damage Sound
if(Time.frameCount != lastFrameHitSound && Time.unscaledTime - lastTimeHitSound > 0.2f)
{
GameController.AudioManager.PlaySound(HIT_HASH);
lastFrameHitSound = Time.frameCount;
lastTimeHitSound = Time.unscaledTime;
}
if (HP <= 0)
{
Die(true);
}
else
{
// Flashing Color on hit
if (!damageCoroutine.ExistsAndActive())
{
FlashHit(true);
}
// Scaling on Hit
if (!scaleCoroutine.ExistsAndActive())
{
var x = transform.localScale.x;
scaleCoroutine = transform.DoLocalScale(new Vector3(x * (1 - hitScaleAmount), (1 + hitScaleAmount), 1), 0.07f).SetEasing(EasingType.SineOut).SetOnFinish(() =>
{
scaleCoroutine = transform.DoLocalScale(new Vector3(x, 1, 1), 0.07f).SetEasing(EasingType.SineInOut);
});
}
}
}
private void FlashHit(bool resetMaterial, UnityAction onFinish = null)
{
spriteRenderer.material = effectsMaterial;
var transparentColor = hitColor;
transparentColor.a = 0;
// Changin properties of a material creates a new instance of a material.
// We cache the material at the start of a game and assign it back when we're done with it
// This way the batching optimization is restored
effectsMaterial.SetColor(_Overlay, transparentColor);
damageCoroutine = effectsMaterial.DoColor(_Overlay, hitColor, 0.05f).SetOnFinish(() =>
{
damageCoroutine = effectsMaterial.DoColor(_Overlay, transparentColor, 0.05f).SetOnFinish(() =>
{
if(resetMaterial) spriteRenderer.material = sharedMaterial;
onFinish?.Invoke();
});
});
}
public void Kill()
{
HP = 0;
Die(false);
}
protected virtual void Die(bool flash)
{
enemyCollider.enabled = false;
damageCoroutine.StopIfExists();
onEnemyDied?.Invoke(this);
fallBackCoroutine.StopIfExists();
rb.simulated = true;
fadeInCoroutine.StopIfExists();
// Changin properties of a material creates a new instance of a material.
// We cache the material at the start of a game and assign it back when we're done with it
// This way the batching optimization is restored
spriteRenderer.material = effectsMaterial;
if (flash)
{
FlashHit(false, () => {
effectsMaterial.SetColor(_Overlay, Color.clear);
effectsMaterial.DoColor(_Overlay, dissolveSettings.DissolveColor, dissolveSettings.Duration - 0.1f);
});
} else
{
effectsMaterial.SetColor(_Overlay, Color.clear);
effectsMaterial.DoColor(_Overlay, dissolveSettings.DissolveColor, dissolveSettings.Duration);
}
effectsMaterial.SetFloat(_Disolve, 0);
effectsMaterial.DoFloat(_Disolve, 1, dissolveSettings.Duration + 0.02f).SetEasingCurve(dissolveSettings.DissolveCurve).SetOnFinish(() =>
{
effectsMaterial.SetColor(_Overlay, Color.clear);
effectsMaterial.SetFloat(_Disolve, 0);
gameObject.SetActive(false);
spriteRenderer.material = sharedMaterial;
});
shadowSprite.DoAlpha(0, dissolveSettings.Duration);
appliedEffects.Clear();
WaveOverride = null;
}
public void KickBack(Vector3 position)
{
var direction = (transform.position - position).normalized;
rb.simulated = false;
fallBackCoroutine.StopIfExists();
fallBackCoroutine = transform.DoPosition(transform.position + direction * 0.6f, 0.15f).SetEasing(EasingType.ExpoOut).SetOnFinish(() => rb.simulated = true);
}
public void AddEffects(List<Effect> effects)
{
for(int i = 0; i < effects.Count; i++)
{
AddEffect(effects[i]);
}
}
public void AddEffect(Effect effect)
{
if (!appliedEffects.ContainsKey(effect.EffectType))
{
appliedEffects.Add(effect.EffectType, new List<Effect>());
}
List<Effect> effects = appliedEffects[effect.EffectType];
if (!effects.Contains(effect))
{
effects.Add(effect);
}
}
public void RemoveEffect(Effect effect)
{
if (!appliedEffects.ContainsKey(effect.EffectType)) return;
List<Effect> effects = appliedEffects[effect.EffectType];
if (effects.Contains(effect))
{
effects.Remove(effect);
}
}
}
}