Add anger-scaled Sarka hunter waves every ten minutes

This commit is contained in:
2026-10-16 16:36:38 +02:00
parent ae9aaafaae
commit a66aca35bf
14 changed files with 332 additions and 20 deletions
+2 -2
View File
@@ -70,7 +70,7 @@ int main(){
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);
}
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==1)c.service.m_Stopped=true;
if(bad==2)c.mode.active=false;
@@ -79,9 +79,9 @@ int main(){
if(bad==5)c.hq.service=&c.foreign;
if(bad==6)c.mode.roster.current=&c.unrelated;
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);
}
{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;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);}
+1 -1
View File
@@ -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 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 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){}};
namespace Math{float RandomFloat(float,float){return 0;}}
namespace ERR_LocationKind{enum{RADIO=1,DEPOT=2,OUTPOST=3};}
+3 -3
View File
@@ -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 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_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{
'''+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,1,960000,960000));
Service s;ERR_HQ hq;ERR_HunterVehicle a,b,c,bike;a.Source="a";b.Source="b";c.Source="c";
bike.Source="bike";bike.Motorbike=true;s.m_Hunters={&a,&b,&c,&bike};
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;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
assert(!s.ReserveSource(&hq,479999));
assert(s.ReserveSource(&hq,480000)==&a); // absence of a first check-in has a bounded deadline
+14 -2
View File
@@ -2,6 +2,7 @@
import re
from policy_harness import ROOT,body,run
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(')
rules=body(p,'static bool Transport(')+body(p,'static bool Seats(')
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']:
s=s.replace(obj+'.',obj+'->')
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+'::')
run(r'''
#include <cassert>
@@ -25,6 +26,9 @@ template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;voi
struct ERR_HunterFleetRules{
'''+rules+r'''
};
struct ERR_SarkaRules{
'''+sarka_rules+r'''
};
struct ERR_MotorbikeRules{
'''+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;}
IEntity*SpawnCharacterInCompartment(int prefab,AIGroup*&,int){if(fail)return nullptr;occupant=new IEntity{prefab};return occupant;}};
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++;}};
namespace ERR_EnemyMedicalCatalog{int Crew(Mode*,bool medic){return medic?2:1;}}
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);
assert(service.FillCrew(&bike)&&bike.Members.Count()==2&&bike.Driver==driver.occupant);
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(false,1,0,14,15)); // transport cannot pass as armed UAZ
assert(!ERR_HunterFleetRules::Seats(true,2,0,14,16));
+51
View File
@@ -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')