Files
EscapistsReintroduced/addons/ai/Scripts/Game/ERR/ERR_MortarFiring.c
T

278 lines
13 KiB
C

// The dedicated manpack operator has no mortar-shell backpack. Preserve the
// native chooser and its caller's original index for every eligible gunner.
modded class SCR_AIStaticArtilleryActivity
{
protected override AIAgent FindBestAgentForArtillery(notnull array<AIAgent> agents, notnull IEntity artilleryEntity, out int outIndex)
{
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system) battery = system.FindGun(artilleryEntity);
if (!battery) return super.FindBestAgentForArtillery(agents, artilleryEntity, outIndex);
array<AIAgent> gunners = {};
foreach (AIAgent agent : agents)
{
if (agent && agent.GetControlledEntity() != battery.RadioOperator && ERR_Actors.CanFight(agent.GetControlledEntity()))
gunners.Insert(agent);
}
int filteredIndex;
AIAgent selected = super.FindBestAgentForArtillery(gunners, artilleryEntity, filteredIndex);
outIndex = -1;
if (selected) outIndex = agents.Find(selected);
return selected;
}
}
// Leave all other artillery (including player-operated captured guns) native.
modded class SCR_AIGetArtilleryAimDistanceCompensation
{
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
{
IEntity gun;
GetVariableIn(PORT_ARTILLERY_ENTITY, gun);
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system)
battery = system.FindGun(gun);
if (!battery)
return super.EOnTaskSimulate(owner, dt);
battery.AimedAt = -1;
battery.AimSolver = null;
battery.AimSolution = null;
float now = GetGame().GetWorld().GetWorldTime();
ERR_HQReport report = system.Latest(battery, now);
if (!ERR_HQ.MissionActive()) { system.Withhold(battery, "aim: mission inactive"); return ENodeResult.FAIL; }
if (!battery.CommandOpen || battery.OrderShots >= ERR_MortarSalvo.ROUNDS) { system.Withhold(battery, "aim: salvo complete or order closed"); return ENodeResult.FAIL; }
if (battery.Released) { system.Withhold(battery, "aim: waiting for committed projectile"); return ENodeResult.FAIL; }
if (!report) { system.Withhold(battery, "aim: " + system.ContactFailure(battery)); return ENodeResult.FAIL; }
battery.Trace("AIM START", "solving moving intercept", string.Format("latest source=M%1; %2", report.MessageId, report.Description(now)));
ERR_MortarAimSolution solution = ERR_Solve(battery, system, report, now);
if (!solution) return ENodeResult.FAIL;
battery.Impact = solution.Impact;
battery.AimOffset = solution.SalvoOffset;
battery.AimedShot = battery.OrderShots;
battery.FlightTime = solution.FlightTime;
battery.AimSolution = solution;
battery.AimSolver = this; // weak: do not retain a destroyed native behavior tree
battery.AimedAt = now;
battery.LastFailure = "";
battery.Trace("AIM SOLUTION", "ready", string.Format("predicted impact=%1 flight=%2s charge index=%3 aim target=%4 latest source=M%5", solution.Impact, solution.FlightTime, solution.Charge, solution.AimTarget, report.MessageId));
SetVariableOut(PORT_AIM_POS, solution.AimTarget);
SetVariableOut(PORT_INIT_SPEED_ID, solution.Charge);
return ENodeResult.SUCCESS;
}
// Reuse this exact native node's coefficient/elevation APIs. A slow native
// barrel can keep its command only if a NEW solve still matches that command.
bool ERR_RevalidateAim(ERR_MortarBattery battery, ERR_MortarSystem system, ERR_HQReport report, float now)
{
IEntity gun;
ResourceName ammo;
GetVariableIn(PORT_ARTILLERY_ENTITY, gun);
GetVariableIn(PORT_AMMO_PREFAB, ammo);
ERR_MortarAimSolution commanded = battery.AimSolution;
if (battery.AimSolver != this || !commanded || !commanded.Matches(battery, report) || gun != battery.Gun || ammo != ERR_MortarBattery.SHELL)
return system.Withhold(battery, "original aim solver or command no longer available");
if (!ERR_MortarPrediction.Fresh(report, now)) return system.Withhold(battery, system.ContactFailure(battery));
ERR_MortarAimSolution updated = ERR_Solve(battery, system, report, now);
if (!updated) return false;
if (!ERR_MortarAimRules.Equivalent(commanded.Charge, updated.Charge, commanded.GunOrigin, updated.GunOrigin, commanded.Impact, updated.Impact, commanded.AimTarget, updated.AimTarget, commanded.FlightTime, updated.FlightTime))
return system.Withhold(battery, "new ballistic solution requires native re-aim", string.Format("gun drift=%1m impact drift=%2m aim drift=%3m charge=%4/%5 flight=%6/%7s", vector.Distance(commanded.GunOrigin, updated.GunOrigin), vector.Distance(commanded.Impact, updated.Impact), vector.Distance(commanded.AimTarget, updated.AimTarget), commanded.Charge, updated.Charge, commanded.FlightTime, updated.FlightTime));
// Keep the ORIGINAL command/impact/flight. Comparing successive refreshed
// solutions would allow a moving target to drift away in small increments.
battery.AimedAt = now;
battery.Trace("AIM REVALIDATED", "same native command", string.Format("fresh ballistic solve matched original; source=M%1 observed age=%2s", report.MessageId, (now - report.ObservedAt) / 1000));
return true;
}
protected ERR_MortarAimSolution ERR_Solve(ERR_MortarBattery battery, ERR_MortarSystem system, ERR_HQReport report, float now)
{
IEntity gun = battery.Gun;
vector gunOrigin = gun.GetOrigin();
ResourceName ammo;
GetVariableIn(PORT_AMMO_PREFAB, ammo);
if (ammo != ERR_MortarBattery.SHELL) { system.Withhold(battery, "unsupported ammunition prefab", ammo); return null; }
Resource resource = Resource.Load(ammo);
if (!resource || !resource.IsValid() || !resource.GetResource()) { system.Withhold(battery, "shell resource load failed", ammo); return null; }
IEntitySource source = resource.GetResource().ToEntitySource();
if (!source) { system.Withhold(battery, "shell resource has no entity source", ammo); return null; }
array<float> coefficients = {};
GetAmmoInitialSpeedCoefficients(ammo, coefficients);
if (coefficients.IsEmpty())
coefficients.Insert(1.0);
float minimum, maximum;
GetVerticalAimLimits(gun, minimum, maximum);
float flight = 30;
float offset;
int charge;
vector impact;
vector salvoOffset = ERR_MortarSalvo.Offset(battery.SiteId, battery.OrderId, battery.OrderShots);
bool converged;
// Iterate the moving intercept using the native shell's flight time.
// Reject non-convergence rather than fire with an arbitrary lead time.
for (int iteration = 0; iteration < 4; iteration++)
{
if (!ERR_MortarPrediction.Predict(report, now, flight + 2, impact)) { system.Withhold(battery, "aim prediction: " + ERR_MortarPrediction.FailureReason, string.Format("flight lead=%1s; %2", flight + 2, report.Description(now))); return null; }
if (!system.OutsideSettlements(impact)) { system.Withhold(battery, "aim target inside settlement exclusion", system.SettlementReason); return null; }
impact += salvoOffset;
impact[1] = SCR_TerrainHelper.GetTerrainY(impact);
float distance = vector.DistanceXZ(gunOrigin, impact);
float altitude = impact[1] - gunOrigin[1];
if (distance < 350 || distance > 4000) { system.Withhold(battery, "predicted impact out of range", string.Format("iteration=%1 distance=%2m impact=%3 allowed=350..4000m", iteration, distance, impact)); return null; }
bool reachable;
float calculated;
for (int i = 0; i < coefficients.Count(); i++)
{
if (!BallisticTable.GetAimHeightOfProjectileAltitudeFromSource(distance, offset, calculated, source, altitude, coefficients[i]))
continue;
float tangent = (offset + altitude) / distance;
if (calculated <= 0 || calculated > 75 || tangent < Math.Tan(minimum) || tangent > Math.Tan(maximum))
continue;
charge = i;
reachable = true;
break;
}
if (!reachable) { system.Withhold(battery, "no native ballistic solution within elevation/flight-time limits", string.Format("iteration=%1 distance=%2 altitude=%3 charges=%4 elevation radians=%5..%6 flight limit=75s", iteration, distance, altitude, coefficients.Count(), minimum, maximum)); return null; }
converged = Math.AbsFloat(calculated - flight) < 1;
flight = calculated;
if (converged)
break;
}
if (!converged) { system.Withhold(battery, "moving intercept did not converge in four iterations"); return null; }
if (!system.Safe(battery, impact, flight + 2)) { system.Withhold(battery, "aim impact safety rejected", system.SafetyReason); return null; }
ERR_MortarAimSolution solution = new ERR_MortarAimSolution();
solution.Gun = gun;
solution.Report = report;
solution.OrderId = battery.OrderId;
solution.Shot = battery.OrderShots;
solution.GunOrigin = gunOrigin;
solution.Impact = impact;
solution.AimTarget = impact + Vector(0, offset, 0);
solution.SalvoOffset = salvoOffset;
solution.Charge = charge;
solution.FlightTime = flight;
return solution;
}
}
modded class SCR_AIShootStaticArtillery
{
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
{
IEntity gun;
GetVariableIn(PORT_ARTILLERY_ENTITY, gun);
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system)
battery = system.FindGun(gun);
if (battery && !system.AllowShot(battery))
{
battery.Trace("SHOOT NODE", "authorization rejected", battery.LastFailure);
return ENodeResult.FAIL;
}
ENodeResult result = super.EOnTaskSimulate(owner, dt);
if (battery)
battery.Trace("SHOOT NODE", string.Format("native result=%1", result), "authorization passed; node result does not confirm a physical shot");
return result;
}
}
modded class SCR_MortarMuzzleComponent
{
protected override void TransferShellToMortar()
{
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system && m_CharController && !EntityUtils.IsPlayer(m_CharController.GetOwner()))
battery = system.FindGun(GetOwner());
if (battery)
{
// The loading animation takes time. Recheck at the actual transfer,
// before native inventory insertion triggers the physical projectile.
if (!system.AllowShot(battery))
{
if (m_ShellComponent)
{
m_ShellComponent.GetOnShellUsed().Remove(TransferShellToMortar);
m_ShellComponent.SetLoadedState(false);
}
CharacterAnimationComponent animation = m_CharController.GetAnimationComponent();
if (animation && animation.GetCommandHandler())
animation.GetCommandHandler().FinishItemUse(false);
battery.Audit("ABORT", "final shell transfer rejected; " + battery.LastFailure);
return;
}
battery.Released = true;
battery.ReserveShellAirspace(GetGame().GetWorld().GetWorldTime());
battery.Audit("SHELL COMMIT", string.Format("final safety passed; gunner=%1 predicted impact=%2 flight=%3s; invoking native transfer, awaiting projectile event", m_CharController.GetOwner(), battery.Impact, battery.FlightTime));
}
super.TransferShellToMortar();
}
override void OnWeaponFired(IEntity effectEntity, BaseMuzzleComponent muzzle, IEntity projectileEntity)
{
IEntity firingCharacter;
if (m_CharController) firingCharacter = m_CharController.GetOwner();
super.OnWeaponFired(effectEntity, muzzle, projectileEntity);
ERR_MortarSystem system = ERR_MortarSystem.Get();
ERR_MortarBattery battery;
if (system)
battery = system.FindGun(GetOwner());
if (!battery)
return;
bool playerGunner;
if (firingCharacter) playerGunner = EntityUtils.IsPlayer(firingCharacter);
battery.ObserveProjectile(projectileEntity, firingCharacter, playerGunner);
}
}
// The native tree normally re-aims only every five shots (and skips the first
// re-aim). Each salvo shell needs its own spread point and fresh authorization.
modded class SCR_AIStaticArtilleryBehavior
{
ERR_MortarBattery ERR_GetMortarBattery()
{
ERR_MortarSystem system = ERR_MortarSystem.Get();
if (!system) return null;
return system.FindGun(m_ArtilleryEntity.m_Value);
}
}
modded class SCR_AIStaticArtilleryBehavior_ShootingLogic
{
override ENodeResult EOnTaskSimulate(AIAgent owner, float dt)
{
ERR_MortarBattery battery;
SCR_AIStaticArtilleryBehavior behavior;
if (m_Utility)
{
behavior = SCR_AIStaticArtilleryBehavior.Cast(m_Utility.GetExecutedAction());
if (behavior) battery = behavior.ERR_GetMortarBattery();
}
if (battery)
{
int gate = ERR_MortarSupplyRules.CycleGate(battery.CommandOpen, battery.OrderShots, behavior.GetShotCount(), ERR_MortarSalvo.ROUNDS, ERR_MortarSupplyState.READY);
if (gate < 0) return ENodeResult.FAIL;
if (gate == 0) return ENodeResult.RUNNING;
// Native super increments the behavior counter. Wait BEFORE calling it,
// so repeated pending ticks cannot count the same projectile twice.
if (battery.OrderShots < ERR_MortarSalvo.ROUNDS)
{
ERR_MortarSupplyState supply = battery.SupplyState(GetGame().GetWorld().GetWorldTime());
if (supply == ERR_MortarSupplyState.PENDING) return ENodeResult.RUNNING;
if (supply == ERR_MortarSupplyState.FAILED)
{
ERR_MortarSystem system = ERR_MortarSystem.Get();
if (system) system.Withhold(battery, "next native load lacks confirmed physical gunner ammunition");
return ENodeResult.FAIL;
}
}
}
ENodeResult result = super.EOnTaskSimulate(owner, dt);
if (battery && result == ENodeResult.SUCCESS)
{
SetVariableOut(PORT_AIMING_NEEDED, true);
}
return result;
}
}