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