mmorpg2d/Assets/Script/enemyScript.cs
2025-08-19 14:19:44 +05:30

642 lines
19 KiB
C#

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<MeshRenderer>();
scanCooldown = Random.Range(0.5f, 1.5f);
}
private void Start(){
rb2 = GetComponent<Rigidbody2D>();
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<playerNetwork>();
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<playerNetwork>()){
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<playerNetwork>()){
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));
}
}