Fix convoy coordination, AI combat and spectator recovery
This commit is contained in:
@@ -11,6 +11,10 @@ class ERR_ContentOption
|
||||
string NativeFactionKey;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Physical guard group belonging to this faction", "et")]
|
||||
ResourceName GuardGroup;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Optional HQ response soldier; use for factions with empty character catalogs", "et")]
|
||||
ResourceName ResponseSoldier;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Optional same-faction response medic with authored medical backpack", "et")]
|
||||
ResourceName ResponseMedic;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Optional faction MANPADS override; must spawn loaded and usable. Otherwise FIM-92 is used.", "et")]
|
||||
ResourceName AntiAirLauncher;
|
||||
[Attribute("", UIWidgets.EditBox, "Exact resource GUIDs for optional equipment owned by this pack")]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Native injection animation/consumption; only resilience is restored over time.
|
||||
// This is not a blood replacement, bandage, full heal, or resurrection.
|
||||
[BaseContainerProps()]
|
||||
class ERR_ConsumableEpinephrine : SCR_ConsumableMorphine
|
||||
{
|
||||
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent));
|
||||
if (!damage || damage.GetState() == EDamageState.DESTROYED || !controller || !controller.IsUnconscious())
|
||||
{ failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||
array<ref SCR_PersistentDamageEffect> effects = damage.GetAllPersistentEffectsOfType(ERR_EpinephrineEffect);
|
||||
if (!effects.IsEmpty()) { failReason = SCR_EConsumableFailReason.ALREADY_APPLIED; return false; }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
class ERR_EpinephrineEffect : SCR_DotDamageEffect
|
||||
{
|
||||
override bool HijackDamageEffect(SCR_ExtendedDamageManagerComponent dmgManager)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
|
||||
if (damage) SetAffectedHitZone(damage.GetResilienceHitZone());
|
||||
return false;
|
||||
}
|
||||
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
|
||||
}
|
||||
// The held injector uses the existing native injection interaction and animation.
|
||||
// Only its displayed name changes; ordinary morphine keeps its native behavior.
|
||||
modded class SCR_MorphineUserAction
|
||||
{
|
||||
protected bool m_ERRHoldingEpinephrine;
|
||||
override bool CanBeShownScript(IEntity user)
|
||||
{
|
||||
m_ERRHoldingEpinephrine = false;
|
||||
bool shown = super.CanBeShownScript(user);
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(user);
|
||||
if (shown && character)
|
||||
{
|
||||
SCR_ConsumableItemComponent item = GetConsumableComponent(character);
|
||||
m_ERRHoldingEpinephrine = item && ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect());
|
||||
}
|
||||
return shown;
|
||||
}
|
||||
override bool GetActionNameScript(out string outName)
|
||||
{
|
||||
if (m_ERRHoldingEpinephrine) { outName = "Administer epinephrine"; return true; }
|
||||
return super.GetActionNameScript(outName);
|
||||
}
|
||||
}
|
||||
@@ -497,7 +497,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
if (!Active()) return;
|
||||
if (m_Recovery.DefeatDue(now))
|
||||
{
|
||||
m_EndReason = "All prisoners are unconscious and no treatment or recovery is progressing.";
|
||||
m_EndReason = "Everybody died. All prisoners are unconscious and nobody can recover or receive help.";
|
||||
ERR_HQ.Event("MISSION FAILED", m_EndReason);
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
return;
|
||||
|
||||
@@ -120,6 +120,24 @@ class ERR_GeneratedSites
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool SkipOptional(ERR_SiteDefinition site)
|
||||
{
|
||||
if (site.Kind != ERR_LocationKind.OUTPOST && site.Kind != ERR_LocationKind.HOSPITAL && site.Kind != ERR_LocationKind.DEPOT) return false;
|
||||
int index = m_Definition.Sites.Find(site);
|
||||
if (index < 0) return false;
|
||||
// Remove before spawning/registration, so no empty map or intel target remains.
|
||||
// Keep the object alive while validating or restoring its original position.
|
||||
ref ERR_SiteDefinition retained = site;
|
||||
m_Definition.Sites.Remove(index);
|
||||
if (!m_Definition.Valid())
|
||||
{
|
||||
m_Definition.Sites.InsertAt(retained, index);
|
||||
PrintFormat("Co10 Escape Reintroduced [site required]: %1 (%2) cannot be skipped: remaining scenario would lack required objectives or accessible radio intel", retained.Label, retained.Id);
|
||||
return false;
|
||||
}
|
||||
PrintFormat("Co10 Escape Reintroduced [site skipped]: %1 (%2): no safe placement after %3 attempts and authored fallback rejected; omitted for this round; mission preparation continues", retained.Label, retained.Id, m_Attempts);
|
||||
return true;
|
||||
}
|
||||
ERR_PrepareResult Tick()
|
||||
{
|
||||
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
|
||||
@@ -145,8 +163,15 @@ class ERR_GeneratedSites
|
||||
}
|
||||
else if (fallback)
|
||||
{
|
||||
m_Error = "No safe terrain or valid authored fallback for " + site.Id;
|
||||
m_Result = ERR_PrepareResult.FAILED; return m_Result;
|
||||
if (SkipOptional(site))
|
||||
{
|
||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Error = "No safe terrain or valid authored fallback for required site " + site.Id;
|
||||
m_Result = ERR_PrepareResult.FAILED; return m_Result;
|
||||
}
|
||||
}
|
||||
if (System.GetTickCount() - tick >= 8) return ERR_PrepareResult.PENDING;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@ class ERR_JournalText
|
||||
if (kind == "MORTAR SITE STATUS") return "Mortar " + Field(message, "site") + ": operational: " + YesNo(Field(message, "operational")) + "; combat-capable crew: " + Field(message, "fighting") + "; HE shells: " + Field(message, "inventory HE") + "; HQ radio connected: " + YesNo(Field(message, "radio connected")) + ".";
|
||||
if (kind == "MORTAR ORDER STATUS") return "Mortar " + Field(message, "site") + ": " + Field(message, "order shots") + " shots in this order; " + Field(message, "total shots") + " shots fired in total.";
|
||||
if (kind == "BODY SEARCH COMPLETE") return "A recovery team finished searching a body. " + Short(message);
|
||||
if (kind == "CONTACT REPORT") return "Squad reporting a captured sighting to HQ: " + Short(message);
|
||||
if (kind == "GUNFIRE REPORT") return "Squad reporting heard gunfire to HQ: " + Short(message);
|
||||
if (kind == "INFANTRY COMBAT") return message;
|
||||
if (kind == "INTEL COMPROMISE") return "HQ learned that intel is missing: " + Short(message);
|
||||
if (kind == "OBJECTIVE DEFENCE") return "Objective security: " + Short(message);
|
||||
if (kind == "HELICOPTER LAUNCH") return "Search helicopter launched: " + Short(message);
|
||||
|
||||
@@ -54,7 +54,8 @@ modded class ERR_GameMode
|
||||
string enabled;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||
string nativeEndCase;
|
||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && (nativeEndCase == "unconscious-camera" || nativeEndCase == "town-garrisons" || nativeEndCase == "site-garrisons" || nativeEndCase == "radio-interaction" || nativeEndCase == "enemy-loadout" || nativeEndCase == "aggression" || nativeEndCase == "native-end-active" || nativeEndCase == "native-end-preparing" || nativeEndCase == "generated-sites" || nativeEndCase == "mortar-station" || nativeEndCase == "opening-tools" || nativeEndCase == "airfield-markers" || nativeEndCase == "air-logistics" || nativeEndCase == "air-assault" || nativeEndCase == "air-recon")) return;
|
||||
array<string> independentFixtures = {"optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
|
||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
|
||||
ERR_NativeObjectiveProbe.BeginRound();
|
||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||
string outcome;
|
||||
@@ -269,7 +270,7 @@ modded class ERR_GameMode
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("All prisoners are unconscious"), "stalled all-unconscious team reaches automatic mission defeat")) return;
|
||||
if (!ERR_BaseCheck(!RecoveryStatus().IsEmpty() && !AbandonRound(), "terminal event observes active warning and rejects repeated abandonment")) return;
|
||||
Print("ERR SMOKE COMPLETE: player casualty protection, native healing and automatic all-unconscious defeat");
|
||||
Print("ERR SMOKE COMPLETE: player unconsciousness, native healing and automatic all-unconscious defeat");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "objective-ui")
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifdef WORKBENCH
|
||||
// Isolate native waypoint lifecycle from the separate long-distance patrol test.
|
||||
// Contacts are explicitly injected; no natural target acquisition claim.
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected ref ERR_NativePatrolHQProbe m_ERRInfantryCombatProbe;
|
||||
protected float m_ERRInfantryCombatDeadline;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "infantry-combat") return;
|
||||
m_ERRInfantryCombatDeadline = System.GetTickCount() + 180000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_InfantryCombatTick, 1000, true);
|
||||
}
|
||||
protected void ERR_InfantryCombatTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERRInfantryCombatDeadline)
|
||||
{
|
||||
Print("ERR SMOKE FAIL: infantry combat fixture timed out", LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_InfantryCombatTick); return;
|
||||
}
|
||||
if (Phase() != ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||
if (!m_ERRInfantryCombatProbe)
|
||||
{
|
||||
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRInfantryCombatProbe = new ERR_NativePatrolHQProbe(this);
|
||||
}
|
||||
int result = m_ERRInfantryCombatProbe.Tick(GetWorld().GetWorldTime());
|
||||
if (result == 0) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_InfantryCombatTick);
|
||||
m_ERRInfantryCombatProbe.Stop();
|
||||
if (result < 0) return;
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: native infantry search pause/resume and preserved home queue; injected contact, not live shooting acceptance");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,159 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_FactionSelection
|
||||
{
|
||||
override bool Prepare(ERR_GameMode mode)
|
||||
{
|
||||
string outcome;
|
||||
if (System.GetCLIParam("errSmokeOutcome", outcome) && (outcome == "hunter-faction" || outcome == "air-recon" || outcome == "air-assault"))
|
||||
{
|
||||
m_Saved.Set("escapists", "us"); m_Saved.Set("enemy", "rhs_ion"); m_Saved.Set("independent", "fia");
|
||||
}
|
||||
return super.Prepare(mode);
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_HQ
|
||||
{
|
||||
void ERR_HunterFixtureSighting(IEntity target, SCR_AIGroup source)
|
||||
{
|
||||
ERR_HQReport report = new ERR_HQReport();
|
||||
report.Target = target; report.Source = source; report.Position = target.GetOrigin();
|
||||
report.ObservedAt = GetGame().GetWorld().GetWorldTime(); report.MessageId = Journal().Message();
|
||||
Accept(report, report.ObservedAt);
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_HunterService
|
||||
{
|
||||
ERR_HunterVehicle ERR_MedicalFixtureHunter()
|
||||
{
|
||||
string outcome;
|
||||
if (!System.GetCLIParam("errSmokeOutcome", outcome) || (outcome != "medical-combat" && outcome != "hunter-faction")) return null;
|
||||
foreach (ERR_HunterVehicle hunter : m_Hunters) if (hunter.Registered && !hunter.Released) return hunter;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected bool m_ERRHunterFactionFixture;
|
||||
protected vector m_ERRHunterFactionStart;
|
||||
protected int m_ERRMedicalCombatStep;
|
||||
protected float m_ERRMedicalCombatDeadline;
|
||||
protected ref ERR_NativeMedicalProbe m_ERRMedicalCombatProbe;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || (outcome != "medical-combat" && outcome != "hunter-faction")) return;
|
||||
m_ERRHunterFactionFixture = outcome == "hunter-faction";
|
||||
m_ERRMedicalCombatDeadline = System.GetTickCount() + 300000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_MedicalCombatTick, 1000, true);
|
||||
}
|
||||
protected bool ERR_MedicalCombatCheck(bool ok, string detail)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: medical-combat " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: medical-combat " + detail, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_MedicalCombatTick); return false;
|
||||
}
|
||||
protected ERR_UnlockPrisonAction ERR_MedicalCombatLock(IEntity entity)
|
||||
{
|
||||
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
array<BaseUserAction> actions = {};
|
||||
manager.GetActionsList(actions);
|
||||
foreach (BaseUserAction action : actions)
|
||||
{
|
||||
ERR_UnlockPrisonAction lock = ERR_UnlockPrisonAction.Cast(action);
|
||||
if (lock) return lock;
|
||||
}
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
ERR_UnlockPrisonAction lock = ERR_MedicalCombatLock(child);
|
||||
if (lock) return lock;
|
||||
child = child.GetSibling();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected void ERR_MedicalCombatTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERRMedicalCombatDeadline) { ERR_MedicalCombatCheck(false, "bounded native fixture deadline"); return; }
|
||||
if (Phase() < ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||
if (m_ERRMedicalCombatStep == 0)
|
||||
{
|
||||
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
ERR_UnlockPrisonAction opening;
|
||||
foreach (IEntity tracked : m_OwnedEntities) { if (tracked) opening = ERR_MedicalCombatLock(tracked); if (opening) break; }
|
||||
if (!ERR_MedicalCombatCheck(opening != null, "owned prison gate located")) return;
|
||||
player.SetOrigin(opening.GetOwner().GetOrigin() + "1 0 1");
|
||||
opening.PerformAction(opening.GetOwner(), player);
|
||||
if (!ERR_MedicalCombatCheck(Phase() == ERR_Phase.RADIO_SEARCH, "actual prison unlock releases response teams")) return;
|
||||
m_ERRMedicalCombatStep = 1;
|
||||
}
|
||||
if (m_ERRMedicalCombatStep == 1)
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Cast(Command());
|
||||
ERR_HunterVehicle hunter; if (hq) hunter = hq.Hunters().ERR_MedicalFixtureHunter();
|
||||
if (!hunter) return;
|
||||
IEntity medic;
|
||||
int specialists;
|
||||
foreach (IEntity actor : hunter.Members)
|
||||
{
|
||||
if (!actor || !actor.GetPrefabData()) continue;
|
||||
string path = actor.GetPrefabData().GetPrefabName();
|
||||
if (ERR_EnemyMedicalCatalog.Medic(path)) medic = actor;
|
||||
if (ERR_EnemyMedicalCatalog.Specialist(path)) specialists++;
|
||||
}
|
||||
if (!medic || hunter.Members.Count() <= 2 || hunter.Members.Count() != hunter.Seats.Count() || (!m_ERRHunterFactionFixture && specialists != hunter.Members.Count()))
|
||||
{ ERR_MedicalCombatCheck(false, "actual hunter UAZ has full special-forces crew and medic"); return; }
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(medic.FindComponent(InventoryStorageManagerComponent));
|
||||
array<IEntity> items = {}; inventory.GetItems(items);
|
||||
int epi, morphine, saline, dressings; bool bag;
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.GetPrefabData()) continue;
|
||||
string prefab = item.GetPrefabData().GetPrefabName();
|
||||
if (prefab == ERR_EnemyMedicalCatalog.EPI) epi++;
|
||||
if (prefab == ERR_EnemyMedicalCatalog.MORPHINE) morphine++;
|
||||
if (prefab.Contains("SalineBag")) saline++;
|
||||
if (prefab.Contains("FieldDressing")) dressings++;
|
||||
if (prefab.Contains("Backpack_Medical") || (m_ERRHunterFactionFixture && prefab.Contains("Backpack_"))) bag = true;
|
||||
}
|
||||
if (epi < 6 || morphine < 6) return;
|
||||
if (!ERR_MedicalCombatCheck(bag && saline >= 4 && dressings >= 4, "medic carries native medical bag, at least four saline/bandages, six morphine and six epinephrine")) return;
|
||||
if (m_ERRHunterFactionFixture)
|
||||
{
|
||||
if (!ERR_MedicalCombatCheck(hunter.Group.GetFaction() == EnemyFaction() && EnemyFaction().GetFactionKey() == "RHS_ION", "response crew belongs specifically to RHS ION")) return;
|
||||
if (!ERR_MedicalCombatCheck(hq.Hunters().VehicleRadioCapability(hunter.Driver) == 2, "seated ION driver can use the physical vehicle manpack")) return;
|
||||
m_ERRHunterFactionStart = hunter.Entity.GetOrigin();
|
||||
hq.ERR_HunterFixtureSighting(player, hunter.Group);
|
||||
m_ERRMedicalCombatStep = 3; return;
|
||||
}
|
||||
m_ERRMedicalCombatProbe = new ERR_NativeMedicalProbe(this); m_ERRMedicalCombatStep = 2;
|
||||
}
|
||||
if (m_ERRMedicalCombatStep == 3)
|
||||
{
|
||||
ERR_HQ command = ERR_HQ.Cast(Command());
|
||||
ERR_HunterVehicle response = command.Hunters().ERR_MedicalFixtureHunter();
|
||||
if (!response || !response.Waypoint || response.SourceMessage <= 0 || vector.DistanceXZ(m_ERRHunterFactionStart, response.Entity.GetOrigin()) < 30) return;
|
||||
if (!ERR_MedicalCombatCheck(response.CrewValid(this), "mod-faction UAZ receives contact order by radio and drives over 30 metres")) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_MedicalCombatTick);
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: selected mod-faction hunter, stocked medic, contact downlink and native vehicle movement; sighting injected at delivered intake");
|
||||
return;
|
||||
}
|
||||
if (m_ERRMedicalCombatStep == 2)
|
||||
{
|
||||
int result = m_ERRMedicalCombatProbe.Tick(GetWorld().GetWorldTime());
|
||||
if (result < 0) { ERR_MedicalCombatCheck(false, "native medical treatment fixture"); return; }
|
||||
if (result == 0) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_MedicalCombatTick);
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: native full hunter crew, stocked medic, bleeding, finite treatment, epinephrine and protected lethal blood loss; injected medical states, not manual/multiplayer acceptance");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -7,6 +7,8 @@ class ERR_NativeMedicalProbe
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Casualty;
|
||||
protected IEntity m_Healer;
|
||||
protected IEntity m_Epinephrine;
|
||||
protected float m_BeforeEpi;
|
||||
protected IEntity m_Dressing;
|
||||
protected IEntity m_Saline;
|
||||
protected IEntity m_SpareSaline;
|
||||
@@ -110,12 +112,12 @@ class ERR_NativeMedicalProbe
|
||||
m_SpareSaline = Spawn("{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", anchor + "6 0 0");
|
||||
if (!Check(m_Casualty && m_Healer && m_Dressing && m_Saline && m_SpareSaline, "two isolated characters and finite stock-type medicine spawned")) return -1;
|
||||
if (!Check(!ERR_Actors.Player(m_Casualty) && !ERR_Actors.Player(m_Healer), "neither medical actor is a real player")) return -1;
|
||||
m_Deadline = now + 120000;
|
||||
m_Deadline = now + 240000;
|
||||
m_Wait = now + 1000;
|
||||
m_Step = 1;
|
||||
return 0;
|
||||
}
|
||||
if (!ERR_Actors.Alive(m_Casualty) || !ERR_Actors.Alive(m_Healer))
|
||||
if (m_Step < 9 && (!ERR_Actors.Alive(m_Casualty) || !ERR_Actors.Alive(m_Healer)))
|
||||
{
|
||||
Check(false, "owned casualty or healer died unexpectedly");
|
||||
return -1;
|
||||
@@ -127,6 +129,7 @@ class ERR_NativeMedicalProbe
|
||||
m_SalineStorage = SCR_SalineStorageComponent.Cast(m_Casualty.FindComponent(SCR_SalineStorageComponent));
|
||||
m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Healer.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
if (!Check(m_Damage && m_Controller && m_SalineStorage && m_Inventory, "native health, controller and medical storage components available")) return -1;
|
||||
m_Damage.ERR_EnableCasualtyProtection();
|
||||
m_Blood = m_Damage.GetBloodHitZone();
|
||||
if (!Check(m_Blood && !m_Controller.IsUnconscious() && !m_Damage.IsBleeding(), "casualty begins healthy and conscious")) return -1;
|
||||
if (!Check(m_Inventory.TryInsertItem(m_Dressing) && m_Inventory.TryInsertItem(m_Saline) && m_Inventory.TryInsertItem(m_SpareSaline), "finite medical items enter healer inventory")) return -1;
|
||||
@@ -177,6 +180,18 @@ class ERR_NativeMedicalProbe
|
||||
{
|
||||
if (!Check(!m_Dressing && !m_Damage.IsBleeding() && m_Damage.GetGroupDamageOverTime(ECharacterHitZoneGroup.LEFTARM, EDamageType.BLEEDING) == 0, "dressing is consumed and native bleeding is stopped")) return -1;
|
||||
if (!Check(m_Controller.IsUnconscious(), "bandage alone has not fabricated blood recovery")) return -1;
|
||||
m_SeedBlood = m_Blood.GetHealth();
|
||||
m_Wait = now + 35000; m_Step = 30; return 0;
|
||||
}
|
||||
if (m_Step == 30)
|
||||
{
|
||||
array<ref SCR_PersistentDamageEffect> diagnostics = m_Damage.GetAllPersistentEffectsOfType(SCR_DotDamageEffect, true);
|
||||
foreach (SCR_PersistentDamageEffect diagnostic : diagnostics)
|
||||
{
|
||||
DotDamageEffect dot = DotDamageEffect.Cast(diagnostic);
|
||||
PrintFormat("ERR MEDICAL EFFECT: type=%1 damageType=%2 dps=%3 zone=%4 bloodZone=%5 resilienceZone=%6", diagnostic.Type(), dot.GetDamageType(), dot.GetDPS(), dot.GetAffectedHitZone(), m_Blood, m_Damage.GetResilienceHitZone());
|
||||
}
|
||||
if (!Check(m_Blood.GetHealth() > m_SeedBlood && m_Controller.IsUnconscious() && m_Damage.ERR_CanRecoverNaturally(), "bandaged casualty regenerates blood slowly without saline and can still recover")) return -1;
|
||||
SCR_ConsumableItemComponent saline = SCR_ConsumableItemComponent.Cast(m_Saline.FindComponent(SCR_ConsumableItemComponent));
|
||||
SCR_ConsumableSalineBag effect = SCR_ConsumableSalineBag.Cast(saline.GetConsumableEffect());
|
||||
SCR_EConsumableFailReason reason;
|
||||
@@ -209,11 +224,39 @@ class ERR_NativeMedicalProbe
|
||||
if (m_Saline || AttachedBags() != 0 || SalineEffects() != 0 || m_Damage.GetGroupSalineBagged(ECharacterHitZoneGroup.RIGHTARM)) return 0;
|
||||
float requiredGain = Math.Min(m_RegenSpeed * 60, (m_Blood.GetMaxHealth() - m_PreInfusionBlood) * 0.8);
|
||||
if (!Check(m_RegenSpeed > 0 && m_Blood.GetHealth() - m_PreInfusionBlood >= requiredGain, "native infusion restores substantial blood without setting health again")) return -1;
|
||||
if (!Check(m_Recovered && !m_Controller.IsUnconscious() && ERR_Actors.CanFight(m_Casualty), "casualty regains consciousness through native health recovery")) return -1;
|
||||
if (!ERR_Actors.CanFight(m_Casualty)) return 0;
|
||||
if (!Check(m_Recovered && !m_Controller.IsUnconscious(), "bandaged casualty eventually wakes through slow native recovery without epinephrine")) return -1;
|
||||
if (!Check(now - m_InfusionStarted >= 85000 && !m_Saline && AttachedBags() == 0 && SalineEffects() == 0, "finite saline expires after native duration and releases treatment state")) return -1;
|
||||
if (!Check(m_SpareSaline != null, "unused spare survives the completed infusion")) return -1;
|
||||
InventoryItemComponent spareItem = InventoryItemComponent.Cast(m_SpareSaline.FindComponent(InventoryItemComponent));
|
||||
if (!Check(spareItem && spareItem.GetParentSlot(), "rejected spare bag remains a physical inventory item")) return -1;
|
||||
m_Damage.ForceUnconsciousness();
|
||||
m_Epinephrine = Spawn(ERR_EnemyMedicalCatalog.EPI, m_Healer.GetOrigin());
|
||||
if (!Check(m_Epinephrine && m_Inventory.TryInsertItem(m_Epinephrine), "finite epinephrine enters healer inventory")) return -1;
|
||||
m_Deadline = now + 45000; m_Wait = now + 1000; m_Step = 6; return 0;
|
||||
}
|
||||
if (m_Step == 6)
|
||||
{
|
||||
SCR_ConsumableItemComponent item = SCR_ConsumableItemComponent.Cast(m_Epinephrine.FindComponent(SCR_ConsumableItemComponent));
|
||||
ERR_ConsumableEpinephrine effect = ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect());
|
||||
SCR_EConsumableFailReason reason;
|
||||
if (!Check(effect && effect.CanApplyEffect(m_Casualty, m_Healer, reason), "unconscious casualty accepts actual epinephrine effect")) return -1;
|
||||
m_BeforeEpi = m_Damage.GetResilienceHitZone().GetHealth();
|
||||
item.ApplyItemEffect(m_Casualty, m_Healer, effect.GetAnimationParameters(m_Epinephrine, m_Casualty), m_Epinephrine, effect.GetDeleteOnUse());
|
||||
if (!Check(!effect.CanApplyEffect(m_Casualty, m_Healer, reason), "duplicate injection refused while treatment active")) return -1;
|
||||
m_Wait = now + 6000; m_Step = 7; return 0;
|
||||
}
|
||||
if (m_Step == 7)
|
||||
{
|
||||
if (!Check(!m_Epinephrine && m_Damage.GetResilienceHitZone().GetHealth() > m_BeforeEpi,
|
||||
"epinephrine is consumed and resilience recovers over time")) return -1;
|
||||
m_Damage.GetBloodHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
||||
m_Wait = now + 1000; m_Step = 9; return 0;
|
||||
}
|
||||
if (m_Step == 9)
|
||||
{
|
||||
if (!Check(ERR_Actors.Alive(m_Casualty) && m_Controller.IsUnconscious() && m_Blood.GetDamageState() != EDamageState.DESTROYED && m_Blood.GetHealthScaled() < 0.1,
|
||||
"otherwise fatal blood loss preserves the same unconscious treatable body without healing it")) return -1;
|
||||
m_Result = 1;
|
||||
Stop();
|
||||
Print("ERR MEDICAL RESULT: native blood loss, finite treatment and recovery; not manual action or multiplayer acceptance");
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifdef WORKBENCH
|
||||
// Force the reported camp-5 terrain refusal only in this opt-in native fixture.
|
||||
modded class ERR_CampTerrain
|
||||
{
|
||||
static bool s_ERROptionalSiteRefused;
|
||||
override bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight)
|
||||
{
|
||||
string outcome;
|
||||
if (site.Id == "everon-camp-5" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "optional-site-skip")
|
||||
{
|
||||
s_ERROptionalSiteRefused = true; return false;
|
||||
}
|
||||
return super.Site(site, position, heading, baseHeight);
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected float m_ERROptionalSiteDeadline;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "optional-site-skip") return;
|
||||
ERR_CampTerrain.s_ERROptionalSiteRefused = false;
|
||||
m_ERROptionalSiteDeadline = System.GetTickCount() + 240000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_OptionalSiteTick, 1000, true);
|
||||
}
|
||||
protected void ERR_OptionalSiteTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERROptionalSiteDeadline)
|
||||
{
|
||||
Print("ERR SMOKE FAIL: optional site skip startup timed out", LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_OptionalSiteTick); return;
|
||||
}
|
||||
if (Phase() != ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_OptionalSiteTick);
|
||||
if (!ERR_CampTerrain.s_ERROptionalSiteRefused || Sites().Find("everon-camp-5"))
|
||||
{ Print("ERR SMOKE FAIL: forced camp refusal must leave no spawned camp runtime", LogLevel.ERROR); return; }
|
||||
if (!Sites().Find("everon-prison-1") || !Sites().Find("everon-radio-1") || !Sites().Find("everon-extraction-1"))
|
||||
{ Print("ERR SMOKE FAIL: required mission sites missing after optional skip", LogLevel.ERROR); return; }
|
||||
for (int index = 1; index <= 4; index++)
|
||||
if (!Sites().Find("everon-mortar-" + index.ToString()))
|
||||
{ Print("ERR SMOKE FAIL: required mortar missing after optional skip", LogLevel.ERROR); return; }
|
||||
Print("ERR SMOKE PASS: forced camp-5 terrain/fallback failure omitted the camp before spawning");
|
||||
Print("ERR SMOKE PASS: required prison, radio, extraction and all four mortars retained");
|
||||
Print("ERR SMOKE PASS: mission preparation completed and a real player was admitted despite the skipped camp");
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: optional site refusal does not block actual mission preparation or player admission");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -40,17 +40,20 @@ class ERR_NativeRecoveryProbe
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!Check(m_Damage && m_Controller && m_Damage.ERR_IsRevivablePlayer(), "real admitted prisoner has player-only casualty protection")) return -1;
|
||||
if (!Check(m_Damage && m_Controller && m_Damage.ERR_IsRevivablePlayer(), "real admitted prisoner has scoped medical policy")) return -1;
|
||||
m_Damage.EnableDamageHandling(true);
|
||||
// Each exceeds full native health, including blood loss and a head injury.
|
||||
m_Damage.GetDefaultHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
||||
m_Damage.GetBloodHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
||||
m_Damage.GetHeadHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
||||
m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
||||
m_Wait = now + 1000; m_Step = 1; return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (!Check(ERR_Actors.Alive(m_Player) && m_Controller.IsUnconscious() && m_Damage.GetState() != EDamageState.DESTROYED && m_Damage.GetBloodHitZone().GetDamageState() != EDamageState.DESTROYED, "otherwise lethal health, head and blood damage leave the same living unconscious body")) return -1;
|
||||
if (!Check(ERR_Actors.Alive(m_Player) && m_Controller.IsUnconscious() && m_Damage.GetState() != EDamageState.DESTROYED && m_Damage.GetBloodHitZone().GetDamageState() != EDamageState.DESTROYED, "survivable knockout leaves the same living unconscious body")) return -1;
|
||||
m_Damage.FullHeal(); m_Damage.ForceUnconsciousness();
|
||||
m_Wait = now + 35000; m_Step = 10; return 0;
|
||||
}
|
||||
if (m_Step == 10)
|
||||
{
|
||||
if (!Check(m_Mode.Active() && (ERR_Actors.CanFight(m_Player) || m_Damage.ERR_CanRecoverNaturally()), "viable natural recovery keeps the all-unconscious round alive beyond the failure grace")) return -1;
|
||||
// Reset fixture injuries, then exercise treatment of an isolated knockout.
|
||||
m_Damage.FullHeal(); m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
||||
m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
||||
|
||||
@@ -16,6 +16,10 @@ class ERR_ObserverRules
|
||||
if (right && !previousRight) return 1;
|
||||
return 0;
|
||||
}
|
||||
static bool Toggle(bool enabled, bool pressed, bool previous)
|
||||
{
|
||||
return enabled && pressed && !previous;
|
||||
}
|
||||
static bool Casualty(bool active, bool currentBody, bool alive, bool conscious)
|
||||
{
|
||||
return active && currentBody && alive && !conscious;
|
||||
@@ -48,7 +52,7 @@ class ERR_ObserverGuidance
|
||||
}
|
||||
if (!survivorAvailable)
|
||||
return message + " No survivor is currently available to follow. The view will follow a survivor when one becomes available.";
|
||||
return message + " Press Left / Right arrow to change teammate (or /esc spectate next / /esc spectate previous). Use camera look controls to orbit and zoom controls to change distance.";
|
||||
return message + " Press Left / Right arrow to change teammate (or /esc spectate next / /esc spectate previous). Press Enter to switch between third-person and player view. Use camera look controls to orbit and zoom controls to change distance in third-person.";
|
||||
}
|
||||
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
||||
{
|
||||
@@ -65,18 +69,55 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
protected float m_Yaw;
|
||||
protected float m_Pitch = 20;
|
||||
protected float m_Distance = 5;
|
||||
protected bool m_LeftDown, m_RightDown;
|
||||
protected bool m_LeftDown, m_RightDown, m_EnterDown;
|
||||
protected bool m_PlayerView;
|
||||
protected bool m_InputContextActive;
|
||||
bool InputContextActive() { return m_InputContextActive; }
|
||||
bool IsPlayerView() { return m_PlayerView; }
|
||||
// Process one edge per press; menus still consume the state so holding a key
|
||||
// while closing a menu cannot unexpectedly change the followed teammate.
|
||||
void Input(bool enabled, bool left, bool right, bool enter)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
int cycle = ERR_ObserverRules.Cycle(enabled, left, right, m_LeftDown, m_RightDown);
|
||||
if (controller && cycle != 0) controller.ERR_CycleObserver(cycle);
|
||||
if (ERR_ObserverRules.Toggle(enabled, enter, m_EnterDown)) m_PlayerView = !m_PlayerView;
|
||||
m_LeftDown = left; m_RightDown = right; m_EnterDown = enter;
|
||||
}
|
||||
static bool PlayerView(IEntity target, out vector transform[4])
|
||||
{
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(target);
|
||||
if (!character || !character.GetAnimation()) return false;
|
||||
int head = character.GetAnimation().GetBoneIndex("Head");
|
||||
if (head < 0) return false;
|
||||
vector bone[4], world[4];
|
||||
if (!character.GetAnimation().GetBoneMatrix(head, bone)) return false;
|
||||
character.GetWorldTransform(world);
|
||||
vector position = bone[3].Multiply4(world);
|
||||
vector direction = world[2];
|
||||
CharacterControllerComponent controls = character.GetCharacterController();
|
||||
if (controls && controls.GetHeadAimingComponent()) direction = controls.GetHeadAimingComponent().GetAimingDirectionWorld();
|
||||
if (direction.LengthSq() < 0.01) direction = world[2];
|
||||
direction.Normalize();
|
||||
Math3D.DirectionAndUpMatrix(direction, "0 1 0", transform);
|
||||
// Just ahead of the face, following posture and vehicle seating. This reads
|
||||
// replicated pose; it does not possess the teammate or change their camera.
|
||||
transform[3] = position + direction * 0.15;
|
||||
return true;
|
||||
}
|
||||
override bool EOnCameraInit() { return true; }
|
||||
override void EOnCameraFrame(SCR_ManualCameraParam param)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller) return;
|
||||
bool left = (Debug.KeyState(KeyCode.KC_LEFT) & 32768) != 0;
|
||||
bool right = (Debug.KeyState(KeyCode.KC_RIGHT) & 32768) != 0;
|
||||
int cycle = ERR_ObserverRules.Cycle(param.isManualInputEnabled, left, right, m_LeftDown, m_RightDown);
|
||||
m_LeftDown = left; m_RightDown = right;
|
||||
if (cycle != 0) controller.ERR_CycleObserver(cycle);
|
||||
m_InputContextActive = GetInputManager().IsContextActive("ERR_ObserverContext");
|
||||
Input(param.isManualInputEnabled, GetInputManager().GetActionValue("ERR_ObserverPrevious") > 0.5, GetInputManager().GetActionValue("ERR_ObserverNext") > 0.5, GetInputManager().GetActionValue("ERR_ObserverView") > 0.5);
|
||||
IEntity target = controller.ERR_ObservedEntity();
|
||||
if (m_PlayerView && target && PlayerView(target, param.transform))
|
||||
{
|
||||
param.isDirty = true;
|
||||
return;
|
||||
}
|
||||
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
||||
vector position = aim + "0 4 -6";
|
||||
IEntity exclude = target;
|
||||
@@ -147,6 +188,8 @@ modded class SCR_PlayerController
|
||||
protected ERR_ObserverReason m_ERRShownObserverReason;
|
||||
protected bool m_ERRShownObserverAvailable;
|
||||
protected float m_ERRLastCycle = -1000;
|
||||
protected IEntity m_ERRRecoveryBody;
|
||||
protected float m_ERRRecoveryDeadline = -1;
|
||||
|
||||
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
|
||||
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
|
||||
@@ -266,6 +309,7 @@ modded class SCR_PlayerController
|
||||
protected void ERR_UpdateObserverClient()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_UpdateRecoveryCamera(mode);
|
||||
bool observe = mode && ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
|
||||
if (!observe)
|
||||
{
|
||||
@@ -299,6 +343,31 @@ modded class SCR_PlayerController
|
||||
m_ERRShownObserverAvailable = survivorAvailable;
|
||||
}
|
||||
}
|
||||
// Track the actual local body even when an editor/other camera owns the view.
|
||||
// Run once per recovery, with retries for asynchronous editor close/camera setup.
|
||||
protected void ERR_UpdateRecoveryCamera(ERR_GameMode mode)
|
||||
{
|
||||
IEntity body = GetControlledEntity();
|
||||
if (!mode || !mode.Active() || !ERR_Actors.Alive(body))
|
||||
{ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1; return; }
|
||||
if (!ERR_Actors.CanFight(body))
|
||||
{ m_ERRRecoveryBody = body; m_ERRRecoveryDeadline = -1; return; }
|
||||
if (!m_ERRRecoveryBody) return;
|
||||
if (body != m_ERRRecoveryBody)
|
||||
{ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1; return; }
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (m_ERRRecoveryDeadline < 0) m_ERRRecoveryDeadline = now + 10000;
|
||||
SCR_EditorManagerEntity editor = SCR_EditorManagerEntity.GetInstance();
|
||||
if (editor && editor.IsOpened()) editor.Close(false);
|
||||
CameraManager manager = GetGame().GetCameraManager();
|
||||
CameraBase playerCamera = GetPlayerCamera();
|
||||
if (manager && playerCamera) manager.SetCamera(playerCamera);
|
||||
if ((manager && playerCamera && manager.CurrentCamera() == playerCamera && (!editor || !editor.IsOpened())) || now >= m_ERRRecoveryDeadline)
|
||||
{
|
||||
if (now >= m_ERRRecoveryDeadline) Print("Reintroduced [spectator]: recovery camera return timed out waiting for editor/camera; player body remains controlled", LogLevel.WARNING);
|
||||
m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1;
|
||||
}
|
||||
}
|
||||
protected void ERR_CloseObserverCamera()
|
||||
{
|
||||
m_ERRObserverReady = false;
|
||||
|
||||
@@ -19,6 +19,7 @@ modded class SCR_CharacterDamageManagerComponent
|
||||
if (!Replication.IsServer()) return;
|
||||
m_ERRRevivablePlayer = true;
|
||||
SetPermitUnconsciousness(true, true);
|
||||
SetRegenScale(0.25, true);
|
||||
}
|
||||
bool ERR_IsRevivablePlayer() { return m_ERRRevivablePlayer; }
|
||||
|
||||
@@ -44,11 +45,34 @@ modded class SCR_CharacterDamageManagerComponent
|
||||
if (!m_ERRRevivablePlayer) { super.Kill(instigator); return; }
|
||||
ERR_RequestKnockout();
|
||||
}
|
||||
// Include delayed but viable native regeneration when deciding team defeat.
|
||||
// A stopped/zero-rate effect or an actively bleeding casualty cannot self-rescue.
|
||||
bool ERR_CanRecoverNaturally()
|
||||
{
|
||||
if (!m_ERRRevivablePlayer || IsBleeding()) return false;
|
||||
array<ref SCR_PersistentDamageEffect> effects = GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect, true);
|
||||
foreach (SCR_PersistentDamageEffect effect : effects)
|
||||
{
|
||||
DotDamageEffect dot = DotDamageEffect.Cast(effect);
|
||||
if (!dot || dot.GetDamageType() != EDamageType.REGENERATION || dot.GetDPS() >= 0) continue;
|
||||
HitZone zone = dot.GetAffectedHitZone();
|
||||
if (!zone || zone.GetHealthScaled() >= 1) continue;
|
||||
if (zone == GetBloodHitZone()) return true;
|
||||
if (zone == GetResilienceHitZone() && GetResilienceRegenScale() > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// A player always retains a living, treatable body.
|
||||
override protected bool IsIndefinitelyUnconscious(bool onlyPlayers = true)
|
||||
{
|
||||
if (m_ERRRevivablePlayer) return false;
|
||||
return super.IsIndefinitelyUnconscious(onlyPlayers);
|
||||
}
|
||||
override float GetResilienceRegenScale()
|
||||
{
|
||||
if (m_ERRRevivablePlayer && IsBleeding()) return 0;
|
||||
return super.GetResilienceRegenScale();
|
||||
}
|
||||
bool ERR_TreatmentInProgress()
|
||||
{
|
||||
if (m_aBeingHealedGroup && !m_aBeingHealedGroup.IsEmpty()) return true;
|
||||
@@ -56,8 +80,7 @@ modded class SCR_CharacterDamageManagerComponent
|
||||
foreach (SCR_PersistentDamageEffect effect : effects)
|
||||
{
|
||||
DotDamageEffect dot = DotDamageEffect.Cast(effect);
|
||||
// Includes saline and ACE epinephrine. Passive regeneration is checked by
|
||||
// observed progress, so a stalled regeneration effect cannot block defeat.
|
||||
// Includes saline and epinephrine; passive regeneration is evaluated separately.
|
||||
if (dot && dot.GetDamageType() == EDamageType.HEALING && dot.GetDPS() < 0) return true;
|
||||
}
|
||||
return false;
|
||||
@@ -77,3 +100,16 @@ modded class SCR_HitZone
|
||||
return allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Native resilience ignores the per-character regeneration setting. Apply the
|
||||
// same slow rate here, only to passive recovery; injected medicine keeps its DPS.
|
||||
modded class SCR_CharacterResilienceHitZone
|
||||
{
|
||||
override float CalculatePassiveRegenDPS(bool considerRegenScale = true)
|
||||
{
|
||||
float rate = super.CalculatePassiveRegenDPS(considerRegenScale);
|
||||
SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());
|
||||
if (considerRegenScale && manager && manager.ERR_IsRevivablePlayer()) return rate * 0.25;
|
||||
return rate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ class ERR_RecoveryService
|
||||
protected float m_StalledSince = -1;
|
||||
protected ref map<IEntity, ref ERR_RecoverySample> m_Samples = new map<IEntity, ref ERR_RecoverySample>();
|
||||
protected string m_Message;
|
||||
protected float m_NextMedicalLog;
|
||||
protected string m_MedicalDetails;
|
||||
protected bool m_DefeatLogged;
|
||||
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
||||
ScriptInvoker OnChanged() { return m_OnChanged; }
|
||||
@@ -46,11 +49,29 @@ class ERR_RecoveryService
|
||||
int Incapacitated() { return m_Incapacitated; }
|
||||
int Reserved() { return m_Reserved; }
|
||||
string Message() { return m_Message; }
|
||||
bool DefeatDue(float now) { return ERR_RecoveryRules.DefeatDue(m_StalledSince, now); }
|
||||
bool DefeatDue(float now)
|
||||
{
|
||||
bool due = ERR_RecoveryRules.DefeatDue(m_StalledSince, now);
|
||||
if (due && !m_DefeatLogged)
|
||||
{
|
||||
m_DefeatLogged = true;
|
||||
ERR_HQ.Event("MEDICAL DEFEAT", "30 seconds without a viable rescuer or recovery; " + m_MedicalDetails);
|
||||
}
|
||||
return due;
|
||||
}
|
||||
|
||||
float StalledSeconds(float now)
|
||||
{
|
||||
if (m_StalledSince < 0) return 0;
|
||||
return Math.Max(0, now - m_StalledSince) / 1000;
|
||||
}
|
||||
|
||||
void Tick()
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
bool logMedical = now >= m_NextMedicalLog;
|
||||
string medicalDetails;
|
||||
int conscious, incapacitated, reserved, reservedRescuers, recovering;
|
||||
map<IEntity, ref ERR_RecoverySample> nextSamples = new map<IEntity, ref ERR_RecoverySample>();
|
||||
array<ref ERR_PlayerLife> lives = {};
|
||||
@@ -76,22 +97,30 @@ class ERR_RecoveryService
|
||||
if (medical.GetResilienceHitZone()) sample.Resilience = medical.GetResilienceHitZone().GetHealth();
|
||||
ERR_RecoverySample previous = m_Samples.Get(life.Actor);
|
||||
bool improving = previous && (sample.Blood > previous.Blood + 0.001 || sample.Resilience > previous.Resilience + 0.001);
|
||||
if (improving || medical.ERR_TreatmentInProgress()) recovering++;
|
||||
bool treatment = medical.ERR_TreatmentInProgress();
|
||||
bool natural = medical.ERR_CanRecoverNaturally();
|
||||
if (improving || treatment || natural) recovering++;
|
||||
medicalDetails += string.Format("player=%1 connected=%2 blood=%3 resilience=%4 bleeding=%5 improving=%6 treatment=%7 naturalRecovery=%8; ", life.ConnectionId, life.Connected, sample.Blood, sample.Resilience, medical.IsBleeding(), improving, treatment, natural);
|
||||
nextSamples.Insert(life.Actor, sample);
|
||||
}
|
||||
m_MedicalDetails = medicalDetails;
|
||||
m_Samples = nextSamples;
|
||||
m_Conscious = conscious;
|
||||
m_Incapacitated = incapacitated;
|
||||
m_Reserved = reserved;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
bool stalled = ERR_RecoveryRules.Stalled(conscious, incapacitated, reservedRescuers, recovering, GetGame().GetPlayerManager().GetPlayerCount() > 0);
|
||||
if (!stalled) m_StalledSince = -1;
|
||||
else if (m_StalledSince < 0 || now < m_StalledSince) m_StalledSince = now;
|
||||
if (logMedical && incapacitated > 0)
|
||||
{
|
||||
m_NextMedicalLog = now + 10000;
|
||||
ERR_HQ.Event("MEDICAL STATUS", string.Format("conscious=%1 unconscious=%2 reservedRescuers=%3 recovering=%4 defeatTimer=%5/30s; %6", conscious, incapacitated, reservedRescuers, recovering, StalledSeconds(now), medicalDetails));
|
||||
}
|
||||
string message;
|
||||
if (ERR_RecoveryRules.NeedsRescuer(conscious, incapacitated))
|
||||
{
|
||||
message = "No conscious prisoner is currently available to help.";
|
||||
if (recovering > 0) message += " Treatment or recovery is progressing; the round continues.";
|
||||
if (recovering > 0) message += " Treatment or natural recovery is possible; the round continues.";
|
||||
else if (reservedRescuers > 0) message += " A conscious prisoner's reconnection may provide help.";
|
||||
else message += " Everyone is unconscious with no treatment or recovery progressing. The mission fails after 30 uninterrupted seconds in this state.";
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
m_Started = true;
|
||||
#ifdef WORKBENCH
|
||||
string historicalFixture;
|
||||
if (System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||
if (System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||
#endif
|
||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||
@@ -38,7 +38,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
#ifdef WORKBENCH
|
||||
// Existing deterministic probes intentionally use authored coordinates.
|
||||
string fixture;
|
||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
||||
#endif
|
||||
m_Generated = new ERR_GeneratedSites();
|
||||
|
||||
@@ -11,6 +11,11 @@ modded class SCR_PlayerController
|
||||
if (m_ERRCameraProbeTarget && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "unconscious-camera") return m_ERRCameraProbeTarget;
|
||||
return super.ERR_ObservedEntity();
|
||||
}
|
||||
ERR_FollowSurvivorCameraComponent ERR_ProbeFollowComponent()
|
||||
{
|
||||
if (!m_ERRObserverCamera) return null;
|
||||
return ERR_FollowSurvivorCameraComponent.Cast(m_ERRObserverCamera.FindCameraComponent(ERR_FollowSurvivorCameraComponent));
|
||||
}
|
||||
bool ERR_ProbeCameraActive()
|
||||
{
|
||||
return m_ERRObserverCamera && GetGame().GetCameraManager().CurrentCamera() == m_ERRObserverCamera;
|
||||
@@ -24,6 +29,7 @@ modded class ERR_GameMode
|
||||
protected float m_ERRCameraWait;
|
||||
protected IEntity m_ERRCameraBody, m_ERRCameraTarget;
|
||||
protected SCR_CharacterDamageManagerComponent m_ERRCameraDamage;
|
||||
protected SCR_ManualCamera m_ERRCameraOther;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
@@ -93,6 +99,7 @@ modded class ERR_GameMode
|
||||
if (m_ERRCameraStage == 2)
|
||||
{
|
||||
vector camera = GetGame().GetCameraManager().CurrentCamera().GetOrigin();
|
||||
PrintFormat("ERR CAMERA TRACK: camera=%1 target=%2 body=%3 active=%4 conscious=%5 reason=%6", camera, m_ERRCameraTarget.GetOrigin(), m_ERRCameraBody.GetOrigin(), pc.ERR_ProbeCameraActive(), ERR_Actors.CanFight(m_ERRCameraBody), pc.ERR_ObserverState());
|
||||
if (!ERR_CameraCheck(vector.Distance(camera, m_ERRCameraTarget.GetOrigin()) < 13 && vector.Distance(camera, m_ERRCameraBody.GetOrigin()) > 15, "camera follows target in 3D without moving the casualty")) return;
|
||||
m_ERRCameraTarget.SetOrigin(ERR_Actors.Ground(m_ERRCameraTarget.GetOrigin() + "15 0 0"));
|
||||
m_ERRCameraStage = 3; m_ERRCameraWait = now + 1500; return;
|
||||
@@ -100,6 +107,39 @@ modded class ERR_GameMode
|
||||
if (m_ERRCameraStage == 3)
|
||||
{
|
||||
if (!ERR_CameraCheck(vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), m_ERRCameraTarget.GetOrigin()) < 13, "camera tracks a moving target")) return;
|
||||
InputManager input = GetGame().GetInputManager();
|
||||
ERR_FollowSurvivorCameraComponent follow = pc.ERR_ProbeFollowComponent();
|
||||
if (!ERR_CameraCheck(follow != null, "follow component exists")) return;
|
||||
ResourceName configuredInput;
|
||||
GameProject.GetModuleConfig("InputManagerSettings").Get("Default", configuredInput);
|
||||
PrintFormat("ERR CAMERA INPUT: default=%1 context=%2 previous=%3 next=%4 view=%5 orbit=%6", configuredInput, input.IsContextActive("ERR_ObserverContext"), input.IsActionActive("ERR_ObserverPrevious"), input.IsActionActive("ERR_ObserverNext"), input.IsActionActive("ERR_ObserverView"), input.IsActionActive("ManualCameraRotateYaw"));
|
||||
for (int inputIndex = 0; inputIndex < input.GetActionCount(); inputIndex++)
|
||||
{
|
||||
string inputName = input.GetActionName(inputIndex);
|
||||
if (inputName.Contains("Observer") || inputName == "ManualCameraRotateYaw") Print("ERR CAMERA INPUT REGISTERED: " + inputName);
|
||||
}
|
||||
if (!ERR_CameraCheck(follow.InputContextActive() && input.IsActionActive("ERR_ObserverPrevious") && input.IsActionActive("ERR_ObserverNext") && input.IsActionActive("ERR_ObserverView") && input.IsActionActive("ManualCameraRotateYaw"), "dedicated arrow/Enter actions and inherited orbit inputs loaded and active")) return;
|
||||
follow.Input(true, false, false, true);
|
||||
if (!ERR_CameraCheck(follow.IsPlayerView(), "Enter handler selects player view")) return;
|
||||
follow.Input(true, false, false, true);
|
||||
if (!ERR_CameraCheck(follow.IsPlayerView(), "held Enter does not toggle again")) return;
|
||||
follow.Input(true, false, false, false);
|
||||
m_ERRCameraStage = 30; m_ERRCameraWait = now + 1000; return;
|
||||
}
|
||||
if (m_ERRCameraStage == 30)
|
||||
{
|
||||
vector expected[4];
|
||||
if (!ERR_CameraCheck(ERR_FollowSurvivorCameraComponent.PlayerView(m_ERRCameraTarget, expected) && vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), expected[3]) < 0.1, "native camera follows teammate head in player view")) return;
|
||||
pc.ERR_ProbeFollowComponent().Input(true, false, false, true);
|
||||
pc.ERR_ProbeFollowComponent().Input(true, false, false, false);
|
||||
m_ERRCameraStage = 31; m_ERRCameraWait = now + 1000; return;
|
||||
}
|
||||
if (m_ERRCameraStage == 31)
|
||||
{
|
||||
if (!ERR_CameraCheck(!pc.ERR_ProbeFollowComponent().IsPlayerView() && vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), m_ERRCameraTarget.GetOrigin()) > 2, "second Enter restores orbit view")) return;
|
||||
Resource otherResource = Resource.Load("{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et");
|
||||
m_ERRCameraOther = SCR_ManualCamera.Cast(GetGame().SpawnEntityPrefab(otherResource, GetWorld()));
|
||||
if (!ERR_CameraCheck(m_ERRCameraOther && GetGame().GetCameraManager().CurrentCamera() == m_ERRCameraOther && !pc.ERR_ProbeCameraActive(), "separate manual camera takes over before recovery")) return;
|
||||
pc.ERR_ProbeCameraTarget(null); ERR_Actors.DeleteSpawned(m_ERRCameraTarget); m_ERRCameraTarget = null;
|
||||
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||
m_ERRCameraDamage.FullHeal(); m_ERRCameraStage = 4; m_ERRCameraWait = now + 4000; return;
|
||||
@@ -108,6 +148,7 @@ modded class ERR_GameMode
|
||||
{
|
||||
if (!ERR_CameraCheck(ERR_Actors.CanFight(m_ERRCameraBody) && pc.GetControlledEntity() == m_ERRCameraBody && pc.ERR_ObserverState() == ERR_ObserverReason.NONE && !pc.ERR_HasObserverCamera(), "native healing closes spectator and retains the original conscious player")) return;
|
||||
if (!ERR_CameraCheck(GetGame().GetCameraManager().CurrentCamera() == pc.GetPlayerCamera(), "normal player camera restored")) return;
|
||||
if (m_ERRCameraOther) { m_ERRCameraOther.Terminate(); delete m_ERRCameraOther; m_ERRCameraOther = null; }
|
||||
if (!ERR_CameraCheck(character.GetWeaponManagerComponent().GetCurrentWeapon() && character.GetWeaponManagerComponent().GetCurrentWeapon().GetOwner() == m_ERRCameraWeapon, "revival equips the same rifle without ground pickup")) return;
|
||||
if (!ERR_CameraCheck(Settings().Set("keepWeaponOnUnconscious", 0), "retention disabled through persistent setting")) return;
|
||||
ERR_CameraKnockout(); m_ERRCameraStage = 5; m_ERRCameraWait = now + 3000; return;
|
||||
|
||||
@@ -22,7 +22,7 @@ class ERR_AirSupportPolicy
|
||||
float ConsideringSince = -1;
|
||||
float NextDrop;
|
||||
string Reason = "waiting for delivered intelligence";
|
||||
static string Refusal(ERR_AirSupportSituation s)
|
||||
static string Refusal(ERR_AirSupportSituation s, bool checkAirspace = true)
|
||||
{
|
||||
if (!s || !s.AircraftReady) return "no ready helicopter passenger load";
|
||||
if (!ERR_HQMemoryRules.Finite(s.Age) || !ERR_HQMemoryRules.Finite(s.Confidence) || !ERR_HQMemoryRules.Finite(s.Radius) || !ERR_HQMemoryRules.Finite(s.Speed)) return "invalid memory sample";
|
||||
@@ -31,13 +31,16 @@ class ERR_AirSupportPolicy
|
||||
int needed = Math.Clamp(s.Spotted * 2, 4, 12);
|
||||
if (s.CommittedFit >= needed) return "ground forces already committed in sufficient strength";
|
||||
if (s.GroundFit + s.CommittedFit >= needed && s.ConfirmedLosses < 2) return "nearby ground forces can handle this contact";
|
||||
if (s.ArtilleryBusy) return "mortar order or shells occupy the approach area";
|
||||
if (checkAirspace && s.ArtilleryBusy) return "mortar order or shells occupy the approach area";
|
||||
return "";
|
||||
}
|
||||
bool Consider(ERR_AirSupportSituation s, float now)
|
||||
{
|
||||
if (!ERR_HQMemoryRules.Finite(now) || now < 0) return false;
|
||||
string refusal = Refusal(s);
|
||||
// Artillery temporarily closes the approach; it does not invalidate fresh
|
||||
// contact history or the confirmed lack of ground support. Restarting the
|
||||
// timer on every salvo could prevent a drop through every clear interval.
|
||||
string refusal = Refusal(s, false);
|
||||
if (!refusal.IsEmpty()) { Candidate = 0;
|
||||
ConsideringSince = -1;
|
||||
Reason = refusal;
|
||||
@@ -49,6 +52,9 @@ class ERR_AirSupportPolicy
|
||||
if (Candidate != s.BeliefId || ConsideringSince < 0) { Candidate = s.BeliefId;
|
||||
ConsideringSince = now;
|
||||
}
|
||||
if (s.ArtilleryBusy) { Reason = Refusal(s);
|
||||
return false;
|
||||
}
|
||||
float delay = 45000;
|
||||
if (s.ConfirmedLosses >= 2) delay = 20000;
|
||||
if (now - ConsideringSince < delay) { Reason = "confirming persistent contact and ground-force shortfall";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Bounded retries only after boarding/approach progress stops. Reissuing a
|
||||
// waypoint while soldiers are still closing on the truck interrupts native AI.
|
||||
class ERR_ConvoyBoardingProgress
|
||||
{
|
||||
int Seated = -1;
|
||||
float Distance = -1;
|
||||
float LastProgress;
|
||||
float NextRetry;
|
||||
int Retries;
|
||||
bool Retry(int seated, float distance, float now)
|
||||
{
|
||||
if (Seated != seated || Distance < 0 || distance <= Distance - 5)
|
||||
{
|
||||
Seated = seated; Distance = distance; LastProgress = now;
|
||||
return false;
|
||||
}
|
||||
if (Retries >= 3 || now - LastProgress < 30000 || now < NextRetry) return false;
|
||||
Retries++; NextRetry = now + 30000;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ class ERR_ConvoyRoadPlan
|
||||
}
|
||||
return route.Count() < bestRoute.Count();
|
||||
}
|
||||
static bool Build(vector seed, out array<vector> positions, out array<vector> angles, out ERR_ForceRoute route)
|
||||
static bool Build(vector seed, out array<vector> positions, out array<vector> angles, out ERR_ForceRoute route, array<vector> excludedStarts = null, float separation = 0)
|
||||
{
|
||||
positions = {}; angles = {}; route = null;
|
||||
if (!Replication.IsServer() || !ERR_ForceRules.FiniteVector(seed)) return false;
|
||||
@@ -144,6 +144,10 @@ class ERR_ConvoyRoadPlan
|
||||
ERR_ForceRoute polyline;
|
||||
float distance;
|
||||
if (!FromPolyline(points, seed, candidatePositions, candidateAngles, polyline, distance, candidateIndex)) continue;
|
||||
// Exclude occupied deployment areas before selecting the best formation.
|
||||
// Returning the nearest clear-but-too-close road used to hide valid
|
||||
// alternatives from startup's outer separation check.
|
||||
if (excludedStarts && !ERR_SupplyConvoyRules.Separated(candidatePositions[0], excludedStarts, separation)) continue;
|
||||
for (int i = 0; i < candidatePositions.Count(); i++) candidatePositions[i] = ERR_Actors.Ground(candidatePositions[i]);
|
||||
ERR_ForceRoute grounded = new ERR_ForceRoute();
|
||||
bool valid = true;
|
||||
|
||||
@@ -80,17 +80,21 @@ class ERR_EnemyEquipmentEntry
|
||||
ref ERR_EnemyOptics Optics;
|
||||
ref ERR_EnemyLauncherGrant Launcher = new ERR_EnemyLauncherGrant();
|
||||
ref ERR_EnemyMineGrant Mines = new ERR_EnemyMineGrant();
|
||||
ref ERR_EnemyMedicine Medicine = new ERR_EnemyMedicine();
|
||||
int SkillChecks;
|
||||
bool Released;
|
||||
void Check(ERR_GameMode mode)
|
||||
{
|
||||
Medicine.Check(mode, Group, Actor);
|
||||
// Medic storage is for treatment; do not consume it with optional mine/AA grants.
|
||||
if (Actor && Actor.GetPrefabData() && ERR_EnemyMedicalCatalog.Medic(Actor.GetPrefabData().GetPrefabName())) return;
|
||||
Launcher.Check(mode, Group, Actor);
|
||||
Mines.Check(mode, Group, Actor);
|
||||
}
|
||||
|
||||
void Stop(ERR_GameMode mode)
|
||||
{
|
||||
Launcher.Stop(); Mines.Stop();
|
||||
Launcher.Stop(); Mines.Stop(); Medicine.Stop();
|
||||
Released = true;
|
||||
if (Optics) Optics.Stop();
|
||||
if (Combat) Combat.ERR_ReleaseEnemySkill(mode);
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
class ERR_EnemyMedicalCatalog
|
||||
{
|
||||
static const ResourceName EPI = "{C68512DB5FC74698}Prefabs/ERR/Epinephrine.et";
|
||||
static const ResourceName MORPHINE = "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et";
|
||||
static bool Specialist(string path)
|
||||
{
|
||||
path.ToLower();
|
||||
return path.Contains("_sf") || path.Contains("spetsnaz") || path.Contains("greenberets") || path.Contains("special") || path.Contains("recon") || path.Contains("marsoc");
|
||||
}
|
||||
static bool Medic(string path) { path.ToLower(); return path.Contains("medic"); }
|
||||
static ResourceName Crew(ERR_GameMode mode, bool medic)
|
||||
{
|
||||
string key = mode.EnemyFaction().GetFactionKey();
|
||||
if (key == "USSR")
|
||||
{
|
||||
if (medic) return "{8CA70597606992EC}Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF_Medic.et";
|
||||
return "{2DB452B3EC386B92}Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF.et";
|
||||
}
|
||||
if (key == "US")
|
||||
{
|
||||
if (medic) return "{9A234C5857D92187}Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF_Medic.et";
|
||||
return "{36E28C628A2F83CB}Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF.et";
|
||||
}
|
||||
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||
if (definition && definition.Options)
|
||||
foreach (ERR_ContentOption option : definition.Options)
|
||||
{
|
||||
if (!option || option.NativeFactionKey != key) continue;
|
||||
ResourceName configured = option.ResponseSoldier;
|
||||
if (medic) configured = option.ResponseMedic;
|
||||
if (ERR_EnemyLoadoutCatalog.Available(configured)) return configured;
|
||||
}
|
||||
SCR_Faction faction = SCR_Faction.Cast(mode.EnemyFaction());
|
||||
SCR_EntityCatalog catalog;
|
||||
if (faction) catalog = faction.GetFactionEntityCatalogOfType(EEntityCatalogType.CHARACTER, false);
|
||||
array<SCR_EntityCatalogEntry> entries = {};
|
||||
if (catalog) catalog.GetEntityList(entries);
|
||||
ResourceName fallback;
|
||||
for (int i = 0; i < Math.Min(entries.Count(), 4096); i++)
|
||||
{
|
||||
SCR_EntityCatalogEntry entry = entries[i];
|
||||
if (!entry || !entry.IsEnabled()) continue;
|
||||
ResourceName prefab = entry.GetPrefab();
|
||||
if (Medic(prefab) != medic || !ERR_EnemyLoadoutCatalog.Available(prefab)) continue;
|
||||
if (Specialist(prefab)) return prefab;
|
||||
if (fallback.IsEmpty()) fallback = prefab;
|
||||
}
|
||||
if (!fallback.IsEmpty()) return fallback;
|
||||
// Never spawn a medic belonging to the wrong faction.
|
||||
if (medic) return "";
|
||||
return mode.FactionSelection().Soldier();
|
||||
}
|
||||
}
|
||||
|
||||
// One finite startup allowance per physical actor, never a periodic refill.
|
||||
class ERR_EnemyMedicine
|
||||
{
|
||||
protected bool m_Started;
|
||||
protected bool m_Done;
|
||||
protected int m_Epi;
|
||||
protected int m_Morphine;
|
||||
protected int m_Checks;
|
||||
protected int m_Issued;
|
||||
protected bool m_Medic;
|
||||
protected ref ERR_EnemyEquipmentCallback m_Operation;
|
||||
void Stop() { m_Done = true; }
|
||||
protected void Finish(IEntity actor, string reason)
|
||||
{
|
||||
m_Done = true;
|
||||
ERR_HQ.Event("MEDICAL EQUIPMENT", string.Format("soldier=%1 position=%2 medic=%3 added=%4; %5", actor, actor.GetOrigin(), m_Medic, m_Issued, reason));
|
||||
}
|
||||
void Check(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||
{
|
||||
if (m_Done || !ERR_EnemyEquipmentRules.Eligible(mode, group, actor)) return;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
if (++m_Checks > 30) { Finish(actor, "startup deadline; no replacement of pending/looted medicine"); return; }
|
||||
if (!inventory) return;
|
||||
array<IEntity> items = {}; inventory.GetItems(items);
|
||||
if (items.Count() > 512) { Finish(actor, "inventory inspection limit"); return; }
|
||||
if (!m_Started)
|
||||
{
|
||||
m_Started = true;
|
||||
string prefab;
|
||||
if (actor.GetPrefabData()) prefab = actor.GetPrefabData().GetPrefabName();
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
bool special = ERR_EnemyMedicalCatalog.Specialist(prefab) || (hq && hq.Hunters().Contains(group));
|
||||
m_Medic = ERR_EnemyMedicalCatalog.Medic(prefab);
|
||||
if (!special && !m_Medic && Math.RandomInt(0, 100) >= 25) { m_Done = true; return; }
|
||||
m_Epi = 1; m_Morphine = 1;
|
||||
if (special) { m_Epi = 2; m_Morphine = 2; }
|
||||
if (m_Medic) { m_Epi = 6; m_Morphine = 6; }
|
||||
foreach (IEntity existing : items)
|
||||
{
|
||||
if (!existing || !existing.GetPrefabData()) continue;
|
||||
ResourceName name = existing.GetPrefabData().GetPrefabName();
|
||||
if (name == ERR_EnemyMedicalCatalog.EPI) m_Epi--;
|
||||
if (name == ERR_EnemyMedicalCatalog.MORPHINE) m_Morphine--;
|
||||
}
|
||||
}
|
||||
if (m_Operation)
|
||||
{
|
||||
if (!m_Operation.Finished) return;
|
||||
if (!m_Operation.Succeeded) { Finish(actor, "native insertion failed; no loose or duplicate medicine"); return; }
|
||||
m_Issued++; m_Operation = null;
|
||||
}
|
||||
if (m_Epi <= 0 && m_Morphine <= 0) { Finish(actor, "finite medicine stocked; authored bandages, saline and medical bag retained"); return; }
|
||||
ResourceName next = ERR_EnemyMedicalCatalog.MORPHINE;
|
||||
if (m_Epi > 0) { next = ERR_EnemyMedicalCatalog.EPI; m_Epi--; }
|
||||
else m_Morphine--;
|
||||
BaseInventoryStorageComponent bag;
|
||||
if (m_Medic)
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.GetPrefabData()) continue;
|
||||
string path = item.GetPrefabData().GetPrefabName(); path.ToLower();
|
||||
if (!path.Contains("backpack")) continue;
|
||||
bag = BaseInventoryStorageComponent.Cast(item.FindComponent(BaseInventoryStorageComponent));
|
||||
if (bag) break;
|
||||
}
|
||||
m_Operation = new ERR_EnemyEquipmentCallback();
|
||||
if (inventory.TrySpawnPrefabToStorage(next, bag, -1, EStoragePurpose.PURPOSE_DEPOSIT, m_Operation)) return;
|
||||
// The native medical bag may already be physically full. A refused operation
|
||||
// has inserted nothing: put the same finite dose into a free personal pouch.
|
||||
if (bag)
|
||||
{
|
||||
m_Operation = new ERR_EnemyEquipmentCallback();
|
||||
if (inventory.TrySpawnPrefabToStorage(next, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, m_Operation)) return;
|
||||
}
|
||||
Finish(actor, "medicine did not fit; existing loadout preserved");
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,12 @@ class ERR_EnemyRadio
|
||||
array<IEntity> items = {}; inventory.GetItems(items);
|
||||
if (items.Count() > 256) return -1;
|
||||
int capability;
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
if (hq && hq.Hunters()) capability = hq.Hunters().VehicleRadioCapability(actor);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
||||
capability = 1;
|
||||
capability = Math.Max(capability, 1);
|
||||
SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||
if (radio && radio.ERR_IsHQManpack()) return 2;
|
||||
}
|
||||
|
||||
@@ -30,9 +30,10 @@ modded class SCR_AIGetAimErrorOffset
|
||||
{
|
||||
override float GetRandomFactor(EAISkill skill, float mu)
|
||||
{
|
||||
// CYLON normally eliminates aim error. Keep the ordinary REGULAR distribution
|
||||
// for these soldiers while leaving skill-dependent decisions at maximum.
|
||||
if (m_CombatComponent && m_CombatComponent.ERR_EnhancedEnemy()) skill = EAISkill.REGULAR;
|
||||
// Maximum decision skill, with REGULAR aim dispersion tightened by 10%.
|
||||
// Preserve the requested mean; only the random error around it is reduced.
|
||||
if (m_CombatComponent && m_CombatComponent.ERR_EnhancedEnemy())
|
||||
return mu + (super.GetRandomFactor(EAISkill.REGULAR, mu) - mu) * 0.9;
|
||||
return super.GetRandomFactor(skill, mu);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ class ERR_HQUnit
|
||||
SCR_AIGroupUtilityComponent Utility;
|
||||
SCR_AIGroupPerception Detection;
|
||||
float NextDetectionLog;
|
||||
ref ERR_HQLocalAwareness LocalAwareness = new ERR_HQLocalAwareness();
|
||||
float NextContact;
|
||||
ref ERR_InfantryCombat InfantryCombat = new ERR_InfantryCombat();
|
||||
IEntity Radio;
|
||||
float NextReport;
|
||||
ref ERR_HQRoute Uplink;
|
||||
@@ -458,7 +461,7 @@ class ERR_HQ : ERR_CommandService
|
||||
SCR_AIGroup group = unit.Group;
|
||||
string factionKey;
|
||||
if (group.GetFaction()) factionKey = group.GetFaction().GetFactionKey();
|
||||
m_Journal.Write("REGISTER", string.Format("group=%1 force=%2 faction=%3 position=%4 members=%5 home=%6 dedicated=%7; policy=maximum skill/regular aim/alert recognition", unit.Id, unit.ForceSourceId, factionKey, group.GetOrigin(), m_Mode.ForceLiving(group), unit.HomeId, unit.ReservedForSupport));
|
||||
m_Journal.Write("REGISTER", string.Format("group=%1 force=%2 faction=%3 position=%4 members=%5 home=%6 dedicated=%7; policy=maximum skill/regular aim with ten percent less dispersion/alert recognition", unit.Id, unit.ForceSourceId, factionKey, group.GetOrigin(), m_Mode.ForceLiving(group), unit.HomeId, unit.ReservedForSupport));
|
||||
array<AIAgent> auditAgents = {}; group.GetAgents(auditAgents);
|
||||
foreach (AIAgent auditAgent : auditAgents)
|
||||
{
|
||||
@@ -523,7 +526,8 @@ class ERR_HQ : ERR_CommandService
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group) continue;
|
||||
if (unit.Detection) { unit.Detection.GetOnEnemyDetected().Remove(Detected); unit.Detection = null; }
|
||||
unit.LocalAwareness.Clear();
|
||||
if (unit.Detection) { unit.Detection.GetOnEnemyDetected().Remove(Detected); unit.Detection.ERR_BindAwareness(null); unit.Detection = null; }
|
||||
ReleaseUnit(unit, GetGame().GetWorld().GetWorldTime());
|
||||
unit.Group = null;
|
||||
unit.Utility = null;
|
||||
@@ -663,12 +667,32 @@ class ERR_HQ : ERR_CommandService
|
||||
unit.AmmoReporting.Queued(now);
|
||||
m_Journal.Write("STATUS REPORT", route.Payload, route.MessageId);
|
||||
}
|
||||
// Due routine status and requested check-ins get the next free uplink.
|
||||
// Positive observations precede a negative search report.
|
||||
// One captured contact every five seconds at most. Existing transmissions
|
||||
// finish normally; overdue status gets a slot before more urgent contacts.
|
||||
protected void ContactReport(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (unit.Uplink || now < unit.NextContact || !unit.Group) return;
|
||||
ERR_HQReport report = unit.LocalAwareness.Peek(now, unit.SentObservations);
|
||||
if (!report) return;
|
||||
unit.NextContact = now + 5000;
|
||||
ERR_HQRoute route = ObservationRoute(unit);
|
||||
if (!route) return;
|
||||
report.MessageId = m_Journal.Message();
|
||||
route.MessageId = report.MessageId; route.Payload = report.Description(now);
|
||||
unit.PendingReport = report; unit.Uplink = route;
|
||||
unit.LocalAwareness.Queued(report, now);
|
||||
if (report.Target) unit.SentObservations.Queued(report.Target, report.ObservedAt, now);
|
||||
unit.NextReport = now + m_Mode.Settings().Runtime("reportIntervalSeconds") * 1000;
|
||||
string kind = "CONTACT REPORT";
|
||||
if (report.TravelMode == "heard gunfire") kind = "GUNFIRE REPORT";
|
||||
m_Journal.Write(kind, route.Payload, report.MessageId);
|
||||
}
|
||||
protected void QueueReports(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (OriginOccupied(unit.Radio, unit)) { if (unit.Task) unit.Task.ArrivedAt = -1; return; }
|
||||
if (ERR_HQLocalAwareness.Priority(now, unit.NextContact, unit.NextStatus)) ContactReport(unit, now);
|
||||
StatusReport(unit, now);
|
||||
ContactReport(unit, now);
|
||||
Observe(unit, now);
|
||||
ObserveBody(unit, now);
|
||||
Sweep(unit, now);
|
||||
@@ -783,6 +807,7 @@ class ERR_HQ : ERR_CommandService
|
||||
}
|
||||
if (!m_Memory.Ingest(report, now)) return;
|
||||
if (ERR_HQMemoryRules.Kind(report) == ERR_HQMemoryKind.SIGHTING) m_Coverage.Forget(report.Position);
|
||||
m_NextAssignment = Math.Min(m_NextAssignment, now);
|
||||
m_Journal.Write("BELIEF", string.Format("delivered report M%1 retained; tracks=%2", report.MessageId, m_Memory.Count()), report.MessageId);
|
||||
// A reported corpse or distress call can justify investigation, never fire.
|
||||
if (ERR_HQMemoryRules.Kind(report) != ERR_HQMemoryKind.SIGHTING) return;
|
||||
@@ -804,10 +829,30 @@ class ERR_HQ : ERR_CommandService
|
||||
m_Contacts.Insert(report.Snapshot());
|
||||
}
|
||||
|
||||
// Current combat suspends a sweep without deleting its native queued orders.
|
||||
// Recent native contacts and the quiet grace period block HQ retasking.
|
||||
protected bool Fighting(ERR_HQUnit unit)
|
||||
{
|
||||
return unit.Utility && unit.Utility.m_Perception && !unit.Utility.m_Perception.m_aTargets.IsEmpty();
|
||||
return unit.InfantryCombat.Held || ERR_HunterCombat.Active(unit.Group);
|
||||
}
|
||||
protected void UpdateInfantryCombat(ERR_HQUnit unit, float now)
|
||||
{
|
||||
bool eligible = unit.Eligible(m_Mode);
|
||||
int changed = unit.InfantryCombat.Update(unit, eligible, eligible && ERR_HunterCombat.Active(unit.Group), now);
|
||||
if (!changed) return;
|
||||
string detail = "contact ended; no current mobile search to resume";
|
||||
if (changed == 1)
|
||||
{
|
||||
unit.Group.ActivateAI();
|
||||
array<AIAgent> reacting = {}; unit.Group.GetAgents(reacting);
|
||||
foreach (AIAgent actor : reacting)
|
||||
if (actor && ERR_Actors.CanFight(actor.GetControlledEntity())) actor.ActivateAI();
|
||||
detail = "enemy contact; native combat takes priority; defensive positions retained";
|
||||
if (unit.InfantryCombat.Paused()) detail = "enemy contact; HQ search paused; fire at will";
|
||||
// Preserve status/convoy transports. Only superseded infantry task orders stop.
|
||||
if (unit.PendingTask) LostOrder(unit, now);
|
||||
}
|
||||
else if (changed == 2) detail = "contact quiet for 15 seconds; existing HQ search resumed";
|
||||
m_Journal.Write("INFANTRY COMBAT", string.Format("Squad %1 (%2): %3", unit.Id, unit.ForceSourceId, detail));
|
||||
}
|
||||
protected int HomeAvailable(ERR_HQUnit candidate) { return m_HomeAvailable.Get(candidate.HomeId); }
|
||||
protected ERR_HQHuntWindow Hunt(int beliefId)
|
||||
@@ -1160,6 +1205,8 @@ class ERR_HQ : ERR_CommandService
|
||||
if (!hunt || now < hunt.NextHunt) continue;
|
||||
for (int slot = 0; slot < limit && assigned < limit && Assigned() < globalLimit; slot++)
|
||||
{
|
||||
// One mobile search squad for uncertain sound; never empty objectives for it.
|
||||
if (belief.Kind == ERR_HQMemoryKind.GUNFIRE && slot > 0) break;
|
||||
float age = (now - belief.ObservedAt) / 1000;
|
||||
ERR_HQPursuitRole role = ERR_HQPursuitRules.Role(belief.Confidence, age, belief.Radius, belief.HasVelocity, slot);
|
||||
vector destination = ERR_HQPursuitRules.Objective(belief.PredictedPosition, belief.Velocity, belief.HasVelocity, belief.Confidence, age, belief.Radius, role, slot);
|
||||
@@ -1200,6 +1247,7 @@ class ERR_HQ : ERR_CommandService
|
||||
best.PendingTask = task; best.PendingOrder = belief.Report.Snapshot(); best.Downlink = bestRoute;
|
||||
bestRoute.MessageId = m_Journal.Message();
|
||||
bestRoute.Payload = string.Format("group %1 %2; belief=%3 from report M%4; destination=%5", best.Id, task.Name(), task.BeliefId, belief.MessageId, destination);
|
||||
if (belief.Kind == ERR_HQMemoryKind.GUNFIRE) bestRoute.Payload += "; investigate heard gunfire, enemy identity unknown";
|
||||
m_Journal.Write("ORDER QUEUED", bestRoute.Payload, bestRoute.MessageId);
|
||||
reserved.Insert(destination);
|
||||
if (first < 0) first = bestBearing;
|
||||
@@ -1242,10 +1290,11 @@ class ERR_HQ : ERR_CommandService
|
||||
for (int i = m_Units.Count() - 1; i >= 0; i--)
|
||||
{
|
||||
ERR_HQUnit unit = m_Units[i];
|
||||
UpdateInfantryCombat(unit, now);
|
||||
if (!unit.Detection && unit.Utility && unit.Group)
|
||||
{
|
||||
unit.Detection = unit.Utility.m_Perception;
|
||||
if (unit.Detection) unit.Detection.GetOnEnemyDetected().Insert(Detected);
|
||||
if (unit.Detection) { unit.Detection.GetOnEnemyDetected().Insert(Detected); unit.Detection.ERR_BindAwareness(unit.LocalAwareness); }
|
||||
}
|
||||
if (unit.Uplink)
|
||||
{
|
||||
@@ -1289,7 +1338,11 @@ class ERR_HQ : ERR_CommandService
|
||||
m_Towns.Stop();
|
||||
foreach (SCR_AICombatComponent specialSkill : m_SpecialSkills) if (specialSkill) specialSkill.ERR_ReleaseEnemySkill(m_Mode);
|
||||
m_SpecialSkills.Clear();
|
||||
foreach (ERR_HQUnit detector : m_Units) if (detector.Detection) detector.Detection.GetOnEnemyDetected().Remove(Detected);
|
||||
foreach (ERR_HQUnit detector : m_Units)
|
||||
{
|
||||
detector.LocalAwareness.Clear();
|
||||
if (detector.Detection) { detector.Detection.GetOnEnemyDetected().Remove(Detected); detector.Detection.ERR_BindAwareness(null); }
|
||||
}
|
||||
m_Airfield.Stop(); m_Recovery.Stop();
|
||||
m_AirAssault.Stop();
|
||||
m_Recon.Stop();
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// Local evidence belongs to the observing squad. HQ sees it only after the
|
||||
// existing physical radio route completes. No live target position is queried.
|
||||
class ERR_HQLocalAwareness
|
||||
{
|
||||
protected ref array<ref ERR_HQReport> m_Visual = {};
|
||||
protected ref ERR_HQReport m_Sound;
|
||||
protected float m_NextSound;
|
||||
|
||||
static bool Fresh(float observedAt, float now)
|
||||
{
|
||||
return ERR_ForceRules.Finite(observedAt) && ERR_ForceRules.Finite(now)
|
||||
&& observedAt >= 0 && now >= observedAt && now - observedAt <= 30000;
|
||||
}
|
||||
static bool Priority(float now, float nextContact, float nextStatus)
|
||||
{
|
||||
// Fresh contacts may precede routine status, but cannot starve overdue status.
|
||||
return now >= nextContact && now < nextStatus + 15000;
|
||||
}
|
||||
static vector SoundArea(vector position)
|
||||
{
|
||||
// Do not distribute the engine's precise shot origin or the shooter's identity.
|
||||
return Vector(Math.Floor(position[0] / 150) * 150 + 75, position[1], Math.Floor(position[2] / 150) * 150 + 75);
|
||||
}
|
||||
void Clear() { m_Visual.Clear(); m_Sound = null; m_NextSound = 0; }
|
||||
void Prune(float now)
|
||||
{
|
||||
for (int i = m_Visual.Count() - 1; i >= 0; i--)
|
||||
if (!m_Visual[i].Target || !Fresh(m_Visual[i].ObservedAt, now)) m_Visual.Remove(i);
|
||||
if (m_Sound && !Fresh(m_Sound.ObservedAt, now)) m_Sound = null;
|
||||
}
|
||||
bool CaptureDue(IEntity target, float now)
|
||||
{
|
||||
foreach (ERR_HQReport known : m_Visual)
|
||||
if (known.Target == target && now - known.ObservedAt < 1000) return false;
|
||||
return true;
|
||||
}
|
||||
void Remember(ERR_HQReport report, float now)
|
||||
{
|
||||
if (!report || !report.Target || !Fresh(report.ObservedAt, now)) return;
|
||||
Prune(now);
|
||||
for (int i = 0; i < m_Visual.Count(); i++)
|
||||
{
|
||||
if (m_Visual[i].Target != report.Target) continue;
|
||||
if (report.ObservedAt > m_Visual[i].ObservedAt) m_Visual[i] = report.Snapshot();
|
||||
return;
|
||||
}
|
||||
if (m_Visual.Count() >= 16) m_Visual.Remove(0);
|
||||
m_Visual.Insert(report.Snapshot());
|
||||
}
|
||||
void Hear(SCR_AIGroup source, vector position, float observedAt, float now)
|
||||
{
|
||||
if (!source || !Fresh(observedAt, now) || !ERR_HQMemoryRules.PositionValid(position)) return;
|
||||
if (m_Sound && observedAt - m_Sound.ObservedAt < 1000) return;
|
||||
m_Sound = new ERR_HQReport();
|
||||
m_Sound.Source = source; m_Sound.Position = SoundArea(position);
|
||||
m_Sound.ObservedAt = observedAt; m_Sound.TravelMode = "heard gunfire";
|
||||
}
|
||||
ERR_HQReport Peek(float now, ERR_HQObservationHistory history)
|
||||
{
|
||||
Prune(now);
|
||||
foreach (ERR_HQReport report : m_Visual)
|
||||
if (history.CanQueue(report.Target, report.ObservedAt, now)) return report.Snapshot();
|
||||
if (m_Sound && now >= m_NextSound) return m_Sound.Snapshot();
|
||||
return null;
|
||||
}
|
||||
void Queued(ERR_HQReport report, float now)
|
||||
{
|
||||
if (report.TravelMode == "heard gunfire") { m_Sound = null; m_NextSound = now + 30000; return; }
|
||||
for (int i = m_Visual.Count() - 1; i >= 0; i--)
|
||||
if (m_Visual[i].Target == report.Target && m_Visual[i].ObservedAt <= report.ObservedAt) m_Visual.Remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIGroupPerception
|
||||
{
|
||||
protected ERR_HQLocalAwareness m_ERRAwareness;
|
||||
void ERR_BindAwareness(ERR_HQLocalAwareness awareness) { m_ERRAwareness = awareness; }
|
||||
protected bool ERR_AwarenessActive()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return m_ERRAwareness && Replication.IsServer() && mode && mode.Active() && mode.Phase() > ERR_Phase.PRISON
|
||||
&& m_Group && !m_Group.IsSlave() && m_Group.GetPlayerCount(true) == 0 && m_Group.GetFaction() == mode.EnemyFaction();
|
||||
}
|
||||
override SCR_AITargetInfo AddOrUpdateTarget(notnull BaseTarget target, out bool outNewTarget)
|
||||
{
|
||||
SCR_AITargetInfo result = super.AddOrUpdateTarget(target, outNewTarget);
|
||||
if (!result || !ERR_AwarenessActive() || target.IsDisarmed()) return result;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (!m_ERRAwareness.CaptureDue(target.GetTargetEntity(), now)) return result;
|
||||
ERR_HQVisualContact seen = ERR_HQObservation.Capture(target, now);
|
||||
if (!seen) return result;
|
||||
ERR_HQReport report = new ERR_HQReport();
|
||||
report.Source = m_Group; report.Target = seen.Entity; report.Position = seen.Position; report.ObservedAt = seen.ObservedAt;
|
||||
report.Engaged = result.m_bEndangering;
|
||||
array<ref ERR_HQVisualContact> samples = {seen};
|
||||
ERR_HQObservation.Describe(report, samples, now);
|
||||
m_ERRAwareness.Remember(report, now);
|
||||
return result;
|
||||
}
|
||||
override void AddOrUpdateGunshot(notnull IEntity shooter, vector worldPos, Faction faction, float timestamp, bool endangering)
|
||||
{
|
||||
super.AddOrUpdateGunshot(shooter, worldPos, faction, timestamp, endangering);
|
||||
if (!ERR_AwarenessActive() || !faction || !m_Group.GetFaction().IsFactionEnemy(faction)) return;
|
||||
PerceptionManager manager = GetGame().GetPerceptionManager();
|
||||
if (!manager) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
float age = manager.GetTime() - timestamp;
|
||||
if (age < 0 || age > 30) return;
|
||||
m_ERRAwareness.Hear(m_Group, worldPos, now - age * 1000, now);
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,14 @@ class ERR_HQLocalHandoff
|
||||
}
|
||||
|
||||
// Only a genuinely newer original sighting may be attempted. Old watermarks
|
||||
// can expire because the observation capture itself admits at most 500ms age.
|
||||
// outlive the 30-second local evidence cache. Visual capture still requires 500ms freshness.
|
||||
class ERR_HQObservationHistory
|
||||
{
|
||||
protected ref array<IEntity> m_Targets = {};
|
||||
protected ref array<float> m_Times = {};
|
||||
bool CanQueue(IEntity target, float observedAt, float now)
|
||||
{
|
||||
if (!target || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observedAt) || observedAt < 0 || now < observedAt || now - observedAt > 500) return false;
|
||||
if (!target || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observedAt) || observedAt < 0 || now < observedAt || now - observedAt > 30000) return false;
|
||||
for (int i = m_Targets.Count() - 1; i >= 0; i--)
|
||||
{
|
||||
if (!m_Targets[i] || now - m_Times[i] > 120000) { m_Targets.Remove(i); m_Times.Remove(i); }
|
||||
|
||||
@@ -4,7 +4,8 @@ enum ERR_HQMemoryKind
|
||||
{
|
||||
SIGHTING,
|
||||
DISTRESS,
|
||||
BODY
|
||||
BODY,
|
||||
GUNFIRE
|
||||
}
|
||||
|
||||
class ERR_HQMemoryRules
|
||||
@@ -22,6 +23,7 @@ class ERR_HQMemoryRules
|
||||
}
|
||||
static int Kind(ERR_HQReport report)
|
||||
{
|
||||
if (report.TravelMode == "heard gunfire") return ERR_HQMemoryKind.GUNFIRE;
|
||||
if (report.Distress) return ERR_HQMemoryKind.DISTRESS;
|
||||
if (report.TravelMode == "body" || report.TravelMode == "dead body") return ERR_HQMemoryKind.BODY;
|
||||
return ERR_HQMemoryKind.SIGHTING;
|
||||
@@ -33,18 +35,21 @@ class ERR_HQMemoryRules
|
||||
}
|
||||
static float BaseConfidence(int kind)
|
||||
{
|
||||
if (kind == ERR_HQMemoryKind.GUNFIRE) return 0.4;
|
||||
if (kind == ERR_HQMemoryKind.DISTRESS) return 0.3;
|
||||
if (kind == ERR_HQMemoryKind.BODY) return 0.2;
|
||||
return 1;
|
||||
}
|
||||
static float HalfLife(int kind)
|
||||
{
|
||||
if (kind == ERR_HQMemoryKind.GUNFIRE) return 60;
|
||||
if (kind == ERR_HQMemoryKind.DISTRESS) return 180;
|
||||
if (kind == ERR_HQMemoryKind.BODY) return 300;
|
||||
return 600;
|
||||
}
|
||||
static float RadiusFloor(int kind, string travel)
|
||||
{
|
||||
if (kind == ERR_HQMemoryKind.GUNFIRE) return 150;
|
||||
if (kind == ERR_HQMemoryKind.DISTRESS) return 150;
|
||||
if (kind == ERR_HQMemoryKind.BODY) return 75;
|
||||
if (travel == "aircraft") return 120;
|
||||
@@ -53,6 +58,7 @@ class ERR_HQMemoryRules
|
||||
}
|
||||
static float RadiusGrowth(int kind, string travel)
|
||||
{
|
||||
if (kind == ERR_HQMemoryKind.GUNFIRE) return 2;
|
||||
if (kind == ERR_HQMemoryKind.DISTRESS) return 2;
|
||||
if (kind == ERR_HQMemoryKind.BODY) return 1.2;
|
||||
if (travel == "aircraft") return 8;
|
||||
@@ -179,6 +185,12 @@ class ERR_HQMemoryTrack
|
||||
belief.Confidence = ConfidenceAt(now);
|
||||
belief.Radius = RadiusAt(now);
|
||||
belief.PredictionSource = "delivered sighting; motion unknown";
|
||||
if (Kind == ERR_HQMemoryKind.GUNFIRE)
|
||||
{
|
||||
belief.Target = null;
|
||||
belief.PredictionSource = "heard gunfire; approximate search area, identity and motion unknown";
|
||||
return belief;
|
||||
}
|
||||
if (Kind == ERR_HQMemoryKind.DISTRESS)
|
||||
{
|
||||
belief.Target = null;
|
||||
@@ -191,7 +203,22 @@ class ERR_HQMemoryTrack
|
||||
return belief;
|
||||
}
|
||||
if (Count() < 2) return belief;
|
||||
ERR_HQReport previous = m_Samples[Count() - 2];
|
||||
// Several observers can report almost simultaneously. Such a sample must
|
||||
// not erase a useful motion baseline already present in bounded history.
|
||||
// Stop at a mode change or the nearest sufficiently spaced observation;
|
||||
// never search past an implausible pair for a more convenient estimate.
|
||||
ERR_HQReport previous;
|
||||
for (int i = Count() - 2; i >= 0; i--)
|
||||
{
|
||||
previous = m_Samples[i];
|
||||
if (previous.TravelMode != last.TravelMode || last.ObservedAt - previous.ObservedAt >= 2000) break;
|
||||
previous = null;
|
||||
}
|
||||
if (!previous)
|
||||
{
|
||||
belief.PredictionSource = "delivered sighting; motion samples too close together";
|
||||
return belief;
|
||||
}
|
||||
float seconds = (last.ObservedAt - previous.ObservedAt) / 1000;
|
||||
float distance = vector.DistanceXZ(previous.Position, last.Position);
|
||||
if (!ERR_HQMemoryRules.MotionCredible(seconds, distance, previous.TravelMode, last.TravelMode))
|
||||
@@ -265,7 +292,7 @@ class ERR_HQMemory
|
||||
if (m_Seen.Count() >= 4096) return Refuse("delivered-report replay guard full");
|
||||
int kind = ERR_HQMemoryRules.Kind(report);
|
||||
IEntity key = report.Target;
|
||||
if (kind == ERR_HQMemoryKind.DISTRESS) key = report.Source;
|
||||
if (kind == ERR_HQMemoryKind.DISTRESS || kind == ERR_HQMemoryKind.GUNFIRE) key = report.Source;
|
||||
if (!key) return Refuse("report has no observation identity");
|
||||
int index = Find(key, kind);
|
||||
if (index >= 0 && report.ObservedAt <= m_Tracks[index].Latest().ObservedAt)
|
||||
@@ -324,7 +351,8 @@ class ERR_HQMemory
|
||||
for (int i = 0; i < m_Tracks.Count(); i++)
|
||||
{
|
||||
ERR_HQMemoryTrack track = m_Tracks[i];
|
||||
if (track.BeliefId != beliefId || track.Kind != ERR_HQMemoryKind.SIGHTING) continue;
|
||||
if (track.BeliefId != beliefId) continue;
|
||||
if (track.Kind != ERR_HQMemoryKind.SIGHTING && track.Kind != ERR_HQMemoryKind.GUNFIRE) continue;
|
||||
if (track.BeliefRevision != revision || track.NegativeRevision == revision) return false;
|
||||
track.SearchConfidence = 0.65;
|
||||
track.NegativeRevision = revision;
|
||||
|
||||
@@ -20,11 +20,13 @@ class ERR_HQRadio
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
int result;
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
if (hq && hq.Hunters()) result = hq.Hunters().VehicleRadioCapability(character);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent))
|
||||
continue;
|
||||
result = 1;
|
||||
result = Math.Max(result, 1);
|
||||
SCR_RadioComponent gadget = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||
if (gadget && gadget.ERR_IsHQManpack())
|
||||
return 2;
|
||||
@@ -105,6 +107,8 @@ class ERR_HQReport
|
||||
|
||||
string Description(float now, bool brief = false)
|
||||
{
|
||||
if (TravelMode == "heard gunfire")
|
||||
return string.Format("Gunfire heard near X=%1 Z=%2; approximate area, enemy identity/count/equipment unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
||||
if (Distress)
|
||||
return string.Format("Distress at friendly X=%1 Z=%2; enemy details unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
||||
string count = "count unknown";
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Local combat owns reactions. HQ uses delivered knowledge for travel only.
|
||||
class ERR_HunterCombat
|
||||
{
|
||||
static bool Recent(int category, float age)
|
||||
{
|
||||
return age >= 0 && age <= 15 && (category == EAITargetInfoCategory.IDENTIFIED || category == EAITargetInfoCategory.DETECTED);
|
||||
}
|
||||
static bool Active(SCR_AIGroup group)
|
||||
{
|
||||
if (!group) return false;
|
||||
SCR_AIGroupUtilityComponent utility = group.GetGroupUtilityComponent();
|
||||
PerceptionManager perception = GetGame().GetPerceptionManager();
|
||||
if (!utility || !utility.m_Perception || !perception) return false;
|
||||
float now = perception.GetTime();
|
||||
foreach (SCR_AITargetInfo target : utility.m_Perception.m_aTargets)
|
||||
if (target && Recent(target.m_eCategory, now - target.m_fTimestamp)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
// Complete only a task still owned by a live native utility. Dead actors and
|
||||
// teardown can clear m_AIInfo before mission services release their references.
|
||||
class ERR_VehicleBehaviorCleanup
|
||||
{
|
||||
static void Complete(SCR_AIVehicleBehavior behavior, IEntity actor)
|
||||
{
|
||||
if (!behavior || !actor) return;
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
|
||||
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) return;
|
||||
array<ref AIActionBase> actions = {}; agent.m_UtilityComponent.GetActions(actions);
|
||||
if (actions.Contains(behavior)) behavior.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
||||
// crew, vehicle teleports, health restoration, or ammunition replacement.
|
||||
class ERR_HunterCrewRecovery
|
||||
@@ -13,7 +27,7 @@ class ERR_HunterCrewRecovery
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
if (m_Boarding) m_Boarding.Complete();
|
||||
ERR_VehicleBehaviorCleanup.Complete(m_Boarding, m_BoardingActor);
|
||||
m_Boarding = null; m_BoardingActor = null;
|
||||
}
|
||||
bool Lost(ERR_GameMode mode)
|
||||
@@ -45,9 +59,15 @@ class ERR_HunterCrewRecovery
|
||||
SCR_AIGroupUtilityComponent groupUtility = m_Unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (groupUtility && groupUtility.m_VehicleMgr && usage) groupUtility.m_VehicleMgr.TryAddVehicle(usage);
|
||||
if (m_Unit.CombatHeld || ERR_HunterCombat.Active(m_Unit.Group))
|
||||
{
|
||||
ERR_VehicleBehaviorCleanup.Complete(m_Boarding, m_BoardingActor);
|
||||
m_Boarding = null; m_BoardingActor = null;
|
||||
Note("local combat active; driver reboarding yields to native combat"); return;
|
||||
}
|
||||
if (m_Unit.CrewValid(mode))
|
||||
{
|
||||
if (m_Boarding) m_Boarding.Complete();
|
||||
ERR_VehicleBehaviorCleanup.Complete(m_Boarding, m_BoardingActor);
|
||||
m_Boarding = null; m_BoardingActor = null;
|
||||
Note("driver seated and conscious; patrol can continue"); return;
|
||||
}
|
||||
@@ -68,7 +88,7 @@ class ERR_HunterCrewRecovery
|
||||
if (m_Boarding && m_BoardingActor == m_Unit.Driver && actions.Contains(m_Boarding)) return;
|
||||
if (now < m_NextAttempt) return;
|
||||
m_NextAttempt = now + 10000;
|
||||
if (m_Boarding) m_Boarding.Complete();
|
||||
ERR_VehicleBehaviorCleanup.Complete(m_Boarding, m_BoardingActor);
|
||||
m_BoardingActor = m_Unit.Driver;
|
||||
IEntity aboard = CompartmentAccessComponent.GetVehicleIn(m_Unit.Driver);
|
||||
if (aboard)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||
{
|
||||
bool RadioRequested;
|
||||
ref ERR_EnemyEquipmentCallback RadioOperation;
|
||||
string Source;
|
||||
float Deadline;
|
||||
float NextOrder;
|
||||
@@ -8,7 +10,12 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||
int SourceMessage;
|
||||
float LastContact = -1;
|
||||
float NextContactOrder;
|
||||
vector ContactPosition;
|
||||
vector DeliveredContactPosition;
|
||||
bool DeliveredContact;
|
||||
ref ERR_HunterCrewRecovery Recovery;
|
||||
bool CombatHeld;
|
||||
float CombatQuietUntil;
|
||||
float NextMotionLog;
|
||||
float ProgressAt;
|
||||
vector ProgressPosition;
|
||||
@@ -54,6 +61,43 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Group == group) return true;
|
||||
return false;
|
||||
}
|
||||
// A finite physical radio in the vehicle avoids replacing a medic's backpack
|
||||
// or depending on a modded vest accepting a vanilla manpack. Only the original
|
||||
// living crew currently inside their uncaptured vehicle can use this link.
|
||||
int VehicleRadioCapability(IEntity actor)
|
||||
{
|
||||
if (!actor || !m_Mode || !m_Mode.Active() || m_Stopped) return 0;
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
if (unit.Released || !unit.Members.Contains(actor) || !unit.Entity || !ERR_Actors.Alive(unit.Entity)) continue;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(actor) != unit.Entity || !unit.CrewValid(m_Mode) || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) continue;
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!storage) continue;
|
||||
array<IEntity> items = {}; storage.GetItems(items);
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
||||
SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||
if (radio && radio.ERR_IsHQManpack()) return 2;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
protected bool PrepareVehicleRadio(ERR_HunterVehicle unit)
|
||||
{
|
||||
if (!unit.RadioRequested)
|
||||
{
|
||||
unit.RadioRequested = true;
|
||||
unit.RadioOperation = new ERR_EnemyEquipmentCallback();
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!storage || !storage.TrySpawnPrefabToStorage(ERR_EnemyRadio.MANPACK, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, unit.RadioOperation))
|
||||
{
|
||||
ERR_HQ.Event("HUNTER RADIO FAILED", unit.Source + ": vehicle refused its one physical HQ radio; no crew backpack is removed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return unit.RadioOperation && unit.RadioOperation.Finished && unit.RadioOperation.Succeeded && VehicleRadioCapability(unit.Driver) == 2;
|
||||
}
|
||||
int Deployed() { return m_Hunters.Count(); }
|
||||
int ActiveHunters()
|
||||
{
|
||||
@@ -93,8 +137,51 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
||||
return false;
|
||||
}
|
||||
// Hunter UAZs carry a full response team, unlike the two-person convoy escort.
|
||||
override protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||
{ ERR_HQ.Event("HUNTER FAILED", "selected faction has no usable soldier/medic profile"); return false; }
|
||||
array<BaseCompartmentSlot> all = {}, cargo = {}, turrets = {}, drivers = {};
|
||||
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||
foreach (BaseCompartmentSlot seat : all)
|
||||
{
|
||||
if (!seat.IsCompartmentAccessible() || seat.GetOccupant()) continue;
|
||||
if (seat.GetType() == ECompartmentType.PILOT) drivers.Insert(seat);
|
||||
else if (seat.GetType() == ECompartmentType.TURRET) turrets.Insert(seat);
|
||||
else if (seat.GetType() == ECompartmentType.CARGO) cargo.Insert(seat);
|
||||
}
|
||||
if (drivers.Count() != 1 || turrets.IsEmpty() || cargo.IsEmpty() || all.Count() > 12) return false;
|
||||
unit.Seats.Insert(drivers[0]);
|
||||
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
||||
int medicIndex = unit.Seats.Count();
|
||||
foreach (BaseCompartmentSlot passenger : cargo) unit.Seats.Insert(passenger);
|
||||
AIGroup nativeGroup = unit.Group;
|
||||
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||
{
|
||||
ResourceName selected = soldier;
|
||||
if (i == medicIndex) selected = medic;
|
||||
IEntity actor = unit.Seats[i].SpawnCharacterInCompartment(selected, nativeGroup, ERR_ConvoyResources.GroupPrefab());
|
||||
if (actor) { unit.Members.Insert(actor); m_Mode.Own(actor); }
|
||||
if (!actor || nativeGroup != unit.Group) return false;
|
||||
}
|
||||
unit.Driver = unit.Members[0];
|
||||
ERR_HQ.Event("HUNTER TEAM", string.Format("crew=%1 including one medic; soldier=%2 medic=%3", unit.Members.Count(), soldier, medic));
|
||||
return true;
|
||||
}
|
||||
protected bool SpawnHunter(ERR_HQ hq, float now, vector position, vector angles, string source)
|
||||
{
|
||||
// Missing faction data is a deferred deployment, not a lost physical team.
|
||||
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||
{
|
||||
m_NextSpawn = now + 30000;
|
||||
ERR_HQ.Event("HUNTER WAIT", string.Format("faction=%1 missing response crew; soldier=%2 medic=%3; no vehicle spawned or allowance consumed; configure ResponseSoldier/ResponseMedic", m_Mode.EnemyFaction().GetFactionKey(), soldier, medic));
|
||||
return false;
|
||||
}
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
||||
unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||
@@ -115,20 +202,22 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
}
|
||||
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
if (!unit.CrewValid(m_Mode)) return false;
|
||||
if (!unit.CrewValid(m_Mode) || !PrepareVehicleRadio(unit)) return false;
|
||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||
if (agents.Count() != unit.Members.Count()) return false;
|
||||
foreach (AIAgent agent : agents) if (!agent || !unit.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) return false;
|
||||
for (int i = 0; i < unit.Seats.Count(); i++) if (unit.Seats[i].GetOccupant() != unit.Members[i]) return false;
|
||||
utility.SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(unit.Source, ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||
if (!record) return false;
|
||||
record.Pinned = true; unit.Registered = true;
|
||||
m_Mode.RegisterGroup(unit.Group); hq.ReserveGroup(unit.Group);
|
||||
utility.AddUsableVehicle(usage); unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
ERR_HQ.Event("HUNTER READY", unit.Source + " driver and gunner seated; HQ radio registration active");
|
||||
ERR_HQ.Event("HUNTER READY", unit.Source + " full response crew and medic seated; physical vehicle HQ radio stocked; fire at will; radio registration active");
|
||||
return true;
|
||||
}
|
||||
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
@@ -148,14 +237,22 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
unit.NextOrder = now + 30000;
|
||||
vector destination, angles;
|
||||
string reason = "road patrol between friendly objectives";
|
||||
unit.SourceMessage = 0;
|
||||
if (best)
|
||||
{
|
||||
// Retain the driver's route for repeated reports from the same area.
|
||||
// Re-projecting every sighting can alternate between nearby roads and
|
||||
// repeatedly restart native navigation even for a stationary target.
|
||||
if (unit.Waypoint && unit.DeliveredContact && vector.DistanceXZ(best.PredictedPosition, unit.DeliveredContactPosition) < 75)
|
||||
{
|
||||
unit.LastContact = best.ObservedAt; unit.NextContactOrder = now + 5000;
|
||||
return;
|
||||
}
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) return;
|
||||
unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
unit.SourceMessage = 0;
|
||||
if (unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group)) return;
|
||||
bool found;
|
||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||
@@ -171,9 +268,26 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
if (best)
|
||||
{
|
||||
unit.NextContactOrder = now + 5000;
|
||||
if (unit.Downlink) unit.LastContact = best.ObservedAt;
|
||||
if (unit.Downlink) { unit.LastContact = best.ObservedAt; unit.ContactPosition = best.PredictedPosition; }
|
||||
}
|
||||
}
|
||||
protected void UpdateCombat(ERR_HunterVehicle unit, float now)
|
||||
{
|
||||
if (unit.Registered && ERR_HunterCombat.Active(unit.Group))
|
||||
{
|
||||
unit.CombatQuietUntil = now + 15000;
|
||||
if (!unit.CombatHeld)
|
||||
{
|
||||
unit.CombatHeld = true; unit.ClearOrder(); unit.Downlink = null;
|
||||
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": local contact; travel and forced reboarding suspended, native combat takes priority");
|
||||
}
|
||||
}
|
||||
if (unit.CombatHeld && now >= unit.CombatQuietUntil)
|
||||
{
|
||||
unit.CombatHeld = false; unit.NextOrder = 0;
|
||||
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": contact quiet for 15 seconds; HQ travel may resume");
|
||||
}
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode || !m_Mode.Active() || m_Mode != ERR_GameMode.Get() || m_Mode.Phase() <= ERR_Phase.PRISON || now < m_NextHunter) return;
|
||||
@@ -208,7 +322,9 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
string loss = unit.Recovery.Reason;
|
||||
unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + loss + "; surviving actors and vehicle retained"); continue;
|
||||
}
|
||||
UpdateCombat(unit, now);
|
||||
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||
if (unit.CombatHeld) continue;
|
||||
if (!unit.Registered)
|
||||
{
|
||||
if (AdmitHunter(unit, hq, now)) continue;
|
||||
@@ -227,6 +343,13 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
{
|
||||
if (delivered > 0)
|
||||
{
|
||||
unit.DeliveredContact = unit.SourceMessage > 0;
|
||||
unit.DeliveredContactPosition = unit.ContactPosition;
|
||||
if (unit.Waypoint && vector.DistanceXZ(unit.Waypoint.GetOrigin(), unit.Destination) < 50)
|
||||
{
|
||||
ERR_HQ.Event("HUNTER ORDER RETAINED", unit.Source + ": updated report confirms current destination; native route preserved", unit.SourceMessage);
|
||||
unit.Downlink = null; continue;
|
||||
}
|
||||
unit.ClearOrder();
|
||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), unit.Destination));
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
@@ -243,8 +366,9 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
vector position = unit.Entity.GetOrigin();
|
||||
if (unit.ProgressAt == 0 || vector.DistanceXZ(position, unit.ProgressPosition) >= 15)
|
||||
{ unit.ProgressAt = now; unit.ProgressPosition = position; unit.Repaths = 0; }
|
||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||
bool combat = utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty();
|
||||
// Old entries remain in native perception after contact ends. They must
|
||||
// not suppress stuck-route recovery indefinitely.
|
||||
bool combat = ERR_HunterCombat.Active(unit.Group);
|
||||
bool travelling = unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
||||
if (!travelling || combat) unit.ProgressAt = now;
|
||||
if (now >= unit.NextMotionLog)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// Suspend only the HQ-owned mobile search. Defensive/home waypoints, perception
|
||||
// and individual native combat actions remain owned by their existing systems.
|
||||
class ERR_InfantryCombat
|
||||
{
|
||||
bool Held;
|
||||
float QuietUntil;
|
||||
protected AIWaypoint m_PausedWaypoint;
|
||||
protected ref ERR_HQTask m_PausedTask;
|
||||
|
||||
static bool Mobile(ERR_HQTask task)
|
||||
{
|
||||
return task && !task.ReleaseOnly && !task.RoadIntercept && task.SecuritySite.IsEmpty();
|
||||
}
|
||||
// Contact is supplied from native group perception by HQ, never player positions.
|
||||
// Return 1 on contact, 2 when search resumes, 3 when combat ends without resuming.
|
||||
int Update(ERR_HQUnit unit, bool eligible, bool contact, float now)
|
||||
{
|
||||
if (!eligible)
|
||||
{
|
||||
// A logistics reservation, player takeover or vehicle entry must not cause
|
||||
// us to insert a waypoint. Retain identity until normal HQ cleanup or return.
|
||||
return 0;
|
||||
}
|
||||
if (contact)
|
||||
{
|
||||
QuietUntil = now + 15000;
|
||||
if (Held) return 0;
|
||||
Held = true;
|
||||
if (unit.Assignment && unit.Waypoint && Mobile(unit.Task))
|
||||
{
|
||||
m_PausedWaypoint = unit.Waypoint; m_PausedTask = unit.Task;
|
||||
unit.Group.RemoveWaypoint(m_PausedWaypoint);
|
||||
unit.Utility.SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (!Held || now < QuietUntil) return 0;
|
||||
Held = false;
|
||||
int result = 3;
|
||||
// An expired/released/replaced task cannot be resurrected after a fight.
|
||||
if (m_PausedWaypoint && unit.Waypoint == m_PausedWaypoint && unit.Task == m_PausedTask
|
||||
&& unit.Assignment && Mobile(unit.Task) && now < unit.Task.ExpiresAt)
|
||||
{
|
||||
unit.Group.AddWaypointAt(m_PausedWaypoint, 0);
|
||||
result = 2;
|
||||
}
|
||||
m_PausedWaypoint = null; m_PausedTask = null;
|
||||
return result;
|
||||
}
|
||||
bool Paused() { return m_PausedWaypoint != null; }
|
||||
}
|
||||
@@ -259,6 +259,15 @@ modded class SCR_MortarMuzzleComponent
|
||||
// re-aim). Each salvo shell needs its own spread point and fresh authorization.
|
||||
modded class SCR_AIStaticArtilleryBehavior
|
||||
{
|
||||
void SCR_AIStaticArtilleryBehavior(SCR_AIUtilityComponent utility, SCR_AIActivityBase groupActivity, IEntity artilleryEntity, vector targetPos, SCR_EAIArtilleryAmmoType ammoType, float priorityLevel = PRIORITY_LEVEL_NORMAL)
|
||||
{
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
// Only mission-owned AI batteries use the rapid loop. Its zero fixed
|
||||
// delays still wait on our physical projectile and ammunition gates.
|
||||
if (system && system.FindGun(artilleryEntity))
|
||||
m_sBehaviorTree = "{D0BDF32990D2C687}AI/BehaviorTrees/ERR/MortarRapidFire.bt";
|
||||
}
|
||||
|
||||
ERR_MortarBattery ERR_GetMortarBattery()
|
||||
{
|
||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||
@@ -301,6 +310,10 @@ modded class SCR_AIStaticArtilleryBehavior_ShootingLogic
|
||||
ENodeResult result = super.EOnTaskSimulate(owner, dt);
|
||||
if (battery && result == ENodeResult.SUCCESS)
|
||||
{
|
||||
// Super counts the confirmed last shot and emits native completion.
|
||||
// Hold here until HQ observes that event; a zero-delay repeater must
|
||||
// not begin a ninth aim and turn successful completion into rejection.
|
||||
if (battery.OrderShots >= ERR_MortarSalvo.ROUNDS) return ENodeResult.RUNNING;
|
||||
SetVariableOut(PORT_AIMING_NEEDED, true);
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -28,7 +28,7 @@ class ERR_MortarStation
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Mount) m_Mount.Complete();
|
||||
ERR_VehicleBehaviorCleanup.Complete(m_Mount, m_Gunner);
|
||||
m_Mount = null;
|
||||
foreach (SCR_CharacterControllerComponent controller : m_Controllers)
|
||||
if (controller) controller.ERR_ClearMortarStation(this);
|
||||
@@ -65,7 +65,7 @@ class ERR_MortarStation
|
||||
}
|
||||
if (!Holds(m_Gunner) || !ERR_Actors.CanFight(m_Gunner))
|
||||
{
|
||||
if (m_Mount) m_Mount.Complete();
|
||||
ERR_VehicleBehaviorCleanup.Complete(m_Mount, m_Gunner);
|
||||
m_Mount = null; m_Gunner = null;
|
||||
foreach (AIAgent candidate : agents)
|
||||
{
|
||||
|
||||
@@ -304,7 +304,9 @@ class ERR_MortarSystem
|
||||
if ((battery.Rejected || battery.RefreshRejected) && !battery.Released)
|
||||
{
|
||||
battery.Cancel("native aim or firing authorization rejected");
|
||||
battery.NextOrder = now + 10000;
|
||||
// Do not churn another ballistic/crew task for the same unsuitable area
|
||||
// every twenty seconds. All exact per-shot checks remain authoritative.
|
||||
battery.NextOrder = now + 60000;
|
||||
}
|
||||
battery.Rejected = false;
|
||||
battery.Update(now);
|
||||
@@ -327,6 +329,13 @@ class ERR_MortarSystem
|
||||
if (!battery.CanRequest(report, now)) continue;
|
||||
float candidate = vector.DistanceXZ(battery.Position, report.Position);
|
||||
if (candidate < 350 || candidate > 4000 || candidate >= distance || !OutsideSettlements(report.Position)) continue;
|
||||
vector predicted;
|
||||
if (!ERR_MortarPrediction.Predict(report, now, 30, predicted)) continue;
|
||||
if (!Safe(battery, predicted, 30))
|
||||
{
|
||||
battery.Trace("HQ HOLD", SafetyReason, "no firing order issued; reported impact=" + predicted.ToString());
|
||||
continue;
|
||||
}
|
||||
selected = battery;
|
||||
target = report;
|
||||
distance = candidate;
|
||||
|
||||
@@ -1,6 +1,41 @@
|
||||
#ifdef WORKBENCH
|
||||
// Arrange a ground-response shortfall, without changing airborne policy,
|
||||
// contacts, radio delivery, artillery safety or the drop order itself.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
bool ERR_AirFixtureSearchAssigned(SCR_AIGroup group, ERR_AirDropOrder order)
|
||||
{
|
||||
if (!group || !order) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group) continue;
|
||||
// Contact can remove the waypoint from the native queue while preserving
|
||||
// this exact HQ assignment for resumption after combat.
|
||||
return unit.Task && unit.Task.BeliefId == order.BeliefId && unit.Assignment && unit.Waypoint
|
||||
&& vector.DistanceXZ(unit.Waypoint.GetOrigin(), order.Target) < 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void ERR_AirFixtureReserveGround()
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group && !unit.ReservedForSupport) ReserveGroup(unit.Group);
|
||||
}
|
||||
}
|
||||
modded class ERR_AirAssaultLoad
|
||||
{
|
||||
ERR_AirDropOrder ERR_TestOrder() { return m_Order; }
|
||||
int ERR_TestSearchingTeams()
|
||||
{
|
||||
if (!m_Order) return 0;
|
||||
int count;
|
||||
foreach (ERR_AirAssaultTeam team : m_Teams)
|
||||
{
|
||||
if (!team.Released || !team.Group) continue;
|
||||
if (ERR_HQ.Get().ERR_AirFixtureSearchAssigned(team.Group, m_Order)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
int ERR_TestGrounded()
|
||||
{
|
||||
int count;
|
||||
@@ -24,7 +59,9 @@ modded class ERR_GameMode
|
||||
protected float m_ERRAirDeadline;
|
||||
protected ERR_AirAssaultLoad m_ERRAirLoad;
|
||||
protected Vehicle m_ERRAirframe;
|
||||
protected bool m_ERRAirStopped;
|
||||
protected bool m_ERRAirStopped, m_ERRAirPlayerDamage;
|
||||
protected IEntity m_ERRAirPlayer;
|
||||
protected float m_ERRAirNextStatus;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
@@ -36,6 +73,11 @@ modded class ERR_GameMode
|
||||
}
|
||||
protected void ERR_AirTestStop() { m_ERRAirStopped = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_AirTestTick);
|
||||
if (m_ERRAirPlayer)
|
||||
{
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(m_ERRAirPlayer.FindComponent(DamageManagerComponent));
|
||||
if (damage) damage.EnableDamageHandling(m_ERRAirPlayerDamage);
|
||||
}
|
||||
}
|
||||
protected bool ERR_AirCheck(bool result, string detail)
|
||||
{
|
||||
@@ -59,6 +101,9 @@ modded class ERR_GameMode
|
||||
ERR_Actors.Players(players, true);
|
||||
if (players.IsEmpty()) return;
|
||||
if (!UnlockPrison(players[0])) return;
|
||||
m_ERRAirPlayer = players[0];
|
||||
DamageManagerComponent playerDamage = DamageManagerComponent.Cast(m_ERRAirPlayer.FindComponent(DamageManagerComponent));
|
||||
m_ERRAirPlayerDamage = playerDamage.IsDamageHandlingEnabled(); playerDamage.EnableDamageHandling(false);
|
||||
if (hq.SupplyConvoys()) hq.SupplyConvoys().Stop();
|
||||
hq.Recon().ERR_TestDeparture(now + 3000);
|
||||
m_ERRAirStage = 1;
|
||||
@@ -74,20 +119,26 @@ modded class ERR_GameMode
|
||||
m_ERRAirLoad = flight.Assault();
|
||||
m_ERRAirframe = flight.Aircraft();
|
||||
if (!ERR_AirCheck(m_ERRAirLoad && m_ERRAirLoad.Count() == 8 && m_ERRAirLoad.Aboard() == 8, "eight original passengers physically seated before takeoff")) return;
|
||||
// Controlled visible runway encounter; no fabricated reports, policy
|
||||
// overrides or direct Begin call. Native perception and radio feed HQ.
|
||||
ChimeraCharacter encounter = ChimeraCharacter.Cast(m_ERRAirPlayer);
|
||||
vector transform[4]; encounter.GetWorldTransform(transform);
|
||||
transform[3] = ERR_Actors.Ground(flight.Home() + Vector(0, 0, 400)); encounter.Teleport(transform);
|
||||
Print("ERR AIR FIXTURE: ground squads reserved for other duties; native sighting, radio and HQ airborne decision remain unchanged");
|
||||
m_ERRAirStage = 2;
|
||||
}
|
||||
if (m_ERRAirStage == 2)
|
||||
{
|
||||
if (!m_ERRAirLoad.Ready()) return;
|
||||
ERR_AirDropOrder order = new ERR_AirDropOrder();
|
||||
order.ExpiresAt = now + 180000;
|
||||
order.Target = flight.Home();
|
||||
order.Report = new ERR_HQReport();
|
||||
order.Report.Position = order.Target;
|
||||
order.Report.ObservedAt = now;
|
||||
// Controlled geometry/transfer fixture; policy and physical radio authorization
|
||||
// are verified independently. This does not fabricate a live sighting result.
|
||||
if (!ERR_AirCheck(m_ERRAirLoad.Begin(order, now), "controlled order finds real clear landing corridor")) return;
|
||||
hq.ERR_AirFixtureReserveGround();
|
||||
if (now >= m_ERRAirNextStatus)
|
||||
{
|
||||
m_ERRAirNextStatus = now + 15000;
|
||||
Print("ERR AIR HQ WAIT: " + hq.AirAssault().Status());
|
||||
}
|
||||
if (!m_ERRAirLoad.Used()) return;
|
||||
ERR_AirDropOrder order = m_ERRAirLoad.ERR_TestOrder();
|
||||
if (!ERR_AirCheck(order && order.BeliefId > 0 && order.MessageId > 0 && order.Report && order.Report.Target == m_ERRAirPlayer,
|
||||
"automatic HQ policy and physical radio downlink authorized drop from native player sightings")) return;
|
||||
m_ERRAirStage = 3;
|
||||
return;
|
||||
}
|
||||
@@ -95,8 +146,9 @@ modded class ERR_GameMode
|
||||
{
|
||||
if (m_ERRAirLoad.ERR_TestGrounded() < 8 || m_ERRAirLoad.ERR_TestReleased() < 2) return;
|
||||
if (!ERR_AirCheck(flight.Aircraft() == m_ERRAirframe && flight.ERR_TestCrew() >= 2, "same eight actors landed and two original groups released; original aircrew retained")) return;
|
||||
if (!ERR_AirCheck(m_ERRAirLoad.ERR_TestSearchingTeams() == 2, "both landed squads received native search waypoints for the carried HQ target")) return;
|
||||
vector center;
|
||||
if (!ERR_AirCheck(m_ERRAirLoad.Cover(now, center) && vector.DistanceXZ(center, flight.Home()) < 1, "post-drop cover active")) return;
|
||||
if (!ERR_AirCheck(m_ERRAirLoad.Cover(now, center) && vector.DistanceXZ(center, m_ERRAirLoad.ERR_TestOrder().Target) < 1, "post-drop cover active")) return;
|
||||
m_ERRAirLoad.ERR_TestCoverEnd(now);
|
||||
m_ERRAirStage = 4;
|
||||
return;
|
||||
|
||||
@@ -12,14 +12,35 @@ modded class ERR_AirReconSystem
|
||||
}
|
||||
modded class ERR_ReconFlightComponent
|
||||
{
|
||||
protected float m_ERRFlightTraceAt;
|
||||
override void EOnSimulate(IEntity owner, float timeSlice)
|
||||
{
|
||||
string fixture;
|
||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture == "air-recon" && m_Initialized && !m_Logistics)
|
||||
{
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
Physics physics = owner.GetPhysics();
|
||||
if (physics && now >= m_ERRFlightTraceAt)
|
||||
{
|
||||
m_ERRFlightTraceAt = now + 1000;
|
||||
PrintFormat("ERR RECON MOTION: t=%1 phase=%2 position=%3 velocity=%4 angular=%5 angles=%6 pilot=%7 alive=%8", now, m_Phase, owner.GetOrigin(), physics.GetVelocity(), physics.GetAngularVelocity(), owner.GetYawPitchRoll(), LivingPilot(), ERR_Actors.Alive(owner));
|
||||
}
|
||||
}
|
||||
super.EOnSimulate(owner, timeSlice);
|
||||
}
|
||||
int ERR_TestCrew() { return m_OriginalCrew.Count(); }
|
||||
bool ERR_TestPilot() { return LivingPilot(); }
|
||||
float ERR_TestCommandSpeed() { return m_Velocity.Length(); }
|
||||
bool ERR_TestCloseContact(IEntity actor)
|
||||
{
|
||||
return m_Observation && m_Observation.Target == actor && vector.DistanceXZ(m_Aircraft.GetOrigin(), m_Observation.Position) < 180;
|
||||
}
|
||||
bool ERR_TestSeen(IEntity actor) { return m_Observation && m_Observation.Target == actor; }
|
||||
protected override void Search(float now)
|
||||
{
|
||||
super.Search(now);
|
||||
string fixture;
|
||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture == "air-recon")
|
||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && (fixture == "air-recon" || fixture == "air-assault"))
|
||||
m_Center = m_Home + Vector(0, 0, 800); // Controlled runway-sector encounter, native perception unchanged.
|
||||
}
|
||||
}
|
||||
@@ -29,7 +50,7 @@ modded class ERR_GameMode
|
||||
protected float m_ERRReconDeadline, m_ERRReconDue, m_ERRReconWait;
|
||||
protected Vehicle m_ERRReconFirst;
|
||||
protected IEntity m_ERRReconPlayer;
|
||||
protected bool m_ERRReconDamage, m_ERRReconTakeoverOnly;
|
||||
protected bool m_ERRReconDamage, m_ERRReconTakeoverOnly, m_ERRReconCloseSeen;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
@@ -79,6 +100,7 @@ modded class ERR_GameMode
|
||||
{
|
||||
if (now < m_ERRReconDue) { if (!ERR_ReconCheck(!flight && system.ERR_TestWaves() == 0, "no aircraft before scheduled deadline")) return; return; }
|
||||
if (!flight || flight.Phase() == ERR_ReconPhase.CREW) return;
|
||||
if (flight.Lost() || flight.Phase() == ERR_ReconPhase.RELEASED) { ERR_ReconCheck(false, "crew preparation: " + flight.Status()); return; }
|
||||
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.TAKEOFF && flight.ERR_TestCrew() >= 2 && flight.ERR_TestPilot(), "native original pilot/gunners seated before takeoff")) return;
|
||||
if (!ERR_ReconCheck(system.ERR_TestAirport(flight.Home()) && system.ERR_TestWaves() == 1 && system.ERR_TestDeadline() < 0, "one actual airfield departure disarms launch timer")) return;
|
||||
m_ERRReconFirst = flight.Aircraft();
|
||||
@@ -100,6 +122,29 @@ modded class ERR_GameMode
|
||||
if (!delivered) return;
|
||||
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.SHADOW, "real native player sighting enters shadow orbit and reaches HQ through crew radio")) return;
|
||||
if (!ERR_ReconCheck(ERR_Actors.Alive(flight.Aircraft()) && flight.ERR_TestPilot() && flight.Aircraft().GetOrigin()[1] - SCR_TerrainHelper.GetTerrainY(flight.Aircraft().GetOrigin()) > 80, "airport takeoff and more than 300 metres of terrain-cleared search flight")) return;
|
||||
// A new close sighting must not add an escape impulse every simulation
|
||||
// frame. Move only the fixture player; perception/radio/aircraft stay native.
|
||||
vector heading = flight.Aircraft().GetPhysics().GetVelocity(); heading[1] = 0; heading.Normalize();
|
||||
ChimeraCharacter closeEncounter = ChimeraCharacter.Cast(m_ERRReconPlayer);
|
||||
vector closeTransform[4]; closeEncounter.GetWorldTransform(closeTransform);
|
||||
closeTransform[3] = ERR_Actors.Ground(flight.Aircraft().GetOrigin() + heading * 100);
|
||||
closeEncounter.Teleport(closeTransform);
|
||||
m_ERRReconWait = now + 60000; m_ERRReconStage = 11; return;
|
||||
}
|
||||
if (m_ERRReconStage == 11)
|
||||
{
|
||||
if (!flight || flight.Lost() || flight.Phase() == ERR_ReconPhase.RELEASED) { ERR_ReconCheck(false, "close-contact flight lost: " + system.Status()); return; }
|
||||
float commanded = flight.ERR_TestCommandSpeed();
|
||||
float measured = flight.Aircraft().GetPhysics().GetVelocity().Length();
|
||||
if (commanded > 70 || measured > 80)
|
||||
{ ERR_ReconCheck(false, string.Format("close-contact overspeed commanded=%1 m/s measured=%2 m/s", commanded, measured)); return; }
|
||||
if (!m_ERRReconCloseSeen && flight.ERR_TestCloseContact(m_ERRReconPlayer))
|
||||
{
|
||||
m_ERRReconCloseSeen = true;
|
||||
Print("ERR SMOKE PASS: air recon native close sighting exercised the 180-metre standoff controller");
|
||||
}
|
||||
if (now < m_ERRReconWait) return;
|
||||
if (!ERR_ReconCheck(m_ERRReconCloseSeen && ERR_Actors.Alive(flight.Aircraft()) && flight.ERR_TestPilot(), "60 seconds of bounded close-contact flight with original living pilot")) return;
|
||||
system.Return();
|
||||
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.RETURNING, "production return-home request accepted")) return;
|
||||
m_ERRReconStage = 3; return;
|
||||
|
||||
@@ -92,7 +92,10 @@ class ERR_NativeMortarSalvoRules
|
||||
}
|
||||
static bool QueueUpdate(bool accepted, bool pending, int shots, float observedAt, float orderedAt, float now)
|
||||
{
|
||||
return accepted && !pending && shots < ERR_MortarSalvo.ROUNDS / 2 && now >= observedAt + 20000 && orderedAt >= 0 && now >= orderedAt && now - orderedAt < 90000;
|
||||
// Keep observing through the sixth round's preparation. Stopping updates at
|
||||
// half a salvo can expire the last report before the final native reload.
|
||||
// Leave two rounds for the last real downlink to complete before closure.
|
||||
return accepted && !pending && shots < ERR_MortarSalvo.ROUNDS - 2 && now >= observedAt + 20000 && orderedAt >= 0 && now >= orderedAt && now - orderedAt < 90000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +437,10 @@ class ERR_NativeMortarSalvoProbe
|
||||
{
|
||||
float observed;
|
||||
int messageId = hq.ERR_QueueMortarRefreshReport(m_Group, m_OriginalRTO, m_Target, m_Impact, observed);
|
||||
if (!Check(messageId > 0 && AddObservation(messageId, observed), "next distinct actual-clock synthetic observation queued only after previous acceptance")) return -1;
|
||||
// Ordinary crew/status traffic can occupy the real uplink. Zero means
|
||||
// no report was queued; wait within the existing deadline, without
|
||||
// fabricating an observation or replacing the delivered report.
|
||||
if (messageId > 0 && !Check(AddObservation(messageId, observed), "next distinct actual-clock synthetic observation queued only after previous acceptance")) return -1;
|
||||
}
|
||||
}
|
||||
if (m_Battery.CommandOpen || m_Battery.Activity) return 0;
|
||||
|
||||
@@ -40,12 +40,22 @@ modded class ERR_HQ
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool ERR_TestPatrolCombat(SCR_AIGroup group, AIWaypoint search, bool contact, float now, int expected)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group || unit.Waypoint != search) continue;
|
||||
return unit.InfantryCombat.Update(unit, unit.Eligible(m_Mode), contact, now) == expected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativePatrolHQProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_SiteRuntime m_Site;
|
||||
protected SCR_AIGroup m_Group;
|
||||
protected AIWaypoint m_Search;
|
||||
protected ref array<AIWaypoint> m_Baseline = {};
|
||||
protected int m_Step;
|
||||
@@ -64,25 +74,35 @@ class ERR_NativePatrolHQProbe
|
||||
if (m_Step == 0)
|
||||
{
|
||||
m_Site = m_Mode.Sites().Find("everon-outpost-1");
|
||||
if (!Check(m_Site && m_Site.Group && m_Site.PatrolCycle, "existing outpost route available")) return -1;
|
||||
m_Site.Group.GetWaypoints(m_Baseline);
|
||||
if (!Check(!m_Baseline.IsEmpty() && hq.ERR_TestPatrolSearch(m_Site.Group, m_Search), "real delivered-order method assigns existing squad")) return -1;
|
||||
if (!Check(m_Site && m_Site.Responders && m_Site.Responders.Group, "existing mobile outpost responders available")) return -1;
|
||||
m_Group = m_Site.Responders.Group;
|
||||
m_Group.GetWaypoints(m_Baseline);
|
||||
if (!Check(!m_Baseline.IsEmpty() && hq.ERR_TestPatrolSearch(m_Group, m_Search), "real delivered-order method assigns existing squad")) return -1;
|
||||
m_Step = 1; m_Wait = now + 750; return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
array<AIWaypoint> queue = {};
|
||||
m_Site.Group.GetWaypoints(queue);
|
||||
if (!Check(m_Search && !queue.IsEmpty() && queue[0] == m_Search && m_Site.Group.GetCurrentWaypoint() == m_Search, "Search preempts native patrol")) return -1;
|
||||
m_Group.GetWaypoints(queue);
|
||||
if (!Check(m_Search && !queue.IsEmpty() && queue[0] == m_Search && m_Group.GetCurrentWaypoint() == m_Search, "Search preempts native patrol")) return -1;
|
||||
foreach (AIWaypoint waypoint : m_Baseline)
|
||||
if (!Check(queue.Contains(waypoint), "preempted baseline route remains queued")) return -1;
|
||||
if (!Check(hq.ERR_TestPatrolRelease(m_Site.Group, m_Search), "real HQ release removes only its assignment")) return -1;
|
||||
if (!Check(hq.ERR_TestPatrolCombat(m_Group, m_Search, true, now, 1), "injected local contact pauses actual HQ search")) return -1;
|
||||
queue.Clear(); m_Group.GetWaypoints(queue);
|
||||
if (!Check(!queue.Contains(m_Search), "combat removes HQ search from native queue without deleting it")) return -1;
|
||||
foreach (AIWaypoint home : m_Baseline)
|
||||
if (!Check(queue.Contains(home), "combat preserves baseline patrol ownership")) return -1;
|
||||
if (!Check(hq.ERR_TestPatrolCombat(m_Group, m_Search, false, now + 14999, 0), "quiet grace period prevents immediate marching")) return -1;
|
||||
if (!Check(hq.ERR_TestPatrolCombat(m_Group, m_Search, false, now + 15000, 2), "quiet period resumes the same unexpired assignment")) return -1;
|
||||
queue.Clear(); m_Group.GetWaypoints(queue);
|
||||
if (!Check(!queue.IsEmpty() && queue[0] == m_Search, "resumed search is queued exactly once at the front")) return -1;
|
||||
if (!Check(hq.ERR_TestPatrolRelease(m_Group, m_Search), "real HQ release removes only its assignment")) return -1;
|
||||
m_Step = 2; m_Wait = now + 750; return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
AIWaypoint current = m_Site.Group.GetCurrentWaypoint();
|
||||
if (!Check(current && (current == m_Site.PatrolCycle || m_Site.PatrolWaypoints.Contains(current)), "native patrol resumes after Search release")) return -1;
|
||||
AIWaypoint current = m_Group.GetCurrentWaypoint();
|
||||
if (!Check(current && m_Baseline.Contains(current), "native home defence returns after Search release")) return -1;
|
||||
m_Step = 3; return 1;
|
||||
}
|
||||
return 1;
|
||||
@@ -90,7 +110,7 @@ class ERR_NativePatrolHQProbe
|
||||
void Stop()
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
if (hq && m_Site && m_Search) hq.ERR_TestPatrolRelease(m_Site.Group, m_Search);
|
||||
if (hq && m_Site && m_Search) hq.ERR_TestPatrolRelease(m_Group, m_Search);
|
||||
}
|
||||
void ~ERR_NativePatrolHQProbe() { Stop(); }
|
||||
}
|
||||
|
||||
@@ -21,9 +21,13 @@ class ERR_ReconFlightRules
|
||||
if (distance >= 180) return velocity;
|
||||
if (distance < 1) away = "1 0 0";
|
||||
away.Normalize();
|
||||
float inward = vector.Dot(velocity, away);
|
||||
if (inward < 0) velocity -= away * inward;
|
||||
return velocity + away * Math.Clamp((180 - distance) * 0.5, 0, 25);
|
||||
// This is a minimum outward speed, not an impulse. The result is stored
|
||||
// in m_Velocity and reused next frame: adding the full correction on every
|
||||
// simulation tick accelerated close encounters to hundreds of m/s.
|
||||
float radial = vector.Dot(velocity, away);
|
||||
float minimum = Math.Clamp((180 - distance) * 0.5, 0, 25);
|
||||
if (radial < minimum) velocity = velocity + away * (minimum - radial);
|
||||
return velocity;
|
||||
}
|
||||
}
|
||||
[ComponentEditorProps(category: "Reintroduced", description: "Airport reconnaissance helicopter")]
|
||||
@@ -113,22 +117,55 @@ class ERR_ReconFlightComponent : ScriptComponent
|
||||
if (!m_Crew) { Fail("crew group unavailable"); return; }
|
||||
mode.Own(m_Crew); m_Crew.SetFaction(mode.EnemyFaction()); m_Crew.SetPermanentLOD(0);
|
||||
m_Crew.GetGroupUtilityComponent().SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||
Seats(m_Aircraft);
|
||||
if (m_Seats.Count() < 2 || m_Seats.Count() > 8) { Fail("unsupported pilot/gunner seat layout"); return; }
|
||||
m_Crew.ActivateAI();
|
||||
// Native compartments/simulation are not ready in the airframe spawn callback.
|
||||
// Update owns one bounded allocation after initialization, including passengers.
|
||||
}
|
||||
protected bool PrepareCrew(float now)
|
||||
{
|
||||
if (!m_OriginalCrew.IsEmpty()) return true;
|
||||
if (now - m_Started < 500) return false;
|
||||
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||
VehicleHelicopterSimulation simulation;
|
||||
if (controller) simulation = controller.GetSimulation();
|
||||
FuelManagerComponent fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
|
||||
if (!simulation || !simulation.IsValid() || !fuel || !fuel.HasFuel()) { m_Reason = "waiting for native airframe simulation and fuel"; return false; }
|
||||
m_Seats.Clear(); Seats(m_Aircraft);
|
||||
if (m_Seats.Count() < 2 || m_Seats.Count() > 8) { m_Reason = "waiting for pilot/gunner seat layout"; return false; }
|
||||
foreach (BaseCompartmentSlot candidate : m_Seats)
|
||||
{
|
||||
if (candidate.IsOccupied()) { Fail("crew seat occupied before allocation; no replacement created"); return false; }
|
||||
if (!candidate.IsCompartmentAccessible() || candidate.IsReserved() || candidate.IsGetInLocked())
|
||||
{ m_Reason = "waiting for accessible crew seat " + candidate.GetCompartmentName(); return false; }
|
||||
}
|
||||
ResourceName pilot = "{C31F210ABE078197}Prefabs/ERR/ReconPilotEast.et";
|
||||
string faction = mode.EnemyFaction().GetFactionKey();
|
||||
string faction = m_Mode.EnemyFaction().GetFactionKey();
|
||||
if (faction == "US" || faction == "RHS_USAF") pilot = "{C31F210ABE078199}Prefabs/ERR/ReconPilotWest.et";
|
||||
foreach (BaseCompartmentSlot seat : m_Seats)
|
||||
{
|
||||
if (seat.IsOccupied() || !seat.IsCompartmentAccessible()) { Fail("crew seat obstructed"); return; }
|
||||
AIGroup nativeCrew = m_Crew;
|
||||
IEntity initialCrew = seat.SpawnCharacterInCompartment(pilot, nativeCrew, "{C31F210ABE078195}Prefabs/ERR/ReconCrew.et");
|
||||
if (initialCrew) m_OriginalCrew.Insert(initialCrew);
|
||||
if (nativeCrew != m_Crew) { Fail("native boarding changed crew identity"); return; }
|
||||
// Native SpawnCharacterInCompartment boards before assigning the group.
|
||||
// Its Soviet/US prefab affiliation can therefore reject an ION airframe.
|
||||
// Assign the selected faction and original group BEFORE native boarding.
|
||||
vector staging = ERR_Actors.Ground(m_Home + Vector(20 + m_OriginalCrew.Count() * 2, 0, 0));
|
||||
IEntity initialCrew = ERR_Actors.Spawn(pilot, staging);
|
||||
if (!initialCrew) { Fail("pilot prefab allocation failed; no retry"); return false; }
|
||||
m_Mode.Own(initialCrew); m_OriginalCrew.Insert(initialCrew);
|
||||
AIControlComponent control = AIControlComponent.Cast(initialCrew.FindComponent(AIControlComponent));
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(initialCrew.FindComponent(CompartmentAccessComponent));
|
||||
if (!control || !access || !ERR_Actors.SetFaction(initialCrew, m_Mode.EnemyFaction()))
|
||||
{ Fail("crew faction/control unavailable; original actor retained"); return false; }
|
||||
control.ActivateAI();
|
||||
m_Crew.AddAgent(control.GetControlAIAgent());
|
||||
if (!access.GetInVehicle(seat.GetOwner(), seat, true, -1, ECloseDoorAfterActions.INVALID, false))
|
||||
{ Fail("native crew boarding refused at " + seat.GetCompartmentName() + "; faction=" + faction + "; original actor retained, no retry"); return false; }
|
||||
}
|
||||
m_Crew.ActivateAI(); Locks(true);
|
||||
if (!logistics) m_Assault = ERR_HQ.Get().AirAssault().Load(this, pad, serial);
|
||||
if (!m_Logistics) m_Assault = ERR_HQ.Get().AirAssault().Load(this, m_Home, m_Serial);
|
||||
m_Reason = "waiting for original crew seating and radio loadouts";
|
||||
ERR_HQ.Event("HELICOPTER CREW", string.Format("airframe initialized; allocated %1 original pilot/gunner seats once", m_OriginalCrew.Count()));
|
||||
return true;
|
||||
}
|
||||
|
||||
protected bool Captured()
|
||||
{
|
||||
if (!m_Crew || m_Crew.GetPlayerCount(true) > 0 || m_Crew.GetFaction() != m_Mode.EnemyFaction() || SCR_FactionManager.SGetFaction(m_Aircraft) != m_Mode.EnemyFaction()) return true;
|
||||
@@ -172,11 +209,12 @@ class ERR_ReconFlightComponent : ScriptComponent
|
||||
}
|
||||
protected bool FinishCrew(float now)
|
||||
{
|
||||
if (now - m_Started > 45000) { Fail("crew preparation timed out: " + m_Reason + "; partial spawn retained"); return false; }
|
||||
if (!PrepareCrew(now)) return false;
|
||||
m_Crew.ActivateAI();
|
||||
array<AIAgent> boardingAgents = {}; m_Crew.GetAgents(boardingAgents);
|
||||
foreach (AIAgent boardingAgent : boardingAgents) { boardingAgent.SetPermanentLOD(0); boardingAgent.ActivateAI(); }
|
||||
if (m_Assault && m_Assault.Preparing()) return false;
|
||||
if (now - m_Started > 45000) { Fail("native crew boarding timed out; partial spawn retained"); return false; }
|
||||
foreach (BaseCompartmentSlot seat : m_Seats)
|
||||
if (!seat || !seat.GetOccupant() || !ERR_Actors.CanFight(seat.GetOccupant()) || ERR_Actors.Group(seat.GetOccupant()) != m_Crew) return false;
|
||||
// Native loadouts can complete after seating; keep the original boarding deadline.
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
// One travel owner for the whole column. Native AI still steers and avoids
|
||||
// obstacles; followers rendezvous with the preceding physical vehicle instead
|
||||
// of independently racing to the same base marker.
|
||||
class ERR_ConvoyColumnRules
|
||||
{
|
||||
static bool WaitForRear(float largestGap, bool waiting)
|
||||
{
|
||||
if (waiting) return largestGap > 90;
|
||||
return largestGap > 160;
|
||||
}
|
||||
static bool TooClose(float gap, bool waiting)
|
||||
{
|
||||
if (waiting) return gap < 65;
|
||||
return gap < 40;
|
||||
}
|
||||
static float Speed(float gap)
|
||||
{
|
||||
if (gap > 110) return 45;
|
||||
if (gap > 70) return 35;
|
||||
return 22;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyDriving
|
||||
{
|
||||
static bool Restore(ERR_GameMode mode, ERR_ConvoyVehicle vehicle)
|
||||
{
|
||||
if (!mode || !vehicle || vehicle.Released || !vehicle.CrewValid(mode) || !ERR_ConvoyMountedCrew.Retain(vehicle.Driver, vehicle.Entity)) return false;
|
||||
SCR_AIGroupUtilityComponent utility = vehicle.Group.GetGroupUtilityComponent();
|
||||
if (!utility || !utility.m_VehicleMgr || utility.IsUsableVehicle(vehicle.Entity)) return false;
|
||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(vehicle.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||
if (!usage || !usage.CanBePiloted()) return false;
|
||||
// Native RemoveStuckVehicle removes this association after a failed path.
|
||||
// Our retained seated crew cannot resume merely by receiving another waypoint.
|
||||
utility.AddUsableVehicle(usage);
|
||||
return utility.IsUsableVehicle(vehicle.Entity);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ConvoyColumnVehicle
|
||||
{
|
||||
bool Holding;
|
||||
vector Progress;
|
||||
float LastProgress = -1;
|
||||
float NextRetry;
|
||||
float NextTarget;
|
||||
float Speed = -1;
|
||||
int Ahead = -1;
|
||||
}
|
||||
|
||||
class ERR_SupplyConvoyColumn
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_SupplyConvoyTask m_Task;
|
||||
protected ref array<ref ERR_ConvoyColumnVehicle> m_State = {};
|
||||
protected ref array<int> m_Order = {};
|
||||
protected int m_Leader = -1;
|
||||
protected int m_RouteIndex;
|
||||
protected bool m_RearWait;
|
||||
protected float m_NextLog;
|
||||
void ERR_SupplyConvoyColumn(ERR_GameMode mode, ERR_SupplyConvoyTask task)
|
||||
{
|
||||
m_Mode = mode; m_Task = task;
|
||||
for (int i = 0; i < task.Vehicles.Count(); i++)
|
||||
{ m_State.Insert(new ERR_ConvoyColumnVehicle()); m_Order.Insert(i); }
|
||||
}
|
||||
protected void Speed(ERR_ConvoyVehicle vehicle, ERR_ConvoyColumnVehicle state, float speed)
|
||||
{
|
||||
if (state.Speed == speed) return;
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));
|
||||
if (movement) movement.SetCruiseSpeed(speed);
|
||||
state.Speed = speed;
|
||||
}
|
||||
// Called before tactics take ownership and after every new base leg. A fresh
|
||||
// progress clock prevents combat, reboarding and supply pauses counting as stuck.
|
||||
protected float DistanceToTarget(int index, vector target)
|
||||
{
|
||||
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||
if (vehicle.Released || !vehicle.Entity) return 1e20;
|
||||
return vector.DistanceXZ(vehicle.Entity.GetOrigin(), target);
|
||||
}
|
||||
protected void Reorder()
|
||||
{
|
||||
vector target = m_Task.Route.At(Math.Min(m_RouteIndex, m_Task.Route.Count() - 1));
|
||||
// Sort only at a new leg or after combat, never every tick. Keep the canonical
|
||||
// vehicle array/roles intact; travel order follows current physical positions.
|
||||
for (int i = 0; i < m_Order.Count(); i++)
|
||||
for (int j = i + 1; j < m_Order.Count(); j++)
|
||||
if (DistanceToTarget(m_Order[j], target) < DistanceToTarget(m_Order[i], target))
|
||||
{ int swap = m_Order[i]; m_Order[i] = m_Order[j]; m_Order[j] = swap; }
|
||||
string order;
|
||||
foreach (int index : m_Order)
|
||||
if (!m_Task.Vehicles[index].Released) order += (index + 1).ToString() + " ";
|
||||
ERR_HQ.Event("CONVOY FORMATION", m_Task.Receipt.SourceId + ": travel order by physical approach to destination: " + order);
|
||||
}
|
||||
void Reset(float now, bool newLeg = false, bool reform = false)
|
||||
{
|
||||
if (newLeg) m_RouteIndex = 0;
|
||||
if (newLeg || reform) { Reorder(); m_Leader = -1; }
|
||||
m_RearWait = false;
|
||||
for (int i = 0; i < m_State.Count(); i++)
|
||||
{
|
||||
ERR_ConvoyColumnVehicle state = m_State[i];
|
||||
state.Speed = -1; // Tactics may have slowed this driver around dismounted infantry.
|
||||
state.LastProgress = -1; state.Holding = false; state.Ahead = -1; state.NextTarget = now;
|
||||
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[i];
|
||||
if (!vehicle.Released && vehicle.Entity) Speed(vehicle, state, 35);
|
||||
}
|
||||
}
|
||||
protected void Hold(ERR_ConvoyVehicle vehicle, ERR_ConvoyColumnVehicle state, float now)
|
||||
{
|
||||
vehicle.ClearOrder(); state.Holding = true;
|
||||
state.Progress = vehicle.Entity.GetOrigin(); state.LastProgress = now; state.NextRetry = now + 45000;
|
||||
}
|
||||
protected bool Progress(ERR_ConvoyVehicle vehicle, ERR_ConvoyColumnVehicle state, float now)
|
||||
{
|
||||
vector position = vehicle.Entity.GetOrigin();
|
||||
if ((state.LastProgress < 0 || now >= state.NextRetry) && ERR_ConvoyDriving.Restore(m_Mode, vehicle))
|
||||
ERR_HQ.Event("CONVOY DRIVER RECOVERY", string.Format("%1 vehicle %2: restored native vehicle registration for its healthy original seated driver after a failed movement task", m_Task.Receipt.SourceId, vehicle.Index + 1));
|
||||
if (state.LastProgress < 0 || vector.DistanceXZ(position, state.Progress) >= 5)
|
||||
{
|
||||
state.Progress = position; state.LastProgress = now; state.NextRetry = now + 45000;
|
||||
}
|
||||
if (now - state.LastProgress >= 180000)
|
||||
{
|
||||
ERR_HQ.Event("CONVOY STRANDED", string.Format("%1 vehicle %2 at %3: no 5 m movement in three minutes of commanded travel. Released from column; vehicle and crew retained, no replacement", m_Task.Receipt.SourceId, vehicle.Index + 1, position));
|
||||
vehicle.Release(m_Mode); return false;
|
||||
}
|
||||
if (now >= state.NextRetry)
|
||||
{
|
||||
vehicle.ClearOrder(); state.NextRetry = now + 45000;
|
||||
ERR_HQ.Event("CONVOY ROUTE RETRY", string.Format("%1 vehicle %2 at %3: stationary for %4 seconds; asking native driver to plan again", m_Task.Receipt.SourceId, vehicle.Index + 1, position, Math.Round((now - state.LastProgress) / 1000)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Tick(float now)
|
||||
{
|
||||
int leader = -1, previous = -1; float largestGap; bool supplyHold;
|
||||
for (int slot = 0; slot < m_Order.Count(); slot++)
|
||||
{
|
||||
int i = m_Order[slot];
|
||||
ERR_ConvoyVehicle scan = m_Task.Vehicles[i];
|
||||
if (scan.Released || !scan.Entity) continue;
|
||||
if (leader < 0) leader = i;
|
||||
if (previous >= 0) largestGap = Math.Max(largestGap, vector.DistanceXZ(scan.Entity.GetOrigin(), m_Task.Vehicles[previous].Entity.GetOrigin()));
|
||||
if (scan.SupplyHeld) supplyHold = true;
|
||||
previous = i;
|
||||
}
|
||||
if (leader < 0) return false;
|
||||
if (m_Leader != leader)
|
||||
{
|
||||
ERR_ConvoyVehicle promoted = m_Task.Vehicles[leader]; promoted.ClearOrder(); promoted.Arrived = false;
|
||||
promoted.RouteIndex = m_RouteIndex;
|
||||
m_Leader = leader;
|
||||
ERR_HQ.Event("CONVOY LEADER", string.Format("%1: vehicle %2 leads surviving column", m_Task.Receipt.SourceId, leader + 1));
|
||||
}
|
||||
bool rearWait = ERR_ConvoyColumnRules.WaitForRear(largestGap, m_RearWait);
|
||||
if (rearWait != m_RearWait)
|
||||
ERR_HQ.Event("CONVOY SPACING", string.Format("%1: leader waiting=%2; largest gap between surviving neighbours=%3 m", m_Task.Receipt.SourceId, rearWait, Math.Round(largestGap)));
|
||||
m_RearWait = rearWait;
|
||||
previous = -1;
|
||||
for (int ordinal = 0; ordinal < m_Order.Count(); ordinal++)
|
||||
{
|
||||
int index = m_Order[ordinal];
|
||||
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||
if (vehicle.Released || !vehicle.Entity) continue;
|
||||
ERR_ConvoyColumnVehicle state = m_State[index];
|
||||
if (vehicle.SupplyHeld || vehicle.Combat.Held)
|
||||
{
|
||||
// The external owner controls its own waypoint. Only suspend our watchdog.
|
||||
state.LastProgress = -1; previous = index; continue;
|
||||
}
|
||||
if (previous < 0)
|
||||
{
|
||||
// A former leader may have finished this route before fighting elsewhere,
|
||||
// or its successor may still be short of the endpoint. Rejoin it physically.
|
||||
if (!vehicle.Arrived && vehicle.RouteIndex >= m_Task.Route.Count() && vector.DistanceXZ(vehicle.Entity.GetOrigin(), m_Task.Route.At(m_Task.Route.Count() - 1)) > 35)
|
||||
vehicle.RouteIndex = m_Task.Route.Count() - 1;
|
||||
m_RouteIndex = vehicle.RouteIndex;
|
||||
if (vehicle.Arrived) { Hold(vehicle, state, now); previous = index; continue; }
|
||||
if (m_RearWait || supplyHold) { Hold(vehicle, state, now); previous = index; continue; }
|
||||
state.Holding = false; Speed(vehicle, state, 35);
|
||||
// Mounted groups can retain their spawn origin: arrival is always physical.
|
||||
while (vehicle.RouteIndex < m_Task.Route.Count() && vector.DistanceXZ(vehicle.Entity.GetOrigin(), m_Task.Route.At(vehicle.RouteIndex)) <= 35)
|
||||
{ vehicle.ClearOrder(); vehicle.RouteIndex++; }
|
||||
m_RouteIndex = vehicle.RouteIndex;
|
||||
if (vehicle.RouteIndex >= m_Task.Route.Count())
|
||||
{ vehicle.Arrived = true; Hold(vehicle, state, now); previous = index; continue; }
|
||||
if (!Progress(vehicle, state, now)) break; // promote once on the next tick
|
||||
if (!vehicle.Waypoint)
|
||||
{
|
||||
// Leave tolerance between native completion and our vehicle-origin check.
|
||||
// A seated driver's position is not exactly the vehicle origin.
|
||||
vehicle.Waypoint = ERR_SupplyConvoyTactics.Move(m_Task.Route.At(vehicle.RouteIndex), 15);
|
||||
if (vehicle.Waypoint) vehicle.Group.AddWaypoint(vehicle.Waypoint);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ERR_ConvoyVehicle ahead = m_Task.Vehicles[previous];
|
||||
float gap = vector.DistanceXZ(vehicle.Entity.GetOrigin(), ahead.Entity.GetOrigin());
|
||||
vehicle.Arrived = ahead.Arrived && gap <= 75;
|
||||
if (vehicle.Arrived || ERR_ConvoyColumnRules.TooClose(gap, state.Holding)) Hold(vehicle, state, now);
|
||||
else
|
||||
{
|
||||
state.Holding = false; Speed(vehicle, state, ERR_ConvoyColumnRules.Speed(gap));
|
||||
if (Progress(vehicle, state, now))
|
||||
{
|
||||
// Keep an existing target through small movements. Replanning every tick
|
||||
// repeatedly cancels native manoeuvres, particularly around junctions.
|
||||
bool changed = state.Ahead != previous;
|
||||
if (vehicle.Waypoint && vector.DistanceXZ(vehicle.Waypoint.GetOrigin(), ahead.Entity.GetOrigin()) >= 60 && now >= state.NextTarget) changed = true;
|
||||
if (vehicle.Waypoint && vector.DistanceXZ(vehicle.Entity.GetOrigin(), vehicle.Waypoint.GetOrigin()) <= 20) changed = true;
|
||||
if (changed) vehicle.ClearOrder();
|
||||
if (!vehicle.Waypoint)
|
||||
{
|
||||
vehicle.Waypoint = ERR_SupplyConvoyTactics.Move(ahead.Entity.GetOrigin(), 15);
|
||||
if (vehicle.Waypoint) vehicle.Group.AddWaypoint(vehicle.Waypoint);
|
||||
state.Ahead = previous; state.NextTarget = now + 8000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!vehicle.Released) previous = index;
|
||||
}
|
||||
if (now >= m_NextLog)
|
||||
{
|
||||
ERR_HQ.Event("CONVOY TRAVEL", string.Format("%1: leg %2, leader vehicle %3, largest neighbour gap %4 m, waiting for rear=%5, supplying infantry=%6", m_Task.Receipt.SourceId, m_Task.Legs, m_Leader + 1, Math.Round(largestGap), m_RearWait, supplyHold));
|
||||
m_NextLog = now + 60000;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,7 @@ class ERR_SupplyConvoyService : ERR_ConvoyService
|
||||
team.Group.SetPermanentLOD(0); team.Group.ActivateAI();
|
||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||
}
|
||||
supply.Column = new ERR_SupplyConvoyColumn(m_Mode, supply);
|
||||
supply.Tactics = new ERR_SupplyConvoyTactics(m_Mode, supply);
|
||||
supply.TravelDeadline = now + 600000;
|
||||
return true;
|
||||
@@ -166,16 +167,10 @@ class ERR_SupplyConvoyService : ERR_ConvoyService
|
||||
}
|
||||
protected void Circuit(ERR_SupplyConvoyTask task, float now)
|
||||
{
|
||||
bool surviving, pending;
|
||||
if (!task.Column || !task.Column.Tick(now)) { task.Stop(m_Mode, "no surviving drivable convoy vehicles"); return; }
|
||||
bool pending;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (!vehicle.Released && now >= task.TravelDeadline && task.TravelDeadline > 0) { vehicle.ClearOrder(); vehicle.Arrived = true; }
|
||||
if (!vehicle.Released && task.Legs > 0 && vector.Distance(vehicle.Entity.GetOrigin(), task.Bases[task.LastBase]) <= 250)
|
||||
{ vehicle.ClearOrder(); vehicle.Arrived = true; }
|
||||
vehicle.Tick(m_Mode, task.Route, now);
|
||||
if (!vehicle.Released) { surviving = true; if (!vehicle.Arrived) pending = true; }
|
||||
}
|
||||
if (!surviving) { task.Stop(m_Mode, "no surviving drivable convoy vehicles"); return; }
|
||||
if (!vehicle.Released && (!vehicle.Arrived || vehicle.SupplyHeld || vehicle.Combat.Held)) pending = true;
|
||||
if (pending) return;
|
||||
if (!task.Waiting) { task.Waiting = true; task.Departure = now + Math.RandomFloat(45000, 90000); return; }
|
||||
if (now < task.Departure || task.Bases.Count() < 2) return;
|
||||
@@ -189,6 +184,7 @@ class ERR_SupplyConvoyService : ERR_ConvoyService
|
||||
if (departure.Released) continue;
|
||||
departure.ClearOrder(); departure.RouteIndex = 0; departure.Arrived = false;
|
||||
}
|
||||
task.Column.Reset(now, true);
|
||||
ERR_HQ.Event("CONVOY DEPARTURE", string.Format("%1 leg=%2 base=%3; native road navigation", task.Receipt.SourceId, task.Legs, task.Bases[next]));
|
||||
}
|
||||
override void Tick(float now)
|
||||
|
||||
@@ -99,7 +99,7 @@ class ERR_SupplyConvoySystem
|
||||
if (attempt == 0) seed = m_Bases[(m_Attempts + m_Service.Count()) % m_Bases.Count()];
|
||||
seed = ERR_Actors.Ground(seed);
|
||||
array<vector> positions, angles; ERR_ForceRoute route;
|
||||
if (!ERR_ConvoyRoadPlan.Build(seed, positions, angles, route) || !ERR_SupplyConvoyRules.Separated(positions[0], m_Starts, separation)) continue;
|
||||
if (!ERR_ConvoyRoadPlan.Build(seed, positions, angles, route, m_Starts, separation) || !ERR_SupplyConvoyRules.Separated(positions[0], m_Starts, separation)) continue;
|
||||
string source = "supply-convoy-" + (m_Service.Count() + 1).ToString();
|
||||
ERR_ConvoyReceipt receipt = m_Pool.Reserve(source);
|
||||
if (!receipt || receipt.Consumed) { m_Reason = "configured convoy capacity or shared force budget unavailable"; return false; }
|
||||
@@ -123,7 +123,7 @@ class ERR_SupplyConvoySystem
|
||||
if (!admitted) { m_Reason = "waiting for existing force admission"; return; }
|
||||
m_Attempts++;
|
||||
StartOne();
|
||||
if (m_Attempts == 120 && m_Service.Count() < m_Target) ERR_HQ.Event("CONVOY STARTUP", "bounded survey exhausted; deployed slots retained without replacement");
|
||||
if (m_Attempts == 120 && m_Service.Count() < m_Target) ERR_HQ.Event("CONVOY STARTUP", string.Format("placement survey finished: %1/%2 convoys deployed; remaining slots could not find a clear separated road formation. Existing crews retained; no replacements", m_Service.Count(), m_Target));
|
||||
}
|
||||
string Status()
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ class ERR_SupplyTeam
|
||||
bool Registered;
|
||||
bool Released;
|
||||
vector OrderedContact;
|
||||
ref ERR_ConvoyBoardingProgress BoardingProgress;
|
||||
float BoardingRetryAt;
|
||||
void Clear()
|
||||
{
|
||||
foreach (AIWaypoint waypoint : Orders)
|
||||
@@ -36,6 +38,17 @@ class ERR_SupplyTeam
|
||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||
}
|
||||
int BoardingCount(Vehicle truck, out float remainingDistance)
|
||||
{
|
||||
int seated; remainingDistance = 0;
|
||||
foreach (IEntity member : Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) continue;
|
||||
if (CompartmentAccessComponent.GetVehicleIn(member) == truck) seated++;
|
||||
else remainingDistance = Math.Max(remainingDistance, vector.DistanceXZ(member.GetOrigin(), truck.GetOrigin()));
|
||||
}
|
||||
return seated;
|
||||
}
|
||||
bool Living()
|
||||
{
|
||||
foreach (IEntity member : Members)
|
||||
@@ -125,6 +138,7 @@ class ERR_SupplyConvoyTask : ERR_ConvoyTask
|
||||
{
|
||||
ref array<ref ERR_SupplyTeam> Teams = {};
|
||||
ref ERR_SupplyConvoyTactics Tactics;
|
||||
ref ERR_SupplyConvoyColumn Column;
|
||||
ref array<vector> Bases = {};
|
||||
ref array<vector> Starts = {};
|
||||
int LastBase = -1;
|
||||
@@ -154,6 +168,8 @@ class ERR_SupplyConvoyTactics
|
||||
protected vector m_Left;
|
||||
protected float m_LastContact = -1;
|
||||
protected float m_NextOrders;
|
||||
protected vector m_LastOrderContact;
|
||||
protected bool m_OrdersIssued;
|
||||
protected float m_BoardDeadline;
|
||||
protected bool m_TruckWithdrawn;
|
||||
protected float m_NextDismount;
|
||||
@@ -302,9 +318,10 @@ class ERR_SupplyConvoyTactics
|
||||
if (SupportPosition(index, desired, chosen)) support = chosen;
|
||||
if (vector.DistanceXZ(vehicle.Entity.GetOrigin(), support) > 20) orders.Add(Move(support));
|
||||
if (index == 1 || index == 2) return;
|
||||
AIWaypoint defend = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), support));
|
||||
if (defend) defend.SetCompletionRadius(20);
|
||||
orders.Add(defend);
|
||||
// Mounted escorts engage using native vehicle combat after reaching cover.
|
||||
// Defend is an infantry occupation task; deleting it during regroup could
|
||||
// leave ActivityDefend executing with an already deleted waypoint.
|
||||
// Do not assign it to a crew whose role requires remaining in the vehicle.
|
||||
}
|
||||
protected void Clear()
|
||||
{
|
||||
@@ -313,13 +330,18 @@ class ERR_SupplyConvoyTactics
|
||||
}
|
||||
protected void Orders(bool dismount, float now)
|
||||
{
|
||||
m_NextOrders = now + 30000;
|
||||
if (m_OrdersIssued && vector.DistanceXZ(m_LastOrderContact, m_Contact) < 40) return;
|
||||
m_LastOrderContact = m_Contact; m_OrdersIssued = true;
|
||||
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, !m_TruckWithdrawn);
|
||||
for (int i = 0; i < m_Task.Teams.Count(); i++) Flank(i, dismount);
|
||||
m_NextOrders = now + 30000;
|
||||
}
|
||||
protected void Start(vector contact, float now)
|
||||
{
|
||||
Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = false;
|
||||
Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = false; m_OrdersIssued = false;
|
||||
if (m_Task.Column) m_Task.Column.Reset(now);
|
||||
m_Task.Waiting = false;
|
||||
vector origin;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
@@ -339,13 +361,62 @@ class ERR_SupplyConvoyTactics
|
||||
Clear(); m_State = 0;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
vehicle.Combat.Held = false;
|
||||
vehicle.Combat.Held = false; vehicle.Arrived = false;
|
||||
// Combat time does not consume the driving deadline.
|
||||
vehicle.LegDeadline = now + 600000;
|
||||
}
|
||||
m_Task.TravelDeadline = now + 600000;
|
||||
if (m_Task.Column) m_Task.Column.Reset(now, false, true);
|
||||
ERR_HQ.Event("CONVOY REGROUP", m_Task.Receipt.SourceId + ": surviving convoy resumes; no replacements");
|
||||
}
|
||||
protected float InfantryDistance(IEntity vehicle)
|
||||
{
|
||||
float distance = 1e20;
|
||||
if (!vehicle) return distance;
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
{
|
||||
if (team.Released) continue;
|
||||
foreach (IEntity member : team.Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || CompartmentAccessComponent.GetVehicleIn(member)) continue;
|
||||
distance = Math.Min(distance, vector.DistanceXZ(member.GetOrigin(), vehicle.GetOrigin()));
|
||||
}
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
protected void ProtectInfantry()
|
||||
{
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||
{
|
||||
if (vehicle.Released || !vehicle.Entity) continue;
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));
|
||||
if (!movement) continue;
|
||||
float speed = 35;
|
||||
if (InfantryDistance(vehicle.Entity) < 45) speed = 8;
|
||||
movement.SetCruiseSpeed(speed);
|
||||
}
|
||||
}
|
||||
protected void RefreshBoarding(float now)
|
||||
{
|
||||
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||
{
|
||||
if (team.Released || team.Aboard(m_Task.Vehicles[2].Entity, true)) continue;
|
||||
if (team.BoardingRetryAt > 0)
|
||||
{
|
||||
if (now < team.BoardingRetryAt) continue;
|
||||
team.BoardingRetryAt = 0;
|
||||
team.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||
continue;
|
||||
}
|
||||
if (!team.BoardingProgress) team.BoardingProgress = new ERR_ConvoyBoardingProgress();
|
||||
float distance; int seated = team.BoardingCount(m_Task.Vehicles[2].Entity, distance);
|
||||
if (!team.BoardingProgress.Retry(seated, distance, now)) continue;
|
||||
// Let the old native activity release reservations before asking for seats
|
||||
// again. Occupants remain seated; no direct compartment move is performed.
|
||||
team.Clear(); team.BoardingRetryAt = now + 1000;
|
||||
ERR_HQ.Event("CONVOY BOARDING RETRY", string.Format("%1: team has %2 aboard; farthest remaining soldier %3 m from truck. No approach/seat progress for 30 s; retry %4/3", m_Task.Receipt.SourceId, seated, Math.Round(distance), team.BoardingProgress.Retries));
|
||||
}
|
||||
}
|
||||
bool Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return false;
|
||||
@@ -367,10 +438,10 @@ class ERR_SupplyConvoyTactics
|
||||
if (found) Report(observed, now, now);
|
||||
if (m_State == 1)
|
||||
{
|
||||
if (!m_TruckWithdrawn && InfantryAboard(false)) { m_TruckWithdrawn = true; Support(2, false); }
|
||||
if (!m_TruckWithdrawn && InfantryAboard(false) && InfantryDistance(m_Task.Vehicles[2].Entity) >= 25) { m_TruckWithdrawn = true; Support(2, false); }
|
||||
// Native exit attempts can fail while the truck brakes or a door is busy.
|
||||
// Retry only still-mounted teams, never teleport actors or reset quiet time.
|
||||
if (!m_TruckWithdrawn && Usable(2) && now >= m_NextDismount && m_DismountAttempts < 6)
|
||||
if (!m_TruckWithdrawn && !InfantryAboard(false) && Usable(2) && now >= m_NextDismount && m_DismountAttempts < 6)
|
||||
{
|
||||
m_DismountAttempts++; m_NextDismount = now + 10000;
|
||||
for (int retry = 0; retry < m_Task.Teams.Count(); retry++)
|
||||
@@ -387,17 +458,23 @@ class ERR_SupplyConvoyTactics
|
||||
if (!Usable(2)) { foreach (ERR_SupplyTeam lost : m_Task.Teams) lost.Release(m_Mode); Finish(now); return false; }
|
||||
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, true);
|
||||
foreach (ERR_SupplyTeam boarding : m_Task.Teams)
|
||||
if (!boarding.Released && boarding.Living()) boarding.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||
m_State = 2; m_BoardDeadline = now + 120000;
|
||||
if (!boarding.Released && boarding.Living())
|
||||
{
|
||||
boarding.BoardingProgress = new ERR_ConvoyBoardingProgress(); boarding.BoardingRetryAt = 0;
|
||||
boarding.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||
}
|
||||
m_State = 2; m_BoardDeadline = now + 300000;
|
||||
ERR_HQ.Event("CONVOY BOARDING", m_Task.Receipt.SourceId + ": both teams return under escort cover");
|
||||
}
|
||||
}
|
||||
if (m_State == 2 && Usable(2) && now < m_BoardDeadline) RefreshBoarding(now);
|
||||
if (m_State == 2 && (!Usable(2) || InfantryAboard(true) || now >= m_BoardDeadline))
|
||||
{
|
||||
foreach (ERR_SupplyTeam stranded : m_Task.Teams)
|
||||
if (!stranded.Aboard(m_Task.Vehicles[2].Entity, true)) stranded.Release(m_Mode);
|
||||
Finish(now);
|
||||
}
|
||||
if (m_State != 0) ProtectInfantry();
|
||||
return m_State != 0;
|
||||
}
|
||||
void Stop() { if (m_Stopped) return; m_Stopped = true; Clear(); }
|
||||
|
||||
@@ -24,6 +24,12 @@ modded class ERR_HQ
|
||||
if (!route.Nodes[i] || !route.Nodes[i].Valid(i == route.Nodes.Count() - 1)) return false;
|
||||
return true;
|
||||
}
|
||||
SCR_AIGroup ERR_AwarenessFixtureSource()
|
||||
{
|
||||
if (!ERR_NativeHQMemoryProbe.Enabled()) return null;
|
||||
foreach (ERR_HQUnit unit : m_Units) if (unit.Group && unit.Utility) return unit.Group;
|
||||
return null;
|
||||
}
|
||||
ERR_HQRoute ERR_MemoryFixtureQueue(ERR_HQReport report)
|
||||
{
|
||||
if (!ERR_NativeHQMemoryProbe.Enabled()) return null;
|
||||
@@ -34,10 +40,15 @@ modded class ERR_HQ
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
||||
if (!ERR_MemoryFixtureHealthy(route)) continue;
|
||||
report.Source = unit.Group;
|
||||
route.MessageId = report.MessageId;
|
||||
route.Payload = "FIXTURE synthetic observed position; " + report.Description(report.ObservedAt);
|
||||
unit.PendingReport = report.Snapshot(); unit.Uplink = route;
|
||||
return route;
|
||||
// Arrange an isolated inbox so natural contacts cannot replace this fixture
|
||||
// payload before it receives its message ID. Radio delivery remains native.
|
||||
unit.LocalAwareness.Clear();
|
||||
unit.LocalAwareness.Remember(report, report.ObservedAt);
|
||||
unit.NextContact = 0;
|
||||
ContactReport(unit, GetGame().GetWorld().GetWorldTime());
|
||||
if (!unit.Uplink || !unit.PendingReport || unit.PendingReport.Target != report.Target) return null;
|
||||
report.MessageId = unit.PendingReport.MessageId;
|
||||
return unit.Uplink;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -95,7 +106,7 @@ class ERR_NativeHQMemoryProbe
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1"
|
||||
&& System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "hq-memory";
|
||||
&& System.GetCLIParam("errSmokeOutcome", outcome) && (outcome == "hq-memory" || outcome == "hq-awareness");
|
||||
}
|
||||
void ERR_NativeHQMemoryProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string description)
|
||||
@@ -187,6 +198,47 @@ class ERR_NativeHQMemoryProbe
|
||||
}
|
||||
return Check(false, "bounded road fixture found no suitable native candidate; no fallback or force mutation");
|
||||
}
|
||||
protected bool LocalAwarenessPolicy(ERR_HQ hq, float now)
|
||||
{
|
||||
ERR_HQLocalAwareness cache = new ERR_HQLocalAwareness();
|
||||
ERR_HQObservationHistory history = new ERR_HQObservationHistory();
|
||||
SCR_AIGroup group = hq.ERR_AwarenessFixtureSource();
|
||||
if (!Check(group != null, "awareness fixture has a native registered source squad")) return false;
|
||||
ERR_HQReport visual = new ERR_HQReport();
|
||||
visual.Target = m_Mode; visual.Source = group; visual.ObservedAt = now;
|
||||
visual.Position = "123 4 456"; visual.SeenWeapons = "rifle";
|
||||
cache.Remember(visual, now); visual.Position = "999 0 999";
|
||||
ERR_HQReport delayed = cache.Peek(now + 2000, history);
|
||||
if (!Check(delayed && delayed.Position == "123 4 456" && delayed.ObservedAt == now && delayed.SeenWeapons == "rifle",
|
||||
"brief sighting keeps original position/time/equipment after two seconds")) return false;
|
||||
history.Queued(delayed.Target, delayed.ObservedAt, now + 2000); cache.Queued(delayed, now + 2000);
|
||||
if (!Check(!cache.Peek(now + 3000, history), "sent cached sighting cannot replay")) return false;
|
||||
cache.Remember(visual, now);
|
||||
if (!Check(!cache.Peek(now + 30001, new ERR_HQObservationHistory()), "local evidence expires after thirty seconds")) return false;
|
||||
cache.Clear();
|
||||
SCR_AIGroupUtilityComponent utility = SCR_AIGroupUtilityComponent.Cast(group.FindComponent(SCR_AIGroupUtilityComponent));
|
||||
// An isolated native perception object exercises the actual callback without
|
||||
// injecting the fake shooter into the real squad's combat targets.
|
||||
SCR_AIGroupPerception perception = new SCR_AIGroupPerception(utility, group);
|
||||
perception.ERR_BindAwareness(cache);
|
||||
perception.AddOrUpdateGunshot(m_Mode, "123 4 456", m_Mode.PlayerFaction(), GetGame().GetPerceptionManager().GetTime(), false);
|
||||
ERR_HQReport sound = cache.Peek(GetGame().GetWorld().GetWorldTime(), history);
|
||||
if (!Check(sound && !sound.Target && sound.Source == group && sound.Position == "75 4 525"
|
||||
&& sound.TravelMode == "heard gunfire" && sound.SeenWeapons == "unknown" && sound.SeenEscapists == -1,
|
||||
"native heard-fire callback captures only a coarse area and original time")) return false;
|
||||
sound.MessageId = 1;
|
||||
ERR_HQMemory memory = new ERR_HQMemory();
|
||||
if (!Check(memory.Ingest(sound, GetGame().GetWorld().GetWorldTime()), "heard-fire value report accepted separately from visual identity")) return false;
|
||||
ERR_HQBelief noise = memory.At(0);
|
||||
if (!Check(noise.Kind == ERR_HQMemoryKind.GUNFIRE && !noise.Target && !noise.HasVelocity && !noise.Actionable()
|
||||
&& noise.Confidence <= 0.4 && noise.Radius >= 150, "sound permits uncertain search, not precision targeting or motion inference")) return false;
|
||||
if (!Check(ERR_HQPursuitRules.Role(noise.Confidence, 0, noise.Radius, noise.HasVelocity, 0) == ERR_HQPursuitRole.SEARCH,
|
||||
"heard gunfire selects a search role")) return false;
|
||||
cache.Clear(); perception.ERR_BindAwareness(null);
|
||||
perception.AddOrUpdateGunshot(m_Mode, "123 4 456", m_Mode.PlayerFaction(), GetGame().GetPerceptionManager().GetTime(), false);
|
||||
if (!Check(!cache.Peek(GetGame().GetWorld().GetWorldTime(), history), "unbound perception cannot retain mission reports")) return false;
|
||||
return true;
|
||||
}
|
||||
protected bool LocalPolicies()
|
||||
{
|
||||
ERR_ForceLedger ledger = new ERR_ForceLedger();
|
||||
@@ -292,18 +344,27 @@ class ERR_NativeHQMemoryProbe
|
||||
if (now > m_Deadline) { Check(false, "fixture deadline"); return -1; }
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!hq.ERR_ProbeSquadsReady()) return 0;
|
||||
bool automaticWasChecked = m_AutoChecked;
|
||||
int automatic = AutomaticConvoy(hq);
|
||||
if (automatic <= 0) return automatic;
|
||||
// Startup admission and native crew readiness have their own bounded wait.
|
||||
// Start the original memory/radio deadline once, after automatic proof.
|
||||
if (!automaticWasChecked) m_Deadline = now + 120000;
|
||||
string outcome;
|
||||
System.GetCLIParam("errSmokeOutcome", outcome);
|
||||
bool awarenessOnly = outcome == "hq-awareness";
|
||||
if (!awarenessOnly)
|
||||
{
|
||||
if (!hq.ERR_ProbeSquadsReady()) return 0;
|
||||
bool automaticWasChecked = m_AutoChecked;
|
||||
int automatic = AutomaticConvoy(hq);
|
||||
if (automatic <= 0) return automatic;
|
||||
// Legacy combined convoy/memory qualification retains its original checks.
|
||||
if (!automaticWasChecked) m_Deadline = now + 120000;
|
||||
}
|
||||
if (!m_ValueChecked)
|
||||
{
|
||||
if (!Check(hq.ERR_MemoryFixtureRegisteredForces() == 6,
|
||||
"actual six site groups are registered once in the Core force ledger")) return -1;
|
||||
if (!LocalPolicies() || !LocalRoadPolicy()) return -1;
|
||||
if (!awarenessOnly)
|
||||
{
|
||||
if (!Check(hq.ERR_MemoryFixtureRegisteredForces() == 6,
|
||||
"actual six site groups are registered once in the Core force ledger")) return -1;
|
||||
if (!LocalPolicies() || !LocalRoadPolicy()) return -1;
|
||||
}
|
||||
if (!LocalAwarenessPolicy(hq, now)) return -1;
|
||||
m_ValueChecked = true;
|
||||
}
|
||||
m_FirstPosition = m_Mode.PrisonPosition() + "300 0 300";
|
||||
|
||||
@@ -19,6 +19,7 @@ modded class ERR_GameMode
|
||||
protected bool m_ERRTestRadioNetwork;
|
||||
protected bool m_ERRTestHQResponse;
|
||||
protected bool m_ERRTestHQMemory;
|
||||
protected bool m_ERRTestHQAwareness;
|
||||
protected bool m_ERRTestLocalHandoff;
|
||||
protected ref ERR_NativeLocalHandoffProbe m_ERRLocalHandoff;
|
||||
protected ERR_PlayerLife m_ERRLocalHandoffLife;
|
||||
@@ -74,7 +75,8 @@ modded class ERR_GameMode
|
||||
m_ERRTestLostHelicopter = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "lost-helicopter";
|
||||
m_ERRTestRadioNetwork = outcome == "radio-network";
|
||||
m_ERRTestHQResponse = outcome == "hq-response";
|
||||
m_ERRTestHQMemory = outcome == "hq-memory";
|
||||
m_ERRTestHQAwareness = outcome == "hq-awareness";
|
||||
m_ERRTestHQMemory = outcome == "hq-memory" || m_ERRTestHQAwareness;
|
||||
m_ERRTestLocalHandoff = outcome == "hq-local-handoff";
|
||||
m_ERRTestHQBody = outcome == "hq-body";
|
||||
m_ERRTestInfantryTransfer = outcome == "infantry-transfer";
|
||||
@@ -323,6 +325,24 @@ modded class ERR_GameMode
|
||||
Print("ERR SMOKE COMPLETE: native HQ patrol preemption and physical resumption");
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestHQAwareness)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 291, "awareness and radio checks completed before explicit fixture reset")) return;
|
||||
m_ERRSmokeDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||
ERR_StopHQPatrol();
|
||||
Print("ERR SMOKE COMPLETE: captured awareness, native heard-fire callback, physical radio delivery and memory; explicit fixture reset");
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestConvoy)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 1061, "convoy checks completed before explicit fixture reset")) return;
|
||||
m_ERRSmokeDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||
ERR_StopHQPatrol();
|
||||
Print("ERR SMOKE COMPLETE: native convoy readiness, cargo and road movement; explicit fixture reset");
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestPlayerDeath)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 91, "death exhausts round life and reaches defeat after spectator grace")) return;
|
||||
@@ -522,6 +542,13 @@ modded class ERR_GameMode
|
||||
if (m_ERRTestMortarSalvo)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
// Keep the isolated firing range clear of automatic UAZ responders.
|
||||
// Otherwise they pursue the inert test report into its impact area.
|
||||
if (ERR_HQ.Get().Hunters()) ERR_HQ.Get().Hunters().Stop();
|
||||
if (!ERR_Check(UnlockPrison(m_ERRSmokePlayer), "mortar fixture unlocks prison to start normal crew AI")) return;
|
||||
// The player is not this fixture's target. Keep the newly released
|
||||
// prisoner away from the battery while its inert report is tested.
|
||||
m_ERRSmokePlayer.SetOrigin(ERR_Actors.Ground("4500 0 11500"));
|
||||
m_ERRMortarSalvo = new ERR_NativeMortarSalvoProbe(this);
|
||||
m_ERRSmokeStep = 190;
|
||||
return;
|
||||
@@ -586,6 +613,12 @@ modded class ERR_GameMode
|
||||
if (convoyOutcome == "supply-convoys")
|
||||
{
|
||||
if (!ERR_Check(UnlockPrison(m_ERRSmokePlayer), "supply fixture unlocks prison to start normal mission AI")) return;
|
||||
if (ERR_HQ.Get().Hunters()) ERR_HQ.Get().Hunters().Stop();
|
||||
m_ERRSmokePlayer.SetOrigin(ERR_Actors.Ground("8800 0 2700"));
|
||||
PerceivableComponent convoyObserver = PerceivableComponent.Cast(m_ERRSmokePlayer.FindComponent(PerceivableComponent));
|
||||
if (!ERR_Check(convoyObserver != null, "convoy fixture observer has native perception component")) return;
|
||||
convoyObserver.SetPerceivedFactionOverride(EnemyFaction());
|
||||
Print("ERR SUPPLY FIXTURE: observer perceived as convoy-friendly; only the labelled contact injection exercises this fixture's combat cycle");
|
||||
m_ERRConvoyProbe = new ERR_NativeSupplyConvoyProbe(this);
|
||||
}
|
||||
else m_ERRConvoyProbe = new ERR_NativeConvoyProbe(this);
|
||||
@@ -836,18 +869,20 @@ modded class ERR_GameMode
|
||||
int result = m_ERRConvoyProbe.Tick(ERR_HQ.Cast(Command()), now);
|
||||
if (result == 0) return;
|
||||
if (!ERR_Check(result == 1, "native six-vehicle convoy readiness, cargo and road movement")) return;
|
||||
m_ERRSmokeStep = 9;
|
||||
SCR_CharacterDamageManagerComponent damage;
|
||||
damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
damage.EnableDamageHandling(true);
|
||||
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
|
||||
m_ERRSmokeWait = now + 4000;
|
||||
m_ERRSmokeStep = 1061;
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
}
|
||||
else if (m_ERRSmokeStep == 105)
|
||||
{
|
||||
int result = m_ERRHQMemoryProbe.Tick(ERR_HQ.Cast(Command()), now);
|
||||
if (result == 0) return;
|
||||
if (!ERR_Check(result == 1, "native HQ memory delivery and search lineage")) return;
|
||||
if (m_ERRTestHQAwareness)
|
||||
{
|
||||
m_ERRSmokeStep = 291;
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
return;
|
||||
}
|
||||
m_ERRSmokeStep = 9;
|
||||
SCR_CharacterDamageManagerComponent damage;
|
||||
damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
|
||||
@@ -52,9 +52,22 @@ class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe
|
||||
}
|
||||
if (m_CombatTask)
|
||||
{
|
||||
for (int car = 0; car < m_CombatTask.Vehicles.Count(); car++)
|
||||
{
|
||||
ERR_ConvoyVehicle diagnosticCar = m_CombatTask.Vehicles[car];
|
||||
vector target; if (diagnosticCar.Waypoint) target = diagnosticCar.Waypoint.GetOrigin();
|
||||
PrintFormat("ERR SUPPLY COLUMN: car=%1 released=%2 arrived=%3 position=%4 target=%5", car + 1, diagnosticCar.Released, diagnosticCar.Arrived, diagnosticCar.Entity.GetOrigin(), target);
|
||||
if (diagnosticCar.Waypoint) PrintFormat("ERR SUPPLY WAYPOINT: car=%1 distance=%2 nativeCurrent=%3 nativeComplete=%4 routeIndex=%5", car + 1, vector.DistanceXZ(diagnosticCar.Entity.GetOrigin(), target), diagnosticCar.Group.GetCurrentWaypoint() == diagnosticCar.Waypoint, diagnosticCar.Waypoint.IsWithinCompletionRadius(diagnosticCar.Group), diagnosticCar.RouteIndex);
|
||||
}
|
||||
ERR_ConvoyVehicle truck = m_CombatTask.Vehicles[2];
|
||||
SCR_VehicleDamageManagerComponent health = SCR_VehicleDamageManagerComponent.Cast(truck.Entity.FindComponent(SCR_VehicleDamageManagerComponent));
|
||||
PrintFormat("ERR SUPPLY TRUCK: released=%1 driverAlive=%2 driverGroup=%3 driverAboard=%4 health=%5 retained=%6 position=%7", truck.Released, ERR_Actors.CanFight(truck.Driver), ERR_Actors.Group(truck.Driver) == truck.Group, CompartmentAccessComponent.GetVehicleIn(truck.Driver) == truck.Entity, health.GetHealthScaled(), ERR_ConvoyMountedCrew.Retain(truck.Driver, truck.Entity), truck.Entity.GetOrigin());
|
||||
SCR_AIGroupUtilityComponent driverGroup = truck.Group.GetGroupUtilityComponent();
|
||||
SCR_ChimeraAIAgent truckDriver = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(truck.Driver));
|
||||
string groupAction = "unavailable", driverAction = "unavailable"; bool registeredVehicle;
|
||||
if (driverGroup) { groupAction = MovementAction(driverGroup.GetCurrentAction()); registeredVehicle = driverGroup.IsUsableVehicle(truck.Entity); }
|
||||
if (truckDriver && truckDriver.m_UtilityComponent) driverAction = MovementAction(truckDriver.m_UtilityComponent.GetCurrentBehavior());
|
||||
PrintFormat("ERR SUPPLY DRIVER: registeredVehicle=%1 groupAction=%2 driverAction=%3", registeredVehicle, groupAction, driverAction);
|
||||
foreach (ERR_SupplyTeam diagnostic : m_CombatTask.Teams)
|
||||
{
|
||||
int aboard;
|
||||
@@ -62,9 +75,13 @@ class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe
|
||||
PrintFormat("ERR SUPPLY INFANTRY: aboard=%1 members=%2 released=%3 groupPosition=%4 orders=%5", aboard, diagnostic.Members.Count(), diagnostic.Released, diagnostic.Group.GetOrigin(), diagnostic.Orders.Count());
|
||||
foreach (IEntity remaining : diagnostic.Members)
|
||||
{
|
||||
if (!remaining) { Print("ERR SUPPLY SEAT: original actor removed"); continue; }
|
||||
AIControlComponent control = AIControlComponent.Cast(remaining.FindComponent(AIControlComponent));
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent());
|
||||
SCR_ChimeraAIAgent agent;
|
||||
if (control) agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent());
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(remaining.FindComponent(CompartmentAccessComponent));
|
||||
if (!agent || !agent.m_UtilityComponent || !access)
|
||||
{ PrintFormat("ERR SUPPLY SEAT: original actor has no live AI/compartment utility; alive=%1 fit=%2", ERR_Actors.Alive(remaining), ERR_Actors.CanFight(remaining)); continue; }
|
||||
PrintFormat("ERR SUPPLY SEAT: name=%1 fit=%2 active=%3 canExit=%4 exiting=%5 action=%6 waypoint=%7", CompartmentAccessComponent.GetVehicleIn(remaining) == truck.Entity, ERR_Actors.CanFight(remaining), agent.IsAIActivated(), access.CanGetOutVehicle(), access.IsGettingOut(), agent.m_UtilityComponent.GetCurrentAction(), diagnostic.Group.GetCurrentWaypoint());
|
||||
}
|
||||
}
|
||||
@@ -110,12 +127,13 @@ class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe
|
||||
if (original.Starts.Count() != 6) return -1;
|
||||
foreach (vector start : original.Starts) { m_StartPositions.Insert(start); m_Moved.Insert(false); }
|
||||
}
|
||||
m_CombatTask = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(0));
|
||||
Print("ERR SUPPLY PASS: production auto-started three six-vehicle convoys, exact ledger admission, two separate seated teams each, finite native cargo");
|
||||
m_Phase = 1; m_PhaseBegan = now;
|
||||
}
|
||||
if (m_Phase == 1)
|
||||
{
|
||||
if (!m_AllMoved && now - m_PhaseBegan < 45000) return 0;
|
||||
if (!m_AllMoved) return 0;
|
||||
m_CombatTask = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(0));
|
||||
vector contact = m_CombatTask.Vehicles[2].Entity.GetOrigin() + "0 0 200";
|
||||
m_CombatTask.Tactics.Report(contact, now, now);
|
||||
@@ -137,18 +155,29 @@ class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe
|
||||
if (boarded.Released || !boarded.Aboard(m_CombatTask.Vehicles[2].Entity, true)) return -1;
|
||||
Print("ERR SUPPLY PASS: both original teams reboarded and production travel resumed after quiet interval");
|
||||
m_TestLegs = m_CombatTask.Legs; m_RouteResumePosition = m_CombatTask.Vehicles[2].Entity.GetOrigin();
|
||||
m_CombatTask.TravelDeadline = now; m_CombatTask.Waiting = false;
|
||||
Print("ERR SUPPLY FIXTURE: expire this trip's travel deadline to exercise the real pause and next base leg without a ten-minute test delay");
|
||||
Print("ERR SUPPLY FIXTURE: waiting for physical column arrival, production pause and next base leg; no forced timeout arrival");
|
||||
m_Phase = 3;
|
||||
}
|
||||
}
|
||||
if (m_Phase == 3 && m_CombatTask.Legs > m_TestLegs && vector.DistanceXZ(m_CombatTask.Vehicles[2].Entity.GetOrigin(), m_RouteResumePosition) >= 20)
|
||||
if (m_Phase >= 3) return Departure();
|
||||
return 0;
|
||||
}
|
||||
protected int Departure()
|
||||
{
|
||||
if (m_CombatTask.Stopped || m_CombatTask.Vehicles[2].Released || m_CombatTask.Bases.Count() < 2) return -1;
|
||||
if (m_Phase == 3 && m_CombatTask.Legs > m_TestLegs)
|
||||
{
|
||||
if (m_CombatTask.Stopped || m_CombatTask.Vehicles[2].Released || m_CombatTask.Bases.Count() < 2) return -1;
|
||||
Print("ERR SUPPLY PASS: original truck drives a new native base leg after the production pause; no replacement convoy");
|
||||
m_RouteResumePosition = m_CombatTask.Vehicles[2].Entity.GetOrigin();
|
||||
m_Phase = 4;
|
||||
Print("ERR SUPPLY FIXTURE: next leg started; record original truck position now, excluding all earlier regroup movement");
|
||||
return 0;
|
||||
}
|
||||
if (m_Phase == 4 && m_AllMoved) return 1;
|
||||
if (m_Phase == 4 && vector.DistanceXZ(m_CombatTask.Vehicles[2].Entity.GetOrigin(), m_RouteResumePosition) >= 20)
|
||||
{
|
||||
Print("ERR SUPPLY PASS: original truck drives twenty metres AFTER departure on the new native base leg; no replacement convoy");
|
||||
m_Phase = 5;
|
||||
}
|
||||
if (m_Phase == 5 && m_AllMoved) return 1;
|
||||
return 0;
|
||||
}
|
||||
override void Stop() {}
|
||||
|
||||
Reference in New Issue
Block a user