// A radio's category is protected in the stock gadget component. modded class SCR_RadioComponent { bool ERR_IsHQManpack() { return m_iRadioCategory == ERadioCategory.MANPACK; } } class ERR_HQRadio { // 0: no radio, 1: basic radio, 2: manpack. Inventory is checked live. static int Capability(IEntity character) { if (!ERR_Actors.CanFight(character)) return 0; InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(character.FindComponent(InventoryStorageManagerComponent)); if (!inventory) return 0; array items = {}; inventory.GetItems(items); int result; foreach (IEntity item : items) { if (!item || !item.FindComponent(BaseRadioComponent)) continue; result = 1; SCR_RadioComponent gadget = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent)); if (gadget && gadget.ERR_IsHQManpack()) return 2; } return result; } } // HQ uses entity identity only for matching observations, never for its origin. // Visible details are captured at intake and copied as scalar snapshots. // Reports and orders contain immutable observed coordinates and timestamps. class ERR_HQReport { int MessageId; IEntity Target; SCR_AIGroup Source; vector Position; vector Velocity; float ObservedAt = -1; float LastSent = -60000; float PerceptionStamp = -1; bool HasVelocity; bool Engaged; bool Distress; int SeenEscapists = -1; string TravelMode = "unknown"; string SeenWeapons = "unknown"; float DetailsObservedAt = -1; ERR_HQReport Snapshot() { ERR_HQReport copy = new ERR_HQReport(); copy.MessageId = MessageId; copy.Target = Target; copy.Source = Source; copy.Position = Position; copy.Velocity = Velocity; copy.ObservedAt = ObservedAt; copy.Engaged = Engaged; copy.Distress = Distress; copy.HasVelocity = HasVelocity; copy.SeenEscapists = SeenEscapists; copy.TravelMode = TravelMode; copy.SeenWeapons = SeenWeapons; copy.DetailsObservedAt = DetailsObservedAt; return copy; } string TravelDirection() { if (!HasVelocity) return "unknown"; if (Velocity.Length() < 0.5) return "stationary"; float x = Math.AbsFloat(Velocity[0]); float z = Math.AbsFloat(Velocity[2]); string northSouth = "N"; string eastWest = "E"; if (Velocity[2] < 0) northSouth = "S"; if (Velocity[0] < 0) eastWest = "W"; if (x < z * 0.414214) return northSouth; if (z < x * 0.414214) return eastWest; return northSouth + eastWest; } string Description(float now, bool brief = false) { if (Distress) return string.Format("Distress at friendly X=%1 Z=%2; enemy details unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000)); string count = "count unknown"; if (SeenEscapists >= 0) count = "at least " + SeenEscapists.ToString() + " spotted nearby"; string motion = "speed/direction unknown"; if (HasVelocity) { float speed = Math.Round(Velocity.Length() * 10) / 10.0; float azimuth = Math.Atan2(Velocity[0], Velocity[2]) * Math.RAD2DEG; if (azimuth < 0) azimuth += 360; int heading = Math.Round(azimuth); motion = string.Format("%1 km/h, azimuth %2 degrees (%3)", Math.Round(speed * 3.6), heading % 360, TravelDirection()); } string details = count + "; " + TravelMode + "; " + motion + "; seen weapons: " + SeenWeapons; if (brief) return details; string velocity = "unknown"; if (HasVelocity) velocity = string.Format("[%1,%2] m/s", Math.Round(Velocity[0] * 10) / 10.0, Math.Round(Velocity[2] * 10) / 10.0); return string.Format("X=%1 Z=%2: %3; vector X/Z=%4 (%5s ago)", Math.Round(Position[0]), Math.Round(Position[2]), details, velocity, Math.Round(Math.Max(0, now - ObservedAt) / 1000)); } static vector Predict(vector position, vector velocity, float seconds) { vector offset = velocity * Math.Clamp(seconds, 0, 20); offset[1] = 0; if (offset.Length() > 300) offset = offset.Normalized() * 300; return position + offset; } int Priority(float now) { if (Distress) return 0; if (now - ObservedAt <= 15000) { if (Engaged) return 3; return 2; } return 1; } }