257 lines
10 KiB
C
257 lines
10 KiB
C
// 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<ref ERR_MortarBattery> 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<IEntity> 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_MortarReadiness.RetireGroup(group);
|
|
ERR_Actors.DeleteSpawned(group);
|
|
return false;
|
|
}
|
|
if (group.GetFaction() != ERR_GameMode.Get().EnemyFaction())
|
|
{
|
|
ERR_Actors.DeleteSpawned(gun);
|
|
ERR_MortarReadiness.RetireGroup(group);
|
|
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"; }
|
|
void Batteries(out array<ref ERR_MortarBattery> batteries)
|
|
{
|
|
batteries.Clear();
|
|
foreach (ERR_MortarBattery battery : m_Batteries) batteries.Insert(battery);
|
|
}
|
|
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<IEntity> 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 (m_Stopped || !ERR_HQ.MissionActive()) return Withhold(battery, "mission inactive or HQ stopped");
|
|
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) return Withhold(battery, "fresh aim solution required");
|
|
ERR_HQReport report = Latest(battery, now);
|
|
if (!report) return Withhold(battery, ContactFailure(battery));
|
|
if (!battery.AimSolution || !battery.AimSolution.Matches(battery, report)) return Withhold(battery, "aim command does not match delivered order");
|
|
if (now - battery.AimedAt > 10000)
|
|
{
|
|
if (!battery.AimSolver) return Withhold(battery, "original native aim solver no longer available");
|
|
if (!battery.AimSolver.ERR_RevalidateAim(battery, this, report, now)) return false;
|
|
}
|
|
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)
|
|
{
|
|
int deliveredState = battery.RadioOrder.Advance(now, m_HQ.Journal());
|
|
if (deliveredState == 0) return;
|
|
ERR_HQReport delivered = battery.PendingRadioReport;
|
|
battery.RadioOrder = null;
|
|
battery.PendingRadioReport = null;
|
|
if (deliveredState < 0 || !ERR_MortarPrediction.Fresh(delivered, now) || !battery.Operational() || battery.Activity || !battery.CommandOpen)
|
|
{
|
|
battery.Cancel("radio delivery, report freshness or battery availability failed");
|
|
battery.NextOrder = now + 10000;
|
|
return;
|
|
}
|
|
battery.PendingSupplyReport = delivered;
|
|
}
|
|
if (!battery.PendingSupplyReport) return;
|
|
if (!battery.CommandOpen || !ERR_MortarPrediction.Fresh(battery.PendingSupplyReport, now) || !battery.Operational() || battery.Activity)
|
|
{
|
|
battery.Cancel("delivered order expired or became unavailable while awaiting physical ammunition");
|
|
battery.NextOrder = now + 10000;
|
|
return;
|
|
}
|
|
ERR_MortarSupplyState supply = battery.SupplyState(now);
|
|
if (supply == ERR_MortarSupplyState.PENDING) return;
|
|
if (supply == ERR_MortarSupplyState.FAILED)
|
|
{
|
|
battery.Cancel("physical gunner ammunition unavailable or transfer failed");
|
|
battery.NextOrder = now + 10000;
|
|
return;
|
|
}
|
|
ERR_HQReport ready = battery.PendingSupplyReport;
|
|
battery.PendingSupplyReport = null;
|
|
battery.Order(ready, 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<ref ERR_HQReport> 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 || battery.PendingSupplyReport || battery.SupplyPending() || now < battery.NextOrder || battery.CountShells() == 0) continue;
|
|
foreach (ERR_HQReport report : reports)
|
|
{
|
|
if (!battery.CanRequest(report, now)) continue;
|
|
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;
|
|
if (!selected.BeginRequest(target, now)) return;
|
|
selected.RadioId = route.MessageId;
|
|
selected.RadioOrder = route;
|
|
selected.PendingRadioReport = target.Snapshot();
|
|
m_NextDecision = now + 20000;
|
|
}
|
|
void AppendGuns(array<IEntity> 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.Stop();
|
|
}
|
|
}
|