using System.Collections; using UnityEngine; using Spine.Unity; using Spine.Unity.Examples; using Mirror; using DG.Tweening; public class enemyScript : NetworkBehaviour { // Health and Damage Constants public const int HEALTH_INC = 2; public const float DAMAGE_INC = 1.2f; // XP System Constants public const float XP_GAIN = 1.5f; // Legacy public const int XP_GAIN_Base = 5; // Legacy public const float XP_EXPONENTIAL_BASE = 1.3f; public const float XP_LEVEL_MULTIPLIER = 8f; public const float XP_PLAYER_LEVEL_BONUS = 0.05f; // Resistance and Shield Constants public const int RESISTANCE_INC = 1; public const int MAX_RESISTANCE = 15; public const float SHIELD_DAMAGE_DIVIDER = 2f; [Header("Health & Shield System")] [SyncVar(hook = nameof(OnHealthChange))] public int health; [SyncVar(hook = nameof(OnMagicalHealthChange))] public int magicalHealth; [SyncVar] public int physicalResistance; [SyncVar] public int magicalResistance; [SyncVar] public bool shieldActive = true; [Header("UI Components")] public SpriteHealthBar healthBar; public SpriteHealthBar MagicalhealthBar; public Transform uiEnemy; public TextMesh enemyName; public TextMesh enemyLevel; [Header("Movement & Combat")] public float speed; public float chaseRadius; public float attackRadius; public bool rotate; public int enemyAttackDamage = 10; public float damageTimingPercent = 0.6f; [Header("Targeting & State")] public playerNetwork target; public bool isInChaseRange; public bool isInAttackRange; [Header("Components")] private Rigidbody2D rb2; public SkeletonAnimation animator; private Vector2 movement; public Vector3 dir; MeshRenderer meshRenderer; [SyncVar] public bool hasDealtDamage = false; void Awake(){ meshRenderer = GetComponent(); scanCooldown = Random.Range(0.5f, 1.5f); } private void Start(){ rb2 = GetComponent(); UpdateAnimation(directionString, animationString); defaultPos = transform.position; } [SyncVar(hook =nameof(OnLevelChanged))] public int level; void OnLevelChanged(int oldVal, int newVal){ if(isServer){return;} SetLevel(newVal); } public void SetLevel(int _level){ if(enemyLevel != null){ enemyLevel.text = _level.ToString(); } level = _level; int healthIncrement =level * HEALTH_INC; maxHealth = 100 + healthIncrement; health = (int)maxHealth; magicalHealth = (int)maxHealth; enemyAttackDamage += (int)(level * DAMAGE_INC); int resistanceIncrement = level * RESISTANCE_INC; physicalResistance = Mathf.Min(resistanceIncrement, MAX_RESISTANCE); magicalResistance = Mathf.Min(resistanceIncrement, MAX_RESISTANCE); shieldActive = true; } public Vector3 defScale; Vector3 defaultPos; float playerDistCheckTimer=0f; void LateUpdate(){ LOD(); } public const float disappearDistFromPlayer = 15f; void LOD(){ if(playerDistCheckTimer > 0){playerDistCheckTimer -= Time.deltaTime;return;} playerDistCheckTimer = Random.Range(1.5f,2.5f); if(playerNetwork.localPlayerTransform == null){return;} float distToPlayer = Vector3.Distance(playerNetwork.localPlayerTransform.position, transform.position); meshRenderer.enabled = distToPlayer < disappearDistFromPlayer; } #if UNITY_SERVER || UNITY_EDITOR [Server] private void Update(){ // animator.skeleton.SetSkin // set animation state to running if in chase Range //isInChaseRange = true // isInChaseRange = Physics2D.OverlapCircle(transform.position, chaseRadius , layerMask); // isInAttackRange = Physics2D.OverlapCircle(transform.position, attackRadius, layerMask); if (health <= 0 ){ return; } if(target != null){ isInChaseRange = Vector3.Distance(transform.position, target.transform.position) < chaseRadius; isInAttackRange = Vector3.Distance(transform.position, target.transform.position) < attackRadius; }else{ isInChaseRange = false; isInAttackRange = false; } ScanPlayers(); if(target !=null){ enemyFollow(); } } #endif [Header("Scanning & LOD")] float scanTimer =0; float scanCooldown; public void ScanPlayers(){ if(scanTimer >0){scanTimer-=Time.deltaTime; return;} scanTimer = scanCooldown; playerNetwork[] playersinNetwork = FindObjectsOfType(); float closestDist = float.MaxValue; playerNetwork closestPlayer = null; foreach(playerNetwork player in playersinNetwork ){ if(player.health <= 0 ){continue;} float dist = Vector3.Distance(transform.position, player.transform.position); if(dist < closestDist){ closestPlayer = player; closestDist = dist; } } if(closestDist < chaseRadius){ target = closestPlayer ; } else { target = null; } //if(target == null) {return;} } // [ClientRpc] // void RpcUpdateAnim(string animDir , string animName, bool isLoop){ // UpdateAnimation(animDir , animName, isLoop); // } [SyncVar(hook =nameof(OnFlipped))] bool isFlipped= false; void OnFlipped(bool oldVal, bool newVal){ if(isServer){return;} transform.localScale = new Vector3(defScale.x * (newVal ? -1 : 1),defScale.y,defScale.z); HandleFlip(); } void HandleFlip(){ if(uiEnemy == null){ return; } if(transform.localScale.x < 0 ){ uiEnemy.localScale = new Vector3(-1,1,1); } else{ uiEnemy.localScale = new Vector3(1,1,1); } } private void enemyFollow(){ if(Mathf.Abs(dir.y) > Mathf.Abs(dir.x)){ if(dir.y < 0){ directionString = "Back"; }else{ directionString = "Front"; } }else{ directionString = "Side"; if(dir.x < 0){ transform.localScale = new Vector3(defScale.x,defScale.y,0); isFlipped=false; }else{ transform.localScale = new Vector3(-defScale.x,defScale.y,0); isFlipped = true; } HandleFlip(); } if(animationHistory != directionString + animationString){ UpdateAnimation(directionString, animationString); // RpcUpdateAnim(directionString, animationString,true); } animationHistory=directionString + animationString; if(target != null){ dir = transform.position - target.transform.position; } float angle = Mathf.Atan2(dir.y , dir.x ) * Mathf.Rad2Deg; dir.Normalize(); movement = dir; if(rotate){ //set anim direction x, y dir } } [Header("Animation System")] string animationHistory =""; [SyncVar(hook =nameof(OnAnimationDirectionChanged))] public string directionString = "Side"; [SyncVar(hook =nameof(OnAnimationNameChanged))] public string animationString = "Idle"; void OnAnimationDirectionChanged(string oldVal, string newVal){ UpdateAnimation(newVal, animationString); } void OnAnimationNameChanged(string oldVal, string newVal){ UpdateAnimation(directionString, newVal); } [Header("Attack Timing")] float attackTimer = 0f; float attackDuration = 1.4f; [SyncVar] public float maxHealth; #if UNITY_SERVER || UNITY_EDITOR [Server] private void FixedUpdate() { if (health <= 0) { return; } healthBar.SetHealth(health, maxHealth); MagicalhealthBar.SetHealth(magicalHealth, maxHealth); if (isInChaseRange && !isInAttackRange) { MoveEnemy(movement); //Set animation to moving animationString = "Walk"; // Reset attack state when not in attack range hasDealtDamage = false; attackTimer = 0; } else if (isInAttackRange) { rb2.velocity = Vector2.zero; // MODIFIED: Only attack if enemy has stopped moving (velocity near zero) if (rb2.velocity.magnitude < 0.1f) { //Set animation to attack animationString = "Attack"; if (attackTimer < attackDuration) { attackTimer += Time.deltaTime; // MODIFIED: Deal damage at specific timing in animation float attackProgress = attackTimer / attackDuration; if (!hasDealtDamage && attackProgress >= damageTimingPercent) { hasDealtDamage = true; Attack(); } } else { // MODIFIED: Reset for next attack cycle attackTimer = 0; hasDealtDamage = false; } } } if (!isInAttackRange && !isInChaseRange) { //SetAnimation to idle animationString = "Idle"; // Reset attack state when idle hasDealtDamage = false; attackTimer = 0; } } #endif public void Attack(){ target.TakeDamage(enemyAttackDamage); } private void MoveEnemy(Vector2 dir){ rb2.MovePosition((Vector2)transform.position + (dir * speed * Time.deltaTime)); } void UpdateAnimation(string direction, string animationName){ // try{ StartCoroutine(CoroutineUpdateAnim(direction, animationName)); } IEnumerator CoroutineUpdateAnim(string direction, string animationName){ while(animator == null){ yield return new WaitForSeconds(0.1f); Debug.LogError("animator is null!"); } while(animator.skeleton == null){ yield return new WaitForSeconds(0.1f); Debug.LogError("animator skelton is null!"); } while(animator.AnimationState == null){ yield return new WaitForSeconds(0.1f); Debug.LogError("animator state is null!"); } animator.skeleton.SetSkin(direction); animator.skeleton.SetSlotsToSetupPose(); animator.AnimationState.SetAnimation(0, $"{direction}_{animationName}", !animationName.ToLower().Contains("death")); // }catch(Exception e){ // Debug.LogError(e.ToString()); // } Debug.Log($"Updating enemy animation {direction}_{animationName}"); } [Command(requiresAuthority =false)] void CmdTakeDamage(int damage,uint id){ takedmg(damage,id); Debug.Log("Enemy Attack Recieved "); } public void TakeDamage(int damage, uint id){ if(isServer){ takedmg(damage,id); } else{ CmdTakeDamage(damage,id); } } void takedmg(int damage,uint id){ if(health<=0){return;} // Apply physical resistance to base damage int damageAfterResistance = Mathf.Max(1, damage - physicalResistance); // If shield is active, damage goes to both magical health (shield) AND regular health if(shieldActive && magicalHealth > 0){ // Shield takes full damage after resistance magicalHealth -= damageAfterResistance; // Regular health takes reduced damage (divided by shield divider) float shieldMultiplier = 1f / SHIELD_DAMAGE_DIVIDER; int healthDamage = Mathf.Max(1, Mathf.RoundToInt(damageAfterResistance * shieldMultiplier)); health -= healthDamage; if(magicalHealth <= 0){ shieldActive = false; PlayShieldBreakAnimation(); } } else { // If shield is broken, damage goes directly to health with full resistance health -= damageAfterResistance; } //hit vfx // GameObject newObject = Instantiate(hitVfx , transform.position , Quaternion.identity ); // newObject.transform.localPosition = Vector3.zero; // newObject.transform.parent = transform; if(health<= 0 ){ StartCoroutine(couroutineDeath()); foreach(playerNetwork player in FindObjectsOfType()){ if(player.netId == id){ //This one attacked me player.OnEnemyKilled(level); } } } // Debug logging removed for cleaner code } [Command(requiresAuthority =false)] void CmdTakeMagicalDamage(int damage,uint id){ takeMagicalDmg(damage,id); Debug.Log("Enemy Attack Recieved "); } public void TakeMagicalDamage(int damage, uint id){ if(isServer){ takeMagicalDmg(damage,id); } else{ CmdTakeMagicalDamage(damage,id); } } void takeMagicalDmg(int damage,uint id){ if(health<=0){return;} // Apply magical resistance to base damage int damageAfterResistance = Mathf.Max(1, damage - magicalResistance); // If shield is active, damage goes to both magical health (shield) AND regular health if(shieldActive && magicalHealth > 0){ // Shield takes full damage after resistance magicalHealth -= damageAfterResistance; // Regular health takes reduced damage (divided by shield divider) float shieldMultiplier = 1f / SHIELD_DAMAGE_DIVIDER; int healthDamage = Mathf.Max(1, Mathf.RoundToInt(damageAfterResistance * shieldMultiplier)); health -= healthDamage; if(magicalHealth <= 0){ shieldActive = false; PlayShieldBreakAnimation(); } } else { // If shield is broken, damage goes directly to health with full resistance health -= damageAfterResistance; } if(health<= 0 ){ StartCoroutine(couroutineDeath()); foreach(playerNetwork player in FindObjectsOfType()){ if(player.netId == id){ //This one attacked me player.OnEnemyKilled(level); } } } // Debug logging removed for cleaner code } IEnumerator couroutineDeath(){ animationString = "Death"; UpdateAnimation(directionString , animationString); // RpcUpdateAnim(directionString, animationString,false); Vector3 lootSpawnPos = transform.position; lootSpawnPos.z = GameManager.instance.LootSpawnPointsParent.GetChild(0).position.z; //instantiate loot item GameObject newLoot = Instantiate(GameManager.instance.GetRandomLoot(), lootSpawnPos, Quaternion.identity); NetworkServer.Spawn(newLoot); yield return new WaitForSecondsRealtime(5); if (!isServer) { CmdDie(); } else { GameManager.OnEnemyDeath(this, defaultPos); } /* transform.position = defaultPos; health = (int)maxHealth; magicalHealth = (int)maxHealth;*/ //animationString = "Idle"; } [Command] void CmdDie() { GameManager.OnEnemyDeath(this,defaultPos); } public void OnHealthChange(int oldVlaue, int newValue){ healthBar.SetHealth(newValue,maxHealth); } public void OnMagicalHealthChange(int oldVlaue, int newValue){ MagicalhealthBar.SetHealth(newValue,maxHealth); } //etc for ui Disspear coroutine IEnumerator PopDisappearUI() { Vector3 originalScale = uiEnemy.localScale; // First, scale up slightly float popDuration = 0.15f; float elapsedTime = 0f; Vector3 popScale = originalScale * 1.2f; while (elapsedTime < popDuration) { float t = elapsedTime / popDuration; uiEnemy.localScale = Vector3.Lerp(originalScale, popScale, t); elapsedTime += Time.deltaTime; yield return null; } // Then scale down to zero quickly float shrinkDuration = 0.3f; elapsedTime = 0f; while (elapsedTime < shrinkDuration) { float t = elapsedTime / shrinkDuration; // Use ease-in curve for faster shrinking float easedT = t * t; uiEnemy.localScale = Vector3.Lerp(popScale, Vector3.zero, easedT); elapsedTime += Time.deltaTime; yield return null; } uiEnemy.localScale = Vector3.zero; uiEnemy.gameObject.SetActive(false); } [Header("Shield Visual Effects")] public Transform shieldUI; public SpriteRenderer shieldIconUI; public ParticleSystem shieldBreakVfx; public void PlayShieldBreakAnimation() { if (shieldBreakVfx != null) shieldBreakVfx.Play(); if (shieldUI != null) { shieldUI.DOScale(1.2f, 0.15f) .SetEase(Ease.OutBack) .OnComplete(() => { shieldUI.DOScale(0f, 0.3f).SetEase(Ease.InQuad); if (shieldIconUI != null) { shieldIconUI.DOFade(0f, 0.3f).SetEase(Ease.InQuad) .OnComplete(() => { shieldUI.gameObject.SetActive(false); }); } }); } } // Helper method to get resistance info for UI or debugging public string GetResistanceInfo() { string shieldStatus = shieldActive ? "Active" : "Broken"; return $"Physical: {physicalResistance}, Magical: {magicalResistance}, Shield: {shieldStatus}"; } public int CalculateEffectiveDamage(int baseDamage, bool isMagical = false) { int resistance = isMagical ? magicalResistance : physicalResistance; if(shieldActive && magicalHealth > 0){ float shieldMultiplier = 1f / SHIELD_DAMAGE_DIVIDER; int damageAfterShield = Mathf.RoundToInt(baseDamage * shieldMultiplier); return Mathf.Max(1, damageAfterShield - resistance); } else { return Mathf.Max(1, baseDamage - resistance); } } public bool IsShieldActive() { return shieldActive && magicalHealth > 0; } public static int CalculateExponentialXP(int enemyLevel, int playerLevel = 0) { float baseXP = XP_LEVEL_MULTIPLIER * Mathf.Pow(XP_EXPONENTIAL_BASE, enemyLevel - 1); float levelDifference = Mathf.Max(0, enemyLevel - playerLevel); float bonusXP = baseXP * XP_PLAYER_LEVEL_BONUS * levelDifference; int totalXP = Mathf.RoundToInt(baseXP + bonusXP); totalXP = Mathf.Min(totalXP, 5000); return Mathf.Max(10, totalXP); } public static int CalculateLegacyXP(int enemyLevel) { return XP_GAIN_Base + Mathf.FloorToInt(XP_GAIN * (enemyLevel - 1)); } }