enemyScript.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. using System.Collections;
  2. using UnityEngine;
  3. using Spine.Unity;
  4. using Spine.Unity.Examples;
  5. using Mirror;
  6. using DG.Tweening;
  7. public class enemyScript : NetworkBehaviour
  8. {
  9. // Health and Damage Constants
  10. public const int HEALTH_INC = 2;
  11. public const float DAMAGE_INC = 1.2f;
  12. // XP System Constants
  13. public const float XP_GAIN = 1.5f; // Legacy
  14. public const int XP_GAIN_Base = 5; // Legacy
  15. public const float XP_EXPONENTIAL_BASE = 1.3f;
  16. public const float XP_LEVEL_MULTIPLIER = 8f;
  17. public const float XP_PLAYER_LEVEL_BONUS = 0.05f;
  18. // Resistance and Shield Constants
  19. public const int RESISTANCE_INC = 1;
  20. public const int MAX_RESISTANCE = 15;
  21. public const float SHIELD_DAMAGE_DIVIDER = 2f;
  22. [Header("Health & Shield System")]
  23. [SyncVar(hook = nameof(OnHealthChange))]
  24. public int health;
  25. [SyncVar(hook = nameof(OnMagicalHealthChange))]
  26. public int magicalHealth;
  27. [SyncVar]
  28. public int physicalResistance;
  29. [SyncVar]
  30. public int magicalResistance;
  31. [SyncVar]
  32. public bool shieldActive = true;
  33. [Header("UI Components")]
  34. public SpriteHealthBar healthBar;
  35. public SpriteHealthBar MagicalhealthBar;
  36. public Transform uiEnemy;
  37. public TextMesh enemyName;
  38. public TextMesh enemyLevel;
  39. [Header("Movement & Combat")]
  40. public float speed;
  41. public float chaseRadius;
  42. public float attackRadius;
  43. public bool rotate;
  44. public int enemyAttackDamage = 10;
  45. public float damageTimingPercent = 0.6f;
  46. [Header("Targeting & State")]
  47. public playerNetwork target;
  48. public bool isInChaseRange;
  49. public bool isInAttackRange;
  50. [Header("Components")]
  51. private Rigidbody2D rb2;
  52. public SkeletonAnimation animator;
  53. private Vector2 movement;
  54. public Vector3 dir;
  55. MeshRenderer meshRenderer;
  56. [SyncVar]
  57. public bool hasDealtDamage = false;
  58. void Awake(){
  59. meshRenderer = GetComponent<MeshRenderer>();
  60. scanCooldown = Random.Range(0.5f, 1.5f);
  61. }
  62. private void Start(){
  63. rb2 = GetComponent<Rigidbody2D>();
  64. UpdateAnimation(directionString, animationString);
  65. defaultPos = transform.position;
  66. }
  67. [SyncVar(hook =nameof(OnLevelChanged))]
  68. public int level;
  69. void OnLevelChanged(int oldVal, int newVal){
  70. if(isServer){return;}
  71. SetLevel(newVal);
  72. }
  73. public void SetLevel(int _level){
  74. if(enemyLevel != null){
  75. enemyLevel.text = _level.ToString();
  76. }
  77. level = _level;
  78. int healthIncrement =level * HEALTH_INC;
  79. maxHealth = 100 + healthIncrement;
  80. health = (int)maxHealth;
  81. magicalHealth = (int)maxHealth;
  82. enemyAttackDamage += (int)(level * DAMAGE_INC);
  83. int resistanceIncrement = level * RESISTANCE_INC;
  84. physicalResistance = Mathf.Min(resistanceIncrement, MAX_RESISTANCE);
  85. magicalResistance = Mathf.Min(resistanceIncrement, MAX_RESISTANCE);
  86. shieldActive = true;
  87. }
  88. public Vector3 defScale;
  89. Vector3 defaultPos;
  90. float playerDistCheckTimer=0f;
  91. void LateUpdate(){
  92. LOD();
  93. }
  94. public const float disappearDistFromPlayer = 15f;
  95. void LOD(){
  96. if(playerDistCheckTimer > 0){playerDistCheckTimer -= Time.deltaTime;return;}
  97. playerDistCheckTimer = Random.Range(1.5f,2.5f);
  98. if(playerNetwork.localPlayerTransform == null){return;}
  99. float distToPlayer = Vector3.Distance(playerNetwork.localPlayerTransform.position, transform.position);
  100. meshRenderer.enabled = distToPlayer < disappearDistFromPlayer;
  101. }
  102. #if UNITY_SERVER || UNITY_EDITOR
  103. [Server]
  104. private void Update(){
  105. // animator.skeleton.SetSkin
  106. // set animation state to running if in chase Range
  107. //isInChaseRange = true
  108. // isInChaseRange = Physics2D.OverlapCircle(transform.position, chaseRadius , layerMask);
  109. // isInAttackRange = Physics2D.OverlapCircle(transform.position, attackRadius, layerMask);
  110. if (health <= 0 ){
  111. return;
  112. }
  113. if(target != null){
  114. isInChaseRange = Vector3.Distance(transform.position, target.transform.position) < chaseRadius;
  115. isInAttackRange = Vector3.Distance(transform.position, target.transform.position) < attackRadius;
  116. }else{
  117. isInChaseRange = false;
  118. isInAttackRange = false;
  119. }
  120. ScanPlayers();
  121. if(target !=null){
  122. enemyFollow();
  123. }
  124. }
  125. #endif
  126. [Header("Scanning & LOD")]
  127. float scanTimer =0;
  128. float scanCooldown;
  129. public void ScanPlayers(){
  130. if(scanTimer >0){scanTimer-=Time.deltaTime; return;}
  131. scanTimer = scanCooldown;
  132. playerNetwork[] playersinNetwork = FindObjectsOfType<playerNetwork>();
  133. float closestDist = float.MaxValue;
  134. playerNetwork closestPlayer = null;
  135. foreach(playerNetwork player in playersinNetwork ){
  136. if(player.health <= 0 ){continue;}
  137. float dist = Vector3.Distance(transform.position, player.transform.position);
  138. if(dist < closestDist){
  139. closestPlayer = player;
  140. closestDist = dist;
  141. }
  142. }
  143. if(closestDist < chaseRadius){
  144. target = closestPlayer ;
  145. }
  146. else {
  147. target = null;
  148. }
  149. //if(target == null) {return;}
  150. }
  151. // [ClientRpc]
  152. // void RpcUpdateAnim(string animDir , string animName, bool isLoop){
  153. // UpdateAnimation(animDir , animName, isLoop);
  154. // }
  155. [SyncVar(hook =nameof(OnFlipped))]
  156. bool isFlipped= false;
  157. void OnFlipped(bool oldVal, bool newVal){
  158. if(isServer){return;}
  159. transform.localScale = new Vector3(defScale.x * (newVal ? -1 : 1),defScale.y,defScale.z);
  160. HandleFlip();
  161. }
  162. void HandleFlip(){
  163. if(uiEnemy == null){
  164. return;
  165. }
  166. if(transform.localScale.x < 0 ){
  167. uiEnemy.localScale = new Vector3(-1,1,1);
  168. }
  169. else{
  170. uiEnemy.localScale = new Vector3(1,1,1);
  171. }
  172. }
  173. private void enemyFollow(){
  174. if(Mathf.Abs(dir.y) > Mathf.Abs(dir.x)){
  175. if(dir.y < 0){
  176. directionString = "Back";
  177. }else{
  178. directionString = "Front";
  179. }
  180. }else{
  181. directionString = "Side";
  182. if(dir.x < 0){
  183. transform.localScale = new Vector3(defScale.x,defScale.y,0);
  184. isFlipped=false;
  185. }else{
  186. transform.localScale = new Vector3(-defScale.x,defScale.y,0);
  187. isFlipped = true;
  188. }
  189. HandleFlip();
  190. }
  191. if(animationHistory != directionString + animationString){
  192. UpdateAnimation(directionString, animationString);
  193. // RpcUpdateAnim(directionString, animationString,true);
  194. }
  195. animationHistory=directionString + animationString;
  196. if(target != null){
  197. dir = transform.position - target.transform.position;
  198. }
  199. float angle = Mathf.Atan2(dir.y , dir.x ) * Mathf.Rad2Deg;
  200. dir.Normalize();
  201. movement = dir;
  202. if(rotate){
  203. //set anim direction x, y dir
  204. }
  205. }
  206. [Header("Animation System")]
  207. string animationHistory ="";
  208. [SyncVar(hook =nameof(OnAnimationDirectionChanged))]
  209. public string directionString = "Side";
  210. [SyncVar(hook =nameof(OnAnimationNameChanged))]
  211. public string animationString = "Idle";
  212. void OnAnimationDirectionChanged(string oldVal, string newVal){
  213. UpdateAnimation(newVal, animationString);
  214. }
  215. void OnAnimationNameChanged(string oldVal, string newVal){
  216. UpdateAnimation(directionString, newVal);
  217. }
  218. [Header("Attack Timing")]
  219. float attackTimer = 0f;
  220. float attackDuration = 1.4f;
  221. [SyncVar]
  222. public float maxHealth;
  223. #if UNITY_SERVER || UNITY_EDITOR
  224. [Server]
  225. private void FixedUpdate() {
  226. if (health <= 0)
  227. {
  228. return;
  229. }
  230. healthBar.SetHealth(health, maxHealth);
  231. MagicalhealthBar.SetHealth(magicalHealth, maxHealth);
  232. if (isInChaseRange && !isInAttackRange)
  233. {
  234. MoveEnemy(movement);
  235. //Set animation to moving
  236. animationString = "Walk";
  237. // Reset attack state when not in attack range
  238. hasDealtDamage = false;
  239. attackTimer = 0;
  240. }
  241. else if (isInAttackRange)
  242. {
  243. rb2.velocity = Vector2.zero;
  244. // MODIFIED: Only attack if enemy has stopped moving (velocity near zero)
  245. if (rb2.velocity.magnitude < 0.1f)
  246. {
  247. //Set animation to attack
  248. animationString = "Attack";
  249. if (attackTimer < attackDuration)
  250. {
  251. attackTimer += Time.deltaTime;
  252. // MODIFIED: Deal damage at specific timing in animation
  253. float attackProgress = attackTimer / attackDuration;
  254. if (!hasDealtDamage && attackProgress >= damageTimingPercent)
  255. {
  256. hasDealtDamage = true;
  257. Attack();
  258. }
  259. }
  260. else
  261. {
  262. // MODIFIED: Reset for next attack cycle
  263. attackTimer = 0;
  264. hasDealtDamage = false;
  265. }
  266. }
  267. }
  268. if (!isInAttackRange && !isInChaseRange)
  269. {
  270. //SetAnimation to idle
  271. animationString = "Idle";
  272. // Reset attack state when idle
  273. hasDealtDamage = false;
  274. attackTimer = 0;
  275. }
  276. }
  277. #endif
  278. public void Attack(){
  279. target.TakeDamage(enemyAttackDamage);
  280. }
  281. private void MoveEnemy(Vector2 dir){
  282. rb2.MovePosition((Vector2)transform.position + (dir * speed * Time.deltaTime));
  283. }
  284. void UpdateAnimation(string direction, string animationName){
  285. // try{
  286. StartCoroutine(CoroutineUpdateAnim(direction, animationName));
  287. }
  288. IEnumerator CoroutineUpdateAnim(string direction, string animationName){
  289. while(animator == null){
  290. yield return new WaitForSeconds(0.1f);
  291. Debug.LogError("animator is null!");
  292. }
  293. while(animator.skeleton == null){
  294. yield return new WaitForSeconds(0.1f);
  295. Debug.LogError("animator skelton is null!");
  296. }
  297. while(animator.AnimationState == null){
  298. yield return new WaitForSeconds(0.1f);
  299. Debug.LogError("animator state is null!");
  300. }
  301. animator.skeleton.SetSkin(direction);
  302. animator.skeleton.SetSlotsToSetupPose();
  303. animator.AnimationState.SetAnimation(0, $"{direction}_{animationName}", !animationName.ToLower().Contains("death"));
  304. // }catch(Exception e){
  305. // Debug.LogError(e.ToString());
  306. // }
  307. Debug.Log($"Updating enemy animation {direction}_{animationName}");
  308. }
  309. [Command(requiresAuthority =false)]
  310. void CmdTakeDamage(int damage,uint id){
  311. takedmg(damage,id);
  312. Debug.Log("Enemy Attack Recieved ");
  313. }
  314. public void TakeDamage(int damage, uint id){
  315. if(isServer){
  316. takedmg(damage,id);
  317. }
  318. else{
  319. CmdTakeDamage(damage,id);
  320. }
  321. }
  322. void takedmg(int damage,uint id){
  323. if(health<=0){return;}
  324. // Apply physical resistance to base damage
  325. int damageAfterResistance = Mathf.Max(1, damage - physicalResistance);
  326. // If shield is active, damage goes to both magical health (shield) AND regular health
  327. if(shieldActive && magicalHealth > 0){
  328. // Shield takes full damage after resistance
  329. magicalHealth -= damageAfterResistance;
  330. // Regular health takes reduced damage (divided by shield divider)
  331. float shieldMultiplier = 1f / SHIELD_DAMAGE_DIVIDER;
  332. int healthDamage = Mathf.Max(1, Mathf.RoundToInt(damageAfterResistance * shieldMultiplier));
  333. health -= healthDamage;
  334. if(magicalHealth <= 0){
  335. shieldActive = false;
  336. PlayShieldBreakAnimation();
  337. }
  338. } else {
  339. // If shield is broken, damage goes directly to health with full resistance
  340. health -= damageAfterResistance;
  341. }
  342. //hit vfx
  343. // GameObject newObject = Instantiate(hitVfx , transform.position , Quaternion.identity );
  344. // newObject.transform.localPosition = Vector3.zero;
  345. // newObject.transform.parent = transform;
  346. if(health<= 0 ){
  347. StartCoroutine(couroutineDeath());
  348. foreach(playerNetwork player in FindObjectsOfType<playerNetwork>()){
  349. if(player.netId == id){
  350. //This one attacked me
  351. player.OnEnemyKilled(level);
  352. }
  353. }
  354. }
  355. // Debug logging removed for cleaner code
  356. }
  357. [Command(requiresAuthority =false)]
  358. void CmdTakeMagicalDamage(int damage,uint id){
  359. takeMagicalDmg(damage,id);
  360. Debug.Log("Enemy Attack Recieved ");
  361. }
  362. public void TakeMagicalDamage(int damage, uint id){
  363. if(isServer){
  364. takeMagicalDmg(damage,id);
  365. }
  366. else{
  367. CmdTakeMagicalDamage(damage,id);
  368. }
  369. }
  370. void takeMagicalDmg(int damage,uint id){
  371. if(health<=0){return;}
  372. // Apply magical resistance to base damage
  373. int damageAfterResistance = Mathf.Max(1, damage - magicalResistance);
  374. // If shield is active, damage goes to both magical health (shield) AND regular health
  375. if(shieldActive && magicalHealth > 0){
  376. // Shield takes full damage after resistance
  377. magicalHealth -= damageAfterResistance;
  378. // Regular health takes reduced damage (divided by shield divider)
  379. float shieldMultiplier = 1f / SHIELD_DAMAGE_DIVIDER;
  380. int healthDamage = Mathf.Max(1, Mathf.RoundToInt(damageAfterResistance * shieldMultiplier));
  381. health -= healthDamage;
  382. if(magicalHealth <= 0){
  383. shieldActive = false;
  384. PlayShieldBreakAnimation();
  385. }
  386. } else {
  387. // If shield is broken, damage goes directly to health with full resistance
  388. health -= damageAfterResistance;
  389. }
  390. if(health<= 0 ){
  391. StartCoroutine(couroutineDeath());
  392. foreach(playerNetwork player in FindObjectsOfType<playerNetwork>()){
  393. if(player.netId == id){
  394. //This one attacked me
  395. player.OnEnemyKilled(level);
  396. }
  397. }
  398. }
  399. // Debug logging removed for cleaner code
  400. }
  401. IEnumerator couroutineDeath(){
  402. animationString = "Death";
  403. UpdateAnimation(directionString , animationString);
  404. // RpcUpdateAnim(directionString, animationString,false);
  405. Vector3 lootSpawnPos = transform.position;
  406. lootSpawnPos.z = GameManager.instance.LootSpawnPointsParent.GetChild(0).position.z;
  407. //instantiate loot item
  408. GameObject newLoot = Instantiate(GameManager.instance.GetRandomLoot(), lootSpawnPos, Quaternion.identity);
  409. NetworkServer.Spawn(newLoot);
  410. yield return new WaitForSecondsRealtime(5);
  411. if (!isServer)
  412. {
  413. CmdDie();
  414. }
  415. else
  416. {
  417. GameManager.OnEnemyDeath(this, defaultPos);
  418. }
  419. /* transform.position = defaultPos;
  420. health = (int)maxHealth;
  421. magicalHealth = (int)maxHealth;*/
  422. //animationString = "Idle";
  423. }
  424. [Command]
  425. void CmdDie()
  426. {
  427. GameManager.OnEnemyDeath(this,defaultPos);
  428. }
  429. public void OnHealthChange(int oldVlaue, int newValue){
  430. healthBar.SetHealth(newValue,maxHealth);
  431. }
  432. public void OnMagicalHealthChange(int oldVlaue, int newValue){
  433. MagicalhealthBar.SetHealth(newValue,maxHealth);
  434. }
  435. //etc for ui Disspear coroutine
  436. IEnumerator PopDisappearUI()
  437. {
  438. Vector3 originalScale = uiEnemy.localScale;
  439. // First, scale up slightly
  440. float popDuration = 0.15f;
  441. float elapsedTime = 0f;
  442. Vector3 popScale = originalScale * 1.2f;
  443. while (elapsedTime < popDuration)
  444. {
  445. float t = elapsedTime / popDuration;
  446. uiEnemy.localScale = Vector3.Lerp(originalScale, popScale, t);
  447. elapsedTime += Time.deltaTime;
  448. yield return null;
  449. }
  450. // Then scale down to zero quickly
  451. float shrinkDuration = 0.3f;
  452. elapsedTime = 0f;
  453. while (elapsedTime < shrinkDuration)
  454. {
  455. float t = elapsedTime / shrinkDuration;
  456. // Use ease-in curve for faster shrinking
  457. float easedT = t * t;
  458. uiEnemy.localScale = Vector3.Lerp(popScale, Vector3.zero, easedT);
  459. elapsedTime += Time.deltaTime;
  460. yield return null;
  461. }
  462. uiEnemy.localScale = Vector3.zero;
  463. uiEnemy.gameObject.SetActive(false);
  464. }
  465. [Header("Shield Visual Effects")]
  466. public Transform shieldUI;
  467. public SpriteRenderer shieldIconUI;
  468. public ParticleSystem shieldBreakVfx;
  469. public void PlayShieldBreakAnimation()
  470. {
  471. if (shieldBreakVfx != null) shieldBreakVfx.Play();
  472. if (shieldUI != null)
  473. {
  474. shieldUI.DOScale(1.2f, 0.15f)
  475. .SetEase(Ease.OutBack)
  476. .OnComplete(() =>
  477. {
  478. shieldUI.DOScale(0f, 0.3f).SetEase(Ease.InQuad);
  479. if (shieldIconUI != null)
  480. {
  481. shieldIconUI.DOFade(0f, 0.3f).SetEase(Ease.InQuad)
  482. .OnComplete(() =>
  483. {
  484. shieldUI.gameObject.SetActive(false);
  485. });
  486. }
  487. });
  488. }
  489. }
  490. // Helper method to get resistance info for UI or debugging
  491. public string GetResistanceInfo()
  492. {
  493. string shieldStatus = shieldActive ? "Active" : "Broken";
  494. return $"Physical: {physicalResistance}, Magical: {magicalResistance}, Shield: {shieldStatus}";
  495. }
  496. public int CalculateEffectiveDamage(int baseDamage, bool isMagical = false)
  497. {
  498. int resistance = isMagical ? magicalResistance : physicalResistance;
  499. if(shieldActive && magicalHealth > 0){
  500. float shieldMultiplier = 1f / SHIELD_DAMAGE_DIVIDER;
  501. int damageAfterShield = Mathf.RoundToInt(baseDamage * shieldMultiplier);
  502. return Mathf.Max(1, damageAfterShield - resistance);
  503. } else {
  504. return Mathf.Max(1, baseDamage - resistance);
  505. }
  506. }
  507. public bool IsShieldActive()
  508. {
  509. return shieldActive && magicalHealth > 0;
  510. }
  511. public static int CalculateExponentialXP(int enemyLevel, int playerLevel = 0)
  512. {
  513. float baseXP = XP_LEVEL_MULTIPLIER * Mathf.Pow(XP_EXPONENTIAL_BASE, enemyLevel - 1);
  514. float levelDifference = Mathf.Max(0, enemyLevel - playerLevel);
  515. float bonusXP = baseXP * XP_PLAYER_LEVEL_BONUS * levelDifference;
  516. int totalXP = Mathf.RoundToInt(baseXP + bonusXP);
  517. totalXP = Mathf.Min(totalXP, 5000);
  518. return Mathf.Max(10, totalXP);
  519. }
  520. public static int CalculateLegacyXP(int enemyLevel)
  521. {
  522. return XP_GAIN_Base + Mathf.FloorToInt(XP_GAIN * (enemyLevel - 1));
  523. }
  524. }