Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
modded class SCR_ChimeraAIAgent
|
||||
{
|
||||
void ProbeReleasePrison() { ERR_PrisonPhaseChanged(ERR_Phase.PRISON, ERR_Phase.RADIO_SEARCH); SetPermanentLOD(0); ActivateAI(); }
|
||||
}
|
||||
modded class ERR_HunterService
|
||||
{
|
||||
ERR_HunterVehicle ProbeUnit()
|
||||
{
|
||||
vector position, angles;
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected("6870 0 3500", position, angles, 500)) return null;
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle(); unit.Source = "native-gun-team";
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_HunterFleetRules.VehiclePrefab(0), position, angles));
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
if (!FillCrew(unit)) return null;
|
||||
unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) { agent.SetPermanentLOD(0); agent.ActivateAI(); }
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
modded class ERR_HunterTactics
|
||||
{
|
||||
void ProbeBegin()
|
||||
{
|
||||
Unit.Group.SetPermanentLOD(0); Unit.Group.ActivateAI();
|
||||
foreach (IEntity member : Unit.Members)
|
||||
{
|
||||
AIControlComponent control = AIControlComponent.Cast(member.FindComponent(AIControlComponent));
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent()); if (agent) agent.ProbeReleasePrison();
|
||||
}
|
||||
Delivered = true; State = 2; Deadline = GetGame().GetWorld().GetWorldTime() + 45000; NextRetry = Deadline;
|
||||
Stage = Unit.Entity.GetOrigin(); Approach = ERR_Actors.Ground(Stage + "40 0 0"); Sector = ERR_Actors.Ground(Stage + "65 0 20");
|
||||
Attack.ReportPosition = Stage + "200 0 0"; Attack.Forward = "1 0 0"; Attack.Teams.Insert(this);
|
||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
utility.m_VehicleMgr.TryAddVehicle(usage); utility.AddUsableVehicle(usage);
|
||||
vector transform[4]; Unit.Entity.GetTransform(transform); Attack.ReportPosition = Stage - transform[2] * 200;
|
||||
bool aligned = TryAlign();
|
||||
bool issued = aligned;
|
||||
if (aligned) { State = 8; Deadline = GetGame().GetWorld().GetWorldTime() + 20000; }
|
||||
else issued = Board(false);
|
||||
PrintFormat("ERR GUN TEAM ALIGN: requested=%1 facing=%2", aligned, FacingSector());
|
||||
PrintFormat("ERR GUN TEAM START: board=%1 loaded=%2 split=%3", issued, ERR_HunterGunTeam.LoadedGunner(Unit) != null, m_GunTeam != null);
|
||||
}
|
||||
bool ProbeFacing() { return FacingSector(); }
|
||||
bool ProbeSplit() { return m_GunTeam != null; }
|
||||
}
|
||||
modded class ArmaReforgerScripted
|
||||
{
|
||||
protected ref ERR_HunterService m_ProbeService;
|
||||
protected ref ERR_HunterVehicle m_ProbeUnit;
|
||||
protected ref ERR_HunterAttack m_ProbeAttack;
|
||||
protected ref ERR_HunterTactics m_ProbeTactics;
|
||||
protected float m_ProbeAge;
|
||||
protected float m_ProbeNext;
|
||||
protected bool m_ProbeDone;
|
||||
override void OnUpdate(BaseWorld world, float timeslice)
|
||||
{
|
||||
super.OnUpdate(world, timeslice);
|
||||
if (m_ProbeDone || !ERR_GameMode.Get() || !ERR_GameMode.Get().EnemyFaction()) return;
|
||||
m_ProbeAge += timeslice;
|
||||
if (m_ProbeAge < 5) return;
|
||||
if (!m_ProbeService)
|
||||
{
|
||||
m_ProbeService = new ERR_HunterService(ERR_GameMode.Get(), null); m_ProbeUnit = m_ProbeService.ProbeUnit();
|
||||
if (!m_ProbeUnit) { Print("ERR GUN TEAM FAILED creation"); m_ProbeDone = true; }
|
||||
return;
|
||||
}
|
||||
if (m_ProbeAge < 10) return;
|
||||
if (!m_ProbeTactics)
|
||||
{
|
||||
m_ProbeAttack = new ERR_HunterAttack(); m_ProbeAttack.Started = world.GetWorldTime();
|
||||
m_ProbeTactics = new ERR_HunterTactics(ERR_GameMode.Get(), m_ProbeUnit, m_ProbeAttack, 1); m_ProbeTactics.ProbeBegin();
|
||||
}
|
||||
m_ProbeTactics.Tick(ERR_HQ.Get(), world.GetWorldTime());
|
||||
if (m_ProbeAge >= m_ProbeNext)
|
||||
{
|
||||
m_ProbeNext = m_ProbeAge + 5;
|
||||
PrintFormat("ERR GUN TEAM PROGRESS: state=%1 driverDistance=%2 passengerDistance=%3 gunnerSeated=%4 split=%5 footReady=%6 facing=%7", m_ProbeTactics.State, vector.DistanceXZ(m_ProbeUnit.Driver.GetOrigin(), m_ProbeUnit.Entity.GetOrigin()), vector.DistanceXZ(m_ProbeUnit.Members[2].GetOrigin(), m_ProbeUnit.Entity.GetOrigin()), m_ProbeUnit.Seats[1].GetOccupant() == m_ProbeUnit.Members[1], m_ProbeTactics.ProbeSplit(), m_ProbeTactics.Aboard(false), m_ProbeTactics.ProbeFacing());
|
||||
}
|
||||
if (m_ProbeAge > 145)
|
||||
{
|
||||
m_ProbeTactics.Stop();
|
||||
PrintFormat("ERR GUN TEAM COMPLETE: holdReleased=%1", !ERR_ConvoyMountedCrew.Retain(m_ProbeUnit.Members[1], m_ProbeUnit.Entity));
|
||||
m_ProbeDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c
|
||||
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c
|
||||
@@ -52,13 +52,13 @@
|
||||
bool CoverAlive()
|
||||
{
|
||||
foreach (ERR_HunterTactics team : Teams)
|
||||
- if (team.Role == 0 && team.Delivered && team.ArmedCover()) return true;
|
||||
+ if (team.Delivered && team.ArmedCover()) return true;
|
||||
return false;
|
||||
}
|
||||
bool CoverReady()
|
||||
{
|
||||
foreach (ERR_HunterTactics team : Teams)
|
||||
- if (team.Role == 0 && team.Covering()) return true;
|
||||
+ if (team.Covering()) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@
|
||||
vector Sector;
|
||||
ref ERR_HQRoute Radio;
|
||||
ref array<AIWaypoint> Orders = {};
|
||||
- int State; // awaiting radio, approach, dismount, wait for cover, flank, search, return, finished
|
||||
+ int State; // 0 radio, 1 approach, 2 dismount, 3 cover, 4 flank, 5 search, 6 return, 8 short alignment
|
||||
float Deadline;
|
||||
float NextRetry;
|
||||
int Retries;
|
||||
@@ -86,6 +86,7 @@
|
||||
protected float m_ApproachProgress;
|
||||
protected vector m_ApproachPosition;
|
||||
protected bool m_UnsupportedSearch;
|
||||
+ protected ref ERR_HunterGunTeam m_GunTeam;
|
||||
protected ERR_GameMode m_Mode;
|
||||
void ERR_HunterTactics(ERR_GameMode mode, ERR_HunterVehicle unit, ERR_HunterAttack attack, int role)
|
||||
{
|
||||
@@ -109,12 +110,18 @@
|
||||
foreach (IEntity member : Unit.Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Unit.Group) continue;
|
||||
+ if (!required && m_GunTeam && m_GunTeam.Retained(member)) continue;
|
||||
if ((CompartmentAccessComponent.GetVehicleIn(member) == Unit.Entity) != required) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
vector Position()
|
||||
{
|
||||
+ // The native group leader can be the stationary gunner. Infantry progress
|
||||
+ // must follow an original dismounted member rather than the parked vehicle.
|
||||
+ if (m_GunTeam)
|
||||
+ foreach (IEntity member : Unit.Members)
|
||||
+ if (ERR_Actors.CanFight(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == Unit.Group && !CompartmentAccessComponent.GetVehicleIn(member)) return member.GetOrigin();
|
||||
IEntity leader = Unit.Group.GetLeaderEntity();
|
||||
if (leader) return leader.GetOrigin();
|
||||
return Unit.Entity.GetOrigin();
|
||||
@@ -141,8 +148,18 @@
|
||||
ERR_HQMountedAmmoSnapshot ammo = ERR_HQMountedAmmoSampler.Inspect(unit.Entity);
|
||||
return ammo && ammo.Complete && ammo.Rounds + ammo.Chambers > 0;
|
||||
}
|
||||
- bool ArmedCover() { return Living() && Delivered && Role == 0 && State == 3 && CoverCrew(Unit, m_Mode); }
|
||||
- bool Covering() { return ArmedCover() && VisualCover(); }
|
||||
+ bool ArmedCover()
|
||||
+ {
|
||||
+ if (!Living() || !Delivered) return false;
|
||||
+ if (m_GunTeam && State >= 3 && State <= 5) return m_GunTeam.Cover();
|
||||
+ return Role == 0 && State == 3 && CoverCrew(Unit, m_Mode);
|
||||
+ }
|
||||
+ bool Covering()
|
||||
+ {
|
||||
+ if (!ArmedCover()) return false;
|
||||
+ if (m_GunTeam) return m_GunTeam.Visual(Attack.ReportPosition);
|
||||
+ return VisualCover();
|
||||
+ }
|
||||
protected void Clear()
|
||||
{
|
||||
foreach (AIWaypoint waypoint : Orders)
|
||||
@@ -179,6 +196,15 @@
|
||||
}
|
||||
}
|
||||
SCR_AIWaypoint commanded = SCR_AIWaypoint.Cast(move);
|
||||
+ if (m_GunTeam)
|
||||
+ {
|
||||
+ vector via; if (corner) via = corner.GetOrigin();
|
||||
+ bool issued = m_GunTeam.Move(position, via);
|
||||
+ SCR_EntityHelper.DeleteEntityAndChildren(move);
|
||||
+ if (corner) SCR_EntityHelper.DeleteEntityAndChildren(corner);
|
||||
+ if (!issued) return false;
|
||||
+ Clear(); m_TravelDistance = travel; return true;
|
||||
+ }
|
||||
if (commanded) commanded.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
|
||||
SCR_AIWaypoint first = SCR_AIWaypoint.Cast(corner);
|
||||
if (first) first.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
|
||||
@@ -196,6 +222,22 @@
|
||||
}
|
||||
protected bool Board(bool enter)
|
||||
{
|
||||
+ if (!enter)
|
||||
+ {
|
||||
+ if (!m_GunTeam)
|
||||
+ {
|
||||
+ m_GunTeam = new ERR_HunterGunTeam(Unit);
|
||||
+ if (!m_GunTeam.Start()) m_GunTeam = null;
|
||||
+ }
|
||||
+ if (m_GunTeam)
|
||||
+ {
|
||||
+ Clear();
|
||||
+ if (Unit.Recovery) Unit.Recovery.SuspendBoarding();
|
||||
+ VehicleUse(false);
|
||||
+ Note("driver and passengers dismount; original loaded gunner stays to cover");
|
||||
+ return true;
|
||||
+ }
|
||||
+ }
|
||||
AIWaypoint created = ERR_SupplyConvoyTactics.Board(Unit.Entity, enter);
|
||||
SCR_BoardingWaypoint waypoint = SCR_BoardingWaypoint.Cast(created);
|
||||
if (!waypoint)
|
||||
@@ -203,10 +245,37 @@
|
||||
if (created) SCR_EntityHelper.DeleteEntityAndChildren(created);
|
||||
return false;
|
||||
}
|
||||
- if (enter) VehicleUse(true);
|
||||
+ if (enter)
|
||||
+ {
|
||||
+ if (m_GunTeam) { m_GunTeam.Stop(); m_GunTeam = null; }
|
||||
+ VehicleUse(true);
|
||||
+ }
|
||||
waypoint.SetPriorityLevel(SCR_AIActionBase.PRIORITY_LEVEL_PLAYER);
|
||||
waypoint.SetAllowance(true, true, true);
|
||||
Clear(); Add(waypoint); return true;
|
||||
+ }
|
||||
+ protected bool FacingSector()
|
||||
+ {
|
||||
+ vector direction = Attack.ReportPosition - Unit.Entity.GetOrigin(); direction[1] = 0;
|
||||
+ if (direction.Length() < 1) return true;
|
||||
+ vector transform[4]; Unit.Entity.GetTransform(transform);
|
||||
+ vector forward = transform[2]; forward[1] = 0;
|
||||
+ return vector.Dot(forward.Normalized(), direction.Normalized()) >= 0.707;
|
||||
+ }
|
||||
+ protected bool TryAlign()
|
||||
+ {
|
||||
+ // Best-effort native steering, not rotation/teleporting. Never delay an
|
||||
+ // already engaged crew or drive a vehicle into the reported enemy position.
|
||||
+ if (FacingSector() || ERR_HunterCombat.Active(Unit.Group) || !ERR_HunterGunTeam.LoadedGunner(Unit)) return false;
|
||||
+ vector origin = Unit.Entity.GetOrigin();
|
||||
+ vector direction = Attack.ReportPosition - origin; direction[1] = 0;
|
||||
+ if (direction.Length() < 180) return false;
|
||||
+ vector point = ERR_Actors.Ground(origin + direction.Normalized() * 20);
|
||||
+ SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
+ if (!utility || !utility.IsPositionAllowed(point) || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) return false;
|
||||
+ ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
+ if (!placement.Clear(point, ERR_ForceRules.Heading(origin, point, 0))) return false;
|
||||
+ return Move(point, 5);
|
||||
}
|
||||
protected void Note(string detail)
|
||||
{
|
||||
@@ -328,7 +397,12 @@
|
||||
// around it to reach the ideal staging point. Continue that last leg on foot.
|
||||
bool onFootReach = ERR_HunterAttackRules.DismountRange(vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition));
|
||||
if (Role == 0 && near) { Clear(); State = 3; Note("in covering position; native gunner owns target acquisition and fire"); }
|
||||
- else if (Role > 0 && (near || local || onFootReach) && Board(false)) { State = 2; Deadline = now + 45000; NextRetry = now + 15000; Note("within infantry approach range; dismounting original crew"); }
|
||||
+ else if (Role > 0 && (near || local || onFootReach))
|
||||
+ {
|
||||
+ if (TryAlign()) { State = 8; Deadline = now + 20000; Note("short native positioning move toward reported sector before dismount"); }
|
||||
+ else if (Board(false)) { State = 2; Deadline = now + 45000; NextRetry = now + 15000; Note("within infantry approach range; dismounting assault crew"); }
|
||||
+ else if (now >= Deadline) { Stop(); return false; }
|
||||
+ }
|
||||
else if (now >= Deadline)
|
||||
{
|
||||
if (ERR_HunterAttackRules.ExtendApproach(m_ApproachStarted, m_ApproachProgress, now))
|
||||
@@ -336,6 +410,12 @@
|
||||
else { Note("approach made no recent progress or exhausted five-minute budget; release this assignment"); Stop(); return false; }
|
||||
}
|
||||
}
|
||||
+ if (State == 8 && (FacingSector() || ERR_HunterCombat.Active(Unit.Group) || now >= Deadline))
|
||||
+ {
|
||||
+ if (Board(false)) { State = 2; Deadline = now + 45000; NextRetry = now + 15000; }
|
||||
+ else if (now >= Deadline) { Stop(); return false; }
|
||||
+ }
|
||||
+ if (m_GunTeam && State >= 2 && State <= 5) m_GunTeam.Tick(now);
|
||||
if (State == 2)
|
||||
{
|
||||
if (Aboard(false)) { Clear(); VehicleUse(false); State = 3; Deadline = now + 45000; Note("on foot; waiting for covering team"); }
|
||||
@@ -362,11 +442,18 @@
|
||||
}
|
||||
}
|
||||
if (State == 4 && !m_UnsupportedSearch && !Attack.CoverAlive())
|
||||
- { Clear(); State = 3; Deadline = now + 45000; Note("covering crew or mounted ammunition unavailable; manoeuvre paused"); }
|
||||
+ { if (m_GunTeam) m_GunTeam.Pause(); Clear(); State = 3; Deadline = now + 45000; Note("covering crew or mounted ammunition unavailable; manoeuvre paused"); }
|
||||
if (State == 4 && vector.DistanceXZ(Position(), Approach) <= 30)
|
||||
{
|
||||
- AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), Sector));
|
||||
- if (search) { search.SetCompletionRadius(25); Clear(); Add(search); State = 5; Deadline = now + 60000; Note("flank reached; searching reported sector=" + Sector.ToString()); }
|
||||
+ if (m_GunTeam)
|
||||
+ {
|
||||
+ if (Move(Sector, 25)) { State = 5; Deadline = now + 60000; Note("flank reached; original infantry investigating sector under mounted cover=" + Sector.ToString()); }
|
||||
+ }
|
||||
+ else
|
||||
+ {
|
||||
+ AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), Sector));
|
||||
+ if (search) { search.SetCompletionRadius(25); Clear(); Add(search); State = 5; Deadline = now + 60000; Note("flank reached; searching reported sector=" + Sector.ToString()); }
|
||||
+ }
|
||||
}
|
||||
if ((State == 4 || State == 5) && now >= Deadline)
|
||||
{ State = 6; Deadline = now + 90000; Board(true); Note("bounded search complete; regrouping original survivors"); }
|
||||
@@ -378,6 +465,7 @@
|
||||
void Stop()
|
||||
{
|
||||
if (Stopped) return;
|
||||
+ if (m_GunTeam) { m_GunTeam.Stop(); m_GunTeam = null; }
|
||||
VehicleUse(true);
|
||||
Stopped = true; Radio = null; Clear();
|
||||
ERR_HQ coordination = ERR_HQ.Get();
|
||||
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterCloseContact.c
|
||||
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterCloseContact.c
|
||||
@@ -59,7 +59,7 @@
|
||||
foreach (IEntity member : m_Unit.Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != m_Unit.Group) continue;
|
||||
- if (IsTurretOccupant(member)) { m_Gunner = member; ERR_ConvoyMountedCrew.Hold(member, m_Unit.Entity); continue; }
|
||||
+ if (IsTurretOccupant(member) && ERR_HunterGunTeam.LoadedGunner(m_Unit) == member) { m_Gunner = member; ERR_ConvoyMountedCrew.Hold(member, m_Unit.Entity); continue; }
|
||||
ERR_ConvoyMountedCrew.Release(member);
|
||||
ERR_HunterExitTask task = new ERR_HunterExitTask(); task.Actor = member; m_Tasks.Insert(task);
|
||||
}
|
||||
@@ -70,11 +70,17 @@
|
||||
m_Unit.CombatHeld = true; m_Unit.CombatQuietUntil = m_QuietUntil;
|
||||
// Cancel a prior driver recovery action before it can compete with getting out.
|
||||
if (m_Unit.Recovery) m_Unit.Recovery.SuspendBoarding();
|
||||
+ if (m_Gunner && ERR_HunterGunTeam.LoadedGunner(m_Unit) != m_Gunner)
|
||||
+ {
|
||||
+ ERR_ConvoyMountedCrew.Release(m_Gunner);
|
||||
+ ERR_HunterExitTask released = new ERR_HunterExitTask(); released.Actor = m_Gunner; m_Tasks.Insert(released); m_Gunner = null;
|
||||
+ ERR_HQ.Event("HUNTER CLOSE CONTACT", m_Unit.Source + ": gun or vehicle no longer usable; gunner allowed to dismount");
|
||||
+ }
|
||||
foreach (ERR_HunterExitTask exitTask : m_Tasks)
|
||||
{
|
||||
IEntity actor = exitTask.Actor;
|
||||
if (!actor) continue;
|
||||
- if (!ERR_HunterCloseRules.Dismount(m_Unit.Members.Contains(actor), ERR_Actors.CanFight(actor), ERR_Actors.Player(actor), ERR_Actors.Group(actor) == m_Unit.Group, CompartmentAccessComponent.GetVehicleIn(actor) == m_Unit.Entity, IsTurretOccupant(actor))) continue;
|
||||
+ if (!ERR_HunterCloseRules.Dismount(m_Unit.Members.Contains(actor), ERR_Actors.CanFight(actor), ERR_Actors.Player(actor), ERR_Actors.Group(actor) == m_Unit.Group, CompartmentAccessComponent.GetVehicleIn(actor) == m_Unit.Entity, actor == m_Gunner)) continue;
|
||||
Physics physics = m_Unit.Entity.GetPhysics();
|
||||
if (!physics || physics.GetVelocity().Length() > 3) continue;
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
|
||||
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterGunTeam.c
|
||||
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterGunTeam.c
|
||||
@@ -0,0 +1,140 @@
|
||||
+// One original crew, two roles: mounted gun support and dismounted infantry.
|
||||
+// Individual exit orders avoid a whole-group GetOut activity pulling the gunner out.
|
||||
+class ERR_HunterFootTask
|
||||
+{
|
||||
+ IEntity Actor;
|
||||
+ ref SCR_AIMoveIndividuallyBehavior Action;
|
||||
+ vector Destination;
|
||||
+ bool Via;
|
||||
+}
|
||||
+class ERR_HunterGunTeam
|
||||
+{
|
||||
+ protected ERR_HunterVehicle m_Unit;
|
||||
+ protected IEntity m_Gunner;
|
||||
+ protected ref array<ref ERR_HunterExitTask> m_Exits = {};
|
||||
+ protected ref array<ref ERR_HunterFootTask> m_Foot = {};
|
||||
+ void ERR_HunterGunTeam(ERR_HunterVehicle unit) { m_Unit = unit; }
|
||||
+ static IEntity LoadedGunner(ERR_HunterVehicle unit)
|
||||
+ {
|
||||
+ if (!unit || !unit.Entity || !unit.Group || unit.Released || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) return null;
|
||||
+ SCR_VehicleDamageManagerComponent damage = SCR_VehicleDamageManagerComponent.Cast(unit.Entity.FindComponent(SCR_VehicleDamageManagerComponent));
|
||||
+ if (!damage || damage.GetState() == EDamageState.DESTROYED || damage.GetHealthScaled() <= 0.25 || !damage.GetEngineFunctional() || !damage.GetGearboxFunctional()) return null;
|
||||
+ ERR_HQMountedAmmoSnapshot ammo = ERR_HQMountedAmmoSampler.Inspect(unit.Entity);
|
||||
+ if (!ammo || !ammo.Complete || ammo.Rounds + ammo.Chambers <= 0) return null;
|
||||
+ foreach (BaseCompartmentSlot seat : unit.Seats)
|
||||
+ {
|
||||
+ if (!seat || seat.GetType() != ECompartmentType.TURRET) continue;
|
||||
+ IEntity actor = seat.GetOccupant();
|
||||
+ if (unit.Members.Contains(actor) && ERR_Actors.CanFight(actor) && !ERR_Actors.Player(actor) && ERR_Actors.Group(actor) == unit.Group) return actor;
|
||||
+ }
|
||||
+ return null;
|
||||
+ }
|
||||
+ bool Retained(IEntity actor) { return actor && actor == m_Gunner && CompartmentAccessComponent.GetVehicleIn(actor) == m_Unit.Entity; }
|
||||
+ bool Cover() { return m_Gunner && LoadedGunner(m_Unit) == m_Gunner; }
|
||||
+ bool Visual(vector sector)
|
||||
+ {
|
||||
+ if (!Cover()) return false;
|
||||
+ SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Gunner));
|
||||
+ if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo || !agent.m_UtilityComponent.m_AIInfo.m_Perception) return false;
|
||||
+ array<BaseTarget> targets = {};
|
||||
+ agent.m_UtilityComponent.m_AIInfo.m_Perception.GetTargetsList(targets, ETargetCategory.ENEMY);
|
||||
+ foreach (BaseTarget target : targets)
|
||||
+ if (target && ERR_HQObservation.IsClearSighting(target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure()) && vector.DistanceXZ(target.GetLastSeenPosition(), sector) <= 200) return true;
|
||||
+ return false;
|
||||
+ }
|
||||
+ protected void Exit(IEntity actor)
|
||||
+ {
|
||||
+ foreach (ERR_HunterExitTask existing : m_Exits) if (existing.Actor == actor) return;
|
||||
+ ERR_HunterExitTask task = new ERR_HunterExitTask(); task.Actor = actor; m_Exits.Insert(task);
|
||||
+ }
|
||||
+ bool Start()
|
||||
+ {
|
||||
+ m_Gunner = LoadedGunner(m_Unit);
|
||||
+ if (!m_Gunner) return false;
|
||||
+ ERR_ConvoyMountedCrew.Hold(m_Gunner, m_Unit.Entity);
|
||||
+ foreach (IEntity actor : m_Unit.Members) if (actor != m_Gunner) Exit(actor);
|
||||
+ return true;
|
||||
+ }
|
||||
+ protected void ClearFoot()
|
||||
+ {
|
||||
+ foreach (ERR_HunterFootTask task : m_Foot)
|
||||
+ {
|
||||
+ if (!task.Actor || !task.Action) continue;
|
||||
+ SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(task.Actor));
|
||||
+ if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
||||
+ array<ref AIActionBase> actions = {}; agent.m_UtilityComponent.GetActions(actions);
|
||||
+ if (actions.Contains(task.Action)) task.Action.Complete();
|
||||
+ }
|
||||
+ m_Foot.Clear();
|
||||
+ }
|
||||
+ void Pause() { ClearFoot(); }
|
||||
+ bool Move(vector destination, vector via = "0 0 0")
|
||||
+ {
|
||||
+ // Native group movement waits for its mounted member. Give the original
|
||||
+ // foot soldiers their own orders without changing groups or HQ identities.
|
||||
+ array<ref ERR_HunterFootTask> replacement = {};
|
||||
+ foreach (IEntity actor : m_Unit.Members)
|
||||
+ {
|
||||
+ if (Retained(actor) || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Unit.Group || CompartmentAccessComponent.GetVehicleIn(actor)) continue;
|
||||
+ SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
|
||||
+ if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
||||
+ ERR_HunterFootTask task = new ERR_HunterFootTask(); task.Actor = actor; task.Destination = destination; task.Via = via != vector.Zero;
|
||||
+ vector point = destination; if (task.Via) point = via;
|
||||
+ task.Action = new SCR_AIMoveIndividuallyBehavior(agent.m_UtilityComponent, null, point, 1000, SCR_AIActionBase.PRIORITY_LEVEL_PLAYER, null, 8);
|
||||
+ replacement.Insert(task);
|
||||
+ }
|
||||
+ if (replacement.IsEmpty()) return false;
|
||||
+ ClearFoot();
|
||||
+ foreach (ERR_HunterFootTask replacementTask : replacement) m_Foot.Insert(replacementTask);
|
||||
+ foreach (ERR_HunterFootTask pending : m_Foot)
|
||||
+ {
|
||||
+ SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(pending.Actor));
|
||||
+ agent.m_UtilityComponent.AddAction(pending.Action);
|
||||
+ }
|
||||
+ return true;
|
||||
+ }
|
||||
+ void Tick(float now)
|
||||
+ {
|
||||
+ if (m_Gunner && !Cover())
|
||||
+ {
|
||||
+ ERR_ConvoyMountedCrew.Release(m_Gunner); Exit(m_Gunner); m_Gunner = null;
|
||||
+ ERR_HQ.Event("HUNTER GUN TEAM", m_Unit.Source + ": mounted support unavailable; gunner released to join original infantry");
|
||||
+ }
|
||||
+ foreach (ERR_HunterExitTask task : m_Exits)
|
||||
+ {
|
||||
+ IEntity actor = task.Actor;
|
||||
+ if (!ERR_HunterCloseRules.Dismount(m_Unit.Members.Contains(actor), ERR_Actors.CanFight(actor), ERR_Actors.Player(actor), ERR_Actors.Group(actor) == m_Unit.Group, CompartmentAccessComponent.GetVehicleIn(actor) == m_Unit.Entity, Retained(actor))) continue;
|
||||
+ Physics physics = m_Unit.Entity.GetPhysics();
|
||||
+ if (!physics || physics.GetVelocity().Length() > 3) continue;
|
||||
+ SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
|
||||
+ if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
||||
+ array<ref AIActionBase> actions = {}; agent.m_UtilityComponent.GetActions(actions);
|
||||
+ if (task.Action && actions.Contains(task.Action)) continue;
|
||||
+ if (now < task.NextAttempt || task.Attempts >= 3) continue;
|
||||
+ ERR_VehicleBehaviorCleanup.Complete(task.Action, actor);
|
||||
+ task.Attempts++; task.NextAttempt = now + 10000;
|
||||
+ task.Action = new SCR_AIGetOutVehicle(agent.m_UtilityComponent, null, m_Unit.Entity, priority: 1000);
|
||||
+ agent.m_UtilityComponent.AddAction(task.Action);
|
||||
+ }
|
||||
+ foreach (ERR_HunterFootTask foot : m_Foot)
|
||||
+ {
|
||||
+ if (!foot.Via || !ERR_Actors.CanFight(foot.Actor) || ERR_Actors.Player(foot.Actor) || ERR_Actors.Group(foot.Actor) != m_Unit.Group) continue;
|
||||
+ if (vector.DistanceXZ(foot.Actor.GetOrigin(), foot.Action.m_vPosition.m_Value) > 10) continue;
|
||||
+ SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(foot.Actor));
|
||||
+ if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
||||
+ array<ref AIActionBase> actions = {}; agent.m_UtilityComponent.GetActions(actions);
|
||||
+ if (actions.Contains(foot.Action)) foot.Action.Complete();
|
||||
+ foot.Via = false;
|
||||
+ foot.Action = new SCR_AIMoveIndividuallyBehavior(agent.m_UtilityComponent, null, foot.Destination, 1000, SCR_AIActionBase.PRIORITY_LEVEL_PLAYER, null, 8);
|
||||
+ agent.m_UtilityComponent.AddAction(foot.Action);
|
||||
+ }
|
||||
+ }
|
||||
+ void Stop()
|
||||
+ {
|
||||
+ ClearFoot();
|
||||
+ foreach (ERR_HunterExitTask task : m_Exits) ERR_VehicleBehaviorCleanup.Complete(task.Action, task.Actor);
|
||||
+ m_Exits.Clear();
|
||||
+ if (m_Gunner) ERR_ConvoyMountedCrew.Release(m_Gunner);
|
||||
+ m_Gunner = null;
|
||||
+ }
|
||||
+}
|
||||
--- a/tests/test_hunter_tactics.py
|
||||
+++ b/tests/test_hunter_tactics.py
|
||||
@@ -41,7 +41,7 @@
|
||||
tick=body(p,'bool Tick(ERR_HQ hq, float now)')
|
||||
for typ in ['ERR_HQ','IEntity','AIWaypoint']:
|
||||
tick=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',tick)
|
||||
-for n in ['Radio','hq','Unit','Attack','search','coordination']:
|
||||
+for n in ['Radio','hq','Unit','Attack','search','coordination','m_GunTeam']:
|
||||
tick=tick.replace(n+'.',n+'->')
|
||||
tick=tick.replace('ERR_HQ.Get()', 'ERR_HQ::Get()').replace('vector reserved', 'Vec reserved').replace('AttackSpace().Claim', 'AttackSpace()->Claim')
|
||||
tick=tick.replace('foreach (IEntity* crew : Unit->Members)','for (IEntity* crew : Unit->Members)')
|
||||
@@ -58,7 +58,9 @@
|
||||
int holds=0;void Hold(IEntity*,IEntity*){holds++;}
|
||||
struct UnitType{struct GroupType*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;void*Downlink=nullptr;bool CombatHeld=false;float CombatQuietUntil=0;int clears=0;void ClearOrder(){clears++;}};
|
||||
struct AttackType{Vec ReportPosition;float Started=0;bool alive=true,ready=false;bool CoverAlive(){return alive;}bool CoverReady(){return ready;}};
|
||||
+struct GunTeam{int ticks=0;void Tick(float){ticks++;}void Pause(){}};
|
||||
struct Tactics{
|
||||
+ GunTeam*m_GunTeam=nullptr;bool align=false,facing=false;bool TryAlign(){return align;}bool FacingSector(){return facing;}
|
||||
UnitType*Unit=nullptr;AttackType*Attack=nullptr;RadioType*Radio=nullptr;int Role=1,State=0,Retries=0;float m_TravelDistance=0,Deadline=0,NextRetry=0,m_ApproachStarted=0,m_ApproachProgress=0;Vec m_ApproachPosition;bool m_UnsupportedSearch=false,Delivered=false,Stopped=false,living=true,aboard=true,moveWorks=true,boardWorks=true,searchWorks=true;int clears=0,moves=0,boards=0;Vec Stage,Approach,Sector,position;AIWaypoint search;
|
||||
bool Living(){return living;}void Stop(){Stopped=true;Radio=nullptr;Clear();if(Delivered)Unit->CombatHeld=false;}bool Move(Vec,float=20){moves++;return moveWorks;}void Clear(){clears++;}bool Board(bool){boards++;return boardWorks;}bool Aboard(bool required){return aboard==required;}Vec Position(){return position;}void VehicleUse(bool){}void Note(string){}void ObserveState(float){}void Add(AIWaypoint*){}AIWaypoint*SpawnSearch(Vec){return searchWorks?&search:nullptr;}
|
||||
'''+tick+r'''
|
||||
@@ -79,6 +81,9 @@
|
||||
{Fixture f;f.radio.result=1;f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);assert(f.t.State==2); } // Road passes within infantry range before exact stage.
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.Tick(&f.hq,47000);assert(f.t.State==4&&f.t.m_UnsupportedSearch);f.t.Tick(&f.hq,48000);assert(f.t.State==4); } // No repeated cover-loss reset.
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.moveWorks=false;f.t.Tick(&f.hq,47000);assert(f.t.State==6); } // Failed foot order still regroups.
|
||||
+ {Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);assert(f.t.State==8&&f.t.boards==0);f.t.Tick(&f.hq,20999);assert(f.t.State==8);f.t.Tick(&f.hq,21000);assert(f.t.State==2&&f.t.boards==1);} // Alignment never blocks beyond 20s.
|
||||
+ {Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);f.t.facing=true;f.t.Tick(&f.hq,2000);assert(f.t.State==2);} // Stop aligning when facing.
|
||||
+ {Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);f.group.active=true;f.t.Tick(&f.hq,2000);assert(f.t.State==2);} // Combat preempts alignment.
|
||||
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.living=false;assert(!f.t.Tick(&f.hq,2000)&&f.t.Stopped);}
|
||||
}
|
||||
''')
|
||||
@@ -90,13 +95,14 @@
|
||||
own=own.replace('protected ','').replace('foreach (','for (')
|
||||
for typ in ['AIWaypoint','SCR_BoardingWaypoint','SCR_AIGroupUtilityComponent','SCR_AIVehicleUsageComponent','IEntity','ERR_HQ']:
|
||||
own=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',own)
|
||||
-for name in ['Unit','utility','waypoint','m_Mode','coordination']:
|
||||
+for name in ['Unit','utility','waypoint','m_Mode','coordination','m_GunTeam']:
|
||||
own=own.replace(name+'.',name+'->')
|
||||
own=own.replace('ERR_HQ.Get()', 'ERR_HQ::Get()').replace('AttackSpace().Release', 'AttackSpace()->Release')
|
||||
own=own.replace('Unit->Group.','Unit->Group->').replace('Unit->Entity.FindComponent(SCR_AIVehicleUsageComponent)','Unit->Entity->usage')
|
||||
own=own.replace('SCR_AIVehicleUsageComponent.Cast','UsageCast').replace('SCR_BoardingWaypoint.Cast','BoardCast')
|
||||
for cls in ['ERR_ConvoyRules','ERR_SupplyConvoyTactics','ERR_ConvoyMountedCrew','SCR_EntityHelper']:
|
||||
own=own.replace(cls+'.',cls+'::')
|
||||
+own=own.replace('Unit->Recovery.SuspendBoarding()', 'Unit->Recovery->SuspendBoarding()')
|
||||
own=own.replace('SCR_AIActionBase.PRIORITY_LEVEL_PLAYER','1000').replace('null','nullptr')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
@@ -110,14 +116,16 @@
|
||||
struct SCR_AIGroupUtilityComponent{int adds=0,removes=0;void AddUsableVehicle(SCR_AIVehicleUsageComponent*){adds++;}void RemoveUsableVehicle(SCR_AIVehicleUsageComponent*){removes++;}};
|
||||
struct Group{SCR_AIGroupUtilityComponent utility;Array<AIWaypoint*>waypoints;auto GetGroupUtilityComponent(){return &utility;}void RemoveWaypoint(AIWaypoint*w){auto&i=waypoints;i.erase(std::remove(i.begin(),i.end(),w),i.end());}void AddWaypoint(AIWaypoint*w){waypoints.Insert(w);}};
|
||||
struct IEntity{SCR_AIVehicleUsageComponent usageValue;SCR_AIVehicleUsageComponent*usage=&usageValue;};
|
||||
-struct UnitType{struct Group*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;bool CombatHeld=true;float NextOrder=99;};
|
||||
+struct Recovery{void SuspendBoarding(){}};
|
||||
+struct UnitType{struct Recovery*Recovery=nullptr;struct Group*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;bool CombatHeld=true;float NextOrder=99;};
|
||||
struct Mode{bool active=true;bool Active(){return active;}};
|
||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(IEntity*,Group*){return captured;}}
|
||||
namespace ERR_ConvoyMountedCrew{int released=0;void Release(IEntity*){released++;}}
|
||||
namespace SCR_EntityHelper{void DeleteEntityAndChildren(AIWaypoint*w){w->deleted++;}}
|
||||
namespace ERR_SupplyConvoyTactics{AIWaypoint*next=nullptr;bool requested=false;AIWaypoint*Board(IEntity*,bool enter){requested=enter;return next;}}
|
||||
struct Space{void Release(void*){}};struct ERR_HQ{Space space;static ERR_HQ*Get(){return nullptr;}Space*AttackSpace(){return &space;}};
|
||||
-struct Tactics{UnitType*Unit=nullptr;Mode*m_Mode=nullptr;int Role=0;bool m_VehicleDetached=false,Stopped=false,Delivered=false,living=true;void*Radio=nullptr;Array<AIWaypoint*>Orders;bool Living(){return living&&!Stopped;}
|
||||
+struct ERR_HunterGunTeam{inline static bool usable=false;int stopped=0;ERR_HunterGunTeam(UnitType*){}bool Start(){return usable;}void Stop(){stopped++;}};
|
||||
+struct Tactics{ERR_HunterGunTeam*m_GunTeam=nullptr;void Note(const char*){}UnitType*Unit=nullptr;Mode*m_Mode=nullptr;int Role=0;bool m_VehicleDetached=false,Stopped=false,Delivered=false,living=true;void*Radio=nullptr;Array<AIWaypoint*>Orders;bool Living(){return living&&!Stopped;}
|
||||
'''+own+r'''
|
||||
};
|
||||
int main(){
|
||||
@@ -132,17 +140,20 @@
|
||||
t.VehicleUse(false);t.VehicleUse(false);assert(g.utility.removes==1&&t.m_VehicleDetached);
|
||||
t.Delivered=true;t.Stop();assert(t.Stopped&&g.utility.adds==1&&exit.deleted==1&&unrelated.deleted==0&&g.waypoints.size()==1&&!u.CombatHeld&&u.NextOrder==0);
|
||||
t.Stop();assert(g.utility.adds==1&&exit.deleted==1&&ERR_ConvoyMountedCrew::released==1);
|
||||
+ ERR_HunterGunTeam::usable=true;Tactics split;split.Unit=&u;split.m_Mode=&mode;assert(split.Board(false)&&split.m_GunTeam);auto held=split.m_GunTeam;
|
||||
+ assert(split.Board(false)&&split.m_GunTeam==held);split.Stop();assert(held->stopped==1&&!split.m_GunTeam);split.Stop();assert(held->stopped==1);ERR_HunterGunTeam::usable=false;
|
||||
+ int added=g.utility.adds;
|
||||
for(int scenario=0;scenario<3;scenario++){
|
||||
Tactics x;x.Unit=&u;x.m_Mode=&mode;x.m_VehicleDetached=true;
|
||||
ERR_ConvoyRules::captured=scenario==0;mode.active=scenario!=1;x.living=scenario!=2;
|
||||
- x.Stop();assert(g.utility.adds==1); // Never reclaim a captured/dead/teardown vehicle.
|
||||
+ x.Stop();assert(g.utility.adds==added); // Never reclaim a captured/dead/teardown vehicle.
|
||||
}
|
||||
}
|
||||
''')
|
||||
print('PASS: production dismount/boarding allocation, all crew allowances, waypoint ownership, one-time vehicle detach/restore and capture/teardown guards')
|
||||
|
||||
armed=(body(p,'static bool CoverCrew(').replace('ERR_HunterVehicle unit','UnitType* unit').replace('ERR_GameMode mode','int mode')+'\n'+body(p,'bool ArmedCover()')).replace('foreach (','for (').replace('BaseCompartmentSlot seat','BaseCompartmentSlot* seat').replace('bool gunner;','bool gunner=false;')
|
||||
-for name in ['Unit','unit','seat']:armed=armed.replace(name+'.',name+'->')
|
||||
+for name in ['Unit','unit','seat','m_GunTeam']:armed=armed.replace(name+'.',name+'->')
|
||||
armed=armed.replace('ECompartmentType.TURRET','1').replace('ERR_Actors.CanFight','CanFight')
|
||||
armed=armed.replace('ERR_HQMountedAmmoSnapshot ammo','ERR_HQMountedAmmoSnapshot* ammo').replace('ERR_HQMountedAmmoSampler.Inspect','Inspect').replace('ammo.','ammo->')
|
||||
cover=body(p,'bool CoverAlive()').replace('foreach (','for (').replace('ERR_HunterTactics team','ERR_HunterTactics* team').replace('team.','team->')
|
||||
@@ -154,10 +165,11 @@
|
||||
struct Members{Man*original;bool Contains(Man*m){return m==original;}};
|
||||
struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Rounds=99,Chambers=1;}ammo;auto Inspect(int){return &ammo;}
|
||||
struct UnitType{int Entity=0;struct Members Members;std::vector<BaseCompartmentSlot*>Seats;bool driver=true;bool CrewValid(int){return driver;}};
|
||||
-struct ERR_HunterTactics{UnitType*Unit;int Role=0,State=3,m_Mode=0;bool Delivered=true,living=true;bool Living(){return living;}
|
||||
+struct GunTeam{bool covered=true;bool Cover(){return covered;}};
|
||||
+struct ERR_HunterTactics{GunTeam*m_GunTeam=nullptr;UnitType*Unit;int Role=0,State=3,m_Mode=0;bool Delivered=true,living=true;bool Living(){return living;}
|
||||
'''+armed+r'''
|
||||
};struct Attack{std::vector<ERR_HunterTactics*>Teams;
|
||||
'''+cover+r'''
|
||||
-};int main(){Man gunner,outsider;BaseCompartmentSlot slot;slot.occupant=&gunner;UnitType u;u.Members.original=&gunner;u.Seats={&slot};ERR_HunterTactics t;t.Unit=&u;Attack a;a.Teams={&t};assert(a.CoverAlive());gunner.fit=false;assert(!a.CoverAlive());gunner.fit=true;slot.occupant=&outsider;assert(!a.CoverAlive());slot.occupant=&gunner;u.driver=false;assert(!a.CoverAlive());u.driver=true;t.Delivered=false;assert(!a.CoverAlive());t.Delivered=true;t.living=false;assert(!a.CoverAlive());t.living=true;ammo.Rounds=0;ammo.Chambers=0;assert(!a.CoverAlive());ammo.Chambers=1;assert(a.CoverAlive());ammo.Complete=false;assert(!a.CoverAlive());}
|
||||
+};int main(){Man gunner,outsider;BaseCompartmentSlot slot;slot.occupant=&gunner;UnitType u;u.Members.original=&gunner;u.Seats={&slot};ERR_HunterTactics t;t.Unit=&u;Attack a;a.Teams={&t};assert(a.CoverAlive());gunner.fit=false;assert(!a.CoverAlive());gunner.fit=true;slot.occupant=&outsider;assert(!a.CoverAlive());slot.occupant=&gunner;u.driver=false;assert(!a.CoverAlive());u.driver=true;t.Delivered=false;assert(!a.CoverAlive());t.Delivered=true;t.living=false;assert(!a.CoverAlive());t.living=true;ammo.Rounds=0;ammo.Chambers=0;assert(!a.CoverAlive());ammo.Chambers=1;assert(a.CoverAlive());ammo.Complete=false;assert(!a.CoverAlive());GunTeam split;t.m_GunTeam=&split;t.Role=1;t.State=4;assert(a.CoverAlive());split.covered=false;assert(!a.CoverAlive());}
|
||||
''')
|
||||
print('PASS: covering support requires the original conscious turret gunner, valid mounted driver, live crew and delivered assignment')
|
||||
--- a/tests/test_hunter_gun_team.py
|
||||
+++ b/tests/test_hunter_gun_team.py
|
||||
@@ -0,0 +1,61 @@
|
||||
+"""Execute gunner retention eligibility and mixed-crew completion boundaries."""
|
||||
+import re
|
||||
+from policy_harness import ROOT,body,run
|
||||
+p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterGunTeam.c'
|
||||
+s=body(p,'static IEntity LoadedGunner(').replace('foreach (','for (')
|
||||
+for t in ['IEntity','ERR_HunterVehicle','SCR_VehicleDamageManagerComponent','ERR_HQMountedAmmoSnapshot','BaseCompartmentSlot']:
|
||||
+ s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
||||
+s=s.replace('static IEntity* LoadedGunner', 'static IEntity* LoadedGunner')
|
||||
+s=s.replace('SCR_VehicleDamageManagerComponent.Cast(unit.Entity.FindComponent(SCR_VehicleDamageManagerComponent))','unit.Entity.damage')
|
||||
+s=s.replace('ERR_HQMountedAmmoSampler.Inspect(unit.Entity)','unit.Entity.ammo')
|
||||
+s=s.replace('unit.Entity.damage','unit->Entity->damage').replace('unit.Entity.ammo','unit->Entity->ammo')
|
||||
+for n in ['unit','damage','ammo','seat']:s=s.replace(n+'.',n+'->')
|
||||
+for cls in ['ERR_Actors','ERR_ConvoyRules','EDamageState','ECompartmentType']:s=s.replace(cls+'.',cls+'::')
|
||||
+s=s.replace('null','nullptr')
|
||||
+run(r'''
|
||||
+#include <cassert>
|
||||
+#include <vector>
|
||||
+struct Group{};
|
||||
+struct SCR_VehicleDamageManagerComponent{int state=0;float health=1;bool engine=true,gear=true;int GetState(){return state;}float GetHealthScaled(){return health;}bool GetEngineFunctional(){return engine;}bool GetGearboxFunctional(){return gear;}};
|
||||
+struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Rounds=1,Chambers=0;};
|
||||
+struct IEntity{bool fit=true,player=false;Group*group=nullptr;};
|
||||
+struct Vehicle{SCR_VehicleDamageManagerComponent*damage=nullptr;ERR_HQMountedAmmoSnapshot*ammo=nullptr;};
|
||||
+struct BaseCompartmentSlot{int type=1;IEntity*occupant=nullptr;int GetType(){return type;}IEntity*GetOccupant(){return occupant;}};
|
||||
+struct Members{IEntity*original=nullptr;bool Contains(IEntity*a){return a&&a==original;}};
|
||||
+struct ERR_HunterVehicle{Vehicle*Entity=nullptr;struct Group*Group=nullptr;bool Released=false;struct Members Members;std::vector<BaseCompartmentSlot*>Seats;};
|
||||
+namespace ERR_Actors{bool CanFight(IEntity*a){return a&&a->fit;}bool Player(IEntity*a){return a&&a->player;}struct Group*Group(IEntity*a){return a?a->group:nullptr;}}
|
||||
+namespace ERR_ConvoyRules{bool captured=false;bool Captured(Vehicle*,Group*){return captured;}}
|
||||
+namespace EDamageState{enum{DESTROYED=1};}namespace ECompartmentType{enum{TURRET=1};}
|
||||
+'''+s+r'''
|
||||
+int main(){
|
||||
+ Group group,other;IEntity gunner;gunner.group=&group;SCR_VehicleDamageManagerComponent damage;ERR_HQMountedAmmoSnapshot ammo;Vehicle car;car.damage=&damage;car.ammo=&ammo;
|
||||
+ ERR_HunterVehicle unit;unit.Entity=&car;unit.Group=&group;unit.Members.original=&gunner;BaseCompartmentSlot seat;seat.occupant=&gunner;unit.Seats={&seat};
|
||||
+ assert(LoadedGunner(&unit)==&gunner); // No driver-seated prerequisite: driver is assault infantry.
|
||||
+ for(int fault=0;fault<13;fault++){
|
||||
+ if(fault==0)unit.Released=true;if(fault==1)ERR_ConvoyRules::captured=true;if(fault==2)damage.state=1;
|
||||
+ if(fault==3)damage.health=.25;if(fault==4)damage.engine=false;if(fault==5)damage.gear=false;
|
||||
+ if(fault==6)ammo.Complete=false;if(fault==7)ammo.Rounds=0;if(fault==8)gunner.fit=false;
|
||||
+ if(fault==9)gunner.player=true;if(fault==10)gunner.group=&other;if(fault==11)unit.Members.original=nullptr;if(fault==12)seat.type=0;
|
||||
+ assert(!LoadedGunner(&unit));unit.Released=false;ERR_ConvoyRules::captured=false;damage={};ammo={};gunner.fit=true;gunner.player=false;gunner.group=&group;unit.Members.original=&gunner;seat.type=1;
|
||||
+ }
|
||||
+ ammo.Rounds=0;ammo.Chambers=1;assert(LoadedGunner(&unit)==&gunner);
|
||||
+ unit.Seats.clear();assert(!LoadedGunner(&unit));assert(!LoadedGunner(nullptr));
|
||||
+}
|
||||
+'''.replace(';if(', ';\n if('))
|
||||
+q=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c'
|
||||
+s=body(q,'bool Aboard(').replace('foreach (','for (').replace('IEntity member','IEntity* member').replace('Unit.','Unit->').replace('m_GunTeam.','m_GunTeam->')
|
||||
+for cls in ['ERR_Actors','CompartmentAccessComponent']:s=s.replace(cls+'.',cls+'::')
|
||||
+run(r'''
|
||||
+#include <cassert>
|
||||
+#include <vector>
|
||||
+struct IEntity{bool fit=true,player=false;int group=1,vehicle=1;};
|
||||
+namespace ERR_Actors{bool CanFight(IEntity*a){return a&&a->fit;}bool Player(IEntity*a){return a&&a->player;}int Group(IEntity*a){return a?a->group:0;}}
|
||||
+namespace CompartmentAccessComponent{int GetVehicleIn(IEntity*a){return a?a->vehicle:0;}}
|
||||
+struct UnitType{int Group=1,Entity=1;std::vector<IEntity*>Members;};
|
||||
+struct Team{IEntity*gunner;bool Retained(IEntity*a){return a==gunner&&a->vehicle==1;}};
|
||||
+struct Tactics{UnitType*Unit;Team*m_GunTeam;
|
||||
+'''+s+r'''
|
||||
+};
|
||||
+int main(){IEntity driver,gunner,passenger;UnitType unit;unit.Members={&driver,&gunner,&passenger};Team team{&gunner};Tactics t{&unit,&team};assert(t.Aboard(true)&&!t.Aboard(false));driver.vehicle=passenger.vehicle=0;assert(t.Aboard(false)&&!t.Aboard(true));t.m_GunTeam=nullptr;assert(!t.Aboard(false));gunner.vehicle=0;assert(t.Aboard(false));}
|
||||
+''')
|
||||
+print('PASS: only original fit loaded healthy uncaptured gunners stay mounted; driver can leave; infantry readiness excludes the retained gunner but regroup requires all survivors')
|
||||
@@ -0,0 +1,88 @@
|
||||
modded class SCR_ChimeraAIAgent
|
||||
{
|
||||
void ProbeReleasePrison() { ERR_PrisonPhaseChanged(ERR_Phase.PRISON, ERR_Phase.RADIO_SEARCH); SetPermanentLOD(0); ActivateAI(); }
|
||||
}
|
||||
modded class ERR_HunterService
|
||||
{
|
||||
ERR_HunterVehicle ProbeUnit()
|
||||
{
|
||||
vector position, angles;
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected("6870 0 3500", position, angles, 500)) return null;
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle(); unit.Source = "native-gun-team";
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_HunterFleetRules.VehiclePrefab(0), position, angles));
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
if (!FillCrew(unit)) return null;
|
||||
unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) { agent.SetPermanentLOD(0); agent.ActivateAI(); }
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
modded class ERR_HunterTactics
|
||||
{
|
||||
void ProbeBegin()
|
||||
{
|
||||
Unit.Group.SetPermanentLOD(0); Unit.Group.ActivateAI();
|
||||
foreach (IEntity member : Unit.Members)
|
||||
{
|
||||
AIControlComponent control = AIControlComponent.Cast(member.FindComponent(AIControlComponent));
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent()); if (agent) agent.ProbeReleasePrison();
|
||||
}
|
||||
Delivered = true; State = 2; Deadline = GetGame().GetWorld().GetWorldTime() + 45000; NextRetry = Deadline;
|
||||
Stage = Unit.Entity.GetOrigin(); Approach = ERR_Actors.Ground(Stage + "40 0 0"); Sector = ERR_Actors.Ground(Stage + "65 0 20");
|
||||
Attack.ReportPosition = Stage + "200 0 0"; Attack.Forward = "1 0 0"; Attack.Teams.Insert(this);
|
||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(Unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
utility.m_VehicleMgr.TryAddVehicle(usage); utility.AddUsableVehicle(usage);
|
||||
vector transform[4]; Unit.Entity.GetTransform(transform); Attack.ReportPosition = Stage - transform[2] * 200;
|
||||
bool aligned = TryAlign();
|
||||
bool issued = aligned;
|
||||
if (aligned) { State = 8; Deadline = GetGame().GetWorld().GetWorldTime() + 20000; }
|
||||
else issued = Board(false);
|
||||
PrintFormat("ERR GUN TEAM ALIGN: requested=%1 facing=%2", aligned, FacingSector());
|
||||
PrintFormat("ERR GUN TEAM START: board=%1 loaded=%2 split=%3", issued, ERR_HunterGunTeam.LoadedGunner(Unit) != null, m_GunTeam != null);
|
||||
}
|
||||
bool ProbeFacing() { return FacingSector(); }
|
||||
bool ProbeSplit() { return m_GunTeam != null; }
|
||||
}
|
||||
modded class ArmaReforgerScripted
|
||||
{
|
||||
protected ref ERR_HunterService m_ProbeService;
|
||||
protected ref ERR_HunterVehicle m_ProbeUnit;
|
||||
protected ref ERR_HunterAttack m_ProbeAttack;
|
||||
protected ref ERR_HunterTactics m_ProbeTactics;
|
||||
protected float m_ProbeAge;
|
||||
protected float m_ProbeNext;
|
||||
protected bool m_ProbeDone;
|
||||
override void OnUpdate(BaseWorld world, float timeslice)
|
||||
{
|
||||
super.OnUpdate(world, timeslice);
|
||||
if (m_ProbeDone || !ERR_GameMode.Get() || !ERR_GameMode.Get().EnemyFaction()) return;
|
||||
m_ProbeAge += timeslice;
|
||||
if (m_ProbeAge < 5) return;
|
||||
if (!m_ProbeService)
|
||||
{
|
||||
m_ProbeService = new ERR_HunterService(ERR_GameMode.Get(), null); m_ProbeUnit = m_ProbeService.ProbeUnit();
|
||||
if (!m_ProbeUnit) { Print("ERR GUN TEAM FAILED creation"); m_ProbeDone = true; }
|
||||
return;
|
||||
}
|
||||
if (m_ProbeAge < 10) return;
|
||||
if (!m_ProbeTactics)
|
||||
{
|
||||
m_ProbeAttack = new ERR_HunterAttack(); m_ProbeAttack.Started = world.GetWorldTime();
|
||||
m_ProbeTactics = new ERR_HunterTactics(ERR_GameMode.Get(), m_ProbeUnit, m_ProbeAttack, 1); m_ProbeTactics.ProbeBegin();
|
||||
}
|
||||
m_ProbeTactics.Tick(ERR_HQ.Get(), world.GetWorldTime());
|
||||
if (m_ProbeAge >= m_ProbeNext)
|
||||
{
|
||||
m_ProbeNext = m_ProbeAge + 5;
|
||||
PrintFormat("ERR GUN TEAM PROGRESS: state=%1 driverDistance=%2 passengerDistance=%3 gunnerSeated=%4 split=%5 footReady=%6 facing=%7", m_ProbeTactics.State, vector.DistanceXZ(m_ProbeUnit.Driver.GetOrigin(), m_ProbeUnit.Entity.GetOrigin()), vector.DistanceXZ(m_ProbeUnit.Members[2].GetOrigin(), m_ProbeUnit.Entity.GetOrigin()), m_ProbeUnit.Seats[1].GetOccupant() == m_ProbeUnit.Members[1], m_ProbeTactics.ProbeSplit(), m_ProbeTactics.Aboard(false), m_ProbeTactics.ProbeFacing());
|
||||
}
|
||||
if (m_ProbeAge > 145)
|
||||
{
|
||||
m_ProbeTactics.Stop();
|
||||
PrintFormat("ERR GUN TEAM COMPLETE: holdReleased=%1", !ERR_ConvoyMountedCrew.Retain(m_ProbeUnit.Members[1], m_ProbeUnit.Entity));
|
||||
m_ProbeDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
15:34:34.698 SCRIPT : ERR GUN TEAM START: board=1 loaded=1 split=1
|
||||
15:34:34.698 SCRIPT : ERR GUN TEAM PROGRESS: state=2 driverDistance=0.492823 passengerDistance=0.537234 gunnerSeated=1 split=1 footReady=0
|
||||
15:34:39.690 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.80016 passengerDistance=1.86545 gunnerSeated=1 split=1 footReady=1
|
||||
15:34:44.690 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.80074 passengerDistance=1.8665 gunnerSeated=1 split=1 footReady=1
|
||||
15:34:49.691 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.80074 passengerDistance=1.8665 gunnerSeated=1 split=1 footReady=1
|
||||
15:34:54.728 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80074 passengerDistance=3.60545 gunnerSeated=1 split=1 footReady=1
|
||||
15:34:59.723 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80074 passengerDistance=3.60502 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:04.723 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80074 passengerDistance=3.6046 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:09.756 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80074 passengerDistance=3.6046 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:14.757 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80074 passengerDistance=3.6046 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:19.792 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80088 passengerDistance=3.60521 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:24.823 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.79988 passengerDistance=3.60521 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:29.856 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.79988 passengerDistance=3.6046 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:34.857 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80102 passengerDistance=3.605 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:39.890 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80102 passengerDistance=3.605 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:44.889 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.80063 passengerDistance=3.605 gunnerSeated=1 split=1 footReady=1
|
||||
15:35:49.689 SCRIPT : ERR GUN TEAM COMPLETE: holdReleased=1
|
||||
@@ -0,0 +1,17 @@
|
||||
15:39:16.749 SCRIPT : ERR GUN TEAM START: board=1 loaded=1 split=1
|
||||
15:39:16.749 SCRIPT : ERR GUN TEAM PROGRESS: state=2 driverDistance=0.492823 passengerDistance=0.537234 gunnerSeated=1 split=1 footReady=0
|
||||
15:39:21.748 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.79902 passengerDistance=1.86536 gunnerSeated=1 split=1 footReady=1
|
||||
15:39:26.748 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.79953 passengerDistance=1.86613 gunnerSeated=1 split=1 footReady=1
|
||||
15:39:31.748 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.79953 passengerDistance=1.86613 gunnerSeated=1 split=1 footReady=1
|
||||
15:39:36.782 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.76566 passengerDistance=3.46243 gunnerSeated=1 split=1 footReady=1
|
||||
15:39:41.782 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=10.0659 passengerDistance=15.6807 gunnerSeated=1 split=1 footReady=1
|
||||
15:39:46.815 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=20.9203 passengerDistance=26.5766 gunnerSeated=1 split=1 footReady=1
|
||||
15:39:51.815 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=35.2788 passengerDistance=41.3818 gunnerSeated=1 split=1 footReady=1
|
||||
15:39:56.848 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=49.4868 passengerDistance=55.5803 gunnerSeated=1 split=1 footReady=1
|
||||
15:40:01.883 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=60.703 passengerDistance=60.9134 gunnerSeated=1 split=1 footReady=1
|
||||
15:40:06.881 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=60.703 passengerDistance=60.9134 gunnerSeated=1 split=1 footReady=1
|
||||
15:40:11.914 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=60.703 passengerDistance=60.9134 gunnerSeated=1 split=1 footReady=1
|
||||
15:40:16.948 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=60.6951 passengerDistance=57.1748 gunnerSeated=1 split=1 footReady=1
|
||||
15:40:21.981 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=60.6951 passengerDistance=57.1748 gunnerSeated=1 split=1 footReady=1
|
||||
15:40:26.980 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=60.6951 passengerDistance=57.1748 gunnerSeated=1 split=1 footReady=1
|
||||
15:40:31.748 SCRIPT : ERR GUN TEAM COMPLETE: holdReleased=1
|
||||
@@ -0,0 +1,32 @@
|
||||
15:41:42.192 SCRIPT : ERR GUN TEAM ALIGN: requested=1 facing=0
|
||||
15:41:42.192 SCRIPT : ERR GUN TEAM START: board=1 loaded=1 split=0
|
||||
15:41:42.193 SCRIPT : ERR GUN TEAM PROGRESS: state=8 driverDistance=0.492823 passengerDistance=0.537234 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:41:47.191 SCRIPT : ERR GUN TEAM PROGRESS: state=8 driverDistance=0.492823 passengerDistance=0.537234 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:41:52.191 SCRIPT : ERR GUN TEAM PROGRESS: state=8 driverDistance=0.49457 passengerDistance=0.541524 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:41:57.225 SCRIPT : ERR GUN TEAM PROGRESS: state=8 driverDistance=0.493549 passengerDistance=0.536792 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:42:02.225 SCRIPT : ERR GUN TEAM PROGRESS: state=2 driverDistance=0.493549 passengerDistance=0.536792 gunnerSeated=1 split=1 footReady=0 facing=0
|
||||
15:42:07.259 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.79955 passengerDistance=1.86603 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:12.259 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.80885 passengerDistance=1.85732 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:17.258 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.80885 passengerDistance=1.85732 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:22.258 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=1.67408 passengerDistance=2.34482 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:27.292 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=8.26509 passengerDistance=16.7438 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:32.325 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=19.7419 passengerDistance=28.4663 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:37.357 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=33.4303 passengerDistance=42.6561 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:42.391 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=47.6512 passengerDistance=56.4781 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:47.391 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=61.6388 passengerDistance=63.3372 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:52.391 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=63.2761 passengerDistance=63.3372 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:42:57.424 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=63.2761 passengerDistance=63.3372 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:43:02.425 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=63.2761 passengerDistance=63.3372 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:43:07.458 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=63.2767 passengerDistance=63.3372 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:43:12.491 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=63.2767 passengerDistance=63.3372 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:43:17.490 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=63.2767 passengerDistance=69.2842 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:43:22.524 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=63.2767 passengerDistance=75.005 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:43:27.557 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=63.2767 passengerDistance=76.8769 gunnerSeated=1 split=1 footReady=1 facing=0
|
||||
15:43:31.524 SCRIPT (E): SCR_AISoundHandling: SetSignalsAndTransmit: Missing callsign for: REPORT_MOUNT_AS
|
||||
15:43:31.524 SCRIPT (E): SCR_AICommsHandler: SCR_AISoundHandling.SetSignalsAndTransmit failed for request: SCR_AITalkRequest<0x000000006547B470>, prio: 5, timeout: 500, txIfBusy: 0, type: REPORT_MOUNT_AS
|
||||
15:43:32.591 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=62.5875 passengerDistance=76.8522 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:43:37.623 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=45.5099 passengerDistance=60.8671 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:43:42.657 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=28.1638 passengerDistance=43.2864 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:43:47.657 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=10.1492 passengerDistance=25.94 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:43:52.690 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=0.536898 passengerDistance=7.93427 gunnerSeated=1 split=0 footReady=0 facing=0
|
||||
15:43:57.190 SCRIPT : ERR GUN TEAM COMPLETE: holdReleased=1
|
||||
@@ -0,0 +1,18 @@
|
||||
15:46:03.496 SCRIPT : ERR GUN TEAM START: board=1 loaded=1 split=1
|
||||
15:46:03.497 SCRIPT : ERR GUN TEAM PROGRESS: state=2 driverDistance=0.492823 passengerDistance=0.537234 gunnerSeated=1 split=1 footReady=0
|
||||
15:46:08.496 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.79902 passengerDistance=1.86536 gunnerSeated=1 split=1 footReady=1
|
||||
15:46:13.496 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.79953 passengerDistance=1.86613 gunnerSeated=1 split=1 footReady=1
|
||||
15:46:18.530 SCRIPT : ERR GUN TEAM PROGRESS: state=3 driverDistance=1.79953 passengerDistance=1.86613 gunnerSeated=1 split=1 footReady=1
|
||||
15:46:23.570 SCRIPT : ERR GUN TEAM PROGRESS: state=4 driverDistance=2.55441 passengerDistance=3.47186 gunnerSeated=1 split=1 footReady=1
|
||||
15:46:28.596 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=10.9894 passengerDistance=17.811 gunnerSeated=1 split=1 footReady=1
|
||||
15:46:33.596 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=23.2187 passengerDistance=28.6112 gunnerSeated=1 split=1 footReady=1
|
||||
15:46:38.608 SCRIPT : ERR GUN TEAM PROGRESS: state=5 driverDistance=38.033 passengerDistance=43.4841 gunnerSeated=1 split=1 footReady=1
|
||||
15:46:43.628 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=51.6298 passengerDistance=57.3002 gunnerSeated=1 split=0 footReady=0
|
||||
15:46:46.996 SCRIPT (E): SCR_AISoundHandling: SetSignalsAndTransmit: Missing callsign for: REPORT_MOUNT_AS
|
||||
15:46:46.996 SCRIPT (E): SCR_AICommsHandler: SCR_AISoundHandling.SetSignalsAndTransmit failed for request: SCR_AITalkRequest<0x00007FED7B9EF060>, prio: 5, timeout: 500, txIfBusy: 0, type: REPORT_MOUNT_AS
|
||||
15:46:48.663 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=49.6764 passengerDistance=55.8003 gunnerSeated=1 split=0 footReady=0
|
||||
15:46:53.662 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=33.2002 passengerDistance=38.7933 gunnerSeated=1 split=0 footReady=0
|
||||
15:46:58.695 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=15.581 passengerDistance=22.2615 gunnerSeated=1 split=0 footReady=0
|
||||
15:47:03.729 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=1.86505 passengerDistance=5.0408 gunnerSeated=1 split=0 footReady=0
|
||||
15:47:08.729 SCRIPT : ERR GUN TEAM PROGRESS: state=6 driverDistance=0.537234 passengerDistance=1.7992 gunnerSeated=1 split=0 footReady=0
|
||||
15:47:09.029 SCRIPT : ERR GUN TEAM COMPLETE: holdReleased=1 allOriginalCrewSeated=1
|
||||
@@ -0,0 +1,82 @@
|
||||
modded class SCR_ChimeraAIAgent
|
||||
{
|
||||
void ProbeReleasePrison() { ERR_PrisonPhaseChanged(ERR_Phase.PRISON, ERR_Phase.RADIO_SEARCH); SetPermanentLOD(0); ActivateAI(); }
|
||||
}
|
||||
modded class ERR_HunterService
|
||||
{
|
||||
ERR_HunterVehicle ProbeUnit()
|
||||
{
|
||||
vector position, angles;
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected("6870 0 3500", position, angles, 500)) return null;
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle(); unit.Source = "native-gun-team";
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_HunterFleetRules.VehiclePrefab(0), position, angles));
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
if (!FillCrew(unit)) return null;
|
||||
unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) { agent.SetPermanentLOD(0); agent.ActivateAI(); }
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
modded class ERR_HunterTactics
|
||||
{
|
||||
void ProbeBegin()
|
||||
{
|
||||
Unit.Group.SetPermanentLOD(0); Unit.Group.ActivateAI();
|
||||
foreach (IEntity member : Unit.Members)
|
||||
{
|
||||
AIControlComponent control = AIControlComponent.Cast(member.FindComponent(AIControlComponent));
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent()); if (agent) agent.ProbeReleasePrison();
|
||||
}
|
||||
Delivered = true; State = 2; Deadline = GetGame().GetWorld().GetWorldTime() + 45000; NextRetry = Deadline;
|
||||
Stage = Unit.Entity.GetOrigin(); Approach = ERR_Actors.Ground(Stage + "40 0 0"); Sector = ERR_Actors.Ground(Stage + "65 0 20");
|
||||
Attack.ReportPosition = Stage + "200 0 0"; Attack.Forward = "1 0 0"; Attack.Teams.Insert(this);
|
||||
bool issued = Board(false);
|
||||
PrintFormat("ERR GUN TEAM START: board=%1 loaded=%2 split=%3", issued, ERR_HunterGunTeam.LoadedGunner(Unit) != null, m_GunTeam != null);
|
||||
}
|
||||
bool ProbeSplit() { return m_GunTeam != null; }
|
||||
}
|
||||
modded class ArmaReforgerScripted
|
||||
{
|
||||
protected ref ERR_HunterService m_ProbeService;
|
||||
protected ref ERR_HunterVehicle m_ProbeUnit;
|
||||
protected ref ERR_HunterAttack m_ProbeAttack;
|
||||
protected ref ERR_HunterTactics m_ProbeTactics;
|
||||
protected float m_ProbeAge;
|
||||
protected float m_ProbeNext;
|
||||
protected bool m_ProbeDone;
|
||||
override void OnUpdate(BaseWorld world, float timeslice)
|
||||
{
|
||||
super.OnUpdate(world, timeslice);
|
||||
if (m_ProbeDone || !ERR_GameMode.Get() || !ERR_GameMode.Get().EnemyFaction()) return;
|
||||
m_ProbeAge += timeslice;
|
||||
if (m_ProbeAge < 5) return;
|
||||
if (!m_ProbeService)
|
||||
{
|
||||
m_ProbeService = new ERR_HunterService(ERR_GameMode.Get(), null); m_ProbeUnit = m_ProbeService.ProbeUnit();
|
||||
if (!m_ProbeUnit) { Print("ERR GUN TEAM FAILED creation"); m_ProbeDone = true; }
|
||||
return;
|
||||
}
|
||||
if (m_ProbeAge < 10) return;
|
||||
if (!m_ProbeTactics)
|
||||
{
|
||||
m_ProbeAttack = new ERR_HunterAttack(); m_ProbeAttack.Started = world.GetWorldTime();
|
||||
m_ProbeTactics = new ERR_HunterTactics(ERR_GameMode.Get(), m_ProbeUnit, m_ProbeAttack, 1); m_ProbeTactics.ProbeBegin();
|
||||
}
|
||||
m_ProbeTactics.Tick(ERR_HQ.Get(), world.GetWorldTime());
|
||||
// End only the fixture's search dwell once both men reached the sector;
|
||||
// exercise production regroup immediately instead of waiting out the timer.
|
||||
if (m_ProbeTactics.State == 5 && vector.DistanceXZ(m_ProbeUnit.Driver.GetOrigin(), m_ProbeUnit.Entity.GetOrigin()) > 50 && vector.DistanceXZ(m_ProbeUnit.Members[2].GetOrigin(), m_ProbeUnit.Entity.GetOrigin()) > 50) m_ProbeTactics.Deadline = world.GetWorldTime();
|
||||
if (m_ProbeAge >= m_ProbeNext)
|
||||
{
|
||||
m_ProbeNext = m_ProbeAge + 5;
|
||||
PrintFormat("ERR GUN TEAM PROGRESS: state=%1 driverDistance=%2 passengerDistance=%3 gunnerSeated=%4 split=%5 footReady=%6", m_ProbeTactics.State, vector.DistanceXZ(m_ProbeUnit.Driver.GetOrigin(), m_ProbeUnit.Entity.GetOrigin()), vector.DistanceXZ(m_ProbeUnit.Members[2].GetOrigin(), m_ProbeUnit.Entity.GetOrigin()), m_ProbeUnit.Seats[1].GetOccupant() == m_ProbeUnit.Members[1], m_ProbeTactics.ProbeSplit(), m_ProbeTactics.Aboard(false));
|
||||
}
|
||||
if (m_ProbeAge > 110 || (m_ProbeTactics.Stopped && m_ProbeTactics.Aboard(true)))
|
||||
{
|
||||
m_ProbeTactics.Stop();
|
||||
PrintFormat("ERR GUN TEAM COMPLETE: holdReleased=%1 allOriginalCrewSeated=%2", !ERR_ConvoyMountedCrew.Retain(m_ProbeUnit.Members[1], m_ProbeUnit.Entity), m_ProbeTactics.Aboard(true));
|
||||
m_ProbeDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"changes": [
|
||||
"Keep original healthy loaded gunner mounted in flanking UAZs; driver and passenger dismount.",
|
||||
"Foot soldiers receive original-group individual movement actions instead of waiting on the mounted member.",
|
||||
"Split infantry readiness excludes retained gunner; regroup still requires all original fit crew.",
|
||||
"Split gun support uses gunner-only native perception for visual-cover readiness.",
|
||||
"Best-effort checked 20m native alignment toward delivered sector, aborted by local combat or 20-second deadline.",
|
||||
"Release held gunner when mounted ammo/vehicle/ownership becomes unsuitable, including close-contact handling.",
|
||||
"Cancel owned foot/exit actions and gunner hold on regroup, capture, loss or teardown."
|
||||
],
|
||||
"tests_passed": [
|
||||
"test_hunter_tactics",
|
||||
"test_hunter_gun_team",
|
||||
"test_hunter_coordination",
|
||||
"test_hunter_close_contact",
|
||||
"test_hunter_capture",
|
||||
"test_hunter_lifecycle",
|
||||
"test_hunter_driver_handoff",
|
||||
"test_hunter_transport"
|
||||
],
|
||||
"native": {
|
||||
"split_test": "hunter-gun-team-4: both infantry advanced around 60m with gunner seated throughout; hold released on stop",
|
||||
"alignment_test": "hunter-gun-team-final: native alignment did not achieve requested heading, timed out at 20s; dismount/search proceeded; both soldiers returned toward car",
|
||||
"compile": "final native run compiled successfully",
|
||||
"fixture_runtime_messages": "Two native missing-callsign speech errors during boarding; fixture group is not registered with normal HQ/callsign setup.",
|
||||
"regroup_test": "hunter-gun-team-regroup: both original infantry moved over 50m, returned, and boarded; original gunner stayed seated throughout; holdReleased=1 allOriginalCrewSeated=1. Fixture shortened only the search dwell to exercise production regroup sooner.",
|
||||
"regroup_result": {
|
||||
"reason": "complete",
|
||||
"exit": -15
|
||||
}
|
||||
},
|
||||
"limitations": [
|
||||
"No new live multiplayer playtest or Workshop publication.",
|
||||
"Vehicle heading is best effort; failed turn demonstrably cannot block infantry dismount.",
|
||||
"No enemy target in native fixture, so actual mounted fire and suppression were not newly measured."
|
||||
],
|
||||
"sha256": {
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HunterGunTeam.c": "0e66d82964ef8eeba29de11d5bb2af954ad72a8090855ac4c1b28fe1e66c0d69",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c": "db387377bcfbeb8def5f9133d3624d3efae20f84855ac86e36a04b6f1aae7b07",
|
||||
"addons/core/Scripts/Game/ERR/10_AI/ERR_HunterCloseContact.c": "56f22051566354ccdc61531af4869b62458a30d945e4d21de5aadf7079d2245b"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user