using System;
using UnityEngine;
namespace UAI
{
///
/// V4 combat and interaction utilities: debug display, item actions, weapon management,
/// look/rotate helpers, and miscellaneous entity queries.
///
/// Differences from :
///
/// - no longer has an unconditional early return;
/// the dead code path is removed and the method is properly implemented.
/// - SetRandomLook is omitted — it was disabled (early return) in the
/// original and has no callers.
/// - uses
/// to avoid a per-call list allocation.
///
///
///
public static class CombatUtils
{
// ── Debug Display ─────────────────────────────────────────────────────────
///
/// Writes the entity's active action, task, and target to its
/// DebugNameInfo string (visible in the debug overlay).
/// Does nothing on a dedicated server or when the debug-task overlay is disabled.
///
public static void DisplayDebugInformation(Context context, string prefix = "", string postfix = "")
{
if (!GameManager.IsDedicatedServer)
{
if (!GamePrefs.GetBool(EnumGamePrefs.DebugMenuShowTasks) || context.Self.IsDead())
{
if (EntityUtilities.GetLeaderOrOwner(context.Self.entityId) == null)
context.Self.DebugNameInfo = String.Empty;
else if (string.IsNullOrEmpty(context.Self.DebugNameInfo))
context.Self.DebugNameInfo = context.Self.EntityName;
return;
}
}
var msg = $" ( {context.Self.entityId} ) {prefix}\n";
msg += $" Active Action: {context.ActionData.Action?.Name}\n";
var taskIndex = context.ActionData.TaskIndex;
var tasks = context.ActionData.Action?.GetTasks();
if (tasks == null)
{
msg += " Active Task: None";
context.Self.DebugNameInfo = msg;
return;
}
msg += $" Active Task: {tasks[taskIndex]}\n";
msg += $" Active Target: {context.ActionData.Target}\n";
msg += $" {postfix}";
context.Self.DebugNameInfo = msg;
}
// ── Item Actions ──────────────────────────────────────────────────────────
///
/// Fires the start/end min-events for item action ,
/// executes the action instantly, and stops the current task.
///
public static void SimulateActionInstantExecution(Context context, int actionIdx, ItemStack itemStack)
{
if (!Equals(itemStack, ItemStack.Empty))
{
context.Self.MinEventContext.ItemValue = itemStack.itemValue;
var startEvent = actionIdx == 0
? MinEventTypes.onSelfPrimaryActionStart
: MinEventTypes.onSelfSecondaryActionStart;
var endEvent = actionIdx == 0
? MinEventTypes.onSelfPrimaryActionEnd
: MinEventTypes.onSelfSecondaryActionEnd;
itemStack.itemValue.FireEvent(startEvent, context.Self.MinEventContext);
context.Self.FireEvent(startEvent, false);
itemStack.itemValue.ItemClass.Actions[actionIdx]
.ExecuteInstantAction(context.Self, itemStack, false, null);
itemStack.itemValue.FireEvent(endEvent, context.Self.MinEventContext);
context.Self.FireEvent(endEvent, false);
}
context.ActionData.CurrentTask.Stop(context);
}
// ── Weapon Management ─────────────────────────────────────────────────────
///
/// Switches the entity's held item to inventory slot 0 (primary weapon slot).
/// No-ops when slot 0 is already active.
///
public static void SetWeapon(Context context)
{
if (context.Self.inventory.holdingItemIdx != 0)
{
context.Self.inventory.SetHoldingItemIdx(0);
context.Self.inventory.OnUpdate();
}
}
///
/// Switches the entity's held item to the dummy (hidden) slot.
/// No-ops when the dummy slot is already active.
///
public static void HideWeapon(Context context)
{
if (context.Self.inventory.holdingItemIdx != context.Self.inventory.DUMMY_SLOT_IDX)
{
context.Self.inventory.SetHoldingItemIdx(context.Self.inventory.DUMMY_SLOT_IDX);
context.Self.inventory.OnUpdate();
}
}
// ── Look / Rotate ─────────────────────────────────────────────────────────
///
/// Rotates the entity to face the nearest non-enemy player (or
/// if provided) within a 5-unit radius.
/// Skips sleeping entities. Does nothing when the entity itself is sleeping.
/// Uses to avoid a per-call list allocation.
///
public static void TurnToFaceEntity(Context context, EntityAlive priorityEntity = null)
{
if (context.Self.IsSleeping)
return;
var buffer = NPCFrameCache.EntityBuffer;
buffer.Clear();
context.Self.world.GetEntitiesInBounds(
typeof(EntityAlive),
new Bounds(context.Self.position, Vector3.one * 5f),
buffer);
if (buffer.Count == 0)
return;
Entity lookTarget = null;
foreach (var entity in buffer)
{
if (entity.IsDead()) continue;
if (priorityEntity != null && entity.entityId == priorityEntity.entityId)
{
lookTarget = entity;
break;
}
if (entity is not EntityPlayerLocal && entity is not EntityPlayer) continue;
if (EntityTargetingUtilities.IsEnemy(context.Self, entity)) continue;
lookTarget = entity;
}
if (lookTarget == null)
return;
context.Self.SetLookPosition(lookTarget.getHeadPosition());
context.Self.RotateTo(lookTarget.position.x, lookTarget.position.y, lookTarget.position.z, 8f, 8f);
}
///
/// Sets the entity's look position and rotation toward .
/// When is an entity alive, a small random yaw/pitch offset
/// is applied. When it is a , the entity rotates directly to it.
///
public static void SetLookPosition(Context context, object target)
{
var entityAlive = UAIUtils.ConvertToEntityAlive(context.ActionData.Target);
if (entityAlive != null)
{
var headPos = entityAlive.getHeadPosition();
var forwardVec = context.Self.GetForwardVector();
forwardVec = Quaternion.Euler(
context.Self.rand.RandomFloat * 60f - 30f,
context.Self.rand.RandomFloat * 120f - 60f,
0f) * forwardVec;
context.Self.SetLookPosition(headPos + forwardVec);
}
if (target is not Vector3 vec)
return;
context.Self.RotateTo(vec.x, vec.y, vec.z, 8f, 8f);
context.Self.SetLookPosition(vec);
}
// ── Stance ────────────────────────────────────────────────────────────────
/// Sets the entity's crouching state.
public static void SetCrouching(Context context, bool crouch = false)
{
// Let a mod pin an NPC's stance regardless of the leader mirror.
// 0 / absent = normal (mirror leader) 1 = force crouch 2 = force stand
float over = context.Self.Buffs.GetCustomVar("CrouchOverride");
if (over == 1f) crouch = true;
else if (over == 2f) crouch = false;
context.Self.Crouching = crouch;
}
// ── Queries ───────────────────────────────────────────────────────────────
///
/// Returns true when the entity has the named buff active.
/// Returns false immediately when is null or empty.
///
public static bool HasBuff(Context context, string buff)
{
return !string.IsNullOrEmpty(buff) && context.Self.Buffs.HasBuff(buff);
}
///
/// Returns a random position near the entity's home position,
/// or when no home is set or no valid
/// spawn point could be calculated.
///
public static Vector3 HasHomePosition(Context context)
{
if (!context.Self.hasHome())
return Vector3.zero;
var homePos = context.Self.getHomePosition();
var position = RandomPositionGenerator.CalcTowards(
context.Self, 5, 100, 10, homePos.position.ToVector3());
return position == Vector3.zero ? Vector3.zero : position;
}
}
}