From 0e71ef1fa78a86345474912f200e94322db8e297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1=C4=8Dek?= Date: Sat, 17 Oct 2026 17:42:41 +0200 Subject: [PATCH] Make Sarka special-forces cars per anger point configurable --- CHANGELOG.md | 7 +++ .../Scripts/Game/ERR/00_Core/ERR_AdminMenu.c | 11 ++-- .../Game/ERR/00_Core/ERR_AdminService.c | 2 + .../Game/ERR/00_Core/ERR_NativeSarkaProbe.c | 53 +++++++++++++++++-- .../Scripts/Game/ERR/00_Core/ERR_Settings.c | 5 +- addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c | 1 + .../Game/ERR/10_AI/ERR_HunterSarkaRules.c | 11 +++- .../Game/ERR/10_AI/ERR_HunterService.c | 17 ++++-- addons/core/UI/layouts/ERR/AdminMenu.layout | 15 +++++- tests/test_admin_menu.py | 2 +- tests/test_admin_setting_policy.py | 3 ++ tests/test_sarka_hunters.py | 15 +++++- 12 files changed, 124 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9a6c5..e60b3a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +Latest published Workshop version: **0.0.110** + +## 2026-10-17 — Configurable Šárka hunters + +- Added a mission-menu setting for the number of Šárka special-forces cars per anger point. +- Set cars per anger point to zero to disable new waves. Deployed teams remain. + ## 2026-10-17 — Discord changelog test - Test notification for automatic mission changelogs. diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c index e463250..c1a3409 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c @@ -8,11 +8,12 @@ class ERR_AdminMenuFields { static array Keys() { - return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "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", "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"}; } static bool Available(string key, bool hq, bool intel) { if (key == "aiDoctrine" || key == "aiShadow") return hq; + if (key == "sarkaCarsPerAnger") 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); @@ -50,6 +51,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 == "motorbikeCount") prefix = "mission motorbikes"; else if (key == "planeReconEnabled") prefix = "mission plane"; else if (key == "planeAltitudeMetres") prefix = "mission planealtitude"; @@ -135,6 +137,7 @@ class ERR_AdminMenu : ChimeraMenuBase names.Insert("RandomDay_startTimeMinutes"); names.Insert("Default_viewDistanceMetres"); names.Insert("OpenForces"); + names.Insert("OpenSarka"); names.Insert("SarkaBack"); names.Insert("RestartEmptyOn"); names.Insert("RestartEmptyOff"); names.Insert("OpenGuards"); names.Insert("GuardsBack"); names.Insert("OpenSites"); names.Insert("SitesBack"); @@ -236,8 +239,8 @@ class ERR_AdminMenu : ChimeraMenuBase if (widget) widget.SetText(value); } // Stable page order is also used by the shared navigation bar and native probe. - static array Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage"}; } - static array Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter"}; } + static array Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage", "StarterPage", "SarkaPage"}; } + static array Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI", "OpenStarter", "OpenSarka"}; } protected void ShowPage(int page) { array pages = Pages(); @@ -438,7 +441,7 @@ class ERR_AdminMenu : ChimeraMenuBase if (name == "RecommendedWeapons") { ShowPage(9); return; } if (name == "PlaneBack") { ShowPage(1); return; } if (name == "AIBack") { ShowPage(1); return; } - if (name == "GuardsBack" || name == "SitesBack") { ShowPage(11); return; } + if (name == "GuardsBack" || name == "SitesBack" || name == "SarkaBack") { ShowPage(11); return; } int page = Navigation().Find(name); if (page >= 0) { ShowPage(page); return; } if (name == "JournalDetails") diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c index 2766f50..616c6b0 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c @@ -6,6 +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 == "motorbikes") return "motorbikeCount"; if (section == "mission" && option == "planealtitude") return "planeAltitudeMetres"; if (section == "mission" && option == "planereplacement") return "planeReplacementSeconds"; @@ -170,6 +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 == "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."; diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSarkaProbe.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSarkaProbe.c index d91c8b7..3a4d6d2 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSarkaProbe.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeSarkaProbe.c @@ -1,7 +1,20 @@ #ifdef WORKBENCH +modded class ERR_AdminMenu +{ + bool ERR_SarkaProbeReady() { return m_Ready && m_AvailableFields.Contains("sarkaCarsPerAnger"); } + bool ERR_SarkaProbeSet(int cars) + { + ShowPage(Navigation().Find("OpenSarka")); + SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent("sarkaCarsPerAnger", GetRootWidget()); + SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("Apply_sarkaCarsPerAnger", GetRootWidget()); + if (!edit || !button || !button.IsEnabled()) return false; + edit.SetValue(cars.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; } void ERR_ProbeSarkas(out array cars) { cars.Clear(); @@ -52,6 +65,23 @@ 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; + ERR_AdminMenu.Open(); m_ERRSarkaStage = 10; return; + } + if (m_ERRSarkaStage == 10) + { + 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; + m_ERRSarkaStage = 11; return; + } + if (m_ERRSarkaStage == 11) + { + if (Settings().Get("sarkaCarsPerAnger") != 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(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; @@ -59,10 +89,26 @@ modded class ERR_GameMode hq.Hunters().ERR_ProbeSarkaDue(now); m_ERRSarkaStage = 1; } array cars = {}; hq.Hunters().ERR_ProbeSarkas(cars); - if (cars.Count() < 2) return; + if (cars.Count() < 4) return; + if (m_ERRSarkaStage == 20) + { + ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu(); + if (!menu || !menu.ERR_SarkaProbeReady()) return; + if (!ERR_SarkaProbeCheck(menu.ERR_SarkaProbeSet(0), "real menu submits disable")) return; + m_ERRSarkaStage = 21; return; + } + if (m_ERRSarkaStage == 21) + { + if (Settings().Get("sarkaCarsPerAnger") != 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; + } if (m_ERRSarkaStage == 1) { - if (!ERR_SarkaProbeCheck(cars.Count() == 2, "two physical cars for two anger points")) return; + if (!ERR_SarkaProbeCheck(cars.Count() == 4, "two cars per anger point produce four physical cars")) return; array positions = {player.GetOrigin()}; foreach (ERR_HunterVehicle car : cars) { @@ -87,8 +133,7 @@ modded class ERR_GameMode 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"); + ERR_AdminMenu.Open(); m_ERRSarkaStage = 20; } } #endif diff --git a/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c b/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c index fbb2907..6363ef8 100644 --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c @@ -9,12 +9,13 @@ class ERR_Settings static array Keys() { - return {"aiDoctrine", "aiShadow", "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", "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"}; } 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 == "motorbikeCount") return value >= 0 && value <= 6; if (key == "planeAltitudeMetres") return value >= 150 && value <= 500; if (key == "planeReplacementSeconds") return value >= 30 && value <= 1800; @@ -117,6 +118,7 @@ class ERR_Settings static int AddedDefault(string key) { if (key == "aiDoctrine" || key == "aiShadow") return 0; + if (key == "sarkaCarsPerAnger") return 1; if (key == "motorbikeCount") return 2; if (key == "planeAltitudeMetres") return 250; if (key == "planeReplacementSeconds") return 60; @@ -152,6 +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("motorbikeCount", AddedDefault("motorbikeCount")); m_Values.Set("planeAltitudeMetres", AddedDefault("planeAltitudeMetres")); m_Values.Set("planeReplacementSeconds", AddedDefault("planeReplacementSeconds")); diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c index 9e15251..b92a2da 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c @@ -173,6 +173,7 @@ class ERR_HQ : ERR_CommandService m_Hunters = new ERR_HunterService(mode, null); m_Hunters.ConfigureOpening(mode.Settings().Get("hunterSpawnDistanceMetres")); m_Hunters.ConfigureMotorbikes(mode.Settings().Get("motorbikeCount")); + m_Hunters.ConfigureSarka(); m_ObjectiveDefence = new ERR_ObjectiveDefence(mode, this); m_Towns = new ERR_TownGarrisons(mode); m_Roadblocks = new ERR_Roadblocks(mode); diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c index dca3df8..7af8c76 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterSarkaRules.c @@ -8,15 +8,22 @@ class ERR_SarkaWaves int Target; int Wave; int Spawned; + int CarsPerAnger = 1; + void Configure(int cars) + { + if (CarsPerAnger == cars) return; + CarsPerAnger = cars; + if (cars == 0) Pause(); + } void Pause() { NextWave = -1; Pending = 0; Target = 0; } bool Due(float now, int aggression, bool playing) { - if (!playing) { Pause(); return false; } + if (!playing || CarsPerAnger == 0) { 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++; + Target = Math.Clamp(Math.Clamp(aggression, 0, 30) * CarsPerAnger, 0, 30); Pending = Target; Wave++; } if (Pending == 0 || now < NextAttempt) return false; NextAttempt = now + 5000; diff --git a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c index 6f5552d..3b35da3 100644 --- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c +++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c @@ -138,10 +138,18 @@ class ERR_HunterService : ERR_ConvoyService protected float m_NextReserve; 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"; + 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"; + } 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); + 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) + string.Format(" %1 cars per anger point (30 per wave maximum).", m_SarkaWaves.CarsPerAnger); } // HQ maintenance runs even while tactical ticks pause in an empty round. void PauseSarkaIfEmpty() @@ -152,6 +160,7 @@ class ERR_HunterService : ERR_ConvoyService } protected void TickSarka(ERR_HQ hq, float now) { + ConfigureSarka(); // 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); @@ -161,7 +170,7 @@ class ERR_HunterService : ERR_ConvoyService 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"; + if (m_SarkaWaves.Target == 0) m_SarkaStatus = "no aggression; waiting for the next interval"; 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; @@ -192,7 +201,7 @@ class ERR_HunterService : ERR_ConvoyService 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"; + m_SarkaStatus = "waiting for a clear road away from players"; } protected bool SpawnSarka(float now, vector position, vector angles) { diff --git a/addons/core/UI/layouts/ERR/AdminMenu.layout b/addons/core/UI/layouts/ERR/AdminMenu.layout index 3a4a132..54b3073 100644 --- a/addons/core/UI/layouts/ERR/AdminMenu.layout +++ b/addons/core/UI/layouts/ERR/AdminMenu.layout @@ -67,7 +67,7 @@ TextWidgetClass { Name "Help_emptyRestartHours" Slot FrameWidgetSlot { Anchor 0 } } FrameWidgetClass { Name "ForcesPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } { TextWidgetClass { - Name "ForcesHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 } + Name "ForcesHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 710 OffsetRight -738 SizeY 50 OffsetBottom -56 } Color 0.9 0.92 0.94 1 Text "Enemy patrols, site guards and equipment. Equipment changes apply to newly equipped soldiers." "Font Size" 19 "Min Font Size" 19 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } @@ -210,6 +210,19 @@ TextWidgetClass { Color 0.72 0.81 0.87 1 Text "Chance of anti-personnel mines for newly equipped soldiers." "Font Size" 14 "Min Font Size" 14 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "OpenSarka" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 4 OffsetTop 4 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -42 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Šárka hunters" } } } +ImageWidgetClass { Name "Selected_OpenSarka" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 45 OffsetTop 45 SizeX 176 OffsetRight -964 SizeY 3 OffsetBottom -48 } Color 0.9 0.66 0.25 1 } +} } +FrameWidgetClass { Name "SarkaPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } { +TextWidgetClass { Name "SarkaHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 710 OffsetRight -738 SizeY 32 OffsetBottom -38 } Color 0.9 0.92 0.94 1 Text "Šárka special-forces hunters" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "SarkaBack" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 788 OffsetLeft 788 PositionY 4 OffsetTop 4 SizeX 176 OffsetRight -964 SizeY 38 OffsetBottom -42 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Back to Forces" } } } +TextWidgetClass { Name "SarkaHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 50 OffsetTop 50 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -94 } Color 0.9 0.92 0.94 1 Text "Four soldiers per car, including a medic. Anger rises from identified enemy casualties." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Label_sarkaCarsPerAnger" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 112 OffsetTop 112 SizeX 590 OffsetRight -618 SizeY 32 OffsetBottom -144 } Color 0.9 0.92 0.94 1 Text "Šárkas per anger point (0-5; 0 disables waves)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" { Name "sarkaCarsPerAnger" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 640 OffsetLeft 640 PositionY 108 OffsetTop 108 SizeX 112 OffsetRight -752 SizeY 38 OffsetBottom -146 } components { SCR_EditBoxComponent "{547290FFBD5B33E9}" { m_sLabel "" } } } +ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "Apply_sarkaCarsPerAnger" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 764 OffsetLeft 764 PositionY 108 OffsetTop 108 SizeX 200 OffsetRight -964 SizeY 38 OffsetBottom -146 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Apply" } } } +TextWidgetClass { Name "Saved_sarkaCarsPerAnger" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 640 OffsetLeft 640 PositionY 148 OffsetTop 148 SizeX 324 OffsetRight -964 SizeY 24 OffsetBottom -172 } Color 0.9 0.92 0.94 1 Text "Waiting for server" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "Help_sarkaCarsPerAnger" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 176 OffsetTop 176 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -202 } Color 0.9 0.92 0.94 1 Text "Default 1; up to 30 cars per wave. Existing teams remain when disabled." "Font Size" 17 "Min Font Size" 17 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } +TextWidgetClass { Name "SarkaSettingsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 424 OffsetTop 424 SizeX 936 OffsetRight -964 SizeY 52 OffsetBottom -476 } Color 0.9 0.92 0.94 1 Text "Apply saves this setting for new waves. Existing teams remain. Empty sessions do not build up extra waves." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } } } FrameWidgetClass { Name "SitesPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } { TextWidgetClass { Name "SitesHeading" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 710 OffsetRight -738 SizeY 32 OffsetBottom -38 } Color 0.9 0.92 0.94 1 Text "Camps and hospitals - next round" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } } diff --git a/tests/test_admin_menu.py b/tests/test_admin_menu.py index 2072992..d682233 100644 --- a/tests/test_admin_menu.py +++ b/tests/test_admin_menu.py @@ -53,7 +53,7 @@ assert 'Apply_planeReconEnabled' not in names # Saved-state toggle pair, not a 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 ('emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): assert ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key in names + if key not in ('sarkaCarsPerAnger','emptyRestartHours','aiDoctrine','aiShadow','motorbikeCount','planeAltitudeMetres','planeReplacementSeconds','planeSectorSeconds','fieldCampCount','fieldHospitalCount','startMode','civilianTrafficCount','roadblockCount','hunterSpawnDistanceMetres','objectGuardCount','objectGuardEvery','objectGuardHQMode','startWeaponCount','startMagazineCount'): 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 diff --git a/tests/test_admin_setting_policy.py b/tests/test_admin_setting_policy.py index fe3d570..e00e73f 100644 --- a/tests/test_admin_setting_policy.py +++ b/tests/test_admin_setting_policy.py @@ -21,6 +21,9 @@ int main(){ assert(!Valid("emptyRestartHours",-1)&&!Valid("emptyRestartHours",169)); for(std::string key:{"aiDoctrine","aiShadow"}){ assert(Valid(key,0)&&Valid(key,1)); assert(!Valid(key,-1)&&!Valid(key,2)); } assert(CoreSetting("mission","motorbikes")=="motorbikeCount"); + assert(CoreSetting("mission","sarkacars")=="sarkaCarsPerAnger"); + assert(Valid("sarkaCarsPerAnger",0)&&Valid("sarkaCarsPerAnger",1)&&Valid("sarkaCarsPerAnger",5)); + assert(!Valid("sarkaCarsPerAnger",-1)&&!Valid("sarkaCarsPerAnger",6)); assert(Valid("motorbikeCount",0)&&Valid("motorbikeCount",2)&&Valid("motorbikeCount",6)); assert(!Valid("motorbikeCount",-1)&&!Valid("motorbikeCount",7)); assert(CoreSetting("mission","planealtitude")=="planeAltitudeMetres"); diff --git a/tests/test_sarka_hunters.py b/tests/test_sarka_hunters.py index d362faa..07c0a08 100644 --- a/tests/test_sarka_hunters.py +++ b/tests/test_sarka_hunters.py @@ -38,6 +38,19 @@ int main(){ 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); + Waves configured{}; + configured.Configure(2); + assert(!configured.Due(0,3,true)); + assert(configured.Due(600000,3,true)&&configured.Target==6); + configured.Commit();assert(configured.Pending==5&&configured.Spawned==1); + configured.Configure(5); // Already sampled wave and deadline stay unchanged. + assert(configured.Pending==5&&configured.NextWave==1200000); + assert(configured.Due(1200000,30,true)&&configured.Target==30); // Keep the formation cap. + configured.Configure(0);assert(configured.Pending==0&&configured.Target==0); + assert(!configured.Due(9999999,30,true)&&configured.Spawned==1); // No waves; retain deployed count. + configured.Configure(2);assert(!configured.Due(10000000,3,true)); + assert(!configured.Due(10599999,3,true)); // Re-enable waits normally, no backlog. + assert(configured.Due(10600000,3,true)&&configured.Target==6); Array players{{0,10,0}}; assert(!Rules::InBand({999,0,0},players)); assert(Rules::InBand({1000,900,0},players)&&Rules::InBand({1250,0,0},players)); @@ -48,4 +61,4 @@ int main(){ 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') +print('PASS: configurable anger multiplier, disable/re-enable, wave cap, staged retries, empty-round reset, no backlog, four seats and nearest-player road distance')