80 lines
5.6 KiB
Python
80 lines
5.6 KiB
Python
"""Execute player policy methods; native damage/regen boundaries are modeled."""
|
|
from policy_harness import ROOT, body, run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
|
|
source=p.read_text()
|
|
methods='\n'.join(body(p,s) for s in ['void ERR_EnableCasualtyProtection(', 'override protected bool IsIndefinitelyUnconscious(', 'override float GetResilienceRegenScale('])
|
|
methods=methods.replace('override ','').replace('protected ','').replace('Replication.IsServer()','server').replace('super.','Native::')
|
|
run('''#include <cassert>
|
|
bool server=true;
|
|
struct Native {bool unconscious=false,changed=false;float regen=1;int writes=0;bool bleeding=false;
|
|
void SetPermitUnconsciousness(bool value,bool c){unconscious=value;changed=c;writes++;}
|
|
void SetRegenScale(float value,bool){regen=value;writes++;}
|
|
bool IsIndefinitelyUnconscious(bool){return true;}bool IsBleeding(){return bleeding;}float GetResilienceRegenScale(){return .8f;}
|
|
};
|
|
struct Policy:Native{bool m_ERRRevivablePlayer=false;
|
|
'''+methods+'''
|
|
};
|
|
int main(){Policy p;server=false;p.ERR_EnableCasualtyProtection();assert(!p.m_ERRRevivablePlayer&&p.writes==0);
|
|
server=true;p.ERR_EnableCasualtyProtection();assert(p.unconscious&&p.changed&&p.regen==.25f);
|
|
assert(!p.IsIndefinitelyUnconscious());p.bleeding=true;assert(p.GetResilienceRegenScale()==0);
|
|
p.bleeding=false;assert(p.GetResilienceRegenScale()==.8f);Policy ai;assert(ai.IsIndefinitelyUnconscious());ai.bleeding=true;assert(ai.GetResilienceRegenScale()==.8f);
|
|
}
|
|
''')
|
|
# Execute the real damage cap across wound/blood scales; healing is untouched.
|
|
cap=body(p, 'static float AllowedDamage(').replace('Math.Clamp','std::clamp').replace('Math.Min','std::min').replace('Math.Max','std::max').replace('0.01','0.01f').replace(', 1)',', 1.0f)').replace('(0,','(0.0f,')
|
|
run('''#include <algorithm>
|
|
#include <cassert>
|
|
#include <cmath>
|
|
'''+cap+'''
|
|
int main(){
|
|
for(float maximum:{100.0f,5000.0f}) for(float threshold:{0.0f,.05f,.2f}) {
|
|
float floor=maximum*(threshold+.01f);
|
|
assert(AllowedDamage(-10,maximum,maximum,threshold)==-10);
|
|
assert(AllowedDamage(100000, floor,maximum,threshold)==0);
|
|
assert(AllowedDamage(100000, floor/2,maximum,threshold)==0);
|
|
float health=maximum;
|
|
for(int i=0;i<1000;i++) health-=AllowedDamage(13,health,maximum,threshold);
|
|
assert(std::abs(health-floor)<.001f);
|
|
assert(AllowedDamage(1,maximum,maximum,threshold)==1);
|
|
}
|
|
}
|
|
''')
|
|
# Passive resilience has its own native rate; treatment DPS bypasses this getter.
|
|
rate=body(p,'override float CalculatePassiveRegenDPS(').replace('override ','').replace('super.','Native::').replace('SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());','Manager* manager = target;').replace('manager.','manager->')
|
|
run('''#include <cassert>
|
|
struct Manager { bool player; bool ERR_IsRevivablePlayer(){return player;} };
|
|
struct Native {float CalculatePassiveRegenDPS(bool){return 4;}};
|
|
struct Policy:Native {Manager* target=nullptr;
|
|
'''+rate+'''
|
|
};
|
|
int main(){Policy p;assert(p.CalculatePassiveRegenDPS()==4);Manager ai{false};p.target=&ai;assert(p.CalculatePassiveRegenDPS()==4);ai.player=true;assert(p.CalculatePassiveRegenDPS()==1);assert(p.CalculatePassiveRegenDPS(false)==4);}
|
|
''')
|
|
assert 'SCR_CharacterResilienceHitZone.Cast(this)) return damage' in source
|
|
assert 'damage <= 0' in source and '!manager.ERR_IsRevivablePlayer()' in source
|
|
assert 'override void Kill(' in source
|
|
assert 'FullHeal(' not in source and 'SetHealth(' not in source
|
|
print('PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing')
|
|
# Run the production recovery prediction over real effect states modeled at the
|
|
# native boundary, including a pending regeneration delay and exhausted effects.
|
|
future=body(p,'bool ERR_CanRecoverNaturally(')
|
|
future=future.replace('array<ref SCR_PersistentDamageEffect> effects','auto effects').replace('GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect, true)','GetAllPersistentEffectsOfType()')
|
|
future=future.replace('foreach (SCR_PersistentDamageEffect effect : effects)','for (Effect* effect : effects)').replace('DotDamageEffect dot = DotDamageEffect.Cast(effect);','Effect* dot = effect;').replace('HitZone zone','Zone* zone').replace('dot.','dot->').replace('zone.','zone->').replace('EDamageType.REGENERATION','1')
|
|
run('''#include <cassert>
|
|
#include <vector>
|
|
struct Zone {float value=0;float GetHealthScaled(){return value;}};
|
|
struct Effect {Zone* zone;float dps;int type=1;int GetDamageType(){return type;}float GetDPS(){return dps;}Zone* GetAffectedHitZone(){return zone;}};
|
|
struct Policy {bool m_ERRRevivablePlayer=true,bleeding=false;float scale=1;Zone blood,resilience;std::vector<Effect*> effects;
|
|
bool IsBleeding(){return bleeding;}auto GetAllPersistentEffectsOfType(){return effects;}
|
|
Zone* GetBloodHitZone(){return &blood;}Zone* GetResilienceHitZone(){return &resilience;}float GetResilienceRegenScale(){return scale;}
|
|
'''+future+'''
|
|
};
|
|
int main(){Policy p;assert(!p.ERR_CanRecoverNaturally());Effect e{&p.blood,-.1f};p.effects={&e};
|
|
assert(p.ERR_CanRecoverNaturally());p.bleeding=true;assert(!p.ERR_CanRecoverNaturally());p.bleeding=false;
|
|
p.blood.value=1;assert(!p.ERR_CanRecoverNaturally());p.blood.value=.2;
|
|
e.dps=0;assert(!p.ERR_CanRecoverNaturally());e.dps=-1;e.type=2;assert(!p.ERR_CanRecoverNaturally());e.type=1;
|
|
e.zone=&p.resilience;p.scale=0;assert(!p.ERR_CanRecoverNaturally());p.scale=.1;assert(p.ERR_CanRecoverNaturally());
|
|
p.m_ERRRevivablePlayer=false;assert(!p.ERR_CanRecoverNaturally());
|
|
}
|
|
''')
|
|
print('PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat')
|