119 lines
10 KiB
Diff
119 lines
10 KiB
Diff
--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c
|
|
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c
|
|
@@ -43,9 +43,11 @@
|
|
{
|
|
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
|
if (!damage || damage.GetState() == EDamageState.DESTROYED) { failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
|
+ CharacterControllerComponent controller = CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent));
|
|
+ if (controller && controller.IsUnconscious()) return true;
|
|
if (damage.IsBleeding()) return true;
|
|
HitZone blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();
|
|
- if ((blood && blood.GetHealthScaled() < 1) || (resilience && resilience.GetHealthScaled() < 1)) return true;
|
|
+ if ((blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING)) || (resilience && resilience.GetHealthScaled() < 1)) return true;
|
|
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
|
|
foreach (HitZone zone : zones) if (zone && zone.GetHealthScaled() < 1) return true;
|
|
failReason = SCR_EConsumableFailReason.UNDAMAGED; return false;
|
|
--- a/tests/test_player_medical.py
|
|
+++ b/tests/test_player_medical.py
|
|
@@ -114,3 +114,100 @@
|
|
}
|
|
''')
|
|
print('PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat')
|
|
+
|
|
+
|
|
+"""Actual combat injector admission/effect; native component/effect boundaries doubled.
|
|
+No animation, inventory consumption, replication or native base-effect qualification.
|
|
+"""
|
|
+from pathlib import Path
|
|
+import os, re
|
|
+from policy_harness import ROOT, body, run
|
|
+p=Path(os.environ.get('ERR_EPINEPHRINE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Epinephrine.c'))
|
|
+s=p.read_text()
|
|
+def method(class_name, signature):
|
|
+ start=s.index('class '+class_name)
|
|
+ a=s.index(signature,start); b=s.index('{',a); end=b+1; depth=1
|
|
+ while depth:
|
|
+ depth+=(s[end]=='{')-(s[end]=='}'); end+=1
|
|
+ return s[a:end]
|
|
+def cpp(t):
|
|
+ t=t.replace('override ', '').replace('notnull ', '').replace('out SCR_EConsumableFailReason failReason','SCR_EConsumableFailReason& failReason')
|
|
+ t=t.replace('SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent))','target->damage')
|
|
+ t=t.replace('SCR_CharacterDamageManagerComponent.Cast(dmgManager)','dmgManager')
|
|
+ t=t.replace('CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent))','target->controller')
|
|
+ t=t.replace('CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent))','damage->owner->controller')
|
|
+ t=t.replace('SCR_CharacterHitZone.Cast(zone)','dynamic_cast<SCR_CharacterHitZone*>(zone)')
|
|
+ t=t.replace('array<HitZone>', 'Array<HitZone*>').replace('foreach (', 'for (').replace('null','nullptr')
|
|
+ for typ in ('SCR_CharacterDamageManagerComponent','SCR_ExtendedDamageManagerComponent','HitZone','SCR_CharacterBloodHitZone','SCR_CharacterResilienceHitZone','SCR_CharacterHitZone','CharacterControllerComponent'):
|
|
+ t=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',t)
|
|
+ t=t.replace('HitZone* blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();','HitZone* blood = damage.GetBloodHitZone(); HitZone* resilience = damage.GetResilienceHitZone();')
|
|
+ for obj in ('damage','blood','resilience','zone','physical','controller'):
|
|
+ t=t.replace(obj+'.',obj+'->')
|
|
+ for obj in ('EDamageState','SCR_EConsumableFailReason','ECharacterBloodState','ECharacterHitZoneGroup','ERR_CombatEpinephrineRules','Replication','Math'):
|
|
+ t=t.replace(obj+'.',obj+'::')
|
|
+ t=t.replace('super.', 'Native::')
|
|
+ return t
|
|
+rules=cpp(body(p,'class ERR_CombatEpinephrineRules')).replace('class ERR_CombatEpinephrineRules','struct ERR_CombatEpinephrineRules')+';'
|
|
+admit=cpp(method('ERR_ConsumableCombatEpinephrine','override bool CanApplyEffect('))
|
|
+effect=cpp(method('ERR_CombatEpinephrineEffect','override void OnEffectAdded('))
|
|
+run(r'''
|
|
+#include <cassert>
|
|
+#include <vector>
|
|
+#include <algorithm>
|
|
+#include <cmath>
|
|
+namespace EDamageState{enum{UNDAMAGED,DESTROYED};}
|
|
+enum class SCR_EConsumableFailReason{NONE,UNDAMAGED};
|
|
+namespace ECharacterBloodState{enum{FAINTING,UNCONSCIOUS};}
|
|
+enum class ECharacterHitZoneGroup{HEAD,UPPERTORSO,LOWERTORSO,LEFTARM,RIGHTARM,LEFTLEG,RIGHTLEG};
|
|
+namespace Replication{bool server=true;bool IsServer(){return server;}}
|
|
+namespace Math{float Min(float a,float b){return std::min(a,b);}}
|
|
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;};
|
|
+struct HitZone{float health=1;virtual ~HitZone()=default;float GetHealthScaled(){return health;}void SetHealthScaled(float n){health=n;}virtual float GetDamageStateThreshold(int){return .7f;}};
|
|
+struct SCR_CharacterBloodHitZone:HitZone{float faint=.7f,unconscious=.5f;float GetDamageStateThreshold(int state)override{return state==ECharacterBloodState::FAINTING?faint:unconscious;}};
|
|
+struct SCR_CharacterResilienceHitZone:HitZone{};
|
|
+struct SCR_CharacterHitZone:HitZone{ECharacterHitZoneGroup group=ECharacterHitZoneGroup::LEFTARM;int GetHitZoneGroup(){return int(group);}};
|
|
+// The source compares enum to an int; Enforce permits this conversion.
|
|
+bool operator==(int a,ECharacterHitZoneGroup b){return a==int(b);}
|
|
+struct CharacterControllerComponent{bool unconscious=false;bool IsUnconscious(){return unconscious;}void SetUnconscious(bool n){unconscious=n;}};
|
|
+struct SCR_CharacterDamageManagerComponent;
|
|
+struct Entity{SCR_CharacterDamageManagerComponent* damage=nullptr;CharacterControllerComponent*controller=nullptr;};using IEntity=Entity*;
|
|
+struct SCR_CharacterDamageManagerComponent{Entity*owner=nullptr;int state=EDamageState::UNDAMAGED;bool bleeding=false;SCR_CharacterBloodHitZone*blood=nullptr;SCR_CharacterResilienceHitZone*resilience=nullptr;Array<HitZone*>zones;
|
|
+ int GetState(){return state;}bool IsBleeding(){return bleeding;}SCR_CharacterBloodHitZone*GetBloodHitZone(){return blood;}SCR_CharacterResilienceHitZone*GetResilienceHitZone(){return resilience;}void GetPhysicalHitZones(Array<HitZone*>&out){out=zones;}void RemoveGroupBleeding(ECharacterHitZoneGroup){bleeding=false;}
|
|
+};
|
|
+using SCR_ExtendedDamageManagerComponent=SCR_CharacterDamageManagerComponent;
|
|
+// Native inherited effect handling is opaque. No extra health restoration is
|
|
+// fabricated: the authored effect is instant and supplies no generic damage.
|
|
+struct Native{int calls=0;void OnEffectAdded(SCR_ExtendedDamageManagerComponent*){calls++;}};
|
|
+'''+rules+'\nstruct Admission{'+admit+'};\nstruct Effect:Native{'+effect+r'''};
|
|
+struct Fixture{Entity actor;CharacterControllerComponent controller;SCR_CharacterDamageManagerComponent damage;SCR_CharacterBloodHitZone blood;SCR_CharacterResilienceHitZone resilience;SCR_CharacterHitZone arm,leg;Admission admission;Effect effect;
|
|
+ Fixture(){actor.damage=&damage;actor.controller=&controller;damage.owner=&actor;damage.blood=&blood;damage.resilience=&resilience;leg.group=ECharacterHitZoneGroup::LEFTLEG;damage.zones={&arm,&leg};Replication::server=true;}
|
|
+ bool Allowed(){SCR_EConsumableFailReason reason=SCR_EConsumableFailReason::NONE;return admission.CanApplyEffect(&actor,&actor,reason);}
|
|
+ void Apply(){effect.OnEffectAdded(&damage);}
|
|
+};
|
|
+int main(){
|
|
+ // Concrete baseline mismatch: mild blood loss is the only injury. Applying the
|
|
+ // actual effect changes no health/bleeding/consciousness, so admission must refuse.
|
|
+ for(float value:{.7001f,.85f,.999f}){Fixture f;f.blood.health=value;f.Apply();assert(f.effect.calls==1&&f.blood.health==value&&f.resilience.health==1&&f.arm.health==1&&f.leg.health==1&&!f.damage.bleeding&&!f.controller.unconscious);assert(!f.Allowed());}
|
|
+ // Native threshold is data-driven, including exact equality; no guessed cutoff.
|
|
+ for(float cutoff:{.6f,.7f,.8f}){Fixture f;f.blood.faint=cutoff;f.blood.health=cutoff;assert(f.Allowed());f.Apply();assert(f.blood.health>cutoff);}
|
|
+ {Fixture f;f.blood.health=.3f;assert(f.Allowed());f.Apply();assert(f.blood.health>.3f);}
|
|
+ // Waking is an independent implemented benefit, including full health.
|
|
+ for(float blood:{.9f,1.f}){Fixture f;f.blood.health=blood;f.controller.unconscious=true;assert(f.Allowed());f.Apply();assert(!f.controller.unconscious&&f.blood.health==blood);}
|
|
+ {Fixture f;f.controller.unconscious=true;f.damage.state=EDamageState::DESTROYED;assert(!f.Allowed());f.Apply();assert(f.controller.unconscious);}
|
|
+ // Preserve all other useful existing benefits even with mildly depleted blood.
|
|
+ {Fixture f;f.blood.health=.9f;f.damage.bleeding=true;assert(f.Allowed());f.Apply();assert(!f.damage.bleeding&&f.blood.health==.9f);}
|
|
+ {Fixture f;f.blood.health=.9f;f.resilience.health=.2f;f.controller.unconscious=true;assert(f.Allowed());f.Apply();assert(f.resilience.health==1&&!f.controller.unconscious&&f.blood.health==.9f);}
|
|
+ {Fixture f;f.blood.health=.9f;f.arm.health=.5f;assert(f.Allowed());f.Apply();assert(std::abs(f.arm.health-.65f)<.00001f);}
|
|
+ {Fixture f;f.leg.health=.2f;assert(f.Allowed());f.Apply();assert(std::abs(f.leg.health-.7f)<.00001f);}
|
|
+ // A missing controller must not suppress non-consciousness benefits.
|
|
+ for(int benefit=0;benefit<4;benefit++){Fixture f;f.actor.controller=nullptr;if(benefit==0)f.damage.bleeding=true;if(benefit==1)f.blood.health=.3f;if(benefit==2)f.resilience.health=.3f;if(benefit==3)f.arm.health=.3f;assert(f.Allowed());f.Apply();}
|
|
+ {Fixture f;assert(!f.Allowed());f.actor.damage=nullptr;assert(!f.Allowed());}
|
|
+ {Fixture f;f.damage.state=EDamageState::DESTROYED;f.damage.bleeding=true;f.blood.health=.1f;assert(!f.Allowed());f.Apply();assert(f.blood.health==.1f&&f.damage.bleeding);}
|
|
+ {Fixture f;f.damage.blood=nullptr;f.damage.resilience=nullptr;assert(!f.Allowed());f.arm.health=.5f;assert(f.Allowed());}
|
|
+ {Fixture f;f.blood.health=.1f;Replication::server=false;f.Apply();assert(f.blood.health==.1f);}
|
|
+}
|
|
+''')
|
|
+prefab=(ROOT/'addons/core/Prefabs/ERR/CombatEpinephrine.et').read_text()
|
|
+assert 'm_fItemRegenerationSpeed 0' in prefab and 'm_fItemRegenerationDuration 0' in prefab
|
|
+assert 'ERR_CombatEpinephrineEffect' in prefab
|
|
+print('PASS: actual combat injector admission/effect alignment; mild blood-only refusal, native threshold equality and all existing useful treatments preserved; engine consumption untested')
|