Configure anger points per Sarka instead of cars per point

This commit is contained in:
2026-10-17 18:58:35 +02:00
parent 82b52ae1fc
commit 372fca7a68
15 changed files with 125 additions and 59 deletions
@@ -3,12 +3,12 @@ class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaCarsPerAnger", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "emptyRestartHours", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "emptyRestartHours", "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 == "aiDoctrine" || key == "aiShadow") return hq;
if (key == "sarkaCarsPerAnger") return hq;
if (key == "sarkaAngerPerCar") return hq;
if (key == "aiIntelChancePercent") return intel;
if (key == "automaticEquipmentChancePercent" || key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "heliTimerMinutes" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
return Keys().Contains(key);
@@ -46,7 +46,7 @@ class ERR_AdminMenuFields
if (key == "aiDoctrine") prefix = "mission doctrine";
else if (key == "aiShadow") prefix = "mission aishadow";
else if (key == "automaticEquipmentChancePercent") prefix = "hq equipment";
else if (key == "sarkaCarsPerAnger") prefix = "mission sarkacars";
else if (key == "sarkaAngerPerCar") prefix = "mission sarkapoints";
else if (key == "motorbikeCount") prefix = "mission motorbikes";
else if (key == "planeReconEnabled") prefix = "mission plane";
else if (key == "planeAltitudeMetres") prefix = "mission planealtitude";
@@ -6,7 +6,7 @@ class ERR_CommandArguments
{
if (section == "mission" && option == "doctrine") return "aiDoctrine";
if (section == "mission" && option == "aishadow") return "aiShadow";
if (section == "mission" && option == "sarkacars") return "sarkaCarsPerAnger";
if (section == "mission" && option == "sarkapoints") return "sarkaAngerPerCar";
if (section == "mission" && option == "motorbikes") return "motorbikeCount";
if (section == "mission" && option == "planealtitude") return "planeAltitudeMetres";
if (section == "mission" && option == "planereplacement") return "planeReplacementSeconds";
@@ -171,7 +171,7 @@ class ERR_AdminService
if (key == "aiShadow") return "On (1) records coordinated proposals without issuing their orders; Off (0) uses the selected doctrine. Saved for next round.";
if (key == "startWeaponCount") return "0..12 pistols in the shared prison/crash supply backpack; default 5. Applies next round.";
if (key == "startMagazineCount") return "0..48 spare pistol magazines in the shared prison/crash supply backpack; default 8. Applies next round.";
if (key == "sarkaCarsPerAnger") return "0-5 cars per anger point; 0 disables new waves. Default 1; maximum 30 cars per wave, four special-forces crew per car. Applies to new waves; existing teams remain.";
if (key == "sarkaAngerPerCar") return "1-30 anger points per car; 0 disables new waves. Default 3. Whole thresholds only: 5 means one car at 5 anger, two at 10. Four special-forces crew per car. Applies to new waves; existing teams remain.";
if (key == "motorbikeCount") return "0-6; default 2; 0 disabled. Optional WCS Motorbikes, two response specialists per bike. Applies next round; losses are not replaced.";
if (key == "planeAltitudeMetres") return "150-500; default 250. Applies next round.";
if (key == "planeReplacementSeconds") return "30-1800; default 60. Applies next round.";
@@ -208,7 +208,7 @@ modded class ERR_AdminMenu
if (!text) return false;
string report = text.GetText();
array<string> parts = {}; report.Split("Sarka waves:", parts, false);
return report.Contains("HQ hunting patrols:") && parts.Count() == 2 && report.Contains("cars per anger point");
return report.Contains("HQ hunting patrols:") && parts.Count() == 2 && report.Contains("One car per");
}
bool ERR_ProbeJournalVisible()
{
@@ -1,20 +1,20 @@
#ifdef WORKBENCH
modded class ERR_AdminMenu
{
bool ERR_SarkaProbeReady() { return m_Ready && m_AvailableFields.Contains("sarkaCarsPerAnger"); }
bool ERR_SarkaProbeSet(int cars)
bool ERR_SarkaProbeReady() { return m_Ready && m_AvailableFields.Contains("sarkaAngerPerCar"); }
bool ERR_SarkaProbeSet(int points)
{
ShowPage(Navigation().Find("OpenSarka"));
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent("sarkaCarsPerAnger", GetRootWidget());
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("Apply_sarkaCarsPerAnger", GetRootWidget());
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent("sarkaAngerPerCar", GetRootWidget());
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("Apply_sarkaAngerPerCar", GetRootWidget());
if (!edit || !button || !button.IsEnabled()) return false;
edit.SetValue(cars.ToString()); OnButton(button); return true;
edit.SetValue(points.ToString()); OnButton(button); return true;
}
}
modded class ERR_HunterService
{
void ERR_ProbeSarkaDue(float now) { m_SarkaWaves.NextWave = now; }
bool ERR_ProbeSarkaDisabled() { return m_SarkaWaves.CarsPerAnger == 0 && m_SarkaWaves.Pending == 0; }
bool ERR_ProbeSarkaDisabled() { return m_SarkaWaves.AngerPerCar == 0 && m_SarkaWaves.Pending == 0; }
void ERR_ProbeSarkas(out array<ERR_HunterVehicle> cars)
{
cars.Clear();
@@ -25,11 +25,11 @@ modded class ERR_HQ
{
void ERR_ProbeSarkaBodies(float now)
{
for (int slot = 0; slot < 6; slot++)
for (int slot = 0; slot < 24; 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.CasualtySourceId = "fixture-sarka-bodies"; report.CasualtyStrength = 24; report.CasualtySlot = slot;
report.ObservedAt = now; report.Position = "5000 50 5000";
Accept(report, now);
}
@@ -66,7 +66,7 @@ modded class ERR_GameMode
float now = GetWorld().GetWorldTime();
if (m_ERRSarkaStage == 0)
{
if (!ERR_SarkaProbeCheck(Settings().Get("sarkaCarsPerAnger") == 1, "default one car per anger point")) return;
if (!ERR_SarkaProbeCheck(Settings().Get("sarkaAngerPerCar") == 3, "default three anger points per car")) return;
ERR_AdminMenu.Open(); m_ERRSarkaStage = 9; return;
}
if (m_ERRSarkaStage == 9)
@@ -100,19 +100,19 @@ modded class ERR_GameMode
{
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (!menu || !menu.ERR_SarkaProbeReady()) return;
if (!ERR_SarkaProbeCheck(menu.ERR_SarkaProbeSet(2), "real menu submits two cars per anger point")) return;
if (!ERR_SarkaProbeCheck(menu.ERR_SarkaProbeSet(2), "real menu submits two anger points per car")) return;
m_ERRSarkaStage = 11; return;
}
if (m_ERRSarkaStage == 11)
{
if (Settings().Get("sarkaCarsPerAnger") != 2) return;
if (Settings().Get("sarkaAngerPerCar") != 2) return;
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu(); if (!menu || !menu.ERR_SarkaProbeReady()) return;
System.MakeScreenshot("$logs:err-sarka-settings"); menu.Close();
ERR_Settings reloaded = new ERR_Settings();
if (!ERR_SarkaProbeCheck(reloaded.Get("sarkaCarsPerAnger") == 2, "menu owner RPC persists multiplier")) return;
if (!ERR_SarkaProbeCheck(reloaded.Get("sarkaAngerPerCar") == 2, "menu owner RPC persists anger threshold")) return;
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;
if (!ERR_SarkaProbeCheck(hq.Watch().Aggression() == 8, "24 delivered bodies produce eight anger points")) return;
// Accelerate only the opt-in wave deadline; production placement/admission run unchanged.
hq.Hunters().ERR_ProbeSarkaDue(now); m_ERRSarkaStage = 1;
}
@@ -127,16 +127,16 @@ modded class ERR_GameMode
}
if (m_ERRSarkaStage == 21)
{
if (Settings().Get("sarkaCarsPerAnger") != 0 || !hq.Hunters().ERR_ProbeSarkaDisabled()) return;
if (Settings().Get("sarkaAngerPerCar") != 0 || !hq.Hunters().ERR_ProbeSarkaDisabled()) return;
foreach (ERR_HunterVehicle retained : cars)
if (!ERR_SarkaProbeCheck(retained.Entity && retained.Members.Count() == 4, "disabling retains deployed cars and teams")) return;
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu(); if (menu) menu.Close();
GetGame().GetCallqueue().Remove(ERR_SarkaProbeTick);
Print("ERR SMOKE COMPLETE: sarka menu save, multiplier, four SF cars, radio, driving and disable"); return;
Print("ERR SMOKE COMPLETE: sarka menu save, anger threshold, four SF cars, radio, driving and disable"); return;
}
if (m_ERRSarkaStage == 1)
{
if (!ERR_SarkaProbeCheck(cars.Count() == 4, "two cars per anger point produce four physical cars")) return;
if (!ERR_SarkaProbeCheck(cars.Count() == 4, "eight anger at two points per car produces four physical cars")) return;
array<vector> positions = {player.GetOrigin()};
foreach (ERR_HunterVehicle car : cars)
{
@@ -9,13 +9,13 @@ class ERR_Settings
static array<string> Keys()
{
return {"aiDoctrine", "aiShadow", "sarkaCarsPerAnger", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticEquipmentChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
return {"aiDoctrine", "aiShadow", "sarkaAngerPerCar", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "planeReconEnabled", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "keepWeaponOnUnconscious", "selfEpinephrine", "restartOnEmpty", "emptyRestartHours", "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 == "aiDoctrine" || key == "aiShadow") return value == 0 || value == 1;
if (key == "sarkaCarsPerAnger") return value >= 0 && value <= 5;
if (key == "sarkaAngerPerCar") return value >= 0 && value <= 30;
if (key == "motorbikeCount") return value >= 0 && value <= 6;
if (key == "planeAltitudeMetres") return value >= 150 && value <= 500;
if (key == "planeReplacementSeconds") return value >= 30 && value <= 1800;
@@ -118,7 +118,7 @@ class ERR_Settings
static int AddedDefault(string key)
{
if (key == "aiDoctrine" || key == "aiShadow") return 0;
if (key == "sarkaCarsPerAnger") return 1;
if (key == "sarkaAngerPerCar") return 3;
if (key == "motorbikeCount") return 2;
if (key == "planeAltitudeMetres") return 250;
if (key == "planeReplacementSeconds") return 60;
@@ -154,7 +154,7 @@ class ERR_Settings
m_Path = path;
m_Values.Set("aiDoctrine", AddedDefault("aiDoctrine"));
m_Values.Set("aiShadow", AddedDefault("aiShadow"));
m_Values.Set("sarkaCarsPerAnger", AddedDefault("sarkaCarsPerAnger"));
m_Values.Set("sarkaAngerPerCar", AddedDefault("sarkaAngerPerCar"));
m_Values.Set("motorbikeCount", AddedDefault("motorbikeCount"));
m_Values.Set("planeAltitudeMetres", AddedDefault("planeAltitudeMetres"));
m_Values.Set("planeReplacementSeconds", AddedDefault("planeReplacementSeconds"));
@@ -214,6 +214,19 @@ class ERR_Settings
values = new map<string, int>();
foreach (string key : Keys())
{
// The former multiplier cannot express a lower frequency. Keep disabled
// saves disabled; migrate enabled saves to the new three-points-per-car default.
// An explicit new setting always takes precedence over the legacy key.
if (key == "sarkaAngerPerCar" && !json.DoesKeyExist(key) && json.DoesKeyExist("sarkaCarsPerAnger"))
{
float oldCars;
if (!json.ReadValue("sarkaCarsPerAnger", oldCars)) return false;
int oldMultiplier = Math.Round(oldCars);
if (oldCars != oldMultiplier || oldMultiplier < 0 || oldMultiplier > 5) return false;
int points = AddedDefault(key);
if (oldMultiplier == 0) points = 0;
values.Set(key, points); continue;
}
// Migrate the former one-in-N setting to the nearest integer percent.
if (key == "antiAirChancePercent" && !json.DoesKeyExist(key) && json.DoesKeyExist("stingerChanceOneIn"))
{
@@ -8,22 +8,23 @@ class ERR_SarkaWaves
int Target;
int Wave;
int Spawned;
int CarsPerAnger = 1;
void Configure(int cars)
int AngerPerCar = 3;
void Configure(int points)
{
if (CarsPerAnger == cars) return;
CarsPerAnger = cars;
if (cars == 0) Pause();
if (AngerPerCar == points) return;
AngerPerCar = points;
if (points == 0) Pause();
}
void Pause() { NextWave = -1; Pending = 0; Target = 0; }
bool Due(float now, int aggression, bool playing)
{
if (!playing || CarsPerAnger == 0) { Pause(); return false; }
if (!playing || AngerPerCar == 0) { Pause(); return false; }
if (NextWave < 0) { NextWave = now + 600000; return false; }
if (now >= NextWave)
{
NextWave = now + 600000; NextAttempt = now;
Target = Math.Clamp(Math.Clamp(aggression, 0, 30) * CarsPerAnger, 0, 30); Pending = Target; Wave++;
// Whole thresholds only: five points per car means zero at four, one at five.
Target = Math.Clamp(aggression, 0, 30) / AngerPerCar; Pending = Target; Wave++;
}
if (Pending == 0 || now < NextAttempt) return false;
NextAttempt = now + 5000;
@@ -141,15 +141,17 @@ class ERR_HunterService : ERR_ConvoyService
protected string m_SarkaStatus = "waiting for first wave after prison escape";
void ConfigureSarka()
{
int cars = m_Mode.Settings().Get("sarkaCarsPerAnger");
bool changed = cars != m_SarkaWaves.CarsPerAnger;
m_SarkaWaves.Configure(cars);
if (changed) m_SarkaStatus = "saved car count applies to the next wave";
if (cars == 0) m_SarkaStatus = "new waves disabled; existing teams remain";
int points = m_Mode.Settings().Get("sarkaAngerPerCar");
bool changed = points != m_SarkaWaves.AngerPerCar;
m_SarkaWaves.Configure(points);
if (changed) m_SarkaStatus = "saved anger threshold applies to the next wave";
if (points == 0) m_SarkaStatus = "new waves disabled; existing teams remain";
}
string SarkaStatus()
{
return string.Format("Sarka waves: %1 deployed, %2/%3 pending in wave %4; %5. %6 cars per anger point (30 per wave maximum).", m_SarkaWaves.Spawned, m_SarkaWaves.Pending, m_SarkaWaves.Target, m_SarkaWaves.Wave, m_SarkaStatus, m_SarkaWaves.CarsPerAnger);
string ratio = "New waves disabled.";
if (m_SarkaWaves.AngerPerCar > 0) ratio = string.Format("One car per %1 anger points (rounded down).", m_SarkaWaves.AngerPerCar);
return string.Format("Sarka waves: %1 deployed, %2/%3 pending in wave %4; %5. %6", m_SarkaWaves.Spawned, m_SarkaWaves.Pending, m_SarkaWaves.Target, m_SarkaWaves.Wave, m_SarkaStatus, ratio);
}
// HQ maintenance runs even while tactical ticks pause in an empty round.
void PauseSarkaIfEmpty()