Files
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

461 lines
31 KiB
Diff

--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c
@@ -8,12 +8,12 @@
{
static array<string> Keys()
{
- return {"roadblockCount", "civilianTrafficCount", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
+ return {"hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
}
static bool Available(string key, bool hq, bool intel)
{
if (key == "aiIntelChancePercent") return intel;
- if (key == "heliTimerMinutes" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
+ if (key == "hunterSpawnDistanceMetres" || key == "heliTimerMinutes" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
return Keys().Contains(key);
}
// A short journal has no older off-screen lines, even if its slider is zero.
@@ -44,7 +44,8 @@
static string Command(string key, string value)
{
string prefix;
- if (key == "roadblockCount") prefix = "mission roadblocks";
+ if (key == "hunterSpawnDistanceMetres") prefix = "mission hunterdistance";
+ else if (key == "roadblockCount") prefix = "mission roadblocks";
else if (key == "civilianTrafficCount") prefix = "mission traffic";
else if (key == "startMode") prefix = "mission start";
else if (key == "radioDefenceSeconds") prefix = "mission radio";
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c
@@ -4,6 +4,7 @@
{
static string CoreSetting(string section, string option)
{
+ if (section == "mission" && option == "hunterdistance") return "hunterSpawnDistanceMetres";
if (section == "mission" && option == "roadblocks") return "roadblockCount";
if (section == "mission" && option == "traffic") return "civilianTrafficCount";
if (section == "mission" && option == "start") return "startMode";
@@ -144,6 +145,7 @@
}
protected string SettingHelp(string key)
{
+ if (key == "hunterSpawnDistanceMetres") return "Set 800..3000 metres (default 1000). Applies next round to the opening UAZs and troop truck, on separate sides. Roads allow 250m variation; unsuitable terrain uses safe objective-road fallback.";
if (key == "roadblockCount") return "Set 0..12 roadblocks (default 4; 0 disables). Applies next round. Each has four guards; unsuitable sites are skipped.";
if (key == "civilianTrafficCount") return "Set 0..6 civilian cars (default 3). Zero disables new traffic. Unclaimed cars retire only far from all players and out of sight; player-entered cars are preserved.";
if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout.";
@@ -155,7 +157,7 @@
}
protected string RoadblockTiming(string key)
{
- if (key == "roadblockCount") return " for next round";
+ if (key == "hunterSpawnDistanceMetres" || key == "roadblockCount") return " for next round";
return "";
}
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c
@@ -9,11 +9,12 @@
static array<string> Keys()
{
- return {"roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
+ return {"hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
}
static bool Valid(string key, int value)
{
+ if (key == "hunterSpawnDistanceMetres") return value >= 800 && value <= 3000;
if (key == "roadblockCount") return value >= 0 && value <= 12;
if (key == "civilianTrafficCount") return value >= 0 && value <= 6;
if (key == "startMode") return value == 0 || value == 1;
@@ -101,6 +102,7 @@
static int AddedDefault(string key)
{
+ if (key == "hunterSpawnDistanceMetres") return 1000;
if (key == "roadblockCount") return 4;
if (key == "civilianTrafficCount") return 3;
if (key == "startMode") return 0;
@@ -120,6 +122,7 @@
void ERR_Settings(string path = PATH)
{
m_Path = path;
+ m_Values.Set("hunterSpawnDistanceMetres", AddedDefault("hunterSpawnDistanceMetres"));
m_Values.Set("roadblockCount", AddedDefault("roadblockCount"));
m_Values.Set("civilianTrafficCount", AddedDefault("civilianTrafficCount"));
m_Values.Set("startMode", AddedDefault("startMode"));
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
@@ -156,6 +156,7 @@
m_Mode = mode;
m_Equipment = new ERR_EnemyEquipment(mode);
m_Hunters = new ERR_HunterService(mode, null);
+ m_Hunters.ConfigureOpening(mode.Settings().Get("hunterSpawnDistanceMetres"));
m_ObjectiveDefence = new ERR_ObjectiveDefence(mode, this);
m_Towns = new ERR_TownGarrisons(mode);
m_Roadblocks = new ERR_Roadblocks(mode);
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterOpeningPlan.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterOpeningPlan.c
@@ -1,6 +1,18 @@
// Deployment geometry only. Pursuit destinations always come from delivered HQ reports.
class ERR_HunterOpeningRules
{
+ static bool InBand(vector center, vector point, float radius)
+ {
+ if (!ERR_ForceRules.FiniteVector(center) || !ERR_ForceRules.FiniteVector(point) || !ERR_ForceRules.Finite(radius) || radius < 800 || radius > 3000) return false;
+ float distance = vector.DistanceXZ(center, point);
+ return distance >= Math.Max(600, radius - 250) && distance <= radius + 250;
+ }
+ static float ScanRadius(float radius, int ring)
+ {
+ if (ring == 1) return radius - 150;
+ if (ring == 2) return radius + 150;
+ return radius;
+ }
static bool Spread(vector center, vector first, vector second)
{
vector a = first - center; a[1] = 0;
@@ -30,6 +42,7 @@
class ERR_HunterOpeningPlan
{
vector Center;
+ float Radius;
ref array<vector> Positions = {};
ref array<vector> Angles = {};
protected ref array<vector> m_Candidates = {};
@@ -37,7 +50,7 @@
protected int m_Scanned;
protected bool m_Done;
protected float m_Heading;
- void ERR_HunterOpeningPlan(vector center) { Center = center; m_Heading = Math.RandomFloat(0, 360); }
+ void ERR_HunterOpeningPlan(vector center, float radius = 1000) { Center = center; Radius = radius; m_Heading = Math.RandomFloat(0, 360); }
bool Ready() { return Positions.Count() == 3; }
bool Done() { return m_Done; }
static bool AwayFromPlayers(vector position, array<IEntity> players)
@@ -55,12 +68,12 @@
int candidate = m_Scanned++;
int ring = candidate / 16;
int sector = candidate % 16;
- float radius = 800 + ring * 450;
- float bearing = (m_Heading + sector * 22.5) * Math.DEG2RAD;
+ float radius = ERR_HunterOpeningRules.ScanRadius(Radius, ring);
+ float bearing = (m_Heading + sector * 22.5 + ring * 5.625) * Math.DEG2RAD;
vector seed = Center + Vector(Math.Sin(bearing) * radius, 0, Math.Cos(bearing) * radius);
vector position, angles;
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles, 300)) continue;
- if (vector.DistanceXZ(Center, position) < 600 || !AwayFromPlayers(position, players)) continue;
+ if (!ERR_HunterOpeningRules.InBand(Center, position, Radius) || !AwayFromPlayers(position, players)) continue;
if (!ERR_SupplyConvoyRules.Separated(position, m_Candidates, 100) || !placement.Clear(position, angles[0])) continue;
m_Candidates.Insert(position); m_Angles.Insert(angles);
}
@@ -69,12 +82,12 @@
{
foreach (int index : selected) { Positions.Insert(m_Candidates[index]); Angles.Insert(m_Angles[index]); }
m_Done = true;
- ERR_HQ.Event("HUNTER OPENING", "three clear road starts reserved at least 90 degrees apart; center=" + Center.ToString());
+ ERR_HQ.Event("HUNTER OPENING", string.Format("two UAZs and one troop truck: target radius=%1m (+/-250m), three clear road starts at least 90 degrees apart; center=%2", Radius, Center));
}
else if (m_Scanned == 64)
{
m_Done = true;
- ERR_HQ.Event("HUNTER OPENING FALLBACK", "no three safe, widely separated road starts in 64 candidates; deployment will use checked roads near friendly objectives");
+ ERR_HQ.Event("HUNTER OPENING FALLBACK", string.Format("no three safe, widely separated road starts near %1m in 64 candidates; deployment will use checked roads near friendly objectives, possibly farther away", Radius));
}
}
}
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c
@@ -19,6 +19,8 @@
class ERR_HunterVehicle : ERR_ConvoyVehicle
{
bool TroopTransport;
+ bool OpeningPatrol;
+ vector PatrolHome;
bool RadioRequested;
ref ERR_EnemyEquipmentCallback RadioOperation;
string Source;
@@ -80,6 +82,12 @@
protected float m_NextSpawn;
protected float m_NextSummary;
protected int m_Target = 3;
+ protected int m_OpeningDistance = 1000;
+ // Called once when the round's HQ is created; menu edits affect the next HQ.
+ void ConfigureOpening(int distance)
+ {
+ if (!m_Opening && m_Hunters.IsEmpty() && ERR_Settings.Valid("hunterSpawnDistanceMetres", distance)) m_OpeningDistance = distance;
+ }
protected ref ERR_HunterOpeningPlan m_Opening;
protected ref ERR_HunterCoordination m_Coordination;
bool Contains(SCR_AIGroup group)
@@ -238,6 +246,7 @@
}
ERR_HunterVehicle unit = new ERR_HunterVehicle();
unit.TroopTransport = ERR_HunterFleetRules.Transport(m_Hunters.Count());
+ unit.OpeningPatrol = m_Hunters.Count() < 3; unit.PatrolHome = position;
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
if (unit.TroopTransport) unit.Source = "aggression-truck-" + (m_Hunters.Count() + 1).ToString();
ResourceName vehiclePrefab = ERR_HunterFleetRules.VehiclePrefab(m_Hunters.Count());
@@ -282,6 +291,19 @@
return true;
}
protected bool Coordinated(vector position) { return m_Coordination && m_Coordination.Covered(position); }
+ // Before a delivered sighting, opening crews patrol their own deployment
+ // sector. This uses their fixed starting position, never a live player position.
+ protected bool OpeningPatrolPoint(ERR_HunterVehicle unit, out vector destination, out vector angles)
+ {
+ for (int attempt = 0; attempt < 8; attempt++)
+ {
+ vector seed = unit.PatrolHome + Vector(Math.RandomFloat(-300, 300), 0, Math.RandomFloat(-300, 300));
+ if (!ERR_SupplyConvoySystem.ApproachDirected(seed, destination, angles, 150)) continue;
+ if (vector.DistanceXZ(destination, unit.PatrolHome) > 500 || vector.DistanceXZ(destination, unit.Entity.GetOrigin()) < 100) continue;
+ return true;
+ }
+ return false; // Hold/retry rather than sending an opening crew across the island.
+ }
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
{
array<ref ERR_HQReport> reports = {}; hq.HunterContacts(reports, now);
@@ -320,11 +342,19 @@
unit.SourceMessage = 0;
if (unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group)) return;
bool found;
+ if (unit.OpeningPatrol && !unit.DeliveredContact)
+ {
+ found = OpeningPatrolPoint(unit, destination, angles);
+ reason = "local patrol around opening sector";
+ }
+ else
+ {
for (int i = 0; i < m_Mode.Sites().Count(); i++)
{
ERR_SiteRuntime site = m_Mode.Sites().At(Math.RandomInt(0, m_Mode.Sites().Count()));
if (!site || !site.Definition || !ERR_ObjectiveDefence.Objective(site.Definition.Kind) || (site.Group && site.Group.GetFaction() != m_Mode.EnemyFaction())) continue;
if (ERR_SupplyConvoySystem.ApproachDirected(site.Definition.Position, destination, angles) && vector.DistanceXZ(destination, unit.Entity.GetOrigin()) > 200) { found = true; break; }
+ }
}
if (!found) return;
}
@@ -371,7 +401,7 @@
vector center = m_Mode.PrisonPosition(); vector sum; int count;
foreach (IEntity player : openingPlayers) if (player) { sum += player.GetOrigin(); count++; }
if (count > 0) center = sum / count;
- m_Opening = new ERR_HunterOpeningPlan(center);
+ m_Opening = new ERR_HunterOpeningPlan(center, m_OpeningDistance);
}
if (!m_Opening.Done())
{
--- a/addons/core/UI/layouts/ERR/AdminMenu.layout
+++ b/addons/core/UI/layouts/ERR/AdminMenu.layout
@@ -385,47 +385,52 @@
}
}
TextWidgetClass {
- Name "Label_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 384 OffsetTop 384 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -412 }
+ Name "Label_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 384 OffsetTop 384 SizeX 444 OffsetRight -472 SizeY 24 OffsetBottom -408 }
Color 0.9 0.92 0.94 1 Text "Anti-personnel mine carriers (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
}
}
TextWidgetClass {
- Name "Saved_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 416 OffsetTop 416 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -440 }
- Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
- Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
- }
- }
-TextWidgetClass {
- Name "Help_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 440 OffsetTop 440 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -460 }
- Color 0.72 0.81 0.87 1 Text "New soldiers only: up to two carried AP mines. Mines are not automatically planted." "Font Size" 16 "Min Font Size" 16 Wrap 1
- FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
-}
-
-ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
- Name "antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 384 OffsetTop 384 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -426 }
- components {
- SCR_EditBoxComponent "{547290FFBD5B33E9}" {
- m_sLabel ""
- }
- }
- }
-ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
- Name "Apply_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 384 OffsetTop 384 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -426 }
- components {
- SCR_ButtonTextComponent "{5548F0143AE99095}" {
- m_sText "Apply"
- }
- }
- }
-ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
- Name "Random_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 384 OffsetTop 384 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -426 }
- components {
- SCR_ButtonTextComponent "{5548F0143AE99095}" {
- m_sText "Random"
- }
- }
- }
+ Name "Saved_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 452 OffsetTop 452 SizeX 444 OffsetRight -472 SizeY 20 OffsetBottom -472 }
+ Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
+ Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
+ }
+ }
+TextWidgetClass {
+ Name "Help_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 472 OffsetTop 472 SizeX 444 OffsetRight -472 SizeY 14 OffsetBottom -486 }
+ Color 0.72 0.81 0.87 1 Text "New soldiers only; carried mines are not planted automatically." "Font Size" 12 "Min Font Size" 12 Wrap 1
+ FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
+}
+
+ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
+ Name "antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 412 OffsetTop 412 SizeX 130 OffsetRight -158 SizeY 38 OffsetBottom -450 }
+ components {
+ SCR_EditBoxComponent "{547290FFBD5B33E9}" {
+ m_sLabel ""
+ }
+ }
+ }
+ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
+ Name "Apply_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 170 OffsetLeft 170 PositionY 412 OffsetTop 412 SizeX 110 OffsetRight -280 SizeY 38 OffsetBottom -450 }
+ components {
+ SCR_ButtonTextComponent "{5548F0143AE99095}" {
+ m_sText "Apply"
+ }
+ }
+ }
+ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
+ Name "Random_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 292 OffsetLeft 292 PositionY 412 OffsetTop 412 SizeX 180 OffsetRight -472 SizeY 38 OffsetBottom -450 }
+ components {
+ SCR_ButtonTextComponent "{5548F0143AE99095}" {
+ m_sText "Random"
+ }
+ }
+ }
+TextWidgetClass { Name "Label_hunterSpawnDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 384 OffsetTop 384 SizeX 456 OffsetRight -964 SizeY 24 OffsetBottom -408 } Color 0.9 0.92 0.94 1 Text "Opening patrol distance (800-3000 m)" "Font Size" 20 "Min Font Size" 20 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
+ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "hunterSpawnDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 412 OffsetTop 412 SizeX 130 OffsetRight -638 SizeY 38 OffsetBottom -450 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } }
+ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_hunterSpawnDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 650 OffsetLeft 650 PositionY 412 OffsetTop 412 SizeX 110 OffsetRight -760 SizeY 38 OffsetBottom -450 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } }
+TextWidgetClass { Name "Saved_hunterSpawnDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 452 OffsetTop 452 SizeX 456 OffsetRight -964 SizeY 20 OffsetBottom -472 } Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
+TextWidgetClass { Name "Help_hunterSpawnDistanceMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 508 OffsetLeft 508 PositionY 472 OffsetTop 472 SizeX 456 OffsetRight -964 SizeY 14 OffsetBottom -486 } Color 0.9 0.92 0.94 1 Text "UAZs + truck; next round. Safe terrain fallback may be farther." "Font Size" 12 "Min Font Size" 12 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
} }
FrameWidgetClass { Name "WorldPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
TextWidgetClass {
--- a/tests/test_admin_menu.py
+++ b/tests/test_admin_menu.py
@@ -10,7 +10,7 @@
layout = BASE / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
-assert len(keys) == 18 and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "automaticEquipmentChancePercent"}
+assert len(keys) == 19 and set(settings) - set(keys) == {"automaticConvoyEnabled", "keepWeaponOnUnconscious", "automaticEquipmentChancePercent"}
assert set(keys) <= set(settings) # Every menu field must have a server setting.
# Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load.
@@ -48,7 +48,7 @@
assert len(names) == len(set(names)), 'Menu widget names must be unique'
for key in keys:
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Help_' + key))
- if key not in ('startMode','civilianTrafficCount','roadblockCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
+ if key not in ('startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names
else: assert 'Random_startMode' not in names
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
@@ -99,7 +99,7 @@
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
- {"roadblockCount","mission roadblocks"},{"civilianTrafficCount","mission traffic"},{"heliTimerMinutes","hq heli timer"},{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
+ {"hunterSpawnDistanceMetres","mission hunterdistance"},{"roadblockCount","mission roadblocks"},{"civilianTrafficCount","mission traffic"},{"heliTimerMinutes","hq heli timer"},{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
for(auto pair:cases){
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
assert(Fields::Command(pair.first,"Random")==pair.second+" set Random");
@@ -122,7 +122,7 @@
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
}
''')
-print('PASS: actual menu command routing covers all eighteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
+print('PASS: actual menu command routing covers all nineteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
assert all(name in names for name in ('KeepWeaponOn', 'KeepWeaponOff', 'Saved_keepWeaponOnUnconscious'))
click=body(source,'protected void OnButton(')
--- a/tests/test_hunter_opening.py
+++ b/tests/test_hunter_opening.py
@@ -2,7 +2,8 @@
import re
from policy_harness import ROOT, body, run
rules_path=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterOpeningPlan.c'
-rules='\n'.join(body(rules_path,x) for x in ('static bool Spread(', 'static bool FreshOrder(', 'static bool Select('))
+rules='\n'.join(body(rules_path,x) for x in ('static bool InBand(', 'static float ScanRadius(', 'static bool Spread(', 'static bool FreshOrder(', 'static bool Select('))
+rules=rules.replace('ERR_ForceRules.FiniteVector', 'FiniteVector').replace('ERR_ForceRules.Finite', 'std::isfinite').replace('Math.Max', 'std::max<float>')
rules=re.sub(r'\bvector (?=\w)', 'Vec ', rules).replace('vector.', 'Geometry::').replace('array<vector> candidates','Array<Vec> candidates').replace('array<int> selected','Array<int>& selected')
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
queue=body(p,'protected void QueueDestination(').replace('protected ','').replace('foreach (','for (')
@@ -28,6 +29,7 @@
using string=std::string;
template<class... T>string Format(string,T...){return "investigate";}void Event(string,string,int){}
struct Vec{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}Vec operator-(Vec b){return {x-b.x,y-b.y,z-b.z};}string ToString(){return "position";}};
+bool FiniteVector(Vec v){return std::isfinite(v.x)&&std::isfinite(v.y)&&std::isfinite(v.z);}
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}float Dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
struct ERR_HunterOpeningRules{
@@ -37,7 +39,7 @@
struct Entity{Vec GetOrigin(){return {};}};
struct Waypoint{bool complete=false;bool IsWithinCompletionRadius(Group*){return complete;}};
struct Route{};
-struct ERR_HunterVehicle{float NextOrder=99999,LastContact=-1,NextContactOrder=0;Route* Downlink=nullptr;int SourceMessage=0;Vec Destination,ContactPosition,DeliveredContactPosition;bool DeliveredContact=false;string Source="hunter";struct Group* Group=nullptr;struct Entity* Entity=nullptr;struct Waypoint* Waypoint=nullptr;};
+struct ERR_HunterVehicle{bool OpeningPatrol=false,TroopTransport=false;Vec PatrolHome;float NextOrder=99999,LastContact=-1,NextContactOrder=0;Route* Downlink=nullptr;int SourceMessage=0;Vec Destination,ContactPosition,DeliveredContactPosition;bool DeliveredContact=false;string Source="hunter";struct Group* Group=nullptr;struct Entity* Entity=nullptr;struct Waypoint* Waypoint=nullptr;};
struct ERR_HQReport{int Target=1;};
struct ERR_HQBelief{bool actionable=true;float ObservedAt=10,Confidence=1,Radius=50;int MessageId=7;Vec PredictedPosition={1000,0,2000};bool Actionable(){return actionable;}};
struct Memory{ERR_HQBelief belief;ERR_HQBelief* Get(int){return &belief;}};
@@ -48,12 +50,23 @@
namespace Math{int RandomInt(int,int){return 0;}}
namespace ERR_ObjectiveDefence{bool Objective(int){return true;}}
namespace ERR_SupplyConvoySystem{bool available=true;int calls=0;bool ApproachDirected(Vec p,Vec& dst,Vec& angles,float radius=500){calls++;dst=p;return available;}}
-struct Service{bool coordinated=false;bool Coordinated(Vec){return coordinated;}Mode mode;Mode* m_Mode=&mode;
+struct Service{int localCalls=0;bool localRoad=true;bool OpeningPatrolPoint(ERR_HunterVehicle*,Vec& point,Vec&){localCalls++;point={800,0,0};return localRoad;}bool coordinated=false;bool Coordinated(Vec){return coordinated;}Mode mode;Mode* m_Mode=&mode;
'''+queue+r'''
};
Vec polar(float degrees,float radius=1000){return {std::sin(degrees*3.14159265f/180)*radius,0,std::cos(degrees*3.14159265f/180)*radius};}
int main(){
Array<int> chosen;Vec center;
+ assert(ERR_HunterOpeningRules::InBand(center,polar(0,750),1000));
+ assert(ERR_HunterOpeningRules::InBand(center,polar(0,1250),1000));
+ assert(!ERR_HunterOpeningRules::InBand(center,polar(0,749),1000));
+ assert(!ERR_HunterOpeningRules::InBand(center,polar(0,1251),1000));
+ assert(!ERR_HunterOpeningRules::InBand(center,polar(0,599),800));
+ assert(ERR_HunterOpeningRules::InBand(center,polar(0,600),800));
+ assert(!ERR_HunterOpeningRules::InBand(center,polar(0,1000),0));
+ assert(ERR_HunterOpeningRules::ScanRadius(1000,0)==1000);
+ assert(ERR_HunterOpeningRules::ScanRadius(1000,1)==850);
+ assert(ERR_HunterOpeningRules::ScanRadius(1000,2)==1150);
+ assert(ERR_HunterOpeningRules::ScanRadius(1000,3)==1000);
assert(ERR_HunterOpeningRules::Select(center,{polar(0),polar(120),polar(240)},chosen)&&chosen.Count()==3);
assert(!ERR_HunterOpeningRules::Select(center,{polar(0),polar(5),polar(10)},chosen)&&chosen.empty());
assert(!ERR_HunterOpeningRules::Spread(center,polar(359),polar(1)));
@@ -63,6 +76,15 @@
assert(!ERR_HunterOpeningRules::FreshOrder(10,10,100,0,false));
assert(!ERR_HunterOpeningRules::FreshOrder(11,10,100,101,false));
assert(!ERR_HunterOpeningRules::FreshOrder(11,10,100,0,true));
+ // Both opening vehicle types use a local patrol until a delivered contact.
+ for(bool transport:{false,true}){
+ Service opening;ERR_HQ command;ERR_HunterVehicle crew;crew.OpeningPatrol=true;crew.TroopTransport=transport;crew.NextOrder=0;
+ opening.QueueDestination(&crew,&command,100);assert(opening.localCalls==1&&command.requests==1&&crew.SourceMessage==0&&crew.Destination.x==800);
+ crew.Downlink=nullptr;crew.NextOrder=0;opening.localRoad=false;
+ opening.QueueDestination(&crew,&command,200);assert(command.requests==1); // unsafe/absent local road holds, no distant fallback
+ crew.NextOrder=0;command.contact=true;
+ opening.QueueDestination(&crew,&command,300);assert(command.requests==2&&crew.SourceMessage==7&&crew.Destination.x==1000);
+ }
Service service;ERR_HQ hq;ERR_HunterVehicle unit;Route patrol;unit.Downlink=&patrol;
service.QueueDestination(&unit,&hq,100);assert(unit.Downlink==&patrol&&hq.requests==0); // no magical position
hq.contact=true;service.QueueDestination(&unit,&hq,100);
--- a/tests/test_start_settings.py
+++ b/tests/test_start_settings.py
@@ -56,6 +56,12 @@
disk["old"]={{"reportIntervalSeconds",10},{"supportSquadLimit",4},{"responseRadiusMetres",2000},{"aiIntelChancePercent",60},{"intelRevealChancePercent",15},{"radioDefenceSeconds",180}};
ERR_Settings settings;assert(settings.Read("old",settings.m_Values)&&settings.Get("startMode")==0);
for(float bad:{-1.f,-2.f,2.f,.5f}){disk["bad"]=disk["old"];disk["bad"]["startMode"]=bad;Values ignored;assert(!settings.Read("bad",ignored));}
+ assert(settings.Get("hunterSpawnDistanceMetres")==1000);
+ assert(!ERR_Settings::Randomizable("hunterSpawnDistanceMetres"));
+ assert(!settings.Set("hunterSpawnDistanceMetres",799)&&!settings.Set("hunterSpawnDistanceMetres",3001));
+ assert(settings.Set("hunterSpawnDistanceMetres",1800));
+ ERR_Settings savedRadius;assert(savedRadius.Read("settings",savedRadius.m_Values)&&savedRadius.Get("hunterSpawnDistanceMetres")==1800);
+ failSave=true;assert(!settings.Set("hunterSpawnDistanceMetres",1000)&&settings.Get("hunterSpawnDistanceMetres")==1800);failSave=false;
Mode mode{&settings,0};Admin admin;Array<string> replies;
assert(!admin.StartCommand(&mode,{"mission","radio","show"},replies));
assert(admin.StartCommand(&mode,{"mission","start","set","crash"},replies));