// Owned and ticked by HQ. Initial assets are authored once; losses are never replaced. class ERR_MortarSystem { protected ERR_HQ m_HQ; protected ref array m_Batteries = {}; protected ref ERR_SettlementSafety m_Settlements = new ERR_SettlementSafety(); protected bool m_Prepared; protected bool m_Stopped; protected float m_NextDecision; string SafetyReason; string SettlementReason; void ERR_MortarSystem(ERR_HQ hq) { m_HQ = hq; } static ERR_MortarSystem Get() { ERR_HQ hq = ERR_HQ.Get(); if (!hq || !Replication.IsServer()) return null; return hq.Mortars(); } static bool NearPlayer(vector position) { array players = {}; ERR_Actors.Players(players); foreach (IEntity player : players) if (vector.DistanceXZ(player.GetOrigin(), position) <= 1200) return true; return false; } bool Prepare() { if (!Replication.IsServer() || m_Prepared || m_Stopped) return false; m_Prepared = true; vector position = ERR_Actors.Ground("6000 0 7000"); if (position[1] < 1) return false; IEntity gun = ERR_Actors.Spawn(ERR_MortarBattery.GUN, position); SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(MortarGroup(), ERR_Actors.Ground(position + "0 0 4"))); if (!gun || !group) { ERR_Actors.DeleteSpawned(gun); ERR_Actors.DeleteSpawned(group); return false; } if (group.GetFaction() != ERR_GameMode.Get().EnemyFaction()) { ERR_Actors.DeleteSpawned(gun); ERR_Actors.DeleteSpawned(group); return false; } FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(gun.FindComponent(FactionAffiliationComponent)); if (affiliation) affiliation.SetAffiliatedFaction(ERR_GameMode.Get().EnemyFaction()); ERR_MortarBattery battery = new ERR_MortarBattery(); battery.Bind(1, group, gun, GetGame().GetWorld().GetWorldTime()); m_Batteries.Insert(battery); ERR_GameMode mode = ERR_GameMode.Get(); mode.Own(gun); mode.RegisterGroup(group); m_HQ.ReserveGroup(group); battery.Audit("DEPLOY", "authored gun and four-person crew; finite ammunition initialization pending"); return true; } static ResourceName MortarGroup() { return "{B84C05941C4C81B8}Prefabs/ERR/MortarCrew.et"; } ERR_MortarBattery FindGun(IEntity gun) { foreach (ERR_MortarBattery battery : m_Batteries) if (gun && battery.Gun == gun) return battery; return null; } ERR_MortarBattery FindGroup(SCR_AIGroup group) { foreach (ERR_MortarBattery battery : m_Batteries) if (group && battery.Group == group) return battery; return null; } ERR_HQReport Latest(ERR_MortarBattery battery, float now) { // Only the coordinates actually delivered to this battery are available. // A newer HQ contact cannot update a gun's knowledge without another downlink. if (!battery || !ERR_MortarPrediction.Fresh(battery.Report, now)) return null; return battery.Report; } string ContactFailure(ERR_MortarBattery battery) { if (!battery.Report) return "no delivered target report"; return "delivered target report expired or invalid"; } bool OutsideSettlements(vector impact) { bool clear = m_Settlements.OutsideSettlements(impact); SettlementReason = m_Settlements.SettlementReason; return clear; } bool Safe(ERR_MortarBattery battery, vector impact, float flight) { SafetyReason = "clear"; if (!battery.Operational()) { SafetyReason = battery.OperationalReason(); return false; } if (!OutsideSettlements(impact)) { SafetyReason = SettlementReason; return false; } if (SCR_TerrainHelper.GetTerrainY(impact) < 0) { SafetyReason = "impact below Everon sea level"; return false; } Faction faction = battery.Group.GetFaction(); if (!faction) { SafetyReason = "battery faction missing"; return false; } array friendlies = {}; m_HQ.PhysicalForces(friendlies); foreach (IEntity entity : friendlies) { Faction friendly = SCR_FactionManager.SGetFaction(entity); if (!friendly || faction.IsFactionEnemy(friendly) || !ERR_Actors.Alive(entity)) continue; vector velocity; IEntity moving = CompartmentAccessComponent.GetVehicleIn(entity); if (!moving) moving = entity; if (moving.GetPhysics()) velocity = moving.GetPhysics().GetVelocity(); if (ERR_MortarPrediction.ThreatensFriendly(impact, entity.GetOrigin(), velocity, flight)) { SafetyReason = "friendly physical force can enter the impact area"; return false; } } return true; } bool Withhold(ERR_MortarBattery battery, string reason, string detail = "") { battery.LastFailure = reason + "; " + detail; battery.Trace("WITHHELD", reason, detail); // Finish the activity from the next HQ tick, never from inside its own node. battery.Rejected = true; return false; } bool AllowShot(ERR_MortarBattery battery) { float now = GetGame().GetWorld().GetWorldTime(); if (!ERR_HQ.MissionActive()) return Withhold(battery, "mission inactive"); if (!battery.Operational()) return Withhold(battery, battery.OperationalReason()); if (!battery.CommandOpen || battery.OrderShots >= ERR_MortarSalvo.ROUNDS) return Withhold(battery, "salvo complete or order closed"); if (battery.Released) return Withhold(battery, "waiting for committed projectile"); if (battery.AimedShot != battery.OrderShots || battery.AimedAt < 0 || now - battery.AimedAt > 10000) return Withhold(battery, "fresh aim solution required"); ERR_HQReport report = Latest(battery, now); if (!report) return Withhold(battery, ContactFailure(battery)); vector updated; if (!ERR_MortarPrediction.Predict(report, now, battery.FlightTime, updated)) return Withhold(battery, ERR_MortarPrediction.FailureReason); updated += battery.AimOffset; if (vector.DistanceXZ(updated, battery.Impact) > 60) return Withhold(battery, "predicted target left aimed impact area"); if (!OutsideSettlements(updated)) return Withhold(battery, SettlementReason); if (!Safe(battery, battery.Impact, battery.FlightTime)) return Withhold(battery, SafetyReason); return true; } protected void AdvanceOrder(ERR_MortarBattery battery, float now) { if (!battery.RadioOrder) return; int state = battery.RadioOrder.Advance(now, m_HQ.Journal()); if (state == 0) return; ERR_HQReport delivered = battery.PendingRadioReport; battery.RadioOrder = null; battery.PendingRadioReport = null; if (state < 0 || !ERR_MortarPrediction.Fresh(delivered, now) || !battery.Operational() || battery.Activity || !battery.SupplyGunner()) { battery.Cancel("radio delivery, report freshness or gunner availability failed"); battery.NextOrder = now + 10000; return; } battery.Order(delivered, now); } void Tick(float now) { if (m_Stopped || !Replication.IsServer()) return; foreach (ERR_MortarBattery battery : m_Batteries) { if (battery.Rejected && !battery.Released) { battery.Cancel("native aim or firing authorization rejected"); battery.NextOrder = now + 10000; } battery.Rejected = false; battery.Update(now); AdvanceOrder(battery, now); } if (now < m_NextDecision) return; m_NextDecision = now + 10000; array reports = {}; m_HQ.SupportContacts(reports, now); ERR_MortarBattery selected; ERR_HQReport target; float distance = float.MAX; foreach (ERR_MortarBattery battery : m_Batteries) { if (!battery.StartupActivated || !battery.Operational() || battery.Activity || battery.RadioOrder || now < battery.NextOrder || battery.CountShells() == 0) continue; foreach (ERR_HQReport report : reports) { float candidate = vector.DistanceXZ(battery.Position, report.Position); if (candidate < 350 || candidate > 4000 || candidate >= distance || !OutsideSettlements(report.Position)) continue; selected = battery; target = report; distance = candidate; } } if (!selected || !target) return; ERR_HQRoute route = m_HQ.Downlink(selected.Group, "MORTAR ORDER", target.Description(now), target.MessageId); if (!route) return; selected.BeginRequest(target, now); selected.RadioId = route.MessageId; selected.RadioOrder = route; selected.PendingRadioReport = target.Snapshot(); m_NextDecision = now + 20000; } void AppendGuns(array entities) { foreach (ERR_MortarBattery battery : m_Batteries) if (battery.Gun && !entities.Contains(battery.Gun)) entities.Insert(battery.Gun); } string Status() { int ready, shells; foreach (ERR_MortarBattery battery : m_Batteries) { if (battery.StartupActivated && battery.Operational()) ready++; shells += battery.CountShells(); } return string.Format("mortars %1/%2; HE shells %3", ready, m_Batteries.Count(), shells); } void Stop() { m_Stopped = true; foreach (ERR_MortarBattery battery : m_Batteries) battery.Cancel("HQ stopped"); } }