Integrate finite physical mortar assets and archive inventory regression

This commit is contained in:
2026-09-19 01:48:16 +02:00
parent 2550bc0423
commit 7a507a7371
28 changed files with 2484 additions and 16 deletions
@@ -0,0 +1,216 @@
// 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_Actors.DeleteSpawned(group);
return false;
}
group.InitFactionKey("USSR");
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<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 (!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<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 || 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<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.Cancel("HQ stopped");
}
}