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> appliedEffects = new Dictionary>(); 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 onEnemyDied; public event UnityAction onHealthChanged; // VAT 适配器(如 EnemyVATAttachment)挂载时,接管朝向 → EnemyBehavior 不再写 transform.localScale.x, // sign 通过 event 推给适配器,适配器走 VATAnimationController.SetFacing 用 Y 旋转转身。 // +1 = 朝右,-1 = 朝左。 public event System.Action 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(); 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 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 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()); } List effects = appliedEffects[effect.EffectType]; if (!effects.Contains(effect)) { effects.Add(effect); } } public void RemoveEffect(Effect effect) { if (!appliedEffects.ContainsKey(effect.EffectType)) return; List effects = appliedEffects[effect.EffectType]; if (effects.Contains(effect)) { effects.Remove(effect); } } } }