This commit is contained in:
2026-07-09 21:33:33 +02:00
commit 84a2a365f3
2364 changed files with 950134 additions and 0 deletions
@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using VOID.Generic.Dict;
using System.Linq;
using Sirenix.Utilities;
using UnityEngine;
using VOID.Generic.Objects.Item;
namespace VOID.Generic.Util
{
public static class ItemObjectSortUtil
{
public static void Sort(ref ItemObject[] items, Func<ItemObject, IComparable> valueGetter, SortDirectionType sortDirection)
{
if (items.IsNullOrEmpty()) return;
// Find items that can be stacked and then merge them (empty leftovers will be destroyed and removed)
StackGroups(GetStackGroups(items));
Array.Sort(items, (item1, item2) =>
{
// Empty item slots always to the end
if (!item1 && !item2) return 0;
if (!item1) return 1;
if (!item2) return -1;
// Existing items sort by given value getter with given sort direction
var result = valueGetter(item1).CompareTo(valueGetter(item2));
if (sortDirection is SortDirectionType.Descending) result *= - 1;
return result;
});
}
/// <summary>Returns list of item groups that can be stacked together</summary>
private static List<List<ItemObject>> GetStackGroups(ItemObject[] items)
{
var groups = new List<List<ItemObject>>();
foreach (var item in items)
{
// Ignore: empty, non-stackable, those with max stack size already
if (!item) continue;
if (!item.itemData.stackable) continue;
if (item.itemData.stackSize >= item.itemData.stackSizeMax) continue;
// Each group has items that can be stacked together
var group = groups.FirstOrDefault(group => group.First().CanStackWith(item));
if (group == null)
{
group = new List<ItemObject>();
groups.Add(group);
}
group.Add(item);
}
// Ignore groups with only 1 item
return groups.Where(group => group.Count > 1).ToList();
}
/// <summary>
/// 1. Stacks items together as tight as possible
/// 2. Destroys and removes items that have no more stacks inside (by invoking event with Remove())
/// </summary>
private static void StackGroups(List<List<ItemObject>> groups)
{
foreach (var group in groups)
{
var stackSize = group.Sum(item => item.itemData.stackSize);
var maxStackSize = group.First().itemData.stackSizeMax;
var maxStackSizeItems = Mathf.FloorToInt((float)stackSize / maxStackSize);
var remainingStackSize = stackSize % maxStackSize;
// Items that will have 100% of its stack size
group.Take(maxStackSizeItems).ForEach(item => item.itemData.stackSize = maxStackSize);
// Remaining of stack will go to next item
if (remainingStackSize > 0) group.Skip(maxStackSizeItems).First().itemData.stackSizeMax = maxStackSize;
// IMPORTANT! Because we got ref to items here, event caused by Remove() should remove items from list itself
// All other items are left without stacks, so they're "empty" - destroy and remove from list
group.Skip(maxStackSizeItems).Skip(remainingStackSize > 0 ? 1 : 0).ForEach(item => item.Remove());
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6e415e9fcda1d53479a3014dfab7ada2
@@ -0,0 +1,307 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Sirenix.Utilities;
using UnityEngine;
using UnityEngine.AI;
using VOID.Generic.Dict;
using VOID.Generic.Navigation;
using VOID.Generic.Objects.Traversable;
using VOID.Generic.Objects.Unit;
namespace VOID.Generic.Util
{
public static class NavMeshPathUtil
{
private const float CorrectingTargetRange = 5f;
private const float CorrectingLengthRange = 0.5f;
private const float CorrectingHeightRange = 0.75f;
public static Vector3 SamplePosition(NavMeshAgent agent, Vector3 position)
{
NavMesh.SamplePosition(position, out var hit, CorrectingTargetRange, agent.areaMask);
return hit.position;
}
/// <summary>
/// Return NavMeshPath to given target destination from current position of agent.
/// </summary>
public static NavMeshPath GetPathTo(NavMeshAgent agent, Vector3 target)
{
var path = new NavMeshPath();
NavMesh.SamplePosition(target, out var correctedTarget, CorrectingTargetRange, agent.areaMask);
if (correctedTarget.hit) target = correctedTarget.position;
NavMesh.CalculatePath(agent.transform.position, target, agent.areaMask, path);
return path;
}
/// <summary>
/// Alias to <see cref="GetPathShortenedByRange(List{Vector3}, Vector3, float)"/>
/// </summary>
/// <inheritdoc cref="GetPathShortenedByRange(List{Vector3}, Vector3, float)" />
public static List<Vector3> GetPathShortenedByRange(NavMeshPath path, Vector3 destination, float untilInRange)
{
return GetPathShortenedByRange(path.corners.ToList(), destination, untilInRange);
}
/// <summary>
/// Path shortened by given range.
/// Result is List of Vector3 instead of NavMeshPath - because it is only internal :(
/// </summary>
public static List<Vector3> GetPathShortenedByRange(List<Vector3> path, Vector3 destination, float untilInRange)
{
// Invalid path with no corners?
if (path.Count == 0) return new List<Vector3>();
// First point of path is already in that range
if (Vector3.Distance(path.First(), destination) < untilInRange) return new List<Vector3> { path[0] };
for (var i = 0; i < path.Count - 1; i++)
{
var p1 = path[i];
var p2 = path[i + 1];
var intersects = LineIntersectSpherePoints(p1, p2, destination, untilInRange);
// No intersects point found
if (intersects.Length == 0) continue;
var fullDistance = Vector3.Distance(p1, p2);
var distance1 = Vector3.Distance(p1, intersects[0]) + Vector3.Distance(intersects[0], p2);
var distance2 = Vector3.Distance(p1, intersects[1]) + Vector3.Distance(intersects[1], p2);
var diff1 = Math.Abs(fullDistance - distance1);
var diff2 = Math.Abs(fullDistance - distance2);
if (diff1 < 0.05f) return path.Take(i + 1).Append(intersects[0]).ToList();
if (diff2 < 0.05f) return path.Take(i + 1).Append(intersects[1]).ToList();
}
// Path cant reach destination at given range
return path;
}
private static Vector3[] LineIntersectSpherePoints(Vector3 p1, Vector3 p2, Vector3 center, float radius)
{
var dp = new Vector3();
Vector3[] sect;
float a, b, c;
float bb4ac;
float mu1;
float mu2;
// get the distance between X and Z on the segment
dp.x = p2.x - p1.x;
dp.z = p2.z - p1.z;
// I don't get the math here
a = dp.x * dp.x + dp.z * dp.z;
b = 2 * (dp.x * (p1.x - center.x) + dp.z * (p1.z - center.z));
c = center.x * center.x + center.z * center.z;
c += p1.x * p1.x + p1.z * p1.z;
c -= 2 * (center.x * p1.x + center.z * p1.z);
c -= radius * radius;
bb4ac = b * b - 4 * a * c;
if (Mathf.Abs(a) < float.Epsilon || bb4ac < 0)
// line does not intersect
return Array.Empty<Vector3>();
mu1 = (-b + Mathf.Sqrt(bb4ac)) / (2 * a);
mu2 = (-b - Mathf.Sqrt(bb4ac)) / (2 * a);
sect = new Vector3[2];
sect[0] = new Vector3(p1.x + mu1 * (p2.x - p1.x), 0, p1.z + mu1 * (p2.z - p1.z));
sect[1] = new Vector3(p1.x + mu2 * (p2.x - p1.x), 0, p1.z + mu2 * (p2.z - p1.z));
return sect;
}
/// <summary>
/// Correcting NavMeshPath with:
/// - Make whole path sticking to ground
/// - If path going through TraversableObject (NavMeshLink) then stick to it (with unit's offset)
/// Result is PathCorrected instead of NavMeshPath - because it is only internal :(
/// </summary>
public static PathCorrected GetCorrectedPath(List<Vector3> path, UnitObject forUnit)
{
if (path.IsNullOrEmpty()) return null;
// Starting point won't change
var correctedPath = new List<Vector3>(path.GetRange(0, 1));
var usedTraversables = new List<PathCorrectedLinkData>();
var canTraverse = true;
for (var i = 1; i < path.Count; i++)
{
if(canTraverse && NavigationManager.current.GetCachedTraversable(path[i - 1], out var traversable, out var isReverse))
{
// Line between those two points is connected by NavMeshLink, thats why
correctedPath.AddRange(GetCorrectedLinkPathBetweenPoints(path[i - 1], traversable, isReverse, forUnit));
usedTraversables.Add(new PathCorrectedLinkData() { traversable = traversable, isReverse = isReverse });
canTraverse = false;
}
else
{
correctedPath.AddRange(GetCorrectedPathBetweenPoints(path[i - 1], path[i]));
canTraverse = true;
}
}
return new PathCorrected() {path = correctedPath, traversables = usedTraversables};
}
private static List<Vector3> GetCorrectedLinkPathBetweenPoints(Vector3 start,
TraversableObject traversable, bool isReverse, UnitObject forUnit)
{
var unitLadderOffset = forUnit.unitData.ladderPosition.localPosition;
var correctedPath = traversable.traversableData.path.AsEnumerable();
if (isReverse) correctedPath = correctedPath.Reverse();
return correctedPath
.Select(t => t.position + t.InverseTransformDirection(unitLadderOffset))
.Prepend(start)
.ToList();
}
private static List<Vector3> GetCorrectedPathBetweenPoints(Vector3 from, Vector3 to)
{
var correctedPath = new List<Vector3>();
var stepsNeeded = Mathf.CeilToInt(Vector3.Distance(from, to) / CorrectingLengthRange);
var stepProgress = Vector3.Scale(to - from, new Vector3(1, 0, 1)) / stepsNeeded;
var point = from;
for (var i = 1; i <= stepsNeeded; i++)
{
point += stepProgress;
point = GetCorrectedPoint(point);
correctedPath.Add(point);
}
return correctedPath;
}
private static Vector3 GetCorrectedPoint(Vector3 point)
{
var isHit = Physics.Raycast(
new Vector3(point.x, point.y + CorrectingHeightRange, point.z),
Vector3.down, out var hit,
CorrectingHeightRange * 2f,
LayerManager.Static);
if (isHit) return hit.point;
var pointString = $"({point.x}, {point.y}, {point.z})";
Debug.LogWarning($"Can't find correct height of {pointString} in path. Using default point.");
return point;
}
/// <summary>
/// Alias to <see cref="GetPathLength(List{Vector3})"/>
/// </summary>
/// <inheritdoc cref="GetPathLength(List{Vector3})" />
public static float GetPathLength(NavMeshPath navMeshPath)
{
return GetPathLength(navMeshPath.corners.ToList());
}
/// <summary>
/// Returns length of given path
/// </summary>
public static float GetPathLength(List<Vector3> points)
{
if (points.Count <= 2) return 0f;
var length = 0f;
for (var i = 1; i < points.Count; i++) length += RangeUtil.GetDistance(points[i - 1], points[i]);
return length;
}
/// <summary>
/// Alias to <see cref="CalculatePathCost(List{Vector3})"/>
/// </summary>
/// <inheritdoc cref="CalculatePathCost(List{Vector3})" />
public static float CalculatePathCost(NavMeshPath navMeshPath)
{
return CalculatePathCost(navMeshPath.corners.ToList());
}
/// <summary>
/// Returns cost for given path
/// </summary>
public static float CalculatePathCost(List<Vector3> points)
{
float cost = 0;
NavMeshHit hit;
NavMesh.SamplePosition(points[0], out hit, 0.1f, NavMesh.AllAreas);
var rayStart = points[0];
var mask = hit.mask;
var areaIndex = IndexFromMask(mask);
for (var index = 1; index < points.Count; ++index)
{
var maxTries = 5;
do
{
var corner = points[index];
if (NavMesh.Raycast(rayStart, corner, out hit, mask))
{
cost += NavMesh.GetAreaCost(areaIndex) * hit.distance;
if (hit.mask != 0)
mask = hit.mask;
areaIndex = IndexFromMask(mask);
rayStart = hit.position;
if (hit.mask == 0)
rayStart += (corner - rayStart).normalized * 0.05f;
}
else
{
rayStart += (corner - rayStart).normalized * 0.05f;
}
maxTries--;
} while (hit.hit && maxTries > 0);
}
return cost;
}
private static int IndexFromMask(int mask)
{
if (mask == 0) return -1;
var i = 0;
while (mask != 1)
{
mask >>= 1;
++i;
}
return i;
}
public static void SplitPath(Vector3[] path, float movePoints, out Vector3[] reachablePath, out Vector3[] unreachablePath)
{
var currentLength = 0f;
for (var i = 1; i < path.Length; i++)
{
var distanceBetweenPoints = Vector3.Distance(path[i - 1], path[i]);
currentLength += distanceBetweenPoints;
// If still reachable then do nothing
if (currentLength <= movePoints) continue;
// Find exact point where unreachable path will start
var interpolate = (currentLength - movePoints) / distanceBetweenPoints;
var pointBetween = Vector3.Lerp(path[i], path[i - 1], interpolate);
reachablePath = path.Where((_, index) => index < i).Append(pointBetween).ToArray();
unreachablePath = path.Where((_, index) => index >= i).Prepend(pointBetween).ToArray();
return;
}
// Destination is reachale, so there is no unreachable path
reachablePath = path.ToArray();
unreachablePath = null;
}
public static void SplitReachablePath(Vector3[] reachablePath, out List<PathType> pathTypes, out List<Vector3[]> paths)
{
// TODO: right now there is no "dangerous" and "natural" areas!
// TODO: when those will be implemented then this place must be updated as well!
pathTypes = new List<PathType>() { PathType.Reachable };
paths = new List<Vector3[]>() { reachablePath };
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b25399d9053e4d70bec6c9bb978becb2
timeCreated: 1703864750
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Random = UnityEngine.Random;
namespace VOID.Generic.Util
{
public static class RandomUtil
{
/// <summary>
/// Reinit random with given seed.
/// </summary>
public static void InitState(int seed)
{
Random.InitState(seed);
}
/// <summary>
/// Returns random integer between min and max.
/// </summary>
public static int RandomInteger(int min = 0, int max = 100)
{
return Mathf.RoundToInt(Random.value * (max-min)) + min;
}
/// <summary>
/// Returns random float between min and max.
/// </summary>
public static float RandomFloat(float min = 0, float max = 1)
{
return Random.value * (max-min) + min;
}
/// <summary>
/// Returns random element from given collection.
/// </summary>
public static T RandomElement<T>(IEnumerable<T> enumerable)
{
return RandomElements(enumerable, 1)[0];
}
/// <summary>
/// Returns random element from given collection.
/// </summary>
public static List<T> RandomElements<T>(IEnumerable<T> enumerable, int count, bool repeatable = false)
{
var list = enumerable.ToList();
// Not repeatable - we can take each value only once - sort by random and get needed amount
if (!repeatable) return list.OrderBy(_ => RandomFloat()).Take(count).ToList();
// Repeatable - randomize each time and get value from list
return new T[count].Select(_ => list[RandomInteger(0, list.Count - 1)]).ToList();
}
/// <summary>
/// Returns random element from given collection by function that determine weight.
/// </summary>
public static T RandomElementByWeight<T>(IEnumerable<T> enumerable, Func<T, float> getWeight)
{
var list = enumerable.ToList();
var maxWeight = list.Sum(getWeight.Invoke);
var randomWeight = RandomFloat(0, maxWeight);
return list.FirstOrDefault(element => (randomWeight -= getWeight.Invoke(element)) <= 0f);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8933774a793349afafeb7f932b89e447
timeCreated: 1706996942
@@ -0,0 +1,73 @@
using UnityEngine;
using VOID.Generic.Objects.Abstract;
namespace VOID.Generic.Util
{
public static class RangeUtil
{
/// <summary>
/// So far only alias to <see cref="Vector3.Distance(Vector3, Vector3)"/>
/// </summary>
public static float GetDistance(Vector3 pos1, Vector3 pos2)
{
return Vector3.Distance(pos1, pos2);
}
/// <summary>
/// Returns distance between object and point.
/// Also includes in calculations object's radius and height.
/// </summary>
public static float GetDistance(AbstractObject obj, Vector3 pos)
{
var objPosition = obj.transform.position;
// Distance calculation can start from any point on object's height
// So we can start calculating distance from any potion between object position and object position plus height
var verticalDifference = objPosition.y - pos.y;
if (verticalDifference < 0f)
objPosition.y += Mathf.Min(Mathf.Abs(verticalDifference), obj.basicData.height);
var fullDistance = Vector3.Distance(objPosition, pos);
return fullDistance - obj.basicData.radius;
}
/// <summary>
/// Returns distance between two objects.
/// Also includes in calculations their radiuses and heights.
/// </summary>
public static float GetDistance(AbstractObject obj1, AbstractObject obj2)
{
var obj1Position = obj1.transform.position;
var obj2Position = obj2.transform.position;
// Distance calculation can start from any point on object's height
// So we can start calculating distance from any potion between object position and object position plus height
var verticalDifference = obj1Position.y - obj2Position.y;
if (verticalDifference > 0f)
obj2Position.y += Mathf.Min(Mathf.Abs(verticalDifference), obj2.basicData.height);
else
obj1Position.y += Mathf.Min(Mathf.Abs(verticalDifference), obj1.basicData.height);
var fullDistance = Vector3.Distance(obj1Position, obj2Position);
return fullDistance - obj1.basicData.radius - obj2.basicData.radius;
}
public static bool IsInRange(Vector3 pos1, Vector3 pos2, float range, bool withRangeError = true)
{
range += withRangeError ? 0.1f : 0f;
return GetDistance(pos1, pos2) < range;
}
public static bool IsInRange(AbstractObject obj, Vector3 pos, float range, bool withRangeError = true)
{
range += withRangeError ? 0.1f : 0f;
return GetDistance(obj, pos) < range;
}
public static bool IsInRange(AbstractObject obj1, AbstractObject obj2, float range, bool withRangeError = true)
{
range += withRangeError ? 0.1f : 0f;
return GetDistance(obj1, obj2) < range;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9293f5c5905042bebe8162355752717c
timeCreated: 1696251210
@@ -0,0 +1,30 @@
using System.Linq;
using VOID.Generic.Dict;
using VOID.Generic.Objects.Accessory;
using VOID.Generic.Objects.Armor;
using VOID.Generic.Objects.Weapon;
using VOID.Generic.Objects.Wearable;
namespace VOID.Generic.Util
{
public static class WearableUtils
{
/// <returns>TRUE if matches given armor type.</returns>
public static bool IsType(WearableObject wearable, ArmorType armorType)
{
return wearable is ArmorObject armor && armor.armorData.armorType == armorType;
}
/// <returns>TRUE if matches given accessory type.</returns>
public static bool IsType(WearableObject wearable, AccessoryType accessoryType)
{
return wearable is AccessoryObject accessory && accessory.accessoryData.accessoryType == accessoryType;
}
/// <returns>TRUE if matches given weapon carry type.</returns>
public static bool IsWeaponCarryType(WearableObject wearable, params WeaponCarryType[] carryType)
{
return wearable is WeaponObject weapon && carryType.Contains(weapon.weaponData.weaponCarryType);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f0fd1e14117e49f0839d229fd830e634
timeCreated: 1705095672