// 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 samples) { samples.Clear(); if (!utility) return; array 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 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 counted = {}; array 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 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 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); } } } }