diff --git a/Assets/GameData/Action/Mage/MageHeal.asset b/Assets/GameData/Action/Mage/MageHeal.asset
index f1d5f0daf5..b30161aaf7 100644
--- a/Assets/GameData/Action/Mage/MageHeal.asset
+++ b/Assets/GameData/Action/Mage/MageHeal.asset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6c16b018e4bfa01b23bea3e0f6ef366ac529f16a749e38c93384e1e38a5c7ab8
-size 1159
+oid sha256:5f28c042dceb2a438a0faec834feaf3b3470fc34629e4ab5e489af33b780ed32
+size 1185
diff --git a/Assets/Scripts/Gameplay/Action/ActionUtils.cs b/Assets/Scripts/Gameplay/Action/ActionUtils.cs
index 19ed37412d..b3d39dc6a7 100644
--- a/Assets/Scripts/Gameplay/Action/ActionUtils.cs
+++ b/Assets/Scripts/Gameplay/Action/ActionUtils.cs
@@ -30,20 +30,35 @@ public static class ActionUtils
const float k_VeryCloseTeleportRange = k_CloseDistanceOffset + 1;
///
- /// Does a melee foe hit detect.
+ /// Detects friends and/or foes near us.
///
- /// true if the attacker is an NPC (and therefore should hit PCs). False for the reverse.
+ /// true if we should detect PCs
+ /// true if we should detect NPCs
/// The collider of the attacking GameObject.
- /// The range in meters to check for foes.
+ /// The range in meters to check.
+ /// The radius in meters to check.
/// Place an uninitialized RayCastHit[] ref in here. It will be set to the results array.
- ///
- /// This method does not alloc. It returns a maximum of 4 results. Consume the results immediately, as the array will be overwritten with
- /// the next similar query.
- ///
- /// Total number of foes encountered.
- public static int DetectMeleeFoe(bool isNPC, Collider attacker, float range, out RaycastHit[] results)
+ ///
+ public static int DetectNearbyEntitiesUseSphere(bool wantPcs, bool wantNpcs, Collider attacker, float range, float radius, out RaycastHit[] results)
{
- return DetectNearbyEntities(isNPC, !isNPC, attacker, range, out results);
+ var myBounds = attacker.bounds;
+
+ if (s_PCLayer == -1)
+ s_PCLayer = LayerMask.NameToLayer("PCs");
+ if (s_NpcLayer == -1)
+ s_NpcLayer = LayerMask.NameToLayer("NPCs");
+
+ int mask = 0;
+ if (wantPcs)
+ mask |= (1 << s_PCLayer);
+ if (wantNpcs)
+ mask |= (1 << s_NpcLayer);
+
+ int numResults = Physics.SphereCastNonAlloc(attacker.transform.position, radius,
+ attacker.transform.forward, s_Hits, range, mask);
+
+ results = s_Hits;
+ return numResults;
}
///
diff --git a/Assets/Scripts/Gameplay/Action/ConcreteActions/DashAttackAction.cs b/Assets/Scripts/Gameplay/Action/ConcreteActions/DashAttackAction.cs
index 4cde3d5229..6f2493e1c2 100644
--- a/Assets/Scripts/Gameplay/Action/ConcreteActions/DashAttackAction.cs
+++ b/Assets/Scripts/Gameplay/Action/ConcreteActions/DashAttackAction.cs
@@ -98,7 +98,7 @@ private void PerformMeleeAttack(ServerCharacter parent)
// perform a typical melee-hit. But note that we are using the Radius field for range, not the Range field!
IDamageable foe = MeleeAction.GetIdealMeleeFoe(Config.IsFriendly ^ parent.IsNpc,
parent.physicsWrapper.DamageCollider,
- Config.Radius,
+ Config.Radius, 0.0f,
(Data.TargetIds != null && Data.TargetIds.Length > 0 ? Data.TargetIds[0] : 0));
if (foe != null)
diff --git a/Assets/Scripts/Gameplay/Action/ConcreteActions/MeleeAction.cs b/Assets/Scripts/Gameplay/Action/ConcreteActions/MeleeAction.cs
index b19f26b9d0..e24196ffd8 100644
--- a/Assets/Scripts/Gameplay/Action/ConcreteActions/MeleeAction.cs
+++ b/Assets/Scripts/Gameplay/Action/ConcreteActions/MeleeAction.cs
@@ -85,7 +85,7 @@ public override bool OnUpdate(ServerCharacter clientCharacter)
///
private IDamageable DetectFoe(ServerCharacter parent, ulong foeHint = 0)
{
- return GetIdealMeleeFoe(Config.IsFriendly ^ parent.IsNpc, parent.physicsWrapper.DamageCollider, Config.Range, foeHint);
+ return GetIdealMeleeFoe(Config.IsFriendly ^ parent.IsNpc, parent.physicsWrapper.DamageCollider, Config.Range, Config.Radius, foeHint);
}
///
@@ -96,25 +96,48 @@ private IDamageable DetectFoe(ServerCharacter parent, ulong foeHint = 0)
/// true if the attacker is an NPC (and therefore should hit PCs). False for the reverse.
/// The collider of the attacking GameObject.
/// The range in meters to check for foes.
+ /// The radius in meters to check for foes.
/// The NetworkObjectId of our preferred foe, or 0 if no preference
/// ideal target's IDamageable, or null if no valid target found
- public static IDamageable GetIdealMeleeFoe(bool isNPC, Collider ourCollider, float meleeRange, ulong preferredTargetNetworkId)
+ ///
+ /// If a Radius value is set (greater than 0), collision checking will be done with a Sphere the size of the Radius, not the size of the Box.
+ /// Also, if multiple targets collide as a result, the target with the highest total damage is prioritized.
+ ///
+ public static IDamageable GetIdealMeleeFoe(bool isNPC, Collider ourCollider, float meleeRange, float meleeRadius, ulong preferredTargetNetworkId)
{
RaycastHit[] results;
- int numResults = ActionUtils.DetectMeleeFoe(isNPC, ourCollider, meleeRange, out results);
+ int numResults = 0.0f < meleeRadius
+ ? ActionUtils.DetectNearbyEntitiesUseSphere(isNPC, !isNPC, ourCollider, meleeRange, meleeRadius, out results)
+ : ActionUtils.DetectNearbyEntities(isNPC, !isNPC, ourCollider, meleeRange, out results);
IDamageable foundFoe = null;
//everything that got hit by the raycast should have an IDamageable component, so we can retrieve that and see if they're appropriate targets.
//we always prefer the hinted foe. If he's still in range, he should take the damage, because he's who the client visualization
//system will play the hit-react on (in case there's any ambiguity).
+ //if that is not the case, we prioritize the target with the highest total damage.
+ int maxDamage = int.MinValue;
+
for (int i = 0; i < numResults; i++)
{
var damageable = results[i].collider.GetComponent();
- if (damageable != null && damageable.IsDamageable() &&
- (damageable.NetworkObjectId == preferredTargetNetworkId || foundFoe == null))
+ if (damageable == null || !damageable.IsDamageable())
+ {
+ continue;
+ }
+
+ if (damageable.NetworkObjectId == preferredTargetNetworkId)
+ {
+ foundFoe = damageable;
+ maxDamage = int.MaxValue;
+ continue;
+ }
+
+ var totalDamage = damageable.GetTotalDamage();
+ if (foundFoe == null || maxDamage < totalDamage)
{
foundFoe = damageable;
+ maxDamage = totalDamage;
}
}
diff --git a/Assets/Scripts/Gameplay/GameplayObjects/Breakable.cs b/Assets/Scripts/Gameplay/GameplayObjects/Breakable.cs
index 246811311c..7249ef775d 100644
--- a/Assets/Scripts/Gameplay/GameplayObjects/Breakable.cs
+++ b/Assets/Scripts/Gameplay/GameplayObjects/Breakable.cs
@@ -116,6 +116,11 @@ public void ReceiveHP(ServerCharacter inflicter, int HP)
}
}
+ public int GetTotalDamage()
+ {
+ return Math.Max(0, m_MaxHealth.Value - m_NetworkHealthState.HitPoints.Value);
+ }
+
private void Break()
{
IsBroken.Value = true;
diff --git a/Assets/Scripts/Gameplay/GameplayObjects/Character/ServerCharacter.cs b/Assets/Scripts/Gameplay/GameplayObjects/Character/ServerCharacter.cs
index 3edac2f84b..623af424e8 100644
--- a/Assets/Scripts/Gameplay/GameplayObjects/Character/ServerCharacter.cs
+++ b/Assets/Scripts/Gameplay/GameplayObjects/Character/ServerCharacter.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections;
using Unity.BossRoom.ConnectionManagement;
using Unity.BossRoom.Gameplay.Actions;
@@ -156,6 +157,7 @@ public override void OnNetworkSpawn()
NetLifeState.LifeState.OnValueChanged += OnLifeStateChanged;
m_DamageReceiver.DamageReceived += ReceiveHP;
m_DamageReceiver.CollisionEntered += CollisionEntered;
+ m_DamageReceiver.GetTotalDamageFunc += GetTotalDamage;
if (IsNpc)
{
@@ -179,6 +181,7 @@ public override void OnNetworkDespawn()
{
m_DamageReceiver.DamageReceived -= ReceiveHP;
m_DamageReceiver.CollisionEntered -= CollisionEntered;
+ m_DamageReceiver.GetTotalDamageFunc -= GetTotalDamage;
}
}
@@ -393,6 +396,11 @@ void CollisionEntered(Collision collision)
}
}
+ int GetTotalDamage()
+ {
+ return Math.Max(0, CharacterClass.BaseHP.Value - HitPoints);
+ }
+
///
/// This character's AIBrain. Will be null if this is not an NPC.
///
diff --git a/Assets/Scripts/Gameplay/GameplayObjects/DamageReceiver.cs b/Assets/Scripts/Gameplay/GameplayObjects/DamageReceiver.cs
index cc4e95c6c6..5d76422407 100644
--- a/Assets/Scripts/Gameplay/GameplayObjects/DamageReceiver.cs
+++ b/Assets/Scripts/Gameplay/GameplayObjects/DamageReceiver.cs
@@ -11,6 +11,8 @@ public class DamageReceiver : NetworkBehaviour, IDamageable
public event Action CollisionEntered;
+ public event Func GetTotalDamageFunc;
+
[SerializeField]
NetworkLifeState m_NetworkLifeState;
@@ -22,6 +24,16 @@ public void ReceiveHP(ServerCharacter inflicter, int HP)
}
}
+ public int GetTotalDamage()
+ {
+ if (!IsDamageable())
+ {
+ return 0;
+ }
+
+ return GetTotalDamageFunc?.Invoke() ?? 0;
+ }
+
public IDamageable.SpecialDamageFlags GetSpecialDamageFlags()
{
return IDamageable.SpecialDamageFlags.None;
diff --git a/Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs b/Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs
index 23185d6313..786eaa2790 100644
--- a/Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs
+++ b/Assets/Scripts/Gameplay/GameplayObjects/IDamageable.cs
@@ -17,6 +17,12 @@ public interface IDamageable
/// The damage done. Negative value is damage, positive is healing.
void ReceiveHP(ServerCharacter inflicter, int HP);
+ ///
+ /// Get the total damage value.
+ ///
+ /// The return value is your total health minus your current health.
+ int GetTotalDamage();
+
///
/// The NetworkId of this object.
///
diff --git a/Assets/Scripts/Gameplay/UserInput/ClientInputSender.cs b/Assets/Scripts/Gameplay/UserInput/ClientInputSender.cs
index 5dd2f0df1d..9c2006aced 100644
--- a/Assets/Scripts/Gameplay/UserInput/ClientInputSender.cs
+++ b/Assets/Scripts/Gameplay/UserInput/ClientInputSender.cs
@@ -420,7 +420,11 @@ void PopulateSkillRequest(Vector3 hitPoint, ActionID actionID, ref ActionRequest
// figure out the Direction in case we want to send it
Vector3 offset = hitPoint - m_PhysicsWrapper.Transform.position;
offset.y = 0;
- Vector3 direction = offset.normalized;
+
+ //there is a bug where the direction is flipped if the hitPos and current position are almost the same,
+ //so we use the character's direction instead.
+ float directionLength = offset.magnitude;
+ Vector3 direction = 1.0f/*epsilon*/ <= directionLength ? (offset / directionLength) : m_PhysicsWrapper.Transform.forward;
switch (actionConfig.Logic)
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04dde187f2..1237536d22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,12 @@ Additional documentation and release notes are available at [Multiplayer Documen
### Cleanup
* Removed ParrelSync from the project
+### Fixed
+* Fix a Healer ability doesn't work (#893)
+ * Changed the way characters are oriented when using skills.
+ * Added the GetTotalDamage API to the IDamagble interface. This number is your maximum health minus your current health.
+ * Changed the way MeleeAction selects a target when there are multiple targets to collide with. The target with the highest GetTotalDamage value (mentioned above) will be selected.
+
## [2.5.0] - 2024-04-18