236 lines
7.4 KiB
C
236 lines
7.4 KiB
C
// These samples exist only at intake. HQ never revisits their entity loadouts.
|
|
class ERR_HQVisualContact
|
|
{
|
|
IEntity Entity;
|
|
vector Position;
|
|
float ObservedAt;
|
|
bool Escapist;
|
|
string Travel = "unknown";
|
|
string Weapon = "unknown";
|
|
}
|
|
|
|
class ERR_HQObservation
|
|
{
|
|
// Require current native line of sight before reading visible equipment.
|
|
static bool IsClearSighting(float age, float traceFraction, float exposure)
|
|
{
|
|
return age >= 0 && age <= 0.5 && traceFraction >= 1 && exposure > 0;
|
|
}
|
|
|
|
static bool CanIdentifyWeapon(float distance, float exposure)
|
|
{
|
|
return distance >= 0 && distance <= 150 && exposure >= 0.5;
|
|
}
|
|
|
|
static string WeaponCategory(EWeaponType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case EWeaponType.WT_HANDGUN: return "pistol";
|
|
case EWeaponType.WT_RIFLE: return "rifle";
|
|
case EWeaponType.WT_SNIPERRIFLE: return "sniper rifle";
|
|
case EWeaponType.WT_MACHINEGUN: return "machine gun";
|
|
case EWeaponType.WT_ROCKETLAUNCHER: return "rocket launcher";
|
|
case EWeaponType.WT_GRENADELAUNCHER: return "grenade launcher";
|
|
case EWeaponType.WT_FRAGGRENADE: return "grenade";
|
|
case EWeaponType.WT_SMOKEGRENADE: return "smoke grenade";
|
|
}
|
|
return "unidentified weapon";
|
|
}
|
|
|
|
static ERR_HQVisualContact Capture(BaseTarget target, float now)
|
|
{
|
|
if (!target || target.GetTargetCategory() != ETargetCategory.ENEMY || !IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()))
|
|
return null;
|
|
DamageManagerComponent damage = target.GetDamageManagerComponent();
|
|
if (damage && damage.IsDestroyed())
|
|
return null;
|
|
IEntity entity = target.GetTargetEntity();
|
|
if (!entity)
|
|
return null;
|
|
ERR_HQVisualContact seen = new ERR_HQVisualContact();
|
|
seen.Entity = entity;
|
|
seen.Position = target.GetLastSeenPosition();
|
|
seen.ObservedAt = now - target.GetTimeSinceSeen() * 1000;
|
|
SCR_ChimeraCharacter character = SCR_ChimeraCharacter.Cast(entity);
|
|
if (character)
|
|
{
|
|
if (GetGame().GetPlayerManager())
|
|
seen.Escapist = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(entity) > 0;
|
|
seen.Travel = "on foot";
|
|
if (CompartmentAccessComponent.GetVehicleIn(entity))
|
|
seen.Travel = "in vehicle";
|
|
// Read only the drawn weapon; never enumerate weapons or inventory.
|
|
if (CanIdentifyWeapon(target.GetDistance(), target.GetExposure()))
|
|
{
|
|
CharacterControllerComponent controller = CharacterControllerComponent.Cast(entity.FindComponent(CharacterControllerComponent));
|
|
if (controller)
|
|
{
|
|
IEntity heldItem = controller.GetCurrentItemInHands();
|
|
BaseWeaponComponent weapon;
|
|
if (heldItem)
|
|
weapon = BaseWeaponComponent.Cast(heldItem.FindComponent(BaseWeaponComponent));
|
|
seen.Weapon = "none drawn";
|
|
if (weapon)
|
|
seen.Weapon = WeaponCategory(weapon.GetWeaponType());
|
|
}
|
|
}
|
|
}
|
|
else if (Vehicle.Cast(entity))
|
|
{
|
|
seen.Travel = "vehicle";
|
|
if (target.GetUnitType() == EAIUnitType.UnitType_Aircraft)
|
|
seen.Travel = "aircraft";
|
|
}
|
|
return seen;
|
|
}
|
|
|
|
static void Gather(SCR_AIGroupUtilityComponent utility, float now, out array<ref ERR_HQVisualContact> samples)
|
|
{
|
|
samples.Clear();
|
|
if (!utility)
|
|
return;
|
|
array<BaseTarget> visible = {};
|
|
utility.ERR_HQGetVisualTargets(visible);
|
|
foreach (BaseTarget target : visible)
|
|
{
|
|
ERR_HQVisualContact seen = Capture(target, now);
|
|
if (!seen)
|
|
continue;
|
|
ERR_HQVisualContact existing;
|
|
foreach (ERR_HQVisualContact sample : samples)
|
|
{
|
|
if (sample.Entity == seen.Entity)
|
|
{
|
|
existing = sample;
|
|
break;
|
|
}
|
|
}
|
|
if (!existing)
|
|
{
|
|
if (samples.Count() < 64)
|
|
samples.Insert(seen);
|
|
}
|
|
else if (seen.ObservedAt > existing.ObservedAt || (seen.ObservedAt == existing.ObservedAt && existing.Weapon == "unknown" && seen.Weapon != "unknown"))
|
|
{
|
|
existing.Position = seen.Position;
|
|
existing.ObservedAt = seen.ObservedAt;
|
|
existing.Travel = seen.Travel;
|
|
existing.Weapon = seen.Weapon;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void Describe(ERR_HQReport report, array<ref ERR_HQVisualContact> samples, float now)
|
|
{
|
|
// New observations must not silently inherit old equipment/travel details.
|
|
report.SeenEscapists = -1;
|
|
report.TravelMode = "unknown";
|
|
report.SeenWeapons = "unknown";
|
|
report.DetailsObservedAt = -1;
|
|
if (report.Distress)
|
|
return;
|
|
ERR_HQVisualContact anchor;
|
|
foreach (ERR_HQVisualContact sample : samples)
|
|
{
|
|
if (sample.Entity == report.Target && now - sample.ObservedAt >= 0 && now - sample.ObservedAt <= 500)
|
|
{
|
|
anchor = sample;
|
|
break;
|
|
}
|
|
}
|
|
if (!anchor)
|
|
return;
|
|
report.DetailsObservedAt = anchor.ObservedAt;
|
|
report.TravelMode = anchor.Travel;
|
|
report.SeenWeapons = anchor.Weapon;
|
|
array<IEntity> counted = {};
|
|
array<string> weapons = {};
|
|
string travel;
|
|
foreach (ERR_HQVisualContact sighting : samples)
|
|
{
|
|
if (!sighting.Escapist || !sighting.Entity || counted.Contains(sighting.Entity) || now - sighting.ObservedAt < 0 || now - sighting.ObservedAt > 500 || vector.DistanceXZ(anchor.Position, sighting.Position) > 100)
|
|
continue;
|
|
counted.Insert(sighting.Entity);
|
|
if (travel.IsEmpty())
|
|
travel = sighting.Travel;
|
|
else if (travel != sighting.Travel)
|
|
travel = "mixed travel";
|
|
if (sighting.Weapon != "unknown" && !weapons.Contains(sighting.Weapon) && weapons.Count() < 4)
|
|
weapons.Insert(sighting.Weapon);
|
|
}
|
|
if (counted.IsEmpty())
|
|
return; // A visible vehicle does not reveal its hidden occupants.
|
|
report.SeenEscapists = counted.Count();
|
|
// A vehicle anchor remains a vehicle report, even with people near it.
|
|
if (anchor.Escapist)
|
|
report.TravelMode = travel;
|
|
report.SeenWeapons = "unknown";
|
|
if (!weapons.IsEmpty())
|
|
{
|
|
report.SeenWeapons = weapons[0];
|
|
for (int i = 1; i < weapons.Count(); i++)
|
|
report.SeenWeapons += ", " + weapons[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
modded class SCR_AIGroupUtilityComponent
|
|
{
|
|
void ERR_HQGetVisualTargets(out array<BaseTarget> targets)
|
|
{
|
|
targets.Clear();
|
|
foreach (SCR_AIInfoComponent info : m_aInfoComponents)
|
|
{
|
|
if (!info || !info.m_Perception)
|
|
continue;
|
|
AIAgent observer = AIAgent.Cast(info.GetOwner());
|
|
if (!observer || !ERR_Actors.CanFight(observer.GetControlledEntity()))
|
|
continue;
|
|
array<BaseTarget> own = {};
|
|
info.m_Perception.GetTargetsList(own, ETargetCategory.ENEMY);
|
|
foreach (BaseTarget target : own)
|
|
{
|
|
if (target && ERR_HQObservation.IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()))
|
|
targets.Insert(target);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Owned AI adaptation: report distinct visible contacts fairly instead of
|
|
// repeatedly choosing the first native perception entry. This is scheduling
|
|
// history only; no positions, equipment or delivered HQ knowledge are retained.
|
|
class ERR_HQReportSelection
|
|
{
|
|
protected ref array<IEntity> m_Recent = {};
|
|
|
|
ERR_HQVisualContact Select(array<ref ERR_HQVisualContact> samples)
|
|
{
|
|
ERR_HQVisualContact selected;
|
|
int oldest = 64;
|
|
foreach (ERR_HQVisualContact sample : samples)
|
|
{
|
|
if (!sample || !sample.Entity) continue;
|
|
int index = m_Recent.Find(sample.Entity);
|
|
if (!selected || index < oldest)
|
|
{
|
|
selected = sample;
|
|
oldest = index;
|
|
}
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
// Call only once an uplink is actually queued. Failed delivery still counts
|
|
// as an attempt, so an interrupted caller cannot starve other sightings.
|
|
void Queued(IEntity target)
|
|
{
|
|
if (!target) return;
|
|
for (int i = m_Recent.Count() - 1; i >= 0; i--)
|
|
if (!m_Recent[i] || m_Recent[i] == target) m_Recent.Remove(i);
|
|
if (m_Recent.Count() >= 64) m_Recent.Remove(0);
|
|
m_Recent.Insert(target);
|
|
}
|
|
}
|