Add anger-scaled Sarka hunter waves every ten minutes
This commit is contained in:
@@ -55,7 +55,7 @@ modded class ERR_GameMode
|
|||||||
string enabled;
|
string enabled;
|
||||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||||
string nativeEndCase;
|
string nativeEndCase;
|
||||||
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "starter-backpack", "speech-download", "bundled-speech", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
|
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "self-epinephrine", "starter-backpack", "speech-download", "bundled-speech", "objective-map", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "sarka-hunters", "native-end-active", "native-end-preparing", "generated-sites", "roadblocks", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
|
||||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
|
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
|
||||||
ERR_NativeObjectiveProbe.BeginRound();
|
ERR_NativeObjectiveProbe.BeginRound();
|
||||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
modded class ERR_HunterService
|
||||||
|
{
|
||||||
|
void ERR_ProbeSarkaDue(float now) { m_SarkaWaves.NextWave = now; }
|
||||||
|
void ERR_ProbeSarkas(out array<ERR_HunterVehicle> cars)
|
||||||
|
{
|
||||||
|
cars.Clear();
|
||||||
|
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Sarka) cars.Insert(unit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_HQ
|
||||||
|
{
|
||||||
|
void ERR_ProbeSarkaBodies(float now)
|
||||||
|
{
|
||||||
|
for (int slot = 0; slot < 6; slot++)
|
||||||
|
{
|
||||||
|
ERR_HQReport report = new ERR_HQReport();
|
||||||
|
report.TravelMode = "body"; report.MessageId = 200000 + slot;
|
||||||
|
report.CasualtySourceId = "fixture-sarka-bodies"; report.CasualtyStrength = 6; report.CasualtySlot = slot;
|
||||||
|
report.ObservedAt = now; report.Position = "5000 50 5000";
|
||||||
|
Accept(report, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected int m_ERRSarkaStage;
|
||||||
|
protected int m_ERRSarkaDeadline;
|
||||||
|
protected bool m_ERRSarkaOrder;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "sarka-hunters") return;
|
||||||
|
m_ERRSarkaDeadline = System.GetTickCount() + 420000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_SarkaProbeTick, 1000, true);
|
||||||
|
}
|
||||||
|
protected bool ERR_SarkaProbeCheck(bool ok, string detail)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: sarka " + detail); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: sarka " + detail, LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_SarkaProbeTick); return false;
|
||||||
|
}
|
||||||
|
protected void ERR_SarkaProbeTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRSarkaDeadline || Phase() == ERR_Phase.DEFEAT) { ERR_SarkaProbeCheck(false, "native spawn/admission/driving deadline"); return; }
|
||||||
|
if (Phase() < ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||||
|
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||||
|
DamageManagerComponent damage = DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent));
|
||||||
|
if (damage) damage.EnableDamageHandling(false);
|
||||||
|
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return;
|
||||||
|
float now = GetWorld().GetWorldTime();
|
||||||
|
if (m_ERRSarkaStage == 0)
|
||||||
|
{
|
||||||
|
if (!ERR_SarkaProbeCheck(UnlockPrison(player) || Phase() >= ERR_Phase.RADIO_SEARCH, "prison released")) return;
|
||||||
|
hq.ERR_ProbeSarkaBodies(now);
|
||||||
|
if (!ERR_SarkaProbeCheck(hq.Watch().Aggression() == 2, "six delivered bodies produce two anger points")) return;
|
||||||
|
// Accelerate only the opt-in wave deadline; production placement/admission run unchanged.
|
||||||
|
hq.Hunters().ERR_ProbeSarkaDue(now); m_ERRSarkaStage = 1;
|
||||||
|
}
|
||||||
|
array<ERR_HunterVehicle> cars = {}; hq.Hunters().ERR_ProbeSarkas(cars);
|
||||||
|
if (cars.Count() < 2) return;
|
||||||
|
if (m_ERRSarkaStage == 1)
|
||||||
|
{
|
||||||
|
if (!ERR_SarkaProbeCheck(cars.Count() == 2, "two physical cars for two anger points")) return;
|
||||||
|
array<vector> positions = {player.GetOrigin()};
|
||||||
|
foreach (ERR_HunterVehicle car : cars)
|
||||||
|
{
|
||||||
|
if (!car.Registered) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(car.Entity.GetPrefabData().GetPrefabName() == ERR_SarkaRules.Prefab() && car.CrewValid(this) && car.Members.Count() == 4 && car.Seats.Count() == 4, "native S105 with four original seated crew")) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(ERR_SarkaRules.InBand(car.PatrolHome, positions), "actual road spawn 1-1.25 km from player")) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(hq.Hunters().VehicleRadioCapability(car.Driver) == 2 && car.Group.GetFaction() == EnemyFaction(), "physical vehicle manpack and selected hostile faction")) return;
|
||||||
|
int medics;
|
||||||
|
foreach (IEntity member : car.Members)
|
||||||
|
{
|
||||||
|
ResourceName profile = member.GetPrefabData().GetPrefabName();
|
||||||
|
if (profile == ERR_EnemyMedicalCatalog.Crew(this, true)) medics++;
|
||||||
|
else if (!ERR_SarkaProbeCheck(profile == ERR_EnemyMedicalCatalog.Crew(this, false), "selected special-forces profile")) return;
|
||||||
|
SCR_AICombatComponent combat = SCR_AICombatComponent.Cast(member.FindComponent(SCR_AICombatComponent));
|
||||||
|
if (!combat || combat.GetAISkill() != EAISkill.CYLON || !combat.ERR_EnhancedEnemy()) return;
|
||||||
|
}
|
||||||
|
if (!ERR_SarkaProbeCheck(medics == 1, "one medic within the four-person team")) return;
|
||||||
|
}
|
||||||
|
m_ERRSarkaStage = 2;
|
||||||
|
}
|
||||||
|
if (cars[0].Waypoint) m_ERRSarkaOrder = true;
|
||||||
|
if (!m_ERRSarkaOrder || vector.DistanceXZ(cars[0].PatrolHome, cars[0].Entity.GetOrigin()) < 30) return;
|
||||||
|
if (!ERR_SarkaProbeCheck(hq.Hunters().Deployed() <= hq.Watch().HunterTarget(), "cars do not spend ordinary hunter allowance")) return;
|
||||||
|
ERR_SarkaProbeCheck(true, "native patrol order and over 30 metres driven; all crew enhanced");
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_SarkaProbeTick);
|
||||||
|
Print("ERR SMOKE COMPLETE: sarka hunters, two anger points, two four-person SF cars, road distance, radio and driving");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -64,7 +64,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
|||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
string historicalFixture;
|
string historicalFixture;
|
||||||
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "normal-escape-route" && historicalFixture != "crash-start" && historicalFixture != "mounted-ammo" && historicalFixture != "alarm-hearing" && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "roadblocks" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||||
if (historicalFixture != "self-epinephrine" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
if (historicalFixture != "sarka-hunters" && historicalFixture != "self-epinephrine" && historicalFixture != "starter-backpack" && historicalFixture != "objective-map" && historicalFixture != "speech-download" && historicalFixture != "bundled-speech") m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||||
#endif
|
#endif
|
||||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||||
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||||
@@ -72,7 +72,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
|||||||
// Existing deterministic probes intentionally use authored coordinates.
|
// Existing deterministic probes intentionally use authored coordinates.
|
||||||
string fixture;
|
string fixture;
|
||||||
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
if (!m_TerrainIndependent && System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "normal-escape-route" && fixture != "crash-start" && fixture != "mounted-ammo" && fixture != "alarm-hearing" && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "roadblocks" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||||
if (fixture != "self-epinephrine" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
if (fixture != "sarka-hunters" && fixture != "self-epinephrine" && fixture != "starter-backpack" && fixture != "objective-map" && fixture != "speech-download" && fixture != "bundled-speech") foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
||||||
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
if (!ERR_ConfigurePatrolHQFixture(fixture)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||||
#endif
|
#endif
|
||||||
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
|
if (m_Progressive && !m_RuntimeDefinition.ConfigurePopulation(m_Definition, mode.Settings().Get("fieldCampCount"), mode.Settings().Get("fieldHospitalCount")))
|
||||||
|
|||||||
@@ -1741,6 +1741,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
|
|
||||||
override void Maintain(float now)
|
override void Maintain(float now)
|
||||||
{
|
{
|
||||||
|
m_Hunters.PauseSarkaIfEmpty();
|
||||||
// Aircraft already airborne keep flying even when the server is empty.
|
// Aircraft already airborne keep flying even when the server is empty.
|
||||||
m_Recon.Tick(now);
|
m_Recon.Tick(now);
|
||||||
m_PlaneRecon.Tick(now);
|
m_PlaneRecon.Tick(now);
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Wave size is sampled from delivered body-driven aggression every ten minutes.
|
||||||
|
// Failed placements remain pending in this wave, never accumulate into a backlog.
|
||||||
|
class ERR_SarkaWaves
|
||||||
|
{
|
||||||
|
float NextWave = -1;
|
||||||
|
float NextAttempt;
|
||||||
|
int Pending;
|
||||||
|
int Target;
|
||||||
|
int Wave;
|
||||||
|
int Spawned;
|
||||||
|
void Pause() { NextWave = -1; Pending = 0; Target = 0; }
|
||||||
|
bool Due(float now, int aggression, bool playing)
|
||||||
|
{
|
||||||
|
if (!playing) { Pause(); return false; }
|
||||||
|
if (NextWave < 0) { NextWave = now + 600000; return false; }
|
||||||
|
if (now >= NextWave)
|
||||||
|
{
|
||||||
|
NextWave = now + 600000; NextAttempt = now;
|
||||||
|
Target = Math.Clamp(aggression, 0, 30); Pending = Target; Wave++;
|
||||||
|
}
|
||||||
|
if (Pending == 0 || now < NextAttempt) return false;
|
||||||
|
NextAttempt = now + 5000;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void Commit() { if (Pending > 0) { Pending--; Spawned++; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_SarkaRules
|
||||||
|
{
|
||||||
|
static ResourceName Prefab() { return "{AD80FB47D868C775}Prefabs/Vehicles/Wheeled/S105/S105_green.et"; }
|
||||||
|
static bool Seats(int drivers, int turrets, int cargo, int total)
|
||||||
|
{
|
||||||
|
return drivers == 1 && turrets == 0 && cargo == 3 && total == 4;
|
||||||
|
}
|
||||||
|
static bool InBand(vector position, array<vector> players)
|
||||||
|
{
|
||||||
|
if (players.IsEmpty() || !ERR_ForceRules.FiniteVector(position)) return false;
|
||||||
|
float nearest = 1000000;
|
||||||
|
foreach (vector player : players)
|
||||||
|
{
|
||||||
|
if (!ERR_ForceRules.FiniteVector(player)) return false;
|
||||||
|
nearest = Math.Min(nearest, vector.DistanceXZ(position, player));
|
||||||
|
}
|
||||||
|
return nearest >= 1000 && nearest <= 1250;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
|||||||
float PendingObservedAt = -1;
|
float PendingObservedAt = -1;
|
||||||
bool TroopTransport;
|
bool TroopTransport;
|
||||||
bool Motorbike;
|
bool Motorbike;
|
||||||
|
bool Sarka;
|
||||||
bool OpeningPatrol;
|
bool OpeningPatrol;
|
||||||
vector PatrolHome;
|
vector PatrolHome;
|
||||||
bool RadioRequested;
|
bool RadioRequested;
|
||||||
@@ -136,6 +137,87 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
protected int m_ReserveSpent;
|
protected int m_ReserveSpent;
|
||||||
protected float m_NextReserve;
|
protected float m_NextReserve;
|
||||||
protected ref ERR_HunterVehicle m_PendingReserve;
|
protected ref ERR_HunterVehicle m_PendingReserve;
|
||||||
|
protected ref ERR_SarkaWaves m_SarkaWaves = new ERR_SarkaWaves();
|
||||||
|
protected string m_SarkaStatus = "first wave ten minutes after prison escape";
|
||||||
|
string SarkaStatus()
|
||||||
|
{
|
||||||
|
return string.Format(" Sarka waves: %1 deployed, %2/%3 pending in wave %4; %5.", m_SarkaWaves.Spawned, m_SarkaWaves.Pending, m_SarkaWaves.Target, m_SarkaWaves.Wave, m_SarkaStatus);
|
||||||
|
}
|
||||||
|
// HQ maintenance runs even while tactical ticks pause in an empty round.
|
||||||
|
void PauseSarkaIfEmpty()
|
||||||
|
{
|
||||||
|
if (m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
||||||
|
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||||
|
if (players.IsEmpty()) m_SarkaWaves.Pause();
|
||||||
|
}
|
||||||
|
protected void TickSarka(ERR_HQ hq, float now)
|
||||||
|
{
|
||||||
|
// Released crews keep their physical/HQ ownership; discard only wave tracking.
|
||||||
|
for (int retired = m_Hunters.Count() - 1; retired >= 0; retired--)
|
||||||
|
if (m_Hunters[retired].Sarka && m_Hunters[retired].Released) m_Hunters.Remove(retired);
|
||||||
|
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||||
|
int previousWave = m_SarkaWaves.Wave;
|
||||||
|
bool due = m_SarkaWaves.Due(now, hq.Watch().Aggression(), !players.IsEmpty());
|
||||||
|
if (previousWave != m_SarkaWaves.Wave)
|
||||||
|
{
|
||||||
|
m_SarkaStatus = "placing four-person special-forces teams on roads";
|
||||||
|
if (m_SarkaWaves.Target == 0) m_SarkaStatus = "no aggression; next wave check in ten minutes";
|
||||||
|
ERR_HQ.Event("SARKA WAVE", string.Format("wave=%1 aggression=%2 cars=%3 identified bodies=%4", m_SarkaWaves.Wave, hq.Watch().Aggression(), m_SarkaWaves.Target, hq.Watch().IdentifiedBodies()));
|
||||||
|
}
|
||||||
|
if (!due) return;
|
||||||
|
int awaiting;
|
||||||
|
foreach (ERR_HunterVehicle pending : m_Hunters) if (!pending.Released && !pending.Registered) awaiting++;
|
||||||
|
if (m_Mode.Forces().Count() + awaiting >= 250) { m_SarkaStatus = "waiting for formation budget"; return; }
|
||||||
|
if (!ERR_EnemyLoadoutCatalog.Available(ERR_EnemyMedicalCatalog.Crew(m_Mode, false)) || !ERR_EnemyLoadoutCatalog.Available(ERR_EnemyMedicalCatalog.Crew(m_Mode, true)))
|
||||||
|
{ m_SarkaStatus = "selected faction lacks response crew"; return; }
|
||||||
|
array<vector> positions = {};
|
||||||
|
foreach (IEntity player : players) positions.Insert(player.GetOrigin());
|
||||||
|
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||||
|
for (int attempt = 0; attempt < 8; attempt++)
|
||||||
|
{
|
||||||
|
vector offset = Vector(Math.RandomFloat(-1, 1), 0, Math.RandomFloat(-1, 1));
|
||||||
|
if (offset.Length() < 0.1) continue;
|
||||||
|
vector seed = positions[Math.RandomInt(0, positions.Count())] + offset.Normalized() * 1125;
|
||||||
|
vector position, angles;
|
||||||
|
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles, 150) || !ERR_SarkaRules.InBand(position, positions) || !placement.Clear(position, angles[0])) continue;
|
||||||
|
bool occupied;
|
||||||
|
foreach (ERR_HunterVehicle other : m_Hunters)
|
||||||
|
if (other.Entity && vector.DistanceXZ(position, other.Entity.GetOrigin()) < 60) occupied = true;
|
||||||
|
if (occupied) continue;
|
||||||
|
if (SpawnSarka(now, position, angles))
|
||||||
|
{
|
||||||
|
m_SarkaWaves.Commit();
|
||||||
|
m_SarkaStatus = "four-person teams join HQ hunters; departures spaced five seconds apart";
|
||||||
|
}
|
||||||
|
else m_SarkaStatus = "vehicle or crew setup failed; retrying this wave";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_SarkaStatus = "waiting for a clear road 1-1.25 km from the nearest player";
|
||||||
|
}
|
||||||
|
protected bool SpawnSarka(float now, vector position, vector angles)
|
||||||
|
{
|
||||||
|
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||||
|
unit.Sarka = true; unit.TroopTransport = true; unit.OpeningPatrol = true; unit.PatrolHome = position;
|
||||||
|
unit.Source = "aggression-sarka-" + (m_SarkaWaves.Spawned + 1).ToString();
|
||||||
|
IEntity spawned = ERR_Actors.Spawn(ERR_SarkaRules.Prefab(), position, angles);
|
||||||
|
unit.Entity = Vehicle.Cast(spawned);
|
||||||
|
if (!unit.Entity || !unit.Entity.FindComponent(AICarMovementComponent) || !unit.Entity.FindComponent(SCR_AIVehicleUsageComponent))
|
||||||
|
{ ERR_Actors.DeleteSpawned(spawned); return false; }
|
||||||
|
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||||
|
if (!unit.Group || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||||
|
{
|
||||||
|
foreach (IEntity failed : unit.Members) ERR_Actors.DeleteSpawned(failed);
|
||||||
|
ERR_Actors.DeleteSpawned(unit.Group); ERR_Actors.DeleteSpawned(unit.Entity); return false;
|
||||||
|
}
|
||||||
|
m_Mode.Own(unit.Entity); m_Mode.Own(unit.Group);
|
||||||
|
ERR_Actors.SetFaction(unit.Entity, m_Mode.EnemyFaction());
|
||||||
|
unit.LaunchedAt = now; unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||||
|
unit.Recovery = new ERR_HunterCrewRecovery(unit); unit.Recovery.Update(m_Mode, now);
|
||||||
|
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||||
|
movement.SetCruiseSpeed(45);
|
||||||
|
ERR_HQ.Event("SARKA SPAWN", string.Format("%1 wave=%2 crew=%3 position=%4; four original response soldiers including driver and medic", unit.Source, m_SarkaWaves.Wave, unit.Members.Count(), position));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
protected int m_OpeningDistance = 1000;
|
protected int m_OpeningDistance = 1000;
|
||||||
// Called once when the round's HQ is created; menu edits affect the next HQ.
|
// Called once when the round's HQ is created; menu edits affect the next HQ.
|
||||||
void ConfigureOpening(int distance)
|
void ConfigureOpening(int distance)
|
||||||
@@ -275,7 +357,6 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
|
if (!m_Mode.Active() || !actor || !entered || !m_Mode.Players()) return;
|
||||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command());
|
||||||
if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
|
if (!hq || hq.Hunters() != this || !m_Mode.Players().CurrentActor(actor)) return;
|
||||||
if (m_Hunters.Count() > 21) return;
|
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
{
|
{
|
||||||
// Released survivors already have independent HQ ownership. Do not revoke
|
// Released survivors already have independent HQ ownership. Do not revoke
|
||||||
@@ -304,7 +385,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
int Deployed()
|
int Deployed()
|
||||||
{
|
{
|
||||||
int count;
|
int count;
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Motorbike) count++;
|
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Motorbike && !unit.Sarka) count++;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
int ActiveHunters()
|
int ActiveHunters()
|
||||||
@@ -324,7 +405,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
{
|
{
|
||||||
int count;
|
int count;
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
if (!unit.Motorbike && !ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) count++;
|
if (!unit.Motorbike && !unit.Sarka && !ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) count++;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
protected ERR_HunterVehicle ReserveSource(ERR_HQ hq, float now)
|
protected ERR_HunterVehicle ReserveSource(ERR_HQ hq, float now)
|
||||||
@@ -332,7 +413,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
if (!ERR_HunterFleetRules.ReserveDue(Deployed(), m_Target, m_ReserveSpent, AvailableToHQ(hq, now), m_NextReserve, now)) return null;
|
if (!ERR_HunterFleetRules.ReserveDue(Deployed(), m_Target, m_ReserveSpent, AvailableToHQ(hq, now), m_NextReserve, now)) return null;
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
{
|
{
|
||||||
if (unit.Motorbike || unit.ReserveCovered || now - unit.LaunchedAt < 480000) continue;
|
if (unit.Motorbike || unit.Sarka || unit.ReserveCovered || now - unit.LaunchedAt < 480000) continue;
|
||||||
if (ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) return unit;
|
if (ERR_HunterFleetRules.Unavailable(hq.Watch().Find(unit.Source), unit.LaunchedAt, now)) return unit;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -344,7 +425,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
ERR_HQ.Event("HUNTER RESERVE", string.Format("%1 replaces unavailable patrol %2; reserve=%3/3; next departure in at least 480s; silence is not a confirmed death", unit.Source, m_PendingReserve.Source, m_ReserveSpent));
|
ERR_HQ.Event("HUNTER RESERVE", string.Format("%1 replaces unavailable patrol %2; reserve=%3/3; next departure in at least 480s; silence is not a confirmed death", unit.Source, m_PendingReserve.Source, m_ReserveSpent));
|
||||||
m_PendingReserve = null;
|
m_PendingReserve = null;
|
||||||
}
|
}
|
||||||
override string Status() { return string.Format("HQ hunting patrols: %1 active including bikes; %2 UAZ/trucks deployed; aggression allowance %3, reserve used %4/3 (15 total maximum). Reserve departures at least eight minutes apart. Motorbikes have a separate allowance.", ActiveHunters(), Deployed(), m_Target, m_ReserveSpent); }
|
override string Status() { return string.Format("HQ hunting patrols: %1 active including bikes and Sarkas; %2 UAZ/trucks deployed; aggression allowance %3, reserve used %4/3 (15 total maximum). Reserve departures at least eight minutes apart. Motorbikes have a separate allowance.", ActiveHunters(), Deployed(), m_Target, m_ReserveSpent) + SarkaStatus(); }
|
||||||
protected bool StartHunter(ERR_HQ hq, float now)
|
protected bool StartHunter(ERR_HQ hq, float now)
|
||||||
{
|
{
|
||||||
if ((!m_PendingReserve && Deployed() >= m_Target + m_ReserveSpent) || Deployed() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
if ((!m_PendingReserve && Deployed() >= m_Target + m_ReserveSpent) || Deployed() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
||||||
@@ -410,6 +491,10 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
{
|
{
|
||||||
if (!ERR_MotorbikeRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
if (!ERR_MotorbikeRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
||||||
}
|
}
|
||||||
|
else if (hunter.Sarka)
|
||||||
|
{
|
||||||
|
if (!ERR_SarkaRules.Seats(drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
||||||
|
}
|
||||||
else if (!ERR_HunterFleetRules.Seats(hunter.TroopTransport, drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
else if (!ERR_HunterFleetRules.Seats(hunter.TroopTransport, drivers.Count(), turrets.Count(), cargo.Count(), all.Count())) return false;
|
||||||
unit.Seats.Insert(drivers[0]);
|
unit.Seats.Insert(drivers[0]);
|
||||||
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
||||||
@@ -653,6 +738,7 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
}
|
}
|
||||||
m_PendingReserve = null;
|
m_PendingReserve = null;
|
||||||
TickMotorbikes(hq, now);
|
TickMotorbikes(hq, now);
|
||||||
|
TickSarka(hq, now);
|
||||||
if (!m_Coordination) m_Coordination = new ERR_HunterCoordination(m_Mode);
|
if (!m_Coordination) m_Coordination = new ERR_HunterCoordination(m_Mode);
|
||||||
m_Coordination.Tick(hq, m_Hunters, now);
|
m_Coordination.Tick(hq, m_Hunters, now);
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
|
|||||||
@@ -2,6 +2,28 @@
|
|||||||
|
|
||||||
Implemented 2026-09-23 in the combined Core addon. These are server-owned mission services; clients do not request spawns or supply casualty counts.
|
Implemented 2026-09-23 in the combined Core addon. These are server-owned mission services; clients do not request spawns or supply casualty counts.
|
||||||
|
|
||||||
|
## Šárka reinforcement waves (2026-10-16)
|
||||||
|
|
||||||
|
After prison escape, every ten minutes the current anger score sends one Šárka
|
||||||
|
per point: anger 3 means three cars, anger 0 means none. Anger still increases by
|
||||||
|
one for every three enemy bodies identified and reported to HQ, capped at 30.
|
||||||
|
Each car carries four selected-faction response troops total: a driver, two
|
||||||
|
soldiers and one medic. Vanilla factions use their special-forces profiles;
|
||||||
|
modded factions use their configured ResponseSoldier/ResponseMedic profiles.
|
||||||
|
|
||||||
|
Cars spawn on checked clear roads 1–1.25 km from the nearest living player.
|
||||||
|
Departures are spread five seconds apart; blocked placement retries within the
|
||||||
|
current wave. Unplaced cars do not accumulate across waves. An empty round
|
||||||
|
cancels pending departures and starts a new ten-minute wait when play resumes.
|
||||||
|
These additional cars use the same radio orders, patrol, combat, dismount and
|
||||||
|
capture behavior as other hunters. Player positions guide initial placement;
|
||||||
|
subsequent pursuit still requires delivered sightings.
|
||||||
|
|
||||||
|
Šárkas have a separate wave allowance from ordinary UAZs, trucks and motorbikes.
|
||||||
|
Existing formation-budget limits still apply, so a full ledger can defer a wave.
|
||||||
|
Original cars and crew remain physical after loss or capture; waves create new
|
||||||
|
teams and never refill casualties in old teams.
|
||||||
|
|
||||||
## Coordinated pursuit candidate (2026-10-13)
|
## Coordinated pursuit candidate (2026-10-13)
|
||||||
|
|
||||||
The new **Coordinated** doctrine organizes available formations into a shared pursuit. It is opt-in for qualification; **Current** remains the compatibility default. The first version runs entirely inside the mod. Python is used for offline replay and performance comparison, with no runtime service or language-model dependency. The intended playtest is two to four players, with a separate solo evasion check. Existing accuracy, damage and native detection settings are unchanged, so comparison can isolate the decisions and their execution.
|
The new **Coordinated** doctrine organizes available formations into a shared pursuit. It is opt-in for qualification; **Current** remains the compatibility default. The first version runs entirely inside the mod. Python is used for offline replay and performance comparison, with no runtime service or language-model dependency. The intended playtest is two to four players, with a separate solo evasion check. Existing accuracy, damage and native detection settings are unchanged, so comparison can isolate the decisions and their execution.
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ int main(){
|
|||||||
assert(!c.mode.ledger.r.Pinned&&c.unit.Entity==&c.vehicle&&c.unit.Group==&c.group);
|
assert(!c.mode.ledger.r.Pinned&&c.unit.Entity==&c.vehicle&&c.unit.Group==&c.group);
|
||||||
c.mode.roster.current=&c.player;c.Enter();assert(c.access.nativeCalls==2&&c.recovery.stops==1&&c.hq.unregistered==1);
|
c.mode.roster.current=&c.player;c.Enter();assert(c.access.nativeCalls==2&&c.recovery.stops==1&&c.hq.unregistered==1);
|
||||||
}
|
}
|
||||||
for(int bad=0;bad<9;bad++){Case c;
|
for(int bad=0;bad<8;bad++){Case c;
|
||||||
if(bad==0)Replication::server=false;
|
if(bad==0)Replication::server=false;
|
||||||
if(bad==1)c.service.m_Stopped=true;
|
if(bad==1)c.service.m_Stopped=true;
|
||||||
if(bad==2)c.mode.active=false;
|
if(bad==2)c.mode.active=false;
|
||||||
@@ -79,9 +79,9 @@ int main(){
|
|||||||
if(bad==5)c.hq.service=&c.foreign;
|
if(bad==5)c.hq.service=&c.foreign;
|
||||||
if(bad==6)c.mode.roster.current=&c.unrelated;
|
if(bad==6)c.mode.roster.current=&c.unrelated;
|
||||||
if(bad==7)c.unit.Entity=nullptr;
|
if(bad==7)c.unit.Entity=nullptr;
|
||||||
if(bad==8)c.service.m_Hunters.resize(22,&c.unit);
|
|
||||||
c.Enter();assert(!c.unit.PlayerCaptureObserved&&!c.unit.Released&&c.hq.unregistered==0&&c.access.nativeCalls==1);
|
c.Enter();assert(!c.unit.PlayerCaptureObserved&&!c.unit.Released&&c.hq.unregistered==0&&c.access.nativeCalls==1);
|
||||||
}
|
}
|
||||||
|
{Case c;c.service.m_Hunters.resize(100,&c.unit);c.Enter();assert(c.unit.PlayerCaptureObserved&&c.unit.Released&&c.hq.unregistered==1);}
|
||||||
{Case c;c.Enter(&c.unrelated);assert(!c.unit.Released&&c.access.nativeCalls==1);}
|
{Case c;c.Enter(&c.unrelated);assert(!c.unit.Released&&c.access.nativeCalls==1);}
|
||||||
{Case c;IEntity chain[33];for(int i=0;i<32;i++)chain[i].parent=&chain[i+1];chain[32].parent=&c.vehicle;c.Enter(&chain[0]);assert(!c.unit.Released);}
|
{Case c;IEntity chain[33];for(int i=0;i<32;i++)chain[i].parent=&chain[i+1];chain[32].parent=&c.vehicle;c.Enter(&chain[0]);assert(!c.unit.Released);}
|
||||||
{Case c;IEntity loop;loop.parent=&loop;c.Enter(&loop);assert(!c.unit.Released);}
|
{Case c;IEntity loop;loop.parent=&loop;c.Enter(&loop);assert(!c.unit.Released);}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ struct ERR_SiteRuntime{struct Definition* Definition=nullptr;struct Group* Group
|
|||||||
struct Sites{Array<ERR_SiteRuntime*> sites;int Count(){return sites.Count();}ERR_SiteRuntime* At(int i){return sites[i];}};
|
struct Sites{Array<ERR_SiteRuntime*> sites;int Count(){return sites.Count();}ERR_SiteRuntime* At(int i){return sites[i];}};
|
||||||
struct Forces{int count=0;int Count(){return count;}};
|
struct Forces{int count=0;int Count(){return count;}};
|
||||||
struct Mode{struct Forces forces;struct Sites sites;struct Forces* Forces(){return &forces;}struct Sites* Sites(){return &sites;}int EnemyFaction(){return 1;}};
|
struct Mode{struct Forces forces;struct Sites sites;struct Forces* Forces(){return &forces;}struct Sites* Sites(){return &sites;}int EnemyFaction(){return 1;}};
|
||||||
struct ERR_HunterVehicle{bool Motorbike=false;bool Released=false;IEntity* Entity=nullptr;};
|
struct ERR_HunterVehicle{bool Motorbike=false;bool Sarka=false;bool Released=false;IEntity* Entity=nullptr;};
|
||||||
struct ERR_HQ{static void Event(string,string){}};
|
struct ERR_HQ{static void Event(string,string){}};
|
||||||
namespace Math{float RandomFloat(float,float){return 0;}}
|
namespace Math{float RandomFloat(float,float){return 0;}}
|
||||||
namespace ERR_LocationKind{enum{RADIO=1,DEPOT=2,OUTPOST=3};}
|
namespace ERR_LocationKind{enum{RADIO=1,DEPOT=2,OUTPOST=3};}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ template<class... T>string Format(string s,T...){return s;}
|
|||||||
struct ERR_HQWatchEntry{bool Closed=false;int Fit=3;float ObservedAt=-1;};
|
struct ERR_HQWatchEntry{bool Closed=false;int Fit=3;float ObservedAt=-1;};
|
||||||
struct Watch{std::map<string,ERR_HQWatchEntry*> entries;ERR_HQWatchEntry* Find(string s){return entries[s];}};
|
struct Watch{std::map<string,ERR_HQWatchEntry*> entries;ERR_HQWatchEntry* Find(string s){return entries[s];}};
|
||||||
struct ERR_HQ{struct Watch watch;struct Watch* Watch(){return &watch;}static void Event(string,string){}};
|
struct ERR_HQ{struct Watch watch;struct Watch* Watch(){return &watch;}static void Event(string,string){}};
|
||||||
struct ERR_HunterVehicle{bool Motorbike=false,ReserveCovered=false,Released=false;float LaunchedAt=0;string Source;};
|
struct ERR_HunterVehicle{bool Motorbike=false,Sarka=false,ReserveCovered=false,Released=false;float LaunchedAt=0;string Source;};
|
||||||
struct ERR_HunterFleetRules{
|
struct ERR_HunterFleetRules{
|
||||||
'''+rules+r'''
|
'''+rules+r'''
|
||||||
};
|
};
|
||||||
@@ -52,8 +52,8 @@ int main(){
|
|||||||
assert(!ERR_HunterFleetRules::ReserveDue(4,3,1,0,960000,959999));
|
assert(!ERR_HunterFleetRules::ReserveDue(4,3,1,0,960000,959999));
|
||||||
assert(ERR_HunterFleetRules::ReserveDue(4,3,1,1,960000,960000));
|
assert(ERR_HunterFleetRules::ReserveDue(4,3,1,1,960000,960000));
|
||||||
|
|
||||||
Service s;ERR_HQ hq;ERR_HunterVehicle a,b,c,bike;a.Source="a";b.Source="b";c.Source="c";
|
Service s;ERR_HQ hq;ERR_HunterVehicle a,b,c,bike,sarka;a.Source="a";b.Source="b";c.Source="c";
|
||||||
bike.Source="bike";bike.Motorbike=true;s.m_Hunters={&a,&b,&c,&bike};
|
bike.Source="bike";bike.Motorbike=true;sarka.Sarka=true;sarka.Source="sarka";s.m_Hunters={&sarka,&a,&b,&c,&bike};assert(s.Deployed()==3);
|
||||||
a.Released=true;assert(s.AvailableToHQ(&hq,100)==3); // physical loss alone reveals nothing
|
a.Released=true;assert(s.AvailableToHQ(&hq,100)==3); // physical loss alone reveals nothing
|
||||||
assert(!s.ReserveSource(&hq,479999));
|
assert(!s.ReserveSource(&hq,479999));
|
||||||
assert(s.ReserveSource(&hq,480000)==&a); // absence of a first check-in has a bounded deadline
|
assert(s.ReserveSource(&hq,480000)==&a); // absence of a first check-in has a bounded deadline
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import re
|
import re
|
||||||
from policy_harness import ROOT,body,run
|
from policy_harness import ROOT,body,run
|
||||||
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
|
||||||
|
sarka_rules=body(p.parent/'ERR_HunterSarkaRules.c','static bool Seats(')
|
||||||
bike_rules=body(p.parent/'ERR_OptionalMotorbikes.c','static bool Seats(')
|
bike_rules=body(p.parent/'ERR_OptionalMotorbikes.c','static bool Seats(')
|
||||||
rules=body(p,'static bool Transport(')+body(p,'static bool Seats(')
|
rules=body(p,'static bool Transport(')+body(p,'static bool Seats(')
|
||||||
s=body(p,'override protected bool FillCrew(').replace('override protected ','')
|
s=body(p,'override protected bool FillCrew(').replace('override protected ','')
|
||||||
@@ -13,7 +14,7 @@ for typ in ['ERR_ConvoyVehicle','ERR_HunterVehicle','BaseCompartmentSlot','AIGro
|
|||||||
for obj in ['unit','seat','hunter','m_Mode']:
|
for obj in ['unit','seat','hunter','m_Mode']:
|
||||||
s=s.replace(obj+'.',obj+'->')
|
s=s.replace(obj+'.',obj+'->')
|
||||||
s=s.replace('unit->Seats[i].','unit->Seats[i]->')
|
s=s.replace('unit->Seats[i].','unit->Seats[i]->')
|
||||||
for cls in ['ERR_EnemyMedicalCatalog','ERR_EnemyLoadoutCatalog','ERR_ConvoyRules','ERR_HunterFleetRules','ERR_MotorbikeRules','ERR_ConvoyResources','ERR_HQ','ECompartmentType','string']:
|
for cls in ['ERR_EnemyMedicalCatalog','ERR_EnemyLoadoutCatalog','ERR_ConvoyRules','ERR_HunterFleetRules','ERR_MotorbikeRules','ERR_SarkaRules','ERR_ConvoyResources','ERR_HQ','ECompartmentType','string']:
|
||||||
s=s.replace(cls+'.',cls+'::')
|
s=s.replace(cls+'.',cls+'::')
|
||||||
run(r'''
|
run(r'''
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
@@ -25,6 +26,9 @@ template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;voi
|
|||||||
struct ERR_HunterFleetRules{
|
struct ERR_HunterFleetRules{
|
||||||
'''+rules+r'''
|
'''+rules+r'''
|
||||||
};
|
};
|
||||||
|
struct ERR_SarkaRules{
|
||||||
|
'''+sarka_rules+r'''
|
||||||
|
};
|
||||||
struct ERR_MotorbikeRules{
|
struct ERR_MotorbikeRules{
|
||||||
'''+bike_rules+r'''
|
'''+bike_rules+r'''
|
||||||
};
|
};
|
||||||
@@ -33,7 +37,7 @@ struct IEntity{int prefab;};struct AIGroup{};
|
|||||||
struct BaseCompartmentSlot{int type;bool available=true,fail=false;IEntity*occupant=nullptr;bool IsCompartmentAccessible(){return available;}IEntity*GetOccupant(){return occupant;}int GetType(){return type;}
|
struct BaseCompartmentSlot{int type;bool available=true,fail=false;IEntity*occupant=nullptr;bool IsCompartmentAccessible(){return available;}IEntity*GetOccupant(){return occupant;}int GetType(){return type;}
|
||||||
IEntity*SpawnCharacterInCompartment(int prefab,AIGroup*&,int){if(fail)return nullptr;occupant=new IEntity{prefab};return occupant;}};
|
IEntity*SpawnCharacterInCompartment(int prefab,AIGroup*&,int){if(fail)return nullptr;occupant=new IEntity{prefab};return occupant;}};
|
||||||
struct Vehicle{Array<BaseCompartmentSlot*> seats;};
|
struct Vehicle{Array<BaseCompartmentSlot*> seats;};
|
||||||
struct ERR_ConvoyVehicle{Vehicle*Entity;AIGroup*Group;bool TroopTransport=false;bool Motorbike=false;Array<BaseCompartmentSlot*>Seats;Array<IEntity*>Members;IEntity*Driver=nullptr;};using ERR_HunterVehicle=ERR_ConvoyVehicle;
|
struct ERR_ConvoyVehicle{Vehicle*Entity;AIGroup*Group;bool TroopTransport=false;bool Motorbike=false;bool Sarka=false;Array<BaseCompartmentSlot*>Seats;Array<IEntity*>Members;IEntity*Driver=nullptr;};using ERR_HunterVehicle=ERR_ConvoyVehicle;
|
||||||
struct Mode{int owned=0;void Own(IEntity*){owned++;}};
|
struct Mode{int owned=0;void Own(IEntity*){owned++;}};
|
||||||
namespace ERR_EnemyMedicalCatalog{int Crew(Mode*,bool medic){return medic?2:1;}}
|
namespace ERR_EnemyMedicalCatalog{int Crew(Mode*,bool medic){return medic?2:1;}}
|
||||||
namespace ERR_EnemyLoadoutCatalog{bool Available(int p){return p>0;}}
|
namespace ERR_EnemyLoadoutCatalog{bool Available(int p){return p>0;}}
|
||||||
@@ -58,6 +62,14 @@ int main(){
|
|||||||
BaseCompartmentSlot driver{ECompartmentType::PILOT},passenger{ECompartmentType::CARGO};vehicle.seats={};vehicle.seats.Insert(&driver);vehicle.seats.Insert(&passenger);
|
BaseCompartmentSlot driver{ECompartmentType::PILOT},passenger{ECompartmentType::CARGO};vehicle.seats={};vehicle.seats.Insert(&driver);vehicle.seats.Insert(&passenger);
|
||||||
assert(service.FillCrew(&bike)&&bike.Members.Count()==2&&bike.Driver==driver.occupant);
|
assert(service.FillCrew(&bike)&&bike.Members.Count()==2&&bike.Driver==driver.occupant);
|
||||||
assert(driver.occupant->prefab==1&&passenger.occupant->prefab==2);}
|
assert(driver.occupant->prefab==1&&passenger.occupant->prefab==2);}
|
||||||
|
for(int passengers=2;passengers<=4;passengers++){
|
||||||
|
Mode mode;Service service{&mode};Vehicle vehicle;AIGroup group;ERR_HunterVehicle car{&vehicle,&group};car.Sarka=true;car.TroopTransport=true;
|
||||||
|
BaseCompartmentSlot driver{ECompartmentType::PILOT},cargo[4];vehicle.seats.Insert(&driver);
|
||||||
|
for(int i=0;i<passengers;i++){cargo[i].type=ECompartmentType::CARGO;vehicle.seats.Insert(&cargo[i]);}
|
||||||
|
assert(service.FillCrew(&car)==(passengers==3));
|
||||||
|
if(passengers==3){assert(car.Members.Count()==4&&car.Driver==driver.occupant);int medics=0;for(auto member:car.Members)if(member->prefab==2)medics++;assert(medics==1);}
|
||||||
|
else assert(mode.owned==0);
|
||||||
|
}
|
||||||
assert(!ERR_HunterFleetRules::Seats(true,1,0,13,14)); // no silent population shrink
|
assert(!ERR_HunterFleetRules::Seats(true,1,0,13,14)); // no silent population shrink
|
||||||
assert(!ERR_HunterFleetRules::Seats(false,1,0,14,15)); // transport cannot pass as armed UAZ
|
assert(!ERR_HunterFleetRules::Seats(false,1,0,14,15)); // transport cannot pass as armed UAZ
|
||||||
assert(!ERR_HunterFleetRules::Seats(true,2,0,14,16));
|
assert(!ERR_HunterFleetRules::Seats(true,2,0,14,16));
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Run production wave scheduling and multiplayer placement boundaries."""
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
|
||||||
|
p = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c'
|
||||||
|
clock = p.read_text().split('class ERR_SarkaWaves', 1)[1].split('class ERR_SarkaRules', 1)[0]
|
||||||
|
clock = clock.replace('Math.Clamp', 'Math::Clamp')
|
||||||
|
band = body(p, 'static bool InBand(').replace('array<vector> players', 'Array<vector> players')
|
||||||
|
band = band.replace('foreach (', 'for (').replace('ERR_ForceRules.', 'ERR_ForceRules::').replace('Math.', 'Math::').replace('vector.DistanceXZ', 'vector::DistanceXZ')
|
||||||
|
seats = body(p, 'static bool Seats(')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
namespace Math { int Clamp(int x,int lo,int hi){return std::clamp(x,lo,hi);} float Min(float a,float b){return std::min(a,b);} }
|
||||||
|
struct vector { float x=0,y=0,z=0; static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);} };
|
||||||
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}};
|
||||||
|
namespace ERR_ForceRules { bool FiniteVector(vector v){return std::isfinite(v.x)&&std::isfinite(v.y)&&std::isfinite(v.z);} }
|
||||||
|
struct Waves
|
||||||
|
'''+clock+r''';
|
||||||
|
struct Rules {'''+band+seats+r'''};
|
||||||
|
int main(){
|
||||||
|
Waves w{};
|
||||||
|
assert(!w.Due(100,3,true)&&w.NextWave==600100);
|
||||||
|
assert(!w.Due(600099,3,true));
|
||||||
|
assert(w.Due(600100,3,true)&&w.Pending==3&&w.Wave==1);
|
||||||
|
// Failed placement spends an attempt, but no car or anger point.
|
||||||
|
assert(!w.Due(605099,3,true)&&w.Pending==3&&w.Spawned==0);
|
||||||
|
assert(w.Due(605100,7,true));w.Commit();assert(w.Pending==2&&w.Spawned==1);
|
||||||
|
assert(w.Due(610100,7,true));w.Commit();
|
||||||
|
assert(w.Due(615100,7,true));w.Commit();
|
||||||
|
assert(!w.Due(620100,7,true)); // anger changes do not enlarge a wave in progress
|
||||||
|
assert(w.Due(1200100,7,true)&&w.Pending==7&&w.Wave==2);
|
||||||
|
assert(w.Due(9999999,2,true)&&w.Pending==2&&w.Wave==3); // no missed-wave backlog
|
||||||
|
w.Pause();assert(w.Pending==0);
|
||||||
|
assert(!w.Due(10000000,30,true)&&w.NextWave==10600000);
|
||||||
|
assert(w.Due(10600000,1000,true)&&w.Pending==30);
|
||||||
|
assert(!w.Due(10600001,30,false)&&w.Pending==0&&w.NextWave==-1);
|
||||||
|
assert(!w.Due(20000000,0,true));assert(!w.Due(20600000,0,true)&&w.Target==0);
|
||||||
|
assert(!w.Due(21200000,-1,true)&&w.Pending==0);
|
||||||
|
Array<vector> players{{0,10,0}};
|
||||||
|
assert(!Rules::InBand({999,0,0},players));
|
||||||
|
assert(Rules::InBand({1000,900,0},players)&&Rules::InBand({1250,0,0},players));
|
||||||
|
assert(!Rules::InBand({1251,0,0},players));
|
||||||
|
players.push_back({1100,0,100});assert(!Rules::InBand({1100,0,0},players));
|
||||||
|
players[1]={3000,0,0};assert(Rules::InBand({1100,0,0},players));
|
||||||
|
assert(!Rules::InBand({NAN,0,0},players)&&!Rules::InBand({0,0,0},{}));
|
||||||
|
assert(Rules::Seats(1,0,3,4)&&!Rules::Seats(1,1,2,4)&&!Rules::Seats(1,0,4,5));
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: ten-minute anger waves, staged retries, empty-round reset, no backlog, four seats and nearest-player road distance')
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -14,7 +14,7 @@ p.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
|
|||||||
p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile')
|
p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile')
|
||||||
p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam')
|
p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam')
|
||||||
p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile')
|
p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile')
|
||||||
p.add_argument('--smoke', choices=['coordinated-ai', 'crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', 'starter-backpack', 'speech-download', 'bundled-speech', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
|
p.add_argument('--smoke', choices=['coordinated-ai', 'crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', 'starter-backpack', 'speech-download', 'bundled-speech', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'sarka-hunters', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory')
|
||||||
p.add_argument('--roadblock-probe-count', type=int, choices=range(1, 31), help='Roadblocks fixture target; default remains 30')
|
p.add_argument('--roadblock-probe-count', type=int, choices=range(1, 31), help='Roadblocks fixture target; default remains 30')
|
||||||
p.add_argument('--speech-probe-url', help='Loopback-only cached audio fixture; requires --smoke speech-download')
|
p.add_argument('--speech-probe-url', help='Loopback-only cached audio fixture; requires --smoke speech-download')
|
||||||
p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records')
|
p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records')
|
||||||
@@ -47,7 +47,7 @@ if a.smoke == 'crash-start' and (a.project != 'chat' or a.rounds != 1 or a.timeo
|
|||||||
if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds')
|
if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds')
|
||||||
if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds')
|
if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds')
|
||||||
if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1')
|
if a.smoke == 'objective-map' and (a.project != 'core' or a.rounds != 1): p.error('objective-map requires --project core --rounds 1')
|
||||||
if a.smoke in {'unconscious-camera', 'self-epinephrine', 'starter-backpack'} and a.project != 'core': p.error('unconscious-camera/self-epinephrine requires --project core to keep the general smoke fixture inactive')
|
if a.smoke in {'unconscious-camera', 'self-epinephrine', 'starter-backpack'} and a.project != 'core': p.error(f'{a.smoke} requires --project core to keep the general smoke fixture inactive')
|
||||||
if a.mine_stock:
|
if a.mine_stock:
|
||||||
if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360:
|
if a.smoke != 'mounted-ammo' or a.project != 'chat' or a.rounds != 1 or a.timeout < 360:
|
||||||
p.error('--mine-stock requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least 360 seconds')
|
p.error('--mine-stock requires --smoke mounted-ammo --project chat --rounds 1 and timeout at least 360 seconds')
|
||||||
@@ -166,7 +166,7 @@ if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1']
|
|||||||
if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1']
|
if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1']
|
||||||
if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1']
|
if a.coordinated_blocked_only: command += ['-errCoordinatedBlockedOnly', '1']
|
||||||
if a.project == 'core':
|
if a.project == 'core':
|
||||||
if a.smoke not in {'coordinated-ai', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', 'starter-backpack', 'speech-download', 'bundled-speech', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'native-end-active', 'native-end-preparing', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
|
if a.smoke not in {'coordinated-ai', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'self-epinephrine', 'starter-backpack', 'speech-download', 'bundled-speech', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'sarka-hunters', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'roadblocks', 'native-end-active', 'native-end-preparing', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture')
|
||||||
command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
|
command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin'
|
||||||
command[command.index('-errSmoke')] = '-errBaseSmoke'
|
command[command.index('-errSmoke')] = '-errBaseSmoke'
|
||||||
if a.smoke in {'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
|
if a.smoke in {'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core')
|
||||||
|
|||||||
Reference in New Issue
Block a user