diff --git a/Assets/Scripts/Game/EnemyMotor.cs b/Assets/Scripts/Game/EnemyMotor.cs index 505693065b..528c1c67be 100644 --- a/Assets/Scripts/Game/EnemyMotor.cs +++ b/Assets/Scripts/Game/EnemyMotor.cs @@ -48,7 +48,6 @@ public class EnemyMotor : MonoBehaviour bool retreating; // Is retreating bool backingUp; // Is backing up bool fallDetected; // Detected a fall in front of us, so don't move there - bool obstacleDetected; bool foundUpwardSlope; bool foundDoor; Vector3 lastPosition; // Used to track whether we have moved or not @@ -69,10 +68,7 @@ public class EnemyMotor : MonoBehaviour int searchMult; int ignoreMaskForShooting; int ignoreMaskForObstacles; - bool canAct; - bool falls; bool flyerFalls; - float lastGroundedY; // Used for fall damage float originalHeight; EnemySenses senses; @@ -84,7 +80,6 @@ public class EnemyMotor : MonoBehaviour DaggerfallEntityBehaviour entityBehaviour; EnemyBlood entityBlood; EntityEffectManager entityEffectManager; - EntityEffectBundle selectedSpell; EnemyAttack attack; EnemyEntity entity; #endregion @@ -97,6 +92,23 @@ public class EnemyMotor : MonoBehaviour public Vector3 KnockbackDirection { get; set; } // Direction to travel while being knocked back public bool Bashing { get; private set; } // Is this enemy bashing a door public int GiveUpTimer { get; set; } // Timer for enemy giving up pursuit of target + public bool ObstacleDetected { get; private set; } + public EntityEffectBundle SelectedSpell { get; set; } + public bool CanAct { get; set; } + public float LastGroundedY { get; set; } // Used for fall damage + public bool Falls { get; private set; } + + //============Delegates to allow mods to extend motor behaviour. + //==When setting a new handler, it may be desired to store the original and call it before/after your own logic. + public delegate void TakeActionCallback(); + public TakeActionCallback TakeActionHandler { get; set; } + + public delegate bool CanCastRangedSpellCallback(); + public CanCastRangedSpellCallback CanCastRangedSpellHandler { get; set; } + + public delegate bool CanCastTouchSpellCallback(); + public CanCastTouchSpellCallback CanCastTouchSpellHandler { get; set; } + #endregion #region Unity Methods @@ -130,10 +142,14 @@ void Start() // Also ignore arrows and "Ignore Raycast" layer for obstacles ignoreMaskForObstacles = ~(1 << LayerMask.NameToLayer("SpellMissiles") | 1 << LayerMask.NameToLayer("Ignore Raycast")); - lastGroundedY = transform.position.y; + LastGroundedY = transform.position.y; // Get original height, before any height adjustments originalHeight = controller.height; + + TakeActionHandler = TakeAction; + CanCastRangedSpellHandler = CanCastRangedSpell; + CanCastTouchSpellHandler = CanCastTouchSpell; } void FixedUpdate() @@ -142,9 +158,9 @@ void FixedUpdate() return; flies = CanFly(); - canAct = true; + CanAct = true; flyerFalls = false; - falls = false; + Falls = false; HandleParalysis(); KnockbackMovement(); @@ -152,8 +168,8 @@ void FixedUpdate() HandleNoAction(); HandleBashing(); UpdateTimers(); - if (canAct) - TakeAction(); + if (CanAct) + TakeActionHandler(); ApplyFallDamage(); UpdateToIdleOrMoveAnim(); OpenDoors(); @@ -218,7 +234,7 @@ public Vector3 FindGroundPosition(float distance = 16) /// Amount to increment to fallstart public void AdjustLastGrounded(float y) { - lastGroundedY += y; + LastGroundedY += y; } #endregion @@ -237,7 +253,7 @@ void HandleParalysis() if (entityBehaviour.Entity.IsParalyzed) { mobile.FreezeAnims = true; - canAct = false; + CanAct = false; flyerFalls = true; } mobile.FreezeAnims = false; @@ -298,7 +314,7 @@ void KnockbackMovement() EvaluateMoveInForAttack(); } - canAct = false; + CanAct = false; flyerFalls = true; } } @@ -309,22 +325,29 @@ void KnockbackMovement() void ApplyGravity() { // Apply gravity - if (!flies && !swims && !IsLevitating && !controller.isGrounded) + if (entity.IsSlowFalling && !flies && !swims && !controller.isGrounded && !IsLevitating) + { + Vector3 velocity = controller.velocity * 0.97f; //gradually slow x/z movement + velocity.y = -1; //slow downward fall + Vector3 move = velocity * Time.deltaTime; + controller.Move(move); + } + else if (!flies && !swims && !IsLevitating && !controller.isGrounded) { controller.SimpleMove(Vector3.zero); - falls = true; + Falls = true; // Only cancel movement if actually falling. Sometimes mobiles can get stuck where they are !isGrounded but SimpleMove(Vector3.zero) doesn't help. // Allowing them to continue and attempt a Move() frees them, but we don't want to allow that if we can avoid it so they aren't moving // while falling, which can also accelerate the fall due to anti-bounce downward movement in Move(). if (lastPosition != transform.position) - canAct = false; + CanAct = false; } - if (flyerFalls && flies && !IsLevitating) + if (flyerFalls && flies && !IsLevitating && !entity.IsSlowFalling) { controller.SimpleMove(Vector3.zero); - falls = true; + Falls = true; } } @@ -338,7 +361,7 @@ void HandleNoAction() SetChangeStateTimer(); searchMult = 0; - canAct = false; + CanAct = false; } } @@ -356,7 +379,7 @@ void HandleBashing() attack.ResetMeleeTimer(); } - canAct = false; + CanAct = false; } } @@ -372,7 +395,7 @@ void UpdateTimers() avoidObstaclesTimer -= Time.deltaTime; // Set avoidObstaclesTimer to 0 if got close enough to detourDestination. Only bother checking if possible to move. - if (avoidObstaclesTimer > 0 && canAct) + if (avoidObstaclesTimer > 0 && CanAct) { Vector3 detourDestination2D = detourDestination; detourDestination2D.y = transform.position.y; @@ -498,6 +521,7 @@ void TakeAction() pursuing = false; retreating = false; } + } /// @@ -546,7 +570,7 @@ void GetDestination() bool DoRangedAttack(Vector3 direction, float moveSpeed, float distance, bool isPlayingOneShot) { bool inRange = senses.DistanceToTarget > EnemyAttack.minRangedDistance && senses.DistanceToTarget < EnemyAttack.maxRangedDistance; - if (inRange && senses.TargetInSight && senses.DetectedTarget && (CanShootBow() || CanCastRangedSpell())) + if (inRange && senses.TargetInSight && senses.DetectedTarget && (CanShootBow() || CanCastRangedSpellHandler())) { if (DaggerfallUnity.Settings.EnhancedCombatAI && senses.TargetIsWithinYawAngle(22.5f, destination) && strafeTimer <= 0) { @@ -574,7 +598,7 @@ bool DoRangedAttack(Vector3 direction, float moveSpeed, float distance, bool isP } } // Random chance to shoot spell - else if (Random.value < 1/40f && entityEffectManager.SetReadySpell(selectedSpell)) + else if (Random.value < 1/40f && entityEffectManager.SetReadySpell(SelectedSpell)) { mobile.ChangeEnemyState(MobileStates.Spell); } @@ -596,7 +620,7 @@ bool DoTouchSpell() { if (senses.TargetInSight && senses.DetectedTarget && attack.MeleeTimer == 0 && senses.DistanceToTarget <= attack.MeleeDistance + senses.TargetRateOfApproach - && CanCastTouchSpell() && entityEffectManager.SetReadySpell(selectedSpell)) + && CanCastTouchSpellHandler() && entityEffectManager.SetReadySpell(SelectedSpell)) { if (mobile.EnemyState != MobileStates.Spell) mobile.ChangeEnemyState(MobileStates.Spell); @@ -608,6 +632,7 @@ bool DoTouchSpell() return false; } + /// /// Decide whether to strafe, and get direction to strafe to. /// @@ -647,7 +672,7 @@ bool ClearPathToPosition(Vector3 location, float dist = 30) ObstacleCheck(sphereCastDir2d); FallCheck(sphereCastDir2d); - if (obstacleDetected || fallDetected) + if (ObstacleDetected || fallDetected) return false; RaycastHit hit; @@ -670,7 +695,7 @@ bool ClearPathToPosition(Vector3 location, float dist = 30) /// /// Returns true if can shoot projectile at target. /// - bool HasClearPathToShootProjectile(float speed, float originDistance, float radius) + public bool HasClearPathToShootProjectile(float speed, float originDistance, float radius) { // Check that there is a clear path to shoot projectile Vector3 sphereCastDir = senses.PredictNextTargetPos(speed); @@ -753,9 +778,9 @@ bool CanCastRangedSpell() return false; EffectBundleSettings selectedSpellSettings = rangeSpells[Random.Range(0, count)]; - selectedSpell = new EntityEffectBundle(selectedSpellSettings, entityBehaviour); + SelectedSpell = new EntityEffectBundle(selectedSpellSettings, entityBehaviour); - if (EffectsAlreadyOnTarget(selectedSpell)) + if (EffectsAlreadyOnTarget(SelectedSpell)) return false; // Check that there is a clear path to shoot a spell @@ -801,9 +826,9 @@ bool CanCastTouchSpell() return false; EffectBundleSettings selectedSpellSettings = rangeSpells[Random.Range(0, count)]; - selectedSpell = new EntityEffectBundle(selectedSpellSettings, entityBehaviour); + SelectedSpell = new EntityEffectBundle(selectedSpellSettings, entityBehaviour); - if (EffectsAlreadyOnTarget(selectedSpell)) + if (EffectsAlreadyOnTarget(SelectedSpell)) return false; return true; @@ -822,7 +847,7 @@ bool CanFly() /// /// Checks whether the target already is affected by all of the effects of the given spell. /// - bool EffectsAlreadyOnTarget(EntityEffectBundle spell) + public bool EffectsAlreadyOnTarget(EntityEffectBundle spell) { if (senses.Target) { @@ -956,7 +981,7 @@ void AttemptMove(Vector3 direction, float moveSpeed, bool backAway = false, bool ObstacleCheck(direction2d); FallCheck(direction2d); - if (fallDetected || obstacleDetected) + if (fallDetected || ObstacleDetected) { if (!strafe && !backAway) FindDetour(direction2d); @@ -993,13 +1018,13 @@ void FindDetour(Vector3 direction2d) testMove = (direction2d + upOrDown).normalized; ObstacleCheck(testMove); - if (obstacleDetected) + if (ObstacleDetected) { upOrDown.y *= -1; testMove = (direction2d + upOrDown).normalized; ObstacleCheck(testMove); } - if (!obstacleDetected) + if (!ObstacleDetected) foundUpDown = true; } @@ -1025,7 +1050,7 @@ void FindDetour(Vector3 direction2d) ObstacleCheck(testMove); FallCheck(testMove); - if (!obstacleDetected && !fallDetected) + if (!ObstacleDetected && !fallDetected) { // First direction was clear, use that way if (angle == 45) @@ -1044,7 +1069,7 @@ void FindDetour(Vector3 direction2d) ObstacleCheck(testMove); FallCheck(testMove); - if (!obstacleDetected && !fallDetected) + if (!ObstacleDetected && !fallDetected) { if (angle == 45) { @@ -1102,7 +1127,7 @@ void FindDetour(Vector3 direction2d) break; } } - while (obstacleDetected || fallDetected); + while (ObstacleDetected || fallDetected); } detourDestination = transform.position + testMove * 2; @@ -1114,7 +1139,7 @@ void FindDetour(Vector3 direction2d) void ObstacleCheck(Vector3 direction) { - obstacleDetected = false; + ObstacleDetected = false; // Rationale: follow walls at 45° incidence; is that optimal? At least it seems very good float checkDistance = controller.radius / Mathf.Sqrt(2f); foundUpwardSlope = false; @@ -1129,7 +1154,7 @@ void ObstacleCheck(Vector3 direction) if (Physics.CapsuleCast(p1, p2, controller.radius / 2, direction, out hit, checkDistance, ignoreMaskForObstacles)) { // Debug.DrawRay(transform.position, direction, Color.red, 2.0f); - obstacleDetected = true; + ObstacleDetected = true; DaggerfallEntityBehaviour entityBehaviour2 = hit.transform.GetComponent(); DaggerfallActionDoor door = hit.transform.GetComponent(); DaggerfallLoot loot = hit.transform.GetComponent(); @@ -1137,11 +1162,11 @@ void ObstacleCheck(Vector3 direction) if (entityBehaviour2) { if (entityBehaviour2 == senses.Target) - obstacleDetected = false; + ObstacleDetected = false; } else if (door) { - obstacleDetected = false; + ObstacleDetected = false; foundDoor = true; if (senses.TargetIsWithinYawAngle(22.5f, door.transform.position)) { @@ -1151,7 +1176,7 @@ void ObstacleCheck(Vector3 direction) } else if (loot) { - obstacleDetected = false; + ObstacleDetected = false; } else if (!swims && !flies && !IsLevitating) { @@ -1165,7 +1190,7 @@ void ObstacleCheck(Vector3 direction) if (!Physics.CapsuleCast(p1, p2, controller.radius / 2, direction, checkDistance)) { - obstacleDetected = false; + ObstacleDetected = false; foundUpwardSlope = true; } } @@ -1178,7 +1203,7 @@ void ObstacleCheck(Vector3 direction) void FallCheck(Vector3 direction) { - if (flies || IsLevitating || swims || obstacleDetected || foundUpwardSlope || foundDoor) + if (flies || IsLevitating || swims || ObstacleDetected || foundUpwardSlope || foundDoor) { fallDetected = false; return; @@ -1365,9 +1390,9 @@ void ApplyFallDamage() if (controller.isGrounded) { // did enemy just land? - if (falls) + if (Falls) { - float fallDistance = lastGroundedY - transform.position.y; + float fallDistance = LastGroundedY - transform.position.y; if (fallDistance > fallingDamageThreshold) { int damage = (int)(HPPerMetre * (fallDistance - fallingDamageThreshold)); @@ -1385,13 +1410,15 @@ void ApplyFallDamage() } } - lastGroundedY = transform.position.y; + LastGroundedY = transform.position.y; } // For flying enemies, "lastGroundedY" is really "lastAltitudeControlY" - else if (flies && !flyerFalls) - lastGroundedY = transform.position.y; + else if ((flies && !flyerFalls) || IsLevitating || entity.IsSlowFalling) + LastGroundedY = transform.position.y; + } + /// /// Open doors that are in the way. /// diff --git a/Assets/Scripts/Game/EnemySenses.cs b/Assets/Scripts/Game/EnemySenses.cs index 0b2b153276..36b948e9cf 100644 --- a/Assets/Scripts/Game/EnemySenses.cs +++ b/Assets/Scripts/Game/EnemySenses.cs @@ -14,7 +14,6 @@ using DaggerfallWorkshop.Game.Entity; using DaggerfallWorkshop.Game.Formulas; using DaggerfallConnect; -using DaggerfallWorkshop.Game.UserInterfaceWindows; using DaggerfallWorkshop.Game.Questing; using DaggerfallWorkshop.Game.Utility; @@ -184,8 +183,37 @@ public float TargetRateOfApproach set { targetRateOfApproach = value; } } + public float LastHadLOSTimer + { + get { return lastHadLOSTimer; } + set { lastHadLOSTimer = value; } + } + + + + //Delegates to allow mods to replace or extend senses logic. + //Mods can potentially save the original value before replacing it, if access to default behaviour is still desired. + public delegate bool BlockedByIllusionEffectCallback(); + public BlockedByIllusionEffectCallback BlockedByIllusionEffectHandler { get; set; } + + public delegate bool CanSeeTargetCallback(DaggerfallEntityBehaviour target); + public CanSeeTargetCallback CanSeeTargetHandler { get; set; } + + public delegate bool CanHearTargetCallback(); + public CanHearTargetCallback CanHearTargetHandler { get; set; } + + public delegate bool CanDetectOtherwiseCallback(DaggerfallEntityBehaviour target); + public CanDetectOtherwiseCallback CanDetectOtherwiseHandler { get; set; } + + void Start() { + //Initialize delegates to standard defaults + BlockedByIllusionEffectHandler = BlockedByIllusionEffect; + CanSeeTargetHandler = CanSeeTarget; + CanHearTargetHandler = CanHearTarget; + CanDetectOtherwiseHandler = delegate (DaggerfallEntityBehaviour target) { return false; }; + mobile = GetComponent().MobileUnit; entityBehaviour = GetComponent(); enemyEntity = entityBehaviour.Entity as EnemyEntity; @@ -353,7 +381,7 @@ void FixedUpdate() { distanceToTarget = distanceToPlayer; directionToTarget = toPlayer.normalized; - playerInSight = CanSeeTarget(player); + playerInSight = CanSeeTargetHandler(player); } if (classicTargetUpdateTimer > 5) @@ -389,20 +417,20 @@ void FixedUpdate() { distanceToTarget = distanceToPlayer; directionToTarget = toPlayer.normalized; - targetInSight = playerInSight; + targetInSight = CanSeeTargetHandler(player); } else { Vector3 toTarget = target.transform.position - transform.position; distanceToTarget = toTarget.magnitude; directionToTarget = toTarget.normalized; - targetInSight = CanSeeTarget(target); + targetInSight = CanSeeTargetHandler(target); } // Classic stealth mechanics would be interfered with by hearing, so only enable // hearing if the enemy has detected the target. If target is visible we can omit hearing. if (detectedTarget && !targetInSight) - targetInEarshot = CanHearTarget(); + targetInEarshot = CanHearTargetHandler(); else targetInEarshot = false; @@ -414,7 +442,7 @@ void FixedUpdate() // to know where the player is. if (GameManager.ClassicUpdate) { - blockedByIllusionEffect = BlockedByIllusionEffect(); + blockedByIllusionEffect = BlockedByIllusionEffectHandler(); if (lastHadLOSTimer > 0) lastHadLOSTimer--; } @@ -435,6 +463,8 @@ void FixedUpdate() if (lastHadLOSTimer <= 0) lastKnownTargetPos = target.transform.position; } + else if (CanDetectOtherwiseHandler(target)) + detectedTarget = true; else detectedTarget = false; @@ -504,6 +534,7 @@ void FixedUpdate() GameManager.Instance.PlayerEntity.SetEnemyAlert(true); } + #region Public Methods public Vector3 PredictNextTargetPos(float interceptSpeed) @@ -734,6 +765,14 @@ void GetTargets() if ((GameManager.Instance.PlayerEntity.NoTargetMode || !motor.IsHostile || enemyEntity.MobileEnemy.Team == MobileTeams.PlayerAlly) && targetBehaviour == player) continue; + //Player allies should not attack pacified enemies. + if (enemyEntity.Team == MobileTeams.PlayerAlly && targetBehaviour != player) + { + EnemyMotor targetMotor = targetBehaviour.GetComponent(); + if (targetMotor && !targetMotor.IsHostile) + continue; + } + // Can't target ally if (targetBehaviour == player && enemyEntity.Team == MobileTeams.PlayerAlly) continue; @@ -764,7 +803,7 @@ void GetTargets() directionToTarget = toTarget.normalized; distanceToTarget = toTarget.magnitude; - bool see = CanSeeTarget(targetBehaviour); + bool see = CanSeeTargetHandler(targetBehaviour); // Is potential target neither visible nor in area around player? If so, reject as target. if (targetSenses && !targetSenses.WouldBeSpawnedInClassic && !see) diff --git a/Assets/Scripts/Game/Entities/EntityConcealmentBehaviour.cs b/Assets/Scripts/Game/Entities/EntityConcealmentBehaviour.cs index b03a901fcf..0cc57681eb 100644 --- a/Assets/Scripts/Game/Entities/EntityConcealmentBehaviour.cs +++ b/Assets/Scripts/Game/Entities/EntityConcealmentBehaviour.cs @@ -53,7 +53,7 @@ protected void CacheReferences() meshRenderer = GetComponentInChildren(); } - protected void MakeConcealed(bool concealed) + protected virtual void MakeConcealed(bool concealed) { if (meshRenderer) { diff --git a/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs b/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs index 6208ea0b4e..0a68e4673c 100644 --- a/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs +++ b/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs @@ -144,6 +144,9 @@ public LiveEffectBundle[] PoisonBundles get { return GetPoisonBundles(); } } + //The original Daggerfall apparently did it this way (original bug?) + public bool UsePlayerCharacterSkillsForEnemyMagicCost { get; set; } = true; + #endregion #region Unity @@ -316,8 +319,12 @@ public bool SetReadySpell(EntityEffectBundle spell, bool noSpellPointCost = fals if (spell == null || spell.Settings.Version < minAcceptedSpellVersion) return false; + //By default, enemy spell costs are calculated using player-character skill levels. + //Mods can alter this to use enemy skills instead. + DaggerfallEntity casterEntity = UsePlayerCharacterSkillsForEnemyMagicCost ? null : entityBehaviour.Entity; + // Get spellpoint costs of this spell - (int _, int spellPointCost) = FormulaHelper.CalculateTotalEffectCosts(spell.Settings.Effects, spell.Settings.TargetType, null, spell.Settings.MinimumCastingCost); + (int _, int spellPointCost) = FormulaHelper.CalculateTotalEffectCosts(spell.Settings.Effects, spell.Settings.TargetType, casterEntity, spell.Settings.MinimumCastingCost); readySpellCastingCost = spellPointCost; // Allow casting spells of any cost if entity is player and godmode enabled