using System.Collections.Generic;
using Unity.BossRoom.Gameplay.GameplayObjects;
using Unity.Netcode;
using UnityEngine;
namespace Unity.BossRoom.Gameplay.Actions
{
public static class ActionUtils
{
//cache Physics Cast hits, to minimize allocs.
static RaycastHit[] s_Hits = new RaycastHit[4];
// cache layer IDs (after first use). -1 is a sentinel value meaning "uninitialized"
static int s_PCLayer = -1;
static int s_NpcLayer = -1;
static int s_EnvironmentLayer = -1;
///
/// When doing line-of-sight checks we assume the characters' "eyes" are at this height above their transform
///
static readonly Vector3 k_CharacterEyelineOffset = new Vector3(0, 1, 0);
///
/// When teleporting to a destination, this is how far away from the destination spot to arrive
///
const float k_CloseDistanceOffset = 1;
///
/// When checking if a teleport-destination is "too close" to the starting spot, anything less than this is too close
///
const float k_VeryCloseTeleportRange = k_CloseDistanceOffset + 1;
///
/// Does a melee foe hit detect.
///
/// 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.
/// 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)
{
return DetectNearbyEntities(isNPC, !isNPC, attacker, range, out results);
}
///
/// Detects friends and/or foes near us.
///
/// true if we should detect PCs
/// true if we should detect NPCs
/// The collider of the attacking GameObject.
/// The range in meters to check.
/// Place an uninitialized RayCastHit[] ref in here. It will be set to the results array.
///
public static int DetectNearbyEntities(bool wantPcs, bool wantNpcs, Collider attacker, float range, out RaycastHit[] results)
{
//this simple detect just does a boxcast out from our position in the direction we're facing, out to the range of the attack.
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.BoxCastNonAlloc(attacker.transform.position, myBounds.extents,
attacker.transform.forward, s_Hits, Quaternion.identity, range, mask);
results = s_Hits;
return numResults;
}
///
/// Does this NetId represent a valid target? Used by Target Action. The target needs to exist, be a
/// NetworkCharacterState, and be alive. In the future, it will be any non-dead IDamageable.
///
/// the NetId of the target to investigate
/// true if this is a valid target
public static bool IsValidTarget(ulong targetId)
{
//note that we DON'T check if you're an ally. It's perfectly valid to target friends,
//because there are friendly skills, such as Heal.
if (NetworkManager.Singleton.SpawnManager == null || !NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out var targetChar))
{
return false;
}
var targetable = targetChar.GetComponent();
return targetable != null && targetable.IsValidTarget;
}
///
/// Given the coordinates of two entities, checks to see if there is an obstacle between them.
/// (Since character coordinates are beneath the feet of the visual avatar, we add a small amount of height to
/// these coordinates to simulate their eye-line.)
///
/// first character's position
/// second character's position
/// the point where an obstruction occurred (or if no obstruction, this is just character2Pos)
/// true if no obstructions, false if there is a Ground-layer object in the way
public static bool HasLineOfSight(Vector3 character1Pos, Vector3 character2Pos, out Vector3 missPos)
{
if (s_EnvironmentLayer == -1)
{
s_EnvironmentLayer = LayerMask.NameToLayer("Environment");
}
int mask = 1 << s_EnvironmentLayer;
character1Pos += k_CharacterEyelineOffset;
character2Pos += k_CharacterEyelineOffset;
var rayDirection = character2Pos - character1Pos;
var distance = rayDirection.magnitude;
var numHits = Physics.RaycastNonAlloc(new Ray(character1Pos, rayDirection), s_Hits, distance, mask);
if (numHits == 0)
{
missPos = character2Pos;
return true;
}
else
{
missPos = s_Hits[0].point;
return false;
}
}
///
/// Helper method that calculates the percent a charge-up action is charged, based on how long it has run, returning a value
/// from 0-1.
///
/// The time when we finished charging up, or 0 if we're still charging.
/// How long the action has been running.
/// when the action started.
/// the total execution time of the action (usually not its duration).
/// Percent charge-up, from 0 to 1.
public static float GetPercentChargedUp(float stoppedChargingUpTime, float timeRunning, float timeStarted, float execTime)
{
float timeSpentChargingUp;
if (stoppedChargingUpTime == 0)
{
timeSpentChargingUp = timeRunning; // we're still charging up, so all of our runtime has been charge-up time
}
else
{
timeSpentChargingUp = stoppedChargingUpTime - timeStarted;
}
return Mathf.Clamp01(timeSpentChargingUp / execTime);
}
///
/// Determines a spot very near a chosen location, so that we can teleport next to the target (rather
/// than teleporting literally on top of the target). Can optionally perform a bunch of additional checks:
/// - can do a line-of-sight check and stop at the first obstruction.
/// - can make sure that the chosen spot is a meaningful distance away from the starting spot.
/// - can make sure that the chosen spot is no further than a specified distance away.
///
/// character's transform
/// location we want to be next to
/// true if we should be blocked by obstructions such as walls
/// if we should fix up very short teleport destinations, the new location will be this far away (in meters). -1 = don't check for short teleports
/// returned location will be no further away from characterTransform than this. -1 = no max distance
/// new coordinates that are near the destination (or near the first obstruction)
public static Vector3 GetDashDestination(Transform characterTransform, Vector3 targetSpot, bool stopAtObstructions, float distanceToUseIfVeryClose = -1, float maxDistance = -1)
{
Vector3 destinationSpot = targetSpot;
if (distanceToUseIfVeryClose != -1)
{
// make sure our stopping point is a meaningful distance away!
if (destinationSpot == Vector3.zero || Vector3.Distance(characterTransform.position, destinationSpot) <= k_VeryCloseTeleportRange)
{
// we don't have a meaningful stopping spot. Find a new one based on the character's current direction
destinationSpot = characterTransform.position + characterTransform.forward * distanceToUseIfVeryClose;
}
}
if (maxDistance != -1)
{
// make sure our stopping point isn't too far away!
float distance = Vector3.Distance(characterTransform.position, destinationSpot);
if (distance > maxDistance)
{
destinationSpot = Vector3.MoveTowards(destinationSpot, characterTransform.position, distance - maxDistance);
}
}
if (stopAtObstructions)
{
// if we're going to hit an obstruction, stop at the obstruction
if (!HasLineOfSight(characterTransform.position, destinationSpot, out Vector3 collidePos))
{
destinationSpot = collidePos;
}
}
// now get a spot "near" the end point
destinationSpot = Vector3.MoveTowards(destinationSpot, characterTransform.position, k_CloseDistanceOffset);
return destinationSpot;
}
}
///
/// Small utility to better understand action start and stop conclusion
///
public static class ActionConclusion
{
public const bool Stop = false;
public const bool Continue = true;
}
///
/// Utility comparer to sort through RaycastHits by distance.
///
public class RaycastHitComparer : IComparer
{
public int Compare(RaycastHit x, RaycastHit y)
{
return x.distance.CompareTo(y.distance);
}
}
}