Files
EscapistsReintroduced/addons/ai/Scripts/Game/ERR/ERR_EnemyEquipment.c
T

210 lines
7.9 KiB
C

// Owned AI startup-equipment adaptation; source hashes in docs/ai-equipment-migration.md.
// HQ supplies the physical groups. No independent spawn/order timers exist here.
class ERR_EnemyEquipmentRules
{
static bool Eligible(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
{
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return false;
if (!group || !actor || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group) return false;
SCR_AIGroupUtilityComponent utility = group.GetGroupUtilityComponent();
return utility && utility.IsMilitary() && group.GetPlayerCount(true) == 0 && group.GetFaction() == mode.EnemyFaction();
}
static EAISkill Floor(EAISkill skill)
{
if (skill < EAISkill.EXPERT) return EAISkill.EXPERT;
return skill;
}
}
// The binding is explicit and mission-local; initialization of ordinary AI is untouched.
modded class SCR_AICombatComponent
{
protected ERR_GameMode m_ERRSkillMode;
protected SCR_AIGroup m_ERRSkillGroup;
protected EAISkill m_ERRPreviousSkill;
void ERR_BindEnemySkill(ERR_GameMode mode, SCR_AIGroup group)
{
if (!ERR_EnemyEquipmentRules.Eligible(mode, group, GetOwner())) return;
if (!m_ERRSkillMode) m_ERRPreviousSkill = GetAISkill();
m_ERRSkillMode = mode; m_ERRSkillGroup = group;
SetAISkill(GetAISkill());
}
void ERR_ReleaseEnemySkill(ERR_GameMode mode)
{
if (!m_ERRSkillMode || m_ERRSkillMode != mode) return;
m_ERRSkillMode = null; m_ERRSkillGroup = null;
super.SetAISkill(m_ERRPreviousSkill);
}
override void SetAISkill(EAISkill skill)
{
if (m_ERRSkillMode && ERR_EnemyEquipmentRules.Eligible(m_ERRSkillMode, m_ERRSkillGroup, GetOwner())) skill = ERR_EnemyEquipmentRules.Floor(skill);
super.SetAISkill(skill);
}
override void ResetAISkill()
{
super.ResetAISkill();
if (m_ERRSkillMode) SetAISkill(GetAISkill());
}
}
class ERR_EnemyEquipmentCallback : ScriptedInventoryOperationCallback
{
bool Finished;
bool Succeeded;
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
protected override void OnComplete() { Finish(true); }
protected override void OnFailed() { Finish(false); }
protected override void OnAllComplete() { Finish(true); }
protected override void OnAllFailed() { Finish(false); }
}
class ERR_EnemyEquipmentEntry
{
static const ResourceName LAUNCHER = "{3DF204EEE0534EF2}Prefabs/Weapons/Launchers/FIM92_Stinger/Launcher_FIM92_Stinger.et";
static const int MAX_CHECKS = 5;
IEntity Actor;
SCR_AIGroup Group;
SCR_AICombatComponent Combat;
ref ERR_EnemyOptics Optics;
int Checks;
int SkillChecks;
bool Rolled;
bool Selected;
bool Submitted;
bool Complete;
bool Released;
ref ERR_EnemyEquipmentCallback Operation;
protected bool Finish(string result)
{
Complete = true;
if (Selected) ERR_HQ.Event("STINGER", result);
return true;
}
bool Check(ERR_GameMode mode)
{
if (Complete) return true;
if (!ERR_EnemyEquipmentRules.Eligible(mode, Group, Actor)) return Finish("startup equipment cancelled: actor no longer eligible");
Checks++;
if (!Rolled)
{
int oneIn = mode.Settings().Get("stingerChanceOneIn");
Rolled = true;
if (oneIn > 0) Selected = Math.RandomInt(0, oneIn) == 0;
}
if (!Selected) return Finish("not selected");
BaseWeaponManagerComponent weapons = BaseWeaponManagerComponent.Cast(Actor.FindComponent(BaseWeaponManagerComponent));
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(Actor.FindComponent(InventoryStorageManagerComponent));
if (!weapons || !inventory)
{
if (Checks >= MAX_CHECKS) return Finish("inventory unavailable after bounded startup checks");
return false;
}
array<IEntity> equipped = {};
weapons.GetWeaponsList(equipped);
foreach (IEntity item : equipped)
{
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == LAUNCHER)
return Finish("physical FIM-92 observed in weapon slot; no replacement will be issued");
}
// A successful callback may precede a changed slot; dropping/looting that item
// before observation must not cause a second grant. Failure permits a bounded retry.
if (Submitted && Operation && Operation.Finished && Operation.Succeeded) return Finish("native insertion completed; equipment will not be replenished");
if (Checks >= MAX_CHECKS) return Finish("startup deadline reached; unresolved insertion quarantined for this actor");
if (Submitted && Operation && !Operation.Finished) return false;
Operation = new ERR_EnemyEquipmentCallback();
Submitted = true;
if (!inventory.TrySpawnPrefabToStorage(LAUNCHER, null, -1, EStoragePurpose.PURPOSE_WEAPON_PROXY, Operation)) Submitted = false;
return false;
}
void Stop(ERR_GameMode mode)
{
Complete = true;
Released = true;
if (Optics) Optics.Stop();
if (Combat) Combat.ERR_ReleaseEnemySkill(mode);
Combat = null;
// Do not interpret Stop as cancellation of an accepted native insertion.
// The service is terminal and retains this exact actor's completed entry.
}
}
class ERR_EnemyEquipmentGroup
{
SCR_AIGroup Group;
int Scans;
float ReadyAt;
}
class ERR_EnemyEquipment
{
protected ERR_GameMode m_Mode;
protected ref array<ref ERR_EnemyEquipmentGroup> m_Groups = {};
protected ref array<ref ERR_EnemyEquipmentEntry> m_Entries = {};
protected float m_NextCheck;
protected bool m_Stopped;
void ERR_EnemyEquipment(ERR_GameMode mode) { m_Mode = mode; }
void RegisterGroup(SCR_AIGroup group)
{
if (m_Stopped || !Replication.IsServer() || !group || m_Groups.Count() >= 128) return;
foreach (ERR_EnemyEquipmentGroup existing : m_Groups) if (existing.Group == group) return;
ERR_EnemyEquipmentGroup state = new ERR_EnemyEquipmentGroup(); state.Group = group;
state.ReadyAt = GetGame().GetWorld().GetWorldTime() + 2000;
m_Groups.Insert(state);
}
void UnregisterGroup(SCR_AIGroup group)
{
foreach (ERR_EnemyEquipmentGroup state : m_Groups) if (state.Group == group) state.Scans = 5;
foreach (ERR_EnemyEquipmentEntry entry : m_Entries) if (entry.Group == group) entry.Stop(m_Mode);
}
protected bool Known(IEntity actor)
{
foreach (ERR_EnemyEquipmentEntry entry : m_Entries) if (entry.Actor == actor) return true;
return false;
}
void Tick(float now)
{
if (m_Stopped || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || now < m_NextCheck) return;
m_NextCheck = now + 3000;
foreach (ERR_EnemyEquipmentGroup state : m_Groups)
{
if (state.Scans >= 5 || now < state.ReadyAt) continue;
state.Scans++;
if (!state.Group) continue;
array<AIAgent> agents = {}; state.Group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent || m_Entries.Count() >= 2048) continue;
IEntity actor = agent.GetControlledEntity();
if (Known(actor) || !ERR_EnemyEquipmentRules.Eligible(m_Mode, state.Group, actor)) continue;
ERR_EnemyEquipmentEntry entry = new ERR_EnemyEquipmentEntry(); entry.Actor = actor; entry.Group = state.Group;
entry.Optics = new ERR_EnemyOptics(actor, state.Group);
entry.Combat = SCR_AICombatComponent.Cast(actor.FindComponent(SCR_AICombatComponent));
if (entry.Combat) entry.Combat.ERR_BindEnemySkill(m_Mode, state.Group);
m_Entries.Insert(entry);
}
}
foreach (ERR_EnemyEquipmentEntry entry : m_Entries)
{
if (entry.Released) continue;
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, entry.Group, entry.Actor)) { entry.Stop(m_Mode); continue; }
// Combat may arrive after the group's initial membership notification.
if (!entry.Combat && entry.SkillChecks < 5)
{
entry.SkillChecks++;
entry.Combat = SCR_AICombatComponent.Cast(entry.Actor.FindComponent(SCR_AICombatComponent));
if (entry.Combat) entry.Combat.ERR_BindEnemySkill(m_Mode, entry.Group);
}
entry.Check(m_Mode);
if (entry.Optics) entry.Optics.Check(m_Mode);
}
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
foreach (ERR_EnemyEquipmentEntry entry : m_Entries) entry.Stop(m_Mode);
m_Groups.Clear();
}
}