Improve AI garrisons, player recovery and mission administration
This commit is contained in:
@@ -8,12 +8,12 @@ class ERR_AdminMenuFields
|
||||
{
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount"};
|
||||
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent"};
|
||||
}
|
||||
static bool Available(string key, bool hq, bool intel)
|
||||
{
|
||||
if (key == "aiIntelChancePercent") return intel;
|
||||
if (key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
|
||||
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
|
||||
return Keys().Contains(key);
|
||||
}
|
||||
// A short journal has no older off-screen lines, even if its slider is zero.
|
||||
@@ -23,10 +23,9 @@ class ERR_AdminMenuFields
|
||||
}
|
||||
// TextWidget interprets hash-prefixed tokens as localization IDs. Keep the
|
||||
// server audit and retained entries intact; render their numeric IDs literally.
|
||||
static string JournalText(string line)
|
||||
static string JournalText(string line, bool details = false)
|
||||
{
|
||||
line.Replace("#", "No. ");
|
||||
return line;
|
||||
return ERR_JournalText.Menu(line, details);
|
||||
}
|
||||
static string Display(string key, string value)
|
||||
{
|
||||
@@ -49,6 +48,8 @@ class ERR_AdminMenuFields
|
||||
else if (key == "reportIntervalSeconds") prefix = "hq interval";
|
||||
else if (key == "supportSquadLimit") prefix = "hq squads";
|
||||
else if (key == "responseRadiusMetres") prefix = "hq radius";
|
||||
else if (key == "antiAirChancePercent") prefix = "hq aa";
|
||||
else if (key == "antiPersonnelMineChancePercent") prefix = "hq mines";
|
||||
else if (key == "convoyCount") prefix = "hq convoy count";
|
||||
value = value.Trim();
|
||||
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
|
||||
@@ -66,6 +67,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
protected ref array<string> m_AvailableFields = {};
|
||||
protected ref array<string> m_LogLines = {};
|
||||
protected bool m_Bound;
|
||||
protected bool m_LogDetails;
|
||||
protected bool m_Ready;
|
||||
protected bool m_HQ;
|
||||
protected bool m_Intel;
|
||||
@@ -98,7 +100,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
|
||||
if (!m_Controller) { Close(); return; }
|
||||
array<string> names = {"Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "LatestJournal", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus"};
|
||||
array<string> names = {"Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus"};
|
||||
names.Insert("RandomDay_startTimeMinutes");
|
||||
foreach (string key : ERR_AdminMenuFields.Keys())
|
||||
{
|
||||
@@ -221,6 +223,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
}
|
||||
else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
|
||||
else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
|
||||
else if (name == "KeepWeaponOn" || name == "KeepWeaponOff") button.SetEnabled(m_Ready);
|
||||
}
|
||||
}
|
||||
void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
||||
@@ -241,6 +244,13 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
if (ERR_AdminMenuFields.Keys().Contains(resolvedKey)) SetText("Saved_" + resolvedKey, "Saved: Random (using " + entry.Substring(divider + 1, entry.Length() - divider - 1) + ")");
|
||||
continue;
|
||||
}
|
||||
if (key == "keepWeaponOnUnconscious")
|
||||
{
|
||||
string retention = "Disabled: held guns drop on unconsciousness.";
|
||||
if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") retention = "Enabled: retain your gun and equip it after revival.";
|
||||
SetText("Saved_keepWeaponOnUnconscious", retention);
|
||||
continue;
|
||||
}
|
||||
if (!ERR_AdminMenuFields.Keys().Contains(key)) continue;
|
||||
string value = ERR_AdminMenuFields.Display(key, entry.Substring(divider + 1, entry.Length() - divider - 1));
|
||||
m_AvailableFields.Insert(key);
|
||||
@@ -262,7 +272,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
}
|
||||
if (summary.IsEmpty()) summary = "No mission status is available yet.";
|
||||
SetText("Status", summary);
|
||||
string connection = "Connected. Saved values update live; your drafts are kept until this menu closes.";
|
||||
string connection = "Connected to server. Editing a value does not save it: press Apply. Unsaved edits are lost on close.";
|
||||
if (!m_HQ || !m_Intel) connection = "Connected. Controls for companion systems absent from this mission are disabled.";
|
||||
SetText("Connection", connection);
|
||||
m_Ready = true; m_Age = 0; UpdateActions();
|
||||
@@ -293,17 +303,21 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
m_LogLines.Insert(line);
|
||||
}
|
||||
m_LogCursor = cursor;
|
||||
string content;
|
||||
foreach (string entry : m_LogLines)
|
||||
{
|
||||
if (!content.IsEmpty()) content += "\n";
|
||||
content += ERR_AdminMenuFields.JournalText(entry);
|
||||
}
|
||||
if (content.IsEmpty()) content = "Waiting for the first HQ event...";
|
||||
SetText("JournalText", content);
|
||||
RenderJournal();
|
||||
m_ScrollToEnd = follow;
|
||||
}
|
||||
SetText("JournalState", "Live. Up to 300 received entries are retained here. Pause to read older entries.");
|
||||
SetText("JournalState", "Live. Times use the server clock (HH:MM:SS). Up to 300 entries; pause to read older ones.");
|
||||
}
|
||||
protected void RenderJournal()
|
||||
{
|
||||
string content;
|
||||
foreach (string entry : m_LogLines)
|
||||
{
|
||||
if (!content.IsEmpty()) content += "\n\n";
|
||||
content += ERR_AdminMenuFields.JournalText(entry, m_LogDetails);
|
||||
}
|
||||
if (content.IsEmpty()) content = "Waiting for the first HQ event...";
|
||||
SetText("JournalText", content);
|
||||
}
|
||||
protected void OnButton(SCR_ButtonBaseComponent button)
|
||||
{
|
||||
@@ -312,6 +326,17 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
if (name == "Close") { Close(); return; }
|
||||
int page = Navigation().Find(name);
|
||||
if (page >= 0) { ShowPage(page); return; }
|
||||
if (name == "JournalDetails")
|
||||
{
|
||||
m_LogDetails = !m_LogDetails;
|
||||
SCR_ButtonTextComponent detailsButton = SCR_ButtonTextComponent.Cast(button);
|
||||
if (detailsButton)
|
||||
{
|
||||
if (m_LogDetails) detailsButton.SetText("Show readable summaries");
|
||||
else detailsButton.SetText("Show technical details");
|
||||
}
|
||||
RenderJournal(); return;
|
||||
}
|
||||
if (name == "PauseJournal")
|
||||
{
|
||||
m_LogPaused = !m_LogPaused;
|
||||
@@ -324,11 +349,18 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
else
|
||||
{
|
||||
if (label) label.SetText("Pause live");
|
||||
SetText("JournalState", "Live - catching up..."); m_LogPoll = 1;
|
||||
SetText("JournalState", "Live - catching up. Times use the server clock (HH:MM:SS)."); m_LogPoll = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return;
|
||||
if (name == "KeepWeaponOn" || name == "KeepWeaponOff")
|
||||
{
|
||||
string retain = "0"; if (name == "KeepWeaponOn") retain = "1";
|
||||
m_ClickDelay = 0.3;
|
||||
SetText("Feedback", "Saving weapon retention for future knockouts...");
|
||||
m_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true); return;
|
||||
}
|
||||
if (name == "LatestJournal")
|
||||
{
|
||||
if (!m_HQ || m_LogPending) return;
|
||||
|
||||
@@ -4,6 +4,7 @@ class ERR_CommandArguments
|
||||
{
|
||||
static string CoreSetting(string section, string option)
|
||||
{
|
||||
if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious";
|
||||
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
|
||||
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
|
||||
return "";
|
||||
@@ -106,6 +107,9 @@ class ERR_AdminService
|
||||
}
|
||||
protected string SettingHelp(string key)
|
||||
{
|
||||
if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout.";
|
||||
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent")
|
||||
return "Set 0..100 percent or Random. Applies to newly equipped enemy soldiers; existing loadouts are not rerolled or replenished.";
|
||||
if (key == "intelRevealChancePercent")
|
||||
return "Use /esc intel reveal set N (0-100) or random. This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
|
||||
return "";
|
||||
|
||||
@@ -11,6 +11,8 @@ class ERR_ContentOption
|
||||
string NativeFactionKey;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Physical guard group belonging to this faction", "et")]
|
||||
ResourceName GuardGroup;
|
||||
[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")]
|
||||
ref array<string> EquipmentGuids;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class ERR_ContentSettings
|
||||
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
|
||||
{
|
||||
m_ReadOnly = true;
|
||||
Print("Escapists: invalid content preferences; authored defaults active, existing files preserved", LogLevel.ERROR);
|
||||
Print("Co10 Escape Reintroduced: invalid content preferences; authored defaults active, existing files preserved", LogLevel.ERROR);
|
||||
}
|
||||
}
|
||||
bool Enabled(string id) { return m_Values.Contains(id) && m_Values.Get(id) == 1; }
|
||||
|
||||
@@ -21,7 +21,7 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
|
||||
if (kind == ERR_LocationKind.RADIO) return "radio";
|
||||
if (kind == ERR_LocationKind.MORTAR) return "artilleryFireMission";
|
||||
if (kind == ERR_LocationKind.OUTPOST) return "building";
|
||||
if (id.Contains("medical")) return "medical";
|
||||
if (kind == ERR_LocationKind.HOSPITAL || id.Contains("medical")) return "medical";
|
||||
return "supplies";
|
||||
}
|
||||
void SetLocationIcon(ERR_LocationKind kind, string id, int knowledge = 2)
|
||||
|
||||
@@ -80,9 +80,9 @@ modded class ERR_AdminMenu
|
||||
if (name == "FactionsRandomAll")
|
||||
{
|
||||
foreach (string role : ERR_FactionChoices.Roles()) SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget()).SetCurrentItem(0);
|
||||
return;
|
||||
}
|
||||
if (name != "FactionsApply") return;
|
||||
// Random all saves the mode immediately; resolving a round never replaces it.
|
||||
if (name != "FactionsApply" && name != "FactionsRandomAll") return;
|
||||
string command = "factions set";
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
|
||||
@@ -97,7 +97,7 @@ class ERR_FactionSelection
|
||||
map<string, string> loaded;
|
||||
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Saved = loaded;
|
||||
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
|
||||
{ m_ReadOnly = true; Print("Escapists: invalid faction selections; defaults active and files preserved", LogLevel.ERROR); }
|
||||
{ m_ReadOnly = true; Print("Co10 Escape Reintroduced: invalid faction selections; defaults active and files preserved", LogLevel.ERROR); }
|
||||
}
|
||||
protected bool Read(string path, out map<string, string> values)
|
||||
{
|
||||
@@ -192,7 +192,7 @@ class ERR_FactionSelection
|
||||
if (!player.IsPlayable()) return false;
|
||||
m_Ready = true;
|
||||
mode.PublishFactionRoles(Choice("escapists").Key, Choice("enemy").Key);
|
||||
PrintFormat("Escapists factions: players=%1 enemy=%2 independent=%3", Choice("escapists").Key, Choice("enemy").Key, Choice("independent").Key);
|
||||
PrintFormat("Co10 Escape Reintroduced factions: players=%1 enemy=%2 independent=%3", Choice("escapists").Key, Choice("enemy").Key, Choice("independent").Key);
|
||||
return true;
|
||||
}
|
||||
void Snapshot(ERR_ContentSettings content, array<string> values)
|
||||
|
||||
@@ -343,6 +343,8 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
{
|
||||
if (!Replication.IsServer() || !actor || ERR_MissionRules.Terminal(m_Phase)) return;
|
||||
m_Roster.Spawned(id, actor);
|
||||
SCR_CharacterDamageManagerComponent medical = SCR_CharacterDamageManagerComponent.Cast(actor.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (medical) medical.ERR_EnableCasualtyProtection();
|
||||
m_StartedPlayers = true;
|
||||
m_NoSurvivorsSince = -1;
|
||||
}
|
||||
@@ -493,6 +495,13 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
if (RefreshRadioFeedback()) Replication.BumpMe();
|
||||
m_Recovery.Tick();
|
||||
if (!Active()) return;
|
||||
if (m_Recovery.DefeatDue(now))
|
||||
{
|
||||
m_EndReason = "All prisoners are unconscious and no treatment or recovery is progressing.";
|
||||
ERR_HQ.Event("MISSION FAILED", m_EndReason);
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
return;
|
||||
}
|
||||
if (m_Command) m_Command.Maintain(now);
|
||||
// Maintenance can finish or stop the round; do not resume tactical work.
|
||||
if (!Active()) return;
|
||||
|
||||
@@ -71,6 +71,7 @@ class ERR_CampTerrain
|
||||
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
|
||||
float guardHeight;
|
||||
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false;
|
||||
if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, heading), heading, 6, 0.6, guardHeight)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -93,6 +94,8 @@ class ERR_GeneratedSites
|
||||
foreach (ERR_SiteDefinition prison : definition.Sites) if (prison.RandomClear && prison.Kind == ERR_LocationKind.PRISON) m_Pending.Insert(prison);
|
||||
foreach (ERR_SiteDefinition radio : definition.Sites) if (radio.RandomClear && radio.Kind == ERR_LocationKind.RADIO) m_Pending.Insert(radio);
|
||||
foreach (ERR_SiteDefinition mortar : definition.Sites) if (mortar.RandomClear && mortar.Kind == ERR_LocationKind.MORTAR) m_Pending.Insert(mortar);
|
||||
foreach (ERR_SiteDefinition optional : definition.Sites)
|
||||
if (optional.RandomClear && (optional.Kind == ERR_LocationKind.HOSPITAL || optional.Kind == ERR_LocationKind.OUTPOST || optional.Kind == ERR_LocationKind.DEPOT)) m_Pending.Insert(optional);
|
||||
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
|
||||
if (!m_Pending.IsEmpty()) m_Terrain.Collect(m_Low, m_High);
|
||||
m_Began = System.GetTickCount();
|
||||
@@ -103,8 +106,12 @@ class ERR_GeneratedSites
|
||||
foreach (ERR_SiteDefinition other : m_Definition.Sites)
|
||||
{
|
||||
if (other == candidate) continue;
|
||||
// Unresolved random sites have no occupied footprint yet. Later placements
|
||||
// compare against all committed positions, not their unused fallback seeds.
|
||||
if (other.RandomClear && !other.PlacementResolved) continue;
|
||||
if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.RADIO && other.RandomClear) continue;
|
||||
float minimum = 300;
|
||||
if (candidate.Kind == ERR_LocationKind.HOSPITAL || candidate.Kind == ERR_LocationKind.OUTPOST || candidate.Kind == ERR_LocationKind.DEPOT) minimum = 600;
|
||||
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.MORTAR) minimum = 1000;
|
||||
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.PRISON) minimum = 500;
|
||||
if (candidate.Kind == ERR_LocationKind.RADIO && other.Kind == ERR_LocationKind.PRISON) minimum = 1000;
|
||||
@@ -133,8 +140,8 @@ class ERR_GeneratedSites
|
||||
{
|
||||
position[1] = floor + 0.02;
|
||||
site.Position = position; site.Heading = heading; site.PlacementResolved = true;
|
||||
PrintFormat("Escapists [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback);
|
||||
m_Index++; m_Attempts = 0;
|
||||
PrintFormat("Co10 Escape Reintroduced [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback);
|
||||
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||
}
|
||||
else if (fallback)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Presentation only. Original records stay available in the menu and log file.
|
||||
class ERR_JournalText
|
||||
{
|
||||
static string Field(string message, string key)
|
||||
{
|
||||
int start = message.IndexOf(key + "=");
|
||||
if (start < 0) return "unknown";
|
||||
start += key.Length() + 1;
|
||||
int end = start;
|
||||
if (start < message.Length() && message.Substring(start, 1) == "<")
|
||||
{
|
||||
while (end < message.Length() && message.Substring(end, 1) != ">") end++;
|
||||
if (end < message.Length()) end++;
|
||||
}
|
||||
else while (end < message.Length() && message.Substring(end, 1) != " " && message.Substring(end, 1) != ";") end++;
|
||||
if (end == start) return "unknown";
|
||||
return message.Substring(start, end - start);
|
||||
}
|
||||
static string Short(string text)
|
||||
{
|
||||
int address = text.IndexOf("<0x");
|
||||
if (address >= 0) text = text.Substring(0, address) + " [entity details available]";
|
||||
int resource = text.IndexOf("{");
|
||||
if (resource >= 0) text = text.Substring(0, resource);
|
||||
if (text.Length() > 190) text = text.Substring(0, 187) + "...";
|
||||
text.Replace("\n", " ");
|
||||
return text;
|
||||
}
|
||||
static string Subject(string message)
|
||||
{
|
||||
int end = message.IndexOf(" ");
|
||||
if (end < 0) return Short(message);
|
||||
string result = message.Substring(0, end);
|
||||
result.Replace("aggression-uaz-", "UAZ patrol ");
|
||||
return result;
|
||||
}
|
||||
static string YesNo(string value)
|
||||
{
|
||||
if (value == "1" || value == "true") return "yes";
|
||||
if (value == "0" || value == "false") return "no";
|
||||
return "unknown";
|
||||
}
|
||||
static string Description(string kind, string message)
|
||||
{
|
||||
if (kind == "REGISTER") return "Group " + Field(message, "group") + " joined HQ: " + Field(message, "members") + " soldiers, faction " + Field(message, "faction") + ". Assignment: " + Field(message, "force") + ".";
|
||||
if (kind == "SPAWN MEMBER") return "Group " + Field(message, "group") + ": soldier spawned at " + Field(message, "position") + ".";
|
||||
if (kind == "CONTACT LOCAL") return "Group " + Field(message, "group") + " spotted a target at " + Field(message, "observed") + ". Local combat has started; the report still needs to reach HQ.";
|
||||
if (kind == "AGGRESSION") return "HQ has identified " + Field(message, "bodies") + " bodies. Aggression: " + Field(message, "aggression") + ". Total hunter UAZ allowance: " + Field(message, "allowance") + ".";
|
||||
if (kind == "HUNTER SPAWN") return Subject(message) + " deployed with " + Field(message, "crew") + " crew at " + Field(message, "position") + ". Deployment source: " + Field(message, "base") + ".";
|
||||
if (kind == "HUNTER READY") return Subject(message) + ": driver and gunner are seated; registered with HQ.";
|
||||
if (kind == "HUNTER ORDER") return Subject(message) + " received its radio order. Drive to " + Field(message, "destination") + ".";
|
||||
if (kind == "HUNTER OPENING") return "Three starting UAZ patrol positions found on clear roads, at least 90 degrees apart around the players.";
|
||||
if (kind == "HUNTER OPENING FAILED") return "Could not find three safe, widely separated road positions. Starting UAZ spawns were withheld.";
|
||||
if (kind == "HUNTER WAIT") return "UAZ deployment delayed: " + Short(message);
|
||||
if (kind == "HUNTER FAILED") return "UAZ deployment failed. This deployment is used up; no replacement is spawned. " + Short(message);
|
||||
if (kind == "HUNTER RELEASE") return "UAZ driving task ended: " + Short(message);
|
||||
if (kind == "REPORT") return "Sighting report queued for radio transmission: " + Short(message);
|
||||
if (kind == "BODY REPORT") return "Body report queued for radio transmission: " + Short(message);
|
||||
if (kind == "STATUS REPORT") return "Squad status report queued: " + Short(message);
|
||||
if (kind == "CALL") return "A soldier is making a radio report; transmission starts after three uninterrupted seconds.";
|
||||
if (kind == "CALL COMPLETE") return "The soldier finished making the report. Radio delivery can now proceed.";
|
||||
if (kind == "TRANSMIT") return "Radio message in transit: " + Short(message);
|
||||
if (kind == "RECEIVE") return "Radio message received: " + Short(message);
|
||||
if (kind == "LOST") return "Radio delivery failed: " + Short(message);
|
||||
if (kind == "BELIEF") return "HQ received and retained a report. Orders can use this reported information.";
|
||||
if (kind == "ORDER QUEUED") return "HQ order queued; units have not received it yet. " + Short(message);
|
||||
if (kind == "ORDER DELIVERED") return "HQ order delivered: " + Short(message);
|
||||
if (kind == "ORDER FAILED") return "HQ order could not be carried out: " + Short(message);
|
||||
if (kind == "CHECK-IN OVERDUE") return "A squad missed its expected check-in. This does not confirm that its soldiers are dead. " + Short(message);
|
||||
if (kind == "AA PROFILE") return "Anti-air equipment selected for faction " + Field(message, "faction") + "; FIM-92 is the fallback.";
|
||||
if (kind == "AA FALLBACK") return "The faction anti-air launcher could not be used. Trying FIM-92 instead.";
|
||||
if (kind == "AA EQUIPMENT")
|
||||
{
|
||||
if (message.Contains("physically equipped")) return "A soldier has a loaded anti-air launcher. Dropping or firing it will not grant a replacement.";
|
||||
if (message.Contains("quarantined")) return "Anti-air preparation timed out. An unresolved grant is held to prevent duplicate weapons.";
|
||||
if (message.Contains("looted")) return "The issued anti-air launcher is no longer with its soldier; no replacement was granted.";
|
||||
if (message.Contains("FIM-92")) return "The FIM-92 fallback could not be confirmed usable. Check technical details for the cause.";
|
||||
return "Anti-air preparation stopped before a usable weapon could be confirmed. Check technical details.";
|
||||
}
|
||||
if (kind == "MINE EQUIPMENT")
|
||||
{
|
||||
if (message.Contains("two-mine allowance")) return "A soldier received carried AP mines (up to two). Used or looted mines are not replaced.";
|
||||
if (message.Contains("capacity")) return "No inventory space for AP mines; no loose mines were spawned.";
|
||||
if (message.Contains("deadline")) return "AP-mine preparation timed out; any pending grant will not be duplicated.";
|
||||
return "AP mines could not be added to a soldier's inventory. No loose mines were spawned.";
|
||||
}
|
||||
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 == "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);
|
||||
if (kind == "HELICOPTER RETURN") return "Helicopter returning to base: " + Short(message);
|
||||
if (kind == "AIR DROP AUTHORIZED") return "HQ approved a paratrooper drop: " + Short(message);
|
||||
if (kind == "AIR DROP WITHHELD") return "HQ withheld the paratrooper drop: " + Short(message);
|
||||
kind.ToLower(); kind.Replace("_", " ");
|
||||
return kind + ": " + Short(message);
|
||||
}
|
||||
static string Entry(int sequence, string clock, string kind, string message, int messageId)
|
||||
{
|
||||
string line = "[" + clock + "] " + Description(kind, message);
|
||||
line += " (event " + sequence.ToString();
|
||||
if (messageId > 0) line += ", message " + messageId.ToString();
|
||||
return line + ")";
|
||||
}
|
||||
static string Menu(string line, bool details)
|
||||
{
|
||||
int split = line.IndexOf("\n#");
|
||||
if (split >= 0 && !details) line = line.Substring(0, split);
|
||||
line.Replace("#", "No. ");
|
||||
return line;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT, MORTAR }
|
||||
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT, MORTAR, HOSPITAL }
|
||||
|
||||
[ComponentEditorProps(category: "Reintroduced", description: "Server-owned location with replicated discovery state")]
|
||||
class ERR_LocationComponentClass : ScriptComponentClass {}
|
||||
|
||||
@@ -88,6 +88,16 @@ modded class ERR_AdminMenu
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ERR_ProbeFactionSavedRandom()
|
||||
{
|
||||
if (!m_FactionFieldsInitialized) return false;
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget());
|
||||
if (!combo || combo.GetCurrentIndex() != 0 || m_FactionSaved.Get(role) != "random") return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ERR_ProbeFactionApply() { OnFactionButton(SCR_ButtonTextComponent.GetButtonText("FactionsApply", GetRootWidget())); }
|
||||
bool ERR_ProbeHQ() { return m_HQ; }
|
||||
bool ERR_ProbeIntel() { return m_Intel; }
|
||||
@@ -115,6 +125,35 @@ modded class ERR_AdminMenu
|
||||
OnModButton(SCR_ButtonTextComponent.GetButtonText(name, row));
|
||||
}
|
||||
int ERR_ProbeLogCursor() { return m_LogCursor; }
|
||||
bool ERR_ProbeJournalDetails()
|
||||
{
|
||||
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("JournalText"));
|
||||
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("JournalDetails", GetRootWidget());
|
||||
if (!text || !button || m_LogDetails || text.GetText().Contains("[session=")) return false;
|
||||
string summary = text.GetText(); int cursor = m_LogCursor; int entries = m_LogLines.Count();
|
||||
OnButton(button);
|
||||
if (!m_LogDetails || !text.GetText().Contains("[session=") || !text.GetText().Contains("No. ")) return false;
|
||||
OnButton(button);
|
||||
return !m_LogDetails && text.GetText() == summary && cursor == m_LogCursor && entries == m_LogLines.Count();
|
||||
}
|
||||
bool ERR_ProbeDescriptions()
|
||||
{
|
||||
foreach (string key : ERR_AdminMenuFields.Keys())
|
||||
{
|
||||
ERR_ProbeFieldPage(key);
|
||||
TextWidget help = TextWidget.Cast(GetRootWidget().FindAnyWidget("Help_" + key));
|
||||
if (!help || help.GetText().IsEmpty()) return false;
|
||||
GetRootWidget().Update();
|
||||
float width, height, textWidth, textHeight;
|
||||
help.GetScreenSize(width, height); help.GetTextSize(textWidth, textHeight);
|
||||
// GetTextSize uses layout units; screen bounds include the workspace DPI scale.
|
||||
width = GetGame().GetWorkspace().DPIUnscale(width);
|
||||
height = GetGame().GetWorkspace().DPIUnscale(height);
|
||||
PrintFormat("ERR ADMIN HELP BOUNDS: key=%1 widget=%2,%3 text=%4,%5", key, width, height, textWidth, textHeight);
|
||||
if (textWidth > width + 2 || textHeight > height + 2) { Print("ERR ADMIN HELP OVERFLOW: " + key); return false; }
|
||||
}
|
||||
ERR_ProbePage(0); return true;
|
||||
}
|
||||
bool ERR_ProbeJournalVisible()
|
||||
{
|
||||
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("JournalText"));
|
||||
@@ -129,7 +168,7 @@ modded class ERR_AdminMenu
|
||||
scroll.GetSliderPos(sliderX, sliderY);
|
||||
Print(string.Format("ERR ADMIN JOURNAL FOLLOW: slider=%1,%2 overflow=%3", sliderX, sliderY, height - viewHeight));
|
||||
if (height > viewHeight && sliderY < 0.98) return false;
|
||||
return m_Page == 2 && text.GetText().Contains("No. ") && width > viewWidth * 0.5 && height > 2 && x < viewX + viewWidth && x + width > viewX && y < viewY + viewHeight && y + height > viewY;
|
||||
return m_Page == 2 && text.GetText().Contains("(event ") && width > viewWidth * 0.5 && height > 2 && x < viewX + viewWidth && x + width > viewX && y < viewY + viewHeight && y + height > viewY;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -175,7 +214,7 @@ class ERR_NativeAdminMenuProbe
|
||||
m_Deadline = now + 90000;
|
||||
m_RhsSaved = m_Mode.ContentSettings().Enabled("rhs");
|
||||
GetGame().OpenPauseMenu(false, true);
|
||||
if (!PauseMenuUI.ERR_ProbeModsEntry()) return Fail("Esc menu missing Escapists Reintroduced entry");
|
||||
if (!PauseMenuUI.ERR_ProbeModsEntry()) return Fail("Esc menu missing Co10 Escape Reintroduced entry");
|
||||
PauseMenuUI.ERR_ProbeOpenMods();
|
||||
Pass("native Esc menu entry opens mission administration");
|
||||
m_Stage = 1; return 0;
|
||||
@@ -187,6 +226,8 @@ class ERR_NativeAdminMenuProbe
|
||||
array<string> capabilities = {}; m_Mode.Admin().Capabilities(capabilities);
|
||||
if (menu.ERR_ProbeHQ() != capabilities.Contains("supports=hq") || menu.ERR_ProbeIntel() != capabilities.Contains("supports=intel")) return Fail("capability snapshot mismatch");
|
||||
Pass("real owner RPC opens menu and returns current settings/capabilities");
|
||||
if (!menu.ERR_ProbeDescriptions()) return Fail("setting descriptions missing or clipped");
|
||||
Pass("all thirteen setting descriptions fit their native text widgets");
|
||||
System.MakeScreenshot("$logs:err-admin-open");
|
||||
if (!menu.ERR_ProbeNavigation(0)) return Fail("Esc entry did not open Mission page with shared navigation");
|
||||
menu.ERR_ProbePage(3);
|
||||
@@ -259,21 +300,35 @@ class ERR_NativeAdminMenuProbe
|
||||
m_FactionActive.Insert(m_Mode.FactionSelection().Active(role));
|
||||
}
|
||||
if (!menu.ERR_ProbeFactionRandomAll()) return Fail("faction selectors or Random all failed");
|
||||
Pass("three faction selectors and Random all create distinct-role drafts");
|
||||
Pass("Random all submits persistent random mode for all three roles");
|
||||
m_Wait = now + 3000; m_Stage = 20; return 0;
|
||||
}
|
||||
if (m_Stage == 20)
|
||||
{
|
||||
if (!menu || !menu.ERR_ProbeFactionRandomAll()) return Fail("snapshot lost faction drafts");
|
||||
menu.ERR_ProbeFactionApply();
|
||||
m_Wait = now + 1500; m_Stage = 21; return 0;
|
||||
if (!menu || !menu.ERR_ProbeFactionSavedRandom()) return Fail("Random all did not save without Apply");
|
||||
ERR_FactionSelection reloaded = new ERR_FactionSelection();
|
||||
foreach (string savedRole : ERR_FactionChoices.Roles())
|
||||
if (reloaded.Saved(savedRole) != "random") return Fail("Random mode did not survive settings reload");
|
||||
menu.Close(); m_Wait = now + 500; m_Stage = 23; return 0;
|
||||
}
|
||||
if (m_Stage == 23)
|
||||
{
|
||||
ERR_AdminMenu.Open(); m_Wait = now + 1500; m_Stage = 24; return 0;
|
||||
}
|
||||
if (m_Stage == 24)
|
||||
{
|
||||
if (!menu || !menu.ERR_ProbeReady()) return 0;
|
||||
menu.ERR_ProbePage(4);
|
||||
if (!menu.ERR_ProbeFactionSavedRandom()) return Fail("Random mode lost after reopening menu");
|
||||
Pass("Random all survives settings reload, fresh snapshots and menu reopen without Apply");
|
||||
m_Stage = 21; return 0;
|
||||
}
|
||||
if (m_Stage == 21)
|
||||
{
|
||||
array<string> roles = ERR_FactionChoices.Roles();
|
||||
for (int i = 0; i < roles.Count(); i++)
|
||||
if (m_Mode.FactionSelection().Saved(roles[i]) != "random" || m_Mode.FactionSelection().Active(roles[i]) != m_FactionActive[i]) return Fail("faction Apply failed or changed current round");
|
||||
Pass("faction Apply persists all three through authenticated owner RPC without changing active round");
|
||||
if (m_Mode.FactionSelection().Saved(roles[i]) != "random" || m_Mode.FactionSelection().Active(roles[i]) != m_FactionActive[i]) return Fail("Random all save failed or changed current round");
|
||||
Pass("Random all persists through authenticated owner RPC without changing active round");
|
||||
m_Controller.ERR_AdminSubmit("factions set " + m_FactionSaved[0] + " " + m_FactionSaved[1] + " " + m_FactionSaved[2], true);
|
||||
m_Wait = now + 1500; m_Stage = 22; return 0;
|
||||
}
|
||||
@@ -290,6 +345,8 @@ class ERR_NativeAdminMenuProbe
|
||||
{
|
||||
if (menu.ERR_ProbeHQ() && menu.ERR_ProbeLogCursor() < 0) return Fail("journal owner RPC did not return");
|
||||
if (menu.ERR_ProbeHQ() && !menu.ERR_ProbeJournalVisible()) return Fail("received journal text is empty, collapsed or outside its viewport");
|
||||
if (menu.ERR_ProbeHQ() && !menu.ERR_ProbeJournalDetails()) return Fail("readable/technical journal toggle lost entries or text");
|
||||
Pass("journal switches between readable summaries and complete technical records without losing cursor/history");
|
||||
System.MakeScreenshot("$logs:err-admin-journal");
|
||||
Pass("HQ capability and journal page remain within current mission services");
|
||||
menu.Close();
|
||||
|
||||
@@ -16,6 +16,20 @@ modded class ERR_ObjectiveDefence
|
||||
}
|
||||
modded class ERR_HunterService
|
||||
{
|
||||
bool ERR_ProbeOpening(ERR_GameMode mode)
|
||||
{
|
||||
if (!m_Opening || !m_Opening.Ready() || m_Hunters.Count() < 3) return false;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
ERR_HunterVehicle unit = m_Hunters[i];
|
||||
if (!unit.Registered || !unit.CrewValid(mode) || unit.Members.Count() != 2) return false;
|
||||
bool manpack;
|
||||
foreach (IEntity actor : unit.Members) if (ERR_EnemyRadio.Capability(actor) == 2) manpack = true;
|
||||
if (!manpack) return false;
|
||||
for (int j = i + 1; j < 3; j++) if (!ERR_HunterOpeningRules.Spread(m_Opening.Center, m_Opening.Positions[i], m_Opening.Positions[j])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
ERR_HunterVehicle ERR_ProbeFirst()
|
||||
{
|
||||
if (m_Hunters.IsEmpty()) return null;
|
||||
@@ -46,6 +60,7 @@ modded class ERR_GameMode
|
||||
protected int m_ERRAggressionStage;
|
||||
protected int m_ERRAggressionDeadline;
|
||||
protected vector m_ERRHunterStart;
|
||||
protected float m_ERRHunterWait;
|
||||
protected bool m_ERRHunterOrder;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
@@ -74,9 +89,7 @@ modded class ERR_GameMode
|
||||
{
|
||||
if (!ERR_AggressionCheck(UnlockPrison(player), "native prison release")) return;
|
||||
player.SetOrigin(ERR_Actors.Ground(Sites().Find("everon-medical-1").Location.Position() + "300 0 0"));
|
||||
for (int i = 0; i < 3; i++) hq.ERR_ProbeBody(i, now);
|
||||
hq.ERR_ProbeBody(2, now);
|
||||
if (!ERR_AggressionCheck(hq.Watch().IdentifiedBodies() == 3 && hq.Watch().Aggression() == 1 && hq.Watch().HunterTarget() == 1, "three distinct delivered bodies unlock one UAZ; duplicate ignored")) return;
|
||||
if (!ERR_AggressionCheck(hq.Watch().HunterTarget() == 3, "opening allowance is three without identified bodies")) return;
|
||||
if (!ERR_AggressionCheck(ERR_HQWatchRules.Aggression(90) == 30 && ERR_HQWatchRules.Hunters(30) == 15 && ERR_HQWatchRules.Hunters(1000) == 15, "maximum 30 aggression / 15 UAZ allowance")) return;
|
||||
for (int j = 0; j < Sites().Count(); j++)
|
||||
{
|
||||
@@ -90,6 +103,11 @@ modded class ERR_GameMode
|
||||
if (!hunter || !hunter.Registered) return;
|
||||
if (m_ERRAggressionStage == 1)
|
||||
{
|
||||
if (!hq.Hunters().ERR_ProbeOpening(this)) return;
|
||||
if (!ERR_AggressionCheck(hq.Hunters().Deployed() == 3, "three native seated UAZs on widely separated roads, each carrying a manpack")) return;
|
||||
for (int bodySlot = 0; bodySlot < 3; bodySlot++) hq.ERR_ProbeBody(bodySlot, now);
|
||||
hq.ERR_ProbeBody(2, now);
|
||||
if (!ERR_AggressionCheck(hq.Watch().IdentifiedBodies() == 3 && hq.Watch().Aggression() == 1 && hq.Watch().HunterTarget() == 4, "three identified bodies add one UAZ beyond opening three; duplicate ignored")) return;
|
||||
if (!ERR_AggressionCheck(hunter.CrewValid(this) && hunter.Members.Count() == 2, "physical UAZ with native seated driver and gunner")) return;
|
||||
m_ERRHunterStart = hunter.Entity.GetOrigin(); m_ERRAggressionStage = 2;
|
||||
}
|
||||
@@ -105,11 +123,40 @@ modded class ERR_GameMode
|
||||
if (!hq.ERR_ProbeMedicalDefence()) return;
|
||||
if (!ERR_AggressionCheck(true, "nearby medical stash gained complete dedicated squad and defend waypoint at terrain height")) return;
|
||||
if (!ERR_AggressionCheck(true, "HQ radio delivered road order; UAZ drove over 30m; maximum scoped crew skill active")) return;
|
||||
if (!ERR_AggressionCheck(hq.Hunters().Deployed() == 1, "bounded allowance does not spawn duplicate UAZs")) return;
|
||||
if (!ERR_AggressionCheck(hq.Hunters().Deployed() >= 3 && hq.Hunters().Deployed() <= 4, "opening and aggression respect cumulative allowance")) return;
|
||||
SCR_CharacterDamageManagerComponent.Cast(hunter.Members[1].FindComponent(SCR_CharacterDamageManagerComponent)).Kill(Instigator.CreateInstigator(player));
|
||||
m_ERRHunterWait = now + 3000; m_ERRAggressionStage = 3; return;
|
||||
}
|
||||
if (m_ERRAggressionStage == 3)
|
||||
{
|
||||
if (now < m_ERRHunterWait) return;
|
||||
if (!ERR_AggressionCheck(!ERR_Actors.Alive(hunter.Members[1]) && !hunter.Released && hunter.CrewValid(this), "gunner casualty preserves the original seated driver's patrol")) return;
|
||||
CharacterControllerComponent.Cast(hunter.Driver.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
||||
m_ERRHunterWait = now + 3000; m_ERRAggressionStage = 4; return;
|
||||
}
|
||||
if (m_ERRAggressionStage == 4)
|
||||
{
|
||||
if (now < m_ERRHunterWait) return;
|
||||
if (!ERR_AggressionCheck(!ERR_Actors.CanFight(hunter.Driver) && !hunter.Released, "temporarily unconscious driver does not discard the vehicle")) return;
|
||||
CharacterControllerComponent.Cast(hunter.Driver.FindComponent(CharacterControllerComponent)).SetUnconscious(false);
|
||||
m_ERRAggressionStage = 5; return;
|
||||
}
|
||||
if (m_ERRAggressionStage == 5)
|
||||
{
|
||||
if (!hunter.CrewValid(this)) return;
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(hunter.Driver.FindComponent(CompartmentAccessComponent));
|
||||
if (!ERR_AggressionCheck(access && access.GetOutVehicle(EGetOutType.TELEPORT, 0, ECloseDoorAfterActions.LEAVE_OPEN, true), "fixture dismounts the original driver through native compartment API")) return;
|
||||
m_ERRHunterWait = now + 2000; m_ERRAggressionStage = 6; return;
|
||||
}
|
||||
if (m_ERRAggressionStage == 6)
|
||||
{
|
||||
if (now < m_ERRHunterWait || !hunter.CrewValid(this)) return;
|
||||
if (!ERR_AggressionCheck(!hunter.Released && hunter.Members.Count() == 2 && !ERR_Actors.Alive(hunter.Members[1]), "native AI reboards original driver without replacing the lost gunner")) return;
|
||||
hq.Hunters().Stop();
|
||||
if (!ERR_AggressionCheck(hunter.Released && !hunter.Waypoint && hunter.Entity && hunter.Members.Count() == 2, "stop removes orders and preserves physical vehicle/crew")) return;
|
||||
foreach (IEntity survivor : hunter.Members)
|
||||
{
|
||||
if (!ERR_Actors.Alive(survivor)) continue;
|
||||
SCR_AICombatComponent survivingCombat = SCR_AICombatComponent.Cast(survivor.FindComponent(SCR_AICombatComponent));
|
||||
if (!ERR_AggressionCheck(survivingCombat && survivingCombat.ERR_EnhancedEnemy(), "released driving task preserves surviving crew AI profile")) return;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ 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 == "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;
|
||||
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;
|
||||
ERR_NativeObjectiveProbe.BeginRound();
|
||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||
string outcome;
|
||||
@@ -267,9 +267,9 @@ modded class ERR_GameMode
|
||||
}
|
||||
if (m_ERRExtractionCase == "recovery")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("administrator abandoned"), "explicit authorized abandonment uses normal defeat result")) return;
|
||||
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: recovery status and explicit administrator abandonment");
|
||||
Print("ERR SMOKE COMPLETE: player casualty protection, native healing and automatic all-unconscious defeat");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "objective-ui")
|
||||
@@ -445,9 +445,7 @@ modded class ERR_GameMode
|
||||
if (result == 1)
|
||||
{
|
||||
m_ERRBaseStep = 502;
|
||||
array<string> replies = {};
|
||||
Admin().Execute(this, true, "mission abandon", replies);
|
||||
ERR_BaseCheck(Phase() == ERR_Phase.DEFEAT && replies.Count() == 1 && replies[0].Contains("Round abandoned") && RecoveryStatus().IsEmpty() && Recovery().Conscious() == 0 && Recovery().Incapacitated() == 0, "administrator command returns accepted outcome after recovery cleanup");
|
||||
ERR_BaseCheck(Active() && ERR_Actors.Alive(m_ERRBasePlayer), "fixture leaves a living casualty for the production failure timer");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -811,12 +809,12 @@ modded class ERR_GameMode
|
||||
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
||||
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
||||
m_ERRBaseStep = 310;
|
||||
m_ERRBaseWait = now + 35000;
|
||||
m_ERRBaseWait = now + 5000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 310)
|
||||
{
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent));
|
||||
if (!ERR_BaseCheck(controller.IsUnconscious() && ERR_Actors.Alive(m_ERRBasePlayer) && Players().HasSurvivor() && Phase() == ERR_Phase.RADIO_DEFENCE, "unconscious survivor does not trigger defeat after the full grace period")) return;
|
||||
if (!ERR_BaseCheck(controller.IsUnconscious() && ERR_Actors.Alive(m_ERRBasePlayer) && Players().HasSurvivor() && Phase() == ERR_Phase.RADIO_DEFENCE, "unconscious survivor remains revivable during the failure grace period")) return;
|
||||
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30, "unconscious radio defender cannot advance transmission")) return;
|
||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Progress has reset"), "radio guide reports interruption after unconsciousness")) return;
|
||||
SCR_PlayerController playerController = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifdef WORKBENCH
|
||||
// Real native inventory fixture; deliberately not registered with the ordinary equipment service.
|
||||
class ERR_ProbeBrokenLauncherGrant : ERR_EnemyLauncherGrant
|
||||
{
|
||||
override protected ResourceName Preferred(ERR_GameMode mode, SCR_AIGroup group)
|
||||
{
|
||||
return "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et";
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERRLoadoutDeadline;
|
||||
protected int m_ERRLoadoutStage;
|
||||
protected SCR_AIGroup m_ERRLoadoutGroup;
|
||||
protected ref ERR_EnemyLauncherGrant m_ERRNativeAA = new ERR_EnemyLauncherGrant();
|
||||
protected ref ERR_ProbeBrokenLauncherGrant m_ERRFallbackAA = new ERR_ProbeBrokenLauncherGrant();
|
||||
protected ref ERR_EnemyMineGrant m_ERRNativeMines = new ERR_EnemyMineGrant();
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "enemy-loadout") return;
|
||||
m_ERRLoadoutDeadline = System.GetTickCount() + 240000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_LoadoutTick, 1000, true);
|
||||
}
|
||||
protected bool ERR_LoadoutCheck(bool ok, string text)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: enemy-loadout " + text); return true; }
|
||||
Print("ERR SMOKE FAIL: enemy-loadout " + text, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_LoadoutTick); return false;
|
||||
}
|
||||
protected bool ERR_LoadoutWeapon(IEntity actor, ResourceName prefab)
|
||||
{
|
||||
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));
|
||||
array<IEntity> weapons = {}; if (manager) manager.GetWeaponsList(weapons);
|
||||
IEntity weapon = ERR_EnemyLoadoutCatalog.Find(prefab, weapons, null);
|
||||
return weapon && ERR_EnemyLoadoutCatalog.Loaded(weapon);
|
||||
}
|
||||
protected void ERR_LoadoutTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERRLoadoutDeadline || Phase() == ERR_Phase.DEFEAT) { ERR_LoadoutCheck(false, "native equipment deadline"); return; }
|
||||
if (Phase() < ERR_Phase.PRISON || !GetGame().GetPlayerController() || !GetGame().GetPlayerController().GetControlledEntity()) return;
|
||||
if (m_ERRLoadoutStage == 0)
|
||||
{
|
||||
if (!ERR_LoadoutCheck(Settings().Set("antiAirChancePercent", 100) && Settings().Set("antiPersonnelMineChancePercent", 100), "percentage settings saved")) return;
|
||||
m_ERRLoadoutGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Actors.Ground(Sites().Find("everon-prison-1").Group.GetOrigin() + "10 0 10")));
|
||||
if (!ERR_LoadoutCheck(m_ERRLoadoutGroup != null, "native test group")) return;
|
||||
Own(m_ERRLoadoutGroup); m_ERRLoadoutGroup.SetFaction(EnemyFaction());
|
||||
m_ERRLoadoutGroup.m_aUnitPrefabSlots = {
|
||||
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et",
|
||||
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et"};
|
||||
m_ERRLoadoutGroup.RequestSpawn(2); m_ERRLoadoutStage = 1; return;
|
||||
}
|
||||
array<AIAgent> agents = {}; m_ERRLoadoutGroup.GetAgents(agents);
|
||||
if (agents.Count() != 2 || !agents[0].GetControlledEntity() || !agents[1].GetControlledEntity()) return;
|
||||
IEntity first = agents[0].GetControlledEntity(); IEntity second = agents[1].GetControlledEntity();
|
||||
foreach (AIAgent agent : agents) agent.DeactivateAI();
|
||||
if (m_ERRLoadoutStage == 1)
|
||||
{
|
||||
m_ERRNativeAA.Check(this, m_ERRLoadoutGroup, first);
|
||||
m_ERRFallbackAA.Check(this, m_ERRLoadoutGroup, second);
|
||||
m_ERRNativeMines.Check(this, m_ERRLoadoutGroup, first);
|
||||
ResourceName preferred = ERR_EnemyLoadoutCatalog.Launcher(this, m_ERRLoadoutGroup.GetFaction());
|
||||
if (!ERR_LoadoutWeapon(first, preferred) || !ERR_LoadoutWeapon(second, ERR_EnemyLoadoutCatalog.FIM)) return;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(first.FindComponent(InventoryStorageManagerComponent));
|
||||
array<IEntity> items = {}; inventory.GetItems(items); int mines;
|
||||
foreach (IEntity item : items) if (item && item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)) mines++;
|
||||
if (mines != 2) return;
|
||||
ERR_LoadoutCheck(true, "preferred=" + preferred + "; fallback FIM physically equipped and loaded; two carried AP mines");
|
||||
ERR_AdminMenu.Open(); m_ERRLoadoutStage = 2; return;
|
||||
}
|
||||
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu(); if (!menu || !menu.ERR_ProbeReady()) return;
|
||||
if (m_ERRLoadoutStage == 2)
|
||||
{
|
||||
menu.ERR_ProbePage(1);
|
||||
if (!ERR_LoadoutCheck(menu.ERR_ProbeNavigation(1), "HQ menu navigation")) return;
|
||||
Print("ERR ADMIN CAPTURE PAGE: 1"); m_ERRLoadoutStage = 3; return;
|
||||
}
|
||||
if (m_ERRLoadoutStage++ < 8) return;
|
||||
System.MakeScreenshot("$logs:err-enemy-loadout-menu");
|
||||
GetGame().GetCallqueue().Remove(ERR_LoadoutTick);
|
||||
Print("ERR SMOKE COMPLETE: enemy-loadout physical preferred/fallback launchers, AP mines and HQ menu");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -178,6 +178,17 @@ class ERR_NativeRadioDefenceProbe
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Radio.Knowledge() == 2 && ERR_Actors.Player(m_Player), "identified radio and real controlled prisoner available")) return -1;
|
||||
ActionsManagerComponent manager = ActionsManagerComponent.Cast(m_Radio.GetOwner().FindComponent(ActionsManagerComponent));
|
||||
if (!Check(manager != null, "radio has native action manager")) return -1;
|
||||
UserActionContext context = manager.GetContext("ERR_Radio");
|
||||
vector contextTransform[4];
|
||||
if (!Check(context && context.GetTransformationWorld(contextTransform), "radio context has a valid world transform")) return -1;
|
||||
PrintFormat("ERR RADIO CONTEXT: radio=%1 context=%2 radius=%3", m_Radio.Position(), contextTransform[3], context.GetRadius());
|
||||
if (!Check(vector.Distance(contextTransform[3], m_Radio.Position()) < 0.6, "radio action context follows the generated radio rather than world origin")) return -1;
|
||||
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
|
||||
ERR_TuneRadioAction tune;
|
||||
foreach (BaseUserAction action : actions) if (ERR_TuneRadioAction.Cast(action)) tune = ERR_TuneRadioAction.Cast(action);
|
||||
if (!Check(tune && tune.CanBeShownScript(m_Player) && tune.CanBePerformedScript(m_Player), "native radio context contains the visible permitted extraction action")) return -1;
|
||||
m_OriginalDuration = m_Mode.Settings().Get("radioDefenceSeconds");
|
||||
m_Player.GetWorldTransform(m_Original);
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#ifdef WORKBENCH
|
||||
// Uses the local native client's nearby-context collection. It does not inject
|
||||
// a context or an action into the interaction handler.
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERRRadioInteractionStage;
|
||||
protected int m_ERRRadioInteractionDeadline;
|
||||
protected float m_ERRRadioInteractionWait;
|
||||
protected ERR_LocationComponent m_ERRInteractionRadio;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "radio-interaction") return;
|
||||
m_ERRRadioInteractionDeadline = System.GetTickCount() + 150000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_RadioInteractionTick, 500, true);
|
||||
}
|
||||
protected bool ERR_RadioInteractionCheck(bool ok, string detail)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: radio interaction " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: radio interaction " + detail, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_RadioInteractionTick); return false;
|
||||
}
|
||||
protected void ERR_RadioInteractionTick()
|
||||
{
|
||||
if (System.GetTickCount() > m_ERRRadioInteractionDeadline) { ERR_RadioInteractionCheck(false, "native nearby-context deadline"); return; }
|
||||
if (!Active() || GetWorld().GetWorldTime() < m_ERRRadioInteractionWait) return;
|
||||
PlayerController controller = GetGame().GetPlayerController();
|
||||
if (!controller || !controller.GetControlledEntity()) return;
|
||||
IEntity player = controller.GetControlledEntity();
|
||||
if (m_ERRRadioInteractionStage == 0)
|
||||
{
|
||||
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
if (!ERR_RadioInteractionCheck(UnlockPrison(player), "normal prison escape")) return;
|
||||
m_ERRInteractionRadio = Sites().Find("everon-radio-1").Location;
|
||||
m_ERRInteractionRadio.Discover(0, 100);
|
||||
vector transform[4]; player.GetWorldTransform(transform);
|
||||
transform[3] = ERR_Actors.Ground(m_ERRInteractionRadio.Position() + ERR_CampTerrain.Rotate("0 0 -1.5", Sites().Find("everon-radio-1").Definition.Heading));
|
||||
ChimeraCharacter.Cast(player).Teleport(transform);
|
||||
CharacterControllerComponent character = CharacterControllerComponent.Cast(player.FindComponent(CharacterControllerComponent));
|
||||
character.SetHeadingAngle(Sites().Find("everon-radio-1").Definition.Heading * Math.DEG2RAD, false);
|
||||
character.SetFreeLook(true, false, false);
|
||||
character.GetAimingComponent().SetAimingRotation("0 -0.3 0");
|
||||
m_ERRRadioInteractionWait = GetWorld().GetWorldTime() + 3000;
|
||||
m_ERRRadioInteractionStage = 1; return;
|
||||
}
|
||||
ActionsManagerComponent manager = ActionsManagerComponent.Cast(m_ERRInteractionRadio.GetOwner().FindComponent(ActionsManagerComponent));
|
||||
UserActionContext context;
|
||||
if (manager) context = manager.GetContext("ERR_Radio");
|
||||
vector world[4];
|
||||
if (!ERR_RadioInteractionCheck(context && context.GetTransformationWorld(world), "native context has authored position")) return;
|
||||
if (!ERR_RadioInteractionCheck(vector.Distance(world[3], m_ERRInteractionRadio.Position()) < 0.6, "context follows randomly placed radio")) return;
|
||||
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
|
||||
ERR_TuneRadioAction tune;
|
||||
foreach (BaseUserAction action : actions) if (ERR_TuneRadioAction.Cast(action)) tune = ERR_TuneRadioAction.Cast(action);
|
||||
if (!ERR_RadioInteractionCheck(tune && tune.CanBeShown(player) && tune.CanBePerformed(player), "native action is showable and permitted")) return;
|
||||
SCR_InteractionHandlerComponent handler = SCR_InteractionHandlerComponent.Cast(controller.FindComponent(SCR_InteractionHandlerComponent));
|
||||
if (!ERR_RadioInteractionCheck(handler != null, "real client interaction handler exists")) return;
|
||||
array<UserActionContext> nearby = {}; handler.GetNearbyAvailableContextList(nearby);
|
||||
PrintFormat("ERR RADIO INTERACTION: player=%1 context=%2 radius=%3 nearby=%4 selected=%5 nearbyEnabled=%6 camera=%7", player.GetOrigin(), world[3], context.GetRadius(), nearby.Count(), handler.GetCurrentContext() == context, handler.GetNearbyCollectionEnabled(), GetGame().GetCameraManager().CurrentCamera().Type().ToString());
|
||||
if (!ERR_RadioInteractionCheck(nearby.Contains(context) || handler.GetCurrentContext() == context, "native client collection discovers the radio in its furnished tent")) return;
|
||||
tune.PerformAction(m_ERRInteractionRadio.GetOwner(), player);
|
||||
if (!ERR_RadioInteractionCheck(Phase() == ERR_Phase.RADIO_DEFENCE, "discovered native action starts transmission")) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_RadioInteractionTick);
|
||||
Print("ERR SMOKE COMPLETE: native radio context collection and extraction action");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,39 +1,38 @@
|
||||
#ifdef WORKBENCH
|
||||
// Explicit disposable-player fixture. Real damage, healing and the live defeat timer.
|
||||
class ERR_NativeRecoveryProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected SCR_CharacterDamageManagerComponent m_Damage;
|
||||
protected CharacterControllerComponent m_Controller;
|
||||
protected float m_Wait;
|
||||
protected float m_RecoveryDeadline;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_ChangedConsciousness;
|
||||
protected bool m_InitiallyUnconscious;
|
||||
protected float m_HealingDeadline;
|
||||
protected int m_Step;
|
||||
protected int m_Changes;
|
||||
protected bool m_Stopped;
|
||||
void ERR_NativeRecoveryProbe(ERR_GameMode mode, IEntity player)
|
||||
{
|
||||
m_Mode = mode; m_Player = player;
|
||||
m_Damage = SCR_CharacterDamageManagerComponent.Cast(player.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
m_Controller = CharacterControllerComponent.Cast(player.FindComponent(CharacterControllerComponent));
|
||||
if (m_Controller) m_InitiallyUnconscious = m_Controller.IsUnconscious();
|
||||
mode.Recovery().OnChanged().Insert(Changed);
|
||||
mode.OnStop().Insert(Stop);
|
||||
}
|
||||
protected void Changed(string message) { m_Changes++; }
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Mode) m_Mode.OnStop().Remove(Stop);
|
||||
if (m_Mode && m_Mode.Recovery()) m_Mode.Recovery().OnChanged().Remove(Changed);
|
||||
if (m_ChangedConsciousness && m_Controller && ERR_Actors.Alive(m_Player)) m_Controller.SetUnconscious(m_InitiallyUnconscious);
|
||||
if (m_Damage) m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||
}
|
||||
protected bool Check(bool ok, string detail)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: recovery " + detail); return true; }
|
||||
Stop();
|
||||
Print("ERR SMOKE FAIL: recovery " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
Print("ERR SMOKE FAIL: recovery " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected void HaltPassiveRecovery()
|
||||
{
|
||||
array<ref SCR_PersistentDamageEffect> effects = m_Damage.GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect);
|
||||
foreach (SCR_PersistentDamageEffect effect : effects) m_Damage.TerminateDamageEffect(effect);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
@@ -41,54 +40,50 @@ class ERR_NativeRecoveryProbe
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(m_Controller && m_Mode.Recovery().Conscious() == 1 && m_Mode.RecoveryStatus().IsEmpty(), "real conscious prisoner is available without recovery warning")) return -1;
|
||||
array<string> replies = {};
|
||||
m_Mode.Admin().Execute(m_Mode, false, "mission abandon", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0].Contains("Administrator") && m_Mode.Phase() == ERR_Phase.PRISON && m_Mode.EndReason().IsEmpty(), "unauthorized abandonment leaves round and result intact")) return -1;
|
||||
m_Mode.Admin().Execute(m_Mode, true, "mission abandon now", replies);
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Mode.EndReason().IsEmpty(), "malformed abandonment does not end a round")) return -1;
|
||||
m_ChangedConsciousness = true;
|
||||
m_Controller.SetUnconscious(true);
|
||||
m_Step = 1; m_Wait = now + 1000;
|
||||
return 0;
|
||||
if (!Check(m_Damage && m_Controller && m_Damage.ERR_IsRevivablePlayer(), "real admitted prisoner has player-only casualty protection")) 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_Wait = now + 1000; m_Step = 1; return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(ERR_Actors.Alive(m_Player) && !ERR_Actors.CanFight(m_Player) && m_Mode.Recovery().Incapacitated() == 1 && m_Mode.Recovery().Conscious() == 0, "native unconscious prisoner is counted as incapacitated, not dead")) return -1;
|
||||
if (!Check(m_Mode.RecoveryStatus().Contains("No conscious prisoner") && m_Mode.Status().Contains("mission abandon") && ERR_ObjectiveText.Description(m_Mode).Contains(m_Mode.RecoveryStatus()), "server status and native objective copy explain the recovery state")) return -1;
|
||||
if (!Check(m_Changes == 1, "recovery state publishes exactly one change")) return -1;
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(m_Changes == 1, "unchanged sampling emits no duplicate recovery event")) return -1;
|
||||
m_Step = 2; m_Wait = now + 35000;
|
||||
return 0;
|
||||
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;
|
||||
// Reset fixture injuries, then exercise treatment of an isolated knockout.
|
||||
m_Damage.FullHeal(); m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
||||
m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
||||
m_Wait = now + 35000; m_Step = 2; return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && ERR_Actors.Alive(m_Player) && !ERR_Actors.CanFight(m_Player) && m_Mode.Players().HasSurvivor(), "all-incapacitated player survives beyond ordinary defeat grace")) return -1;
|
||||
m_Controller.SetUnconscious(false);
|
||||
m_RecoveryDeadline = now + 12000;
|
||||
m_Step = 3; m_Wait = now + 1000;
|
||||
return 0;
|
||||
if (!Check(m_Mode.Active() && m_Controller.IsUnconscious() && m_Damage.ERR_TreatmentInProgress(), "active native treatment keeps an all-unconscious round alive past 30 seconds")) return -1;
|
||||
m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||
SCR_PassiveHitZoneRegenDamageEffect healing = new SCR_PassiveHitZoneRegenDamageEffect();
|
||||
healing.SetAffectedHitZone(m_Damage.GetResilienceHitZone());
|
||||
healing.SetDamageType(EDamageType.HEALING); healing.SetDPS(-100);
|
||||
healing.SetMaxDuration(10); healing.SkipRegenDelay(true);
|
||||
m_Damage.AddDamageEffect(healing);
|
||||
if (!Check(m_Damage.ERR_TreatmentInProgress(), "native resilience-healing effect is recognized as ongoing treatment")) return -1;
|
||||
m_HealingDeadline = now + 12000;
|
||||
m_Wait = now + 1000; m_Step = 3; return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(m_Player) && now < m_RecoveryDeadline) return 0;
|
||||
PrintFormat("ERR RECOVERY HEAL: blood=%1 resilience=%2 state=%3 unconscious=%4", m_Damage.GetBloodHitZone().GetHealthScaled(), m_Damage.GetResilienceHitZone().GetHealthScaled(), m_Controller.GetLifeState(), m_Controller.IsUnconscious());
|
||||
if (!ERR_Actors.CanFight(m_Player) && now < m_HealingDeadline) return 0;
|
||||
if (!Check(ERR_Actors.CanFight(m_Player) && m_Mode.Active(), "native resilience healing restores consciousness on the original body")) return -1;
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(ERR_Actors.CanFight(m_Player) && m_Mode.RecoveryStatus().IsEmpty() && m_Mode.Recovery().Conscious() == 1 && m_Changes == 2, "native recovery clears the warning once")) return -1;
|
||||
int id = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_Player);
|
||||
ERR_PlayerLife life = m_Mode.Players().Get(id);
|
||||
if (!Check(life && life.UsedLife && life.Actor == m_Player, "recovery preserves original actor and spent life")) return -1;
|
||||
m_Controller.SetUnconscious(true);
|
||||
m_Step = 4; m_Wait = now + 1000;
|
||||
return 0;
|
||||
if (!Check(!m_Mode.Recovery().DefeatDue(now + 31000), "successful recovery resets the defeat timer")) return -1;
|
||||
m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
||||
m_Wait = now + 1000; m_Step = 4; return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(!m_Mode.RecoveryStatus().IsEmpty() && m_Changes == 3, "second incapacitation restores warning before abandonment")) return -1;
|
||||
return 1;
|
||||
if (!Check(ERR_Actors.Alive(m_Player) && m_Controller.IsUnconscious() && m_Mode.RecoveryStatus().Contains("30 uninterrupted seconds"), "stalled all-unconscious team starts a clear failure grace period")) return -1;
|
||||
return 1; // Parent now waits for the production timer, never forces defeat.
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_HQ
|
||||
{
|
||||
bool ERR_ProbeSiteRoles(ERR_SiteRuntime site)
|
||||
{
|
||||
bool fixedGuard, mobile;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group == site.Group) fixedGuard = unit.ReservedForSupport && unit.HomeGuard && !unit.Eligible(m_Mode) && !unit.PendingOrder && !unit.Assignment;
|
||||
if (site.Responders && unit.Group == site.Responders.Group) mobile = !unit.ReservedForSupport && !unit.HomeGuard && unit.Eligible(m_Mode) && unit.HomeId == site.Definition.Id;
|
||||
}
|
||||
return fixedGuard && mobile;
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERRSiteStage, m_ERRSiteDeadline;
|
||||
protected float m_ERRSiteWait;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "site-garrisons") return;
|
||||
m_ERRSiteDeadline = System.GetTickCount() + 300000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_SiteGarrisonTick, 1000, true);
|
||||
}
|
||||
protected bool ERR_SiteCheck(bool ok, string message)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: sites " + message); return true; }
|
||||
Print("ERR SMOKE FAIL: sites " + message, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_SiteGarrisonTick); return false;
|
||||
}
|
||||
protected void ERR_SiteGarrisonTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERRSiteDeadline || !PreparationError().IsEmpty()) { ERR_SiteCheck(false, "startup deadline/preparation: " + PreparationError()); return; }
|
||||
if (!Active() || !GetGame().GetPlayerController()) return;
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity();
|
||||
if (!player || GetWorld().GetWorldTime() < m_ERRSiteWait) return;
|
||||
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return;
|
||||
if (m_ERRSiteStage == 0)
|
||||
{
|
||||
int hospitals, camps, defenders, responders;
|
||||
for (int i = 0; i < Sites().Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = Sites().At(i);
|
||||
if (site.Definition.ResponseCount == 0) continue;
|
||||
if (!ERR_SiteCheck(site.Definition.PlacementResolved && site.Props.Count() >= 4, site.Definition.Id + " randomized composition and scenery created")) return;
|
||||
if (!ERR_SiteCheck(site.Group && site.Responders && site.Responders.Group && site.Group != site.Responders.Group && ForceLiving(site.Group) == 4 && ForceLiving(site.Responders.Group) == 2, site.Definition.Id + " distinct native 4 + 2 rosters")) return;
|
||||
if (!ERR_SiteCheck(hq.ERR_ProbeSiteRoles(site), site.Definition.Id + " permanent defenders reserved, responders HQ eligible")) return;
|
||||
if (!ERR_SiteCheck(Forces().FindGroup(site.Group) && Forces().FindGroup(site.Responders.Group), site.Definition.Id + " both finite forces registered")) return;
|
||||
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(site.Container.FindComponent(InventoryStorageManagerComponent));
|
||||
array<IEntity> items = {}; if (storage) storage.GetItems(items);
|
||||
int expected; foreach (ERR_SiteStock stock : site.Definition.Stock) expected += stock.Count;
|
||||
if (hq.Logistics().Cache() == site.Container)
|
||||
{
|
||||
array<IEntity> courierStock = {}; hq.Logistics().InitialStock(courierStock);
|
||||
expected += courierStock.Count();
|
||||
if (!ERR_SiteCheck(hq.Logistics().Group() == site.Responders.Group, "mortar courier uses mobile team, never the permanent garrison")) return;
|
||||
}
|
||||
int topLevel;
|
||||
foreach (IEntity stored : items)
|
||||
{
|
||||
InventoryItemComponent item = InventoryItemComponent.Cast(stored.FindComponent(InventoryItemComponent));
|
||||
if (item && item.GetParentSlot() && item.GetParentSlot().GetStorage() && item.GetParentSlot().GetStorage().GetOwner() == site.Container) topLevel++;
|
||||
}
|
||||
// GetItems also enumerates native magazines/attachments inside supplied
|
||||
// rifles. Only items directly stored in this cache belong to its stock list.
|
||||
if (!ERR_SiteCheck(topLevel == expected, string.Format("%1 finite supplies inserted before admission: expected=%2 direct=%3 including nested=%4", site.Definition.Id, expected, topLevel, items.Count()))) return;
|
||||
if (!ERR_SiteCheck(site.Location.Knowledge() == 0 && site.Location.ERR_ProbeGeneratedMarker(false), site.Definition.Id + " undiscovered marker hidden")) return;
|
||||
site.Location.Discover(99, 0);
|
||||
if (!ERR_SiteCheck(site.Location.Knowledge() == 1 && site.Location.ERR_ProbeGeneratedMarker(true), site.Definition.Id + " partial clue displays approximate marker")) return;
|
||||
site.Location.Discover(99, 0);
|
||||
if (!ERR_SiteCheck(site.Location.Knowledge() == 2 && site.Location.ERR_ProbeGeneratedMarker(true), site.Definition.Id + " exact marker at generated position")) return;
|
||||
if (site.Definition.Kind == ERR_LocationKind.HOSPITAL)
|
||||
{
|
||||
hospitals++;
|
||||
if (!ERR_SiteCheck(ERR_DiscoveryMarker.Icon(site.Definition.Kind, site.Definition.Id) == "medical", "hospital uses medical map icon")) return;
|
||||
}
|
||||
else camps++;
|
||||
defenders += ForceLiving(site.Group); responders += ForceLiving(site.Responders.Group);
|
||||
for (int j = 0; j < i; j++)
|
||||
{
|
||||
ERR_SiteRuntime previous = Sites().At(j);
|
||||
if (previous.Definition.ResponseCount > 0 && !ERR_SiteCheck(vector.DistanceXZ(previous.Location.Position(), site.Location.Position()) >= 600, "field sites separated by at least 600 metres")) return;
|
||||
}
|
||||
}
|
||||
if (!ERR_SiteCheck(hospitals == 3 && camps == 5 && defenders == 32 && responders == 16, "three hospitals, five camps, 32 fixed defenders and 16 mobile soldiers")) return;
|
||||
if (!ERR_SiteCheck(UnlockPrison(player), "normal prison release")) return;
|
||||
m_ERRSiteStage = 1; m_ERRSiteWait = GetWorld().GetWorldTime() + 10000; return;
|
||||
}
|
||||
for (int index = 0; index < Sites().Count(); index++)
|
||||
{
|
||||
ERR_SiteRuntime garrison = Sites().At(index);
|
||||
if (garrison.Responders && !ERR_SiteCheck(hq.ERR_ProbeSiteRoles(garrison), garrison.Definition.Id + " role separation survives live HQ ticks")) return;
|
||||
}
|
||||
GetGame().GetCallqueue().Remove(ERR_SiteGarrisonTick);
|
||||
Print("ERR SMOKE COMPLETE: randomized hospitals/camps, native rosters, finite stock, discovery and HQ role separation");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -101,7 +101,7 @@ class ERR_NativeTimeProbe
|
||||
bool savedSpeed = mode.Settings().Get("timeMultiplier") == 4 && replies[0].Contains("next round");
|
||||
bool idempotent = mode.Time().Start(mode) && Math.AbsFloat(manager.GetTimeOfTheDay() - currentHours) < 0.01 && manager.GetDayDuration() == currentDuration && mode.Time().StartMinutes() == appliedStart && mode.Time().Multiplier() == appliedSpeed;
|
||||
mode.Admin().Execute(mode, true, "mission time show", replies);
|
||||
bool shown = replies.Count() == 2 && replies[0].Contains("Clock") && replies[1] == "Next round: 06:15, 4x progression.";
|
||||
bool shown = replies.Count() == 2 && replies[0].Contains("Clock") && replies[1] == "Next round: 06:15, 4 progression.";
|
||||
bool restoredTime = mode.Settings().Set("startTimeMinutes", originalTime);
|
||||
bool restoredSpeed = mode.Settings().Set("timeMultiplier", originalSpeed);
|
||||
if (!Check(savedTime && savedSpeed && restoredTime && restoredSpeed, "authorized settings persist and test restores originals")) return false;
|
||||
|
||||
@@ -3,7 +3,29 @@ enum ERR_ObserverReason
|
||||
NONE,
|
||||
SPENT_LIFE,
|
||||
LATE_EXTRACTION,
|
||||
SPAWN_UNAVAILABLE
|
||||
SPAWN_UNAVAILABLE,
|
||||
UNCONSCIOUS
|
||||
}
|
||||
|
||||
class ERR_ObserverRules
|
||||
{
|
||||
static int Cycle(bool enabled, bool left, bool right, bool previousLeft, bool previousRight)
|
||||
{
|
||||
if (!enabled || left == right) return 0;
|
||||
if (left && !previousLeft) return -1;
|
||||
if (right && !previousRight) return 1;
|
||||
return 0;
|
||||
}
|
||||
static bool Casualty(bool active, bool currentBody, bool alive, bool conscious)
|
||||
{
|
||||
return active && currentBody && alive && !conscious;
|
||||
}
|
||||
static bool View(int reason, bool active, bool alive, bool conscious)
|
||||
{
|
||||
if (!active || reason == ERR_ObserverReason.NONE) return false;
|
||||
if (reason == ERR_ObserverReason.UNCONSCIOUS) return alive && !conscious;
|
||||
return !alive;
|
||||
}
|
||||
}
|
||||
|
||||
// Presentation only. Existing server admission and observer selection own state.
|
||||
@@ -19,9 +41,14 @@ class ERR_ObserverGuidance
|
||||
message = "Extraction has started. New prisoners can join next round.";
|
||||
else if (reason == ERR_ObserverReason.SPAWN_UNAVAILABLE)
|
||||
message = "Your prisoner could not be spawned. You are spectating until the next round.";
|
||||
else if (reason == ERR_ObserverReason.UNCONSCIOUS)
|
||||
{
|
||||
message = "You are unconscious. Your body remains where you fell and teammates can still treat you. Your view returns automatically when you wake up.";
|
||||
if (!survivorAvailable) return message + " No conscious teammate is currently available; the camera stays near your body.";
|
||||
}
|
||||
if (!survivorAvailable)
|
||||
return message + " No survivor is currently available to follow. The view will follow a survivor when one becomes available.";
|
||||
return message + " Use /esc spectate next or /esc spectate previous to change who you follow.";
|
||||
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.";
|
||||
}
|
||||
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
||||
{
|
||||
@@ -33,11 +60,22 @@ class ERR_ObserverGuidance
|
||||
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
|
||||
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
{
|
||||
protected IEntity m_Target;
|
||||
protected IEntity m_Vehicle;
|
||||
protected float m_Yaw;
|
||||
protected float m_Pitch = 20;
|
||||
protected float m_Distance = 5;
|
||||
protected bool m_LeftDown, m_RightDown;
|
||||
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);
|
||||
IEntity target = controller.ERR_ObservedEntity();
|
||||
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
||||
vector position = aim + "0 4 -6";
|
||||
@@ -52,15 +90,32 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
vector lower, upper;
|
||||
vehicle.GetWorldBounds(lower, upper);
|
||||
aim = (lower + upper) * 0.5;
|
||||
position = aim + "0 5 0" - transform[2] * 10;
|
||||
exclude = vehicle;
|
||||
}
|
||||
else
|
||||
{
|
||||
target.GetWorldTransform(transform);
|
||||
aim = transform[3] + "0 1.5 0";
|
||||
position = aim + "0 1.5 0" - transform[2] * 4;
|
||||
}
|
||||
if (target != m_Target || vehicle != m_Vehicle)
|
||||
{
|
||||
vector angles = transform[2].VectorToAngles();
|
||||
m_Yaw = angles[0] + 180; m_Pitch = 20; m_Distance = 5;
|
||||
if (vehicle) m_Distance = 12;
|
||||
m_Target = target; m_Vehicle = vehicle;
|
||||
}
|
||||
if (param.isManualInputEnabled)
|
||||
{
|
||||
m_Yaw += GetInputManager().GetActionValue("ManualCameraRotateYaw") * 90 * param.timeSlice;
|
||||
m_Pitch = Math.Clamp(m_Pitch - GetInputManager().GetActionValue("ManualCameraRotatePitch") * 90 * param.timeSlice, 5, 75);
|
||||
float scale = Math.Clamp(1 + GetInputManager().GetActionValue("ManualCameraZoom") * 12 * param.timeSlice, 0.5, 1.5);
|
||||
float minimum = 2, maximum = 12;
|
||||
if (vehicle) { minimum = 6; maximum = 25; }
|
||||
m_Distance = Math.Clamp(m_Distance * scale, minimum, maximum);
|
||||
}
|
||||
float yaw = m_Yaw * Math.DEG2RAD, pitch = m_Pitch * Math.DEG2RAD;
|
||||
vector offset = Vector(Math.Sin(yaw) * Math.Cos(pitch), Math.Sin(pitch), Math.Cos(yaw) * Math.Cos(pitch));
|
||||
position = aim + offset * m_Distance;
|
||||
}
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = aim;
|
||||
@@ -106,7 +161,7 @@ modded class SCR_PlayerController
|
||||
void ERR_SetObserver(ERR_ObserverReason reason)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity())) return;
|
||||
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity()) && reason != ERR_ObserverReason.UNCONSCIOUS) return;
|
||||
m_ERRObserverReason = reason;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && reason != ERR_ObserverReason.NONE) m_ERRObserverAnchor = mode.PrisonPosition();
|
||||
@@ -132,9 +187,17 @@ modded class SCR_PlayerController
|
||||
}
|
||||
void ERR_UpdateObserverServer(int direction = 0)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ERRObserverReason == ERR_ObserverReason.NONE) return;
|
||||
if (!Replication.IsServer()) return;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Active() || ERR_Actors.Alive(GetControlledEntity()))
|
||||
IEntity body = GetControlledEntity();
|
||||
bool casualty = mode && ERR_ObserverRules.Casualty(mode.Active(), mode.Players() && mode.Players().CurrentActor(body) != null, ERR_Actors.Alive(body), ERR_Actors.CanFight(body));
|
||||
if (casualty && m_ERRObserverReason != ERR_ObserverReason.UNCONSCIOUS)
|
||||
{
|
||||
m_ERRObserverReason = ERR_ObserverReason.UNCONSCIOUS;
|
||||
m_ERRObserverAnchor = body.GetOrigin();
|
||||
}
|
||||
if (m_ERRObserverReason == ERR_ObserverReason.NONE) return;
|
||||
if (!mode || !mode.Active() || (ERR_Actors.Alive(body) && !casualty) || (m_ERRObserverReason == ERR_ObserverReason.UNCONSCIOUS && !casualty))
|
||||
{
|
||||
m_ERRObserverReason = ERR_ObserverReason.NONE;
|
||||
ERR_ReleaseObserved();
|
||||
@@ -145,7 +208,7 @@ modded class SCR_PlayerController
|
||||
ERR_Actors.Players(survivors);
|
||||
Faction friendly = mode.PlayerFaction();
|
||||
for (int i = survivors.Count() - 1; i >= 0; i--)
|
||||
if (!friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
||||
if (survivors[i] == body || (casualty && !ERR_Actors.CanFight(survivors[i])) || !friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
||||
int index = survivors.Find(m_ERRServerObserved);
|
||||
if (index < 0) index = 0;
|
||||
else index += direction;
|
||||
@@ -175,6 +238,7 @@ modded class SCR_PlayerController
|
||||
ERR_StreamObserved(vehicle, false);
|
||||
}
|
||||
}
|
||||
else if (casualty) m_ERRObserverAnchor = body.GetOrigin();
|
||||
Replication.BumpMe();
|
||||
}
|
||||
void ERR_CycleObserver(int direction)
|
||||
@@ -202,7 +266,7 @@ modded class SCR_PlayerController
|
||||
protected void ERR_UpdateObserverClient()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
bool observe = mode && mode.Active() && m_ERRObserverReason != ERR_ObserverReason.NONE && !ERR_Actors.Alive(GetControlledEntity());
|
||||
bool observe = mode && ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
|
||||
if (!observe)
|
||||
{
|
||||
ERR_CloseObserverCamera();
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Player-only casualty protection. Damage still wounds; healing remains native.
|
||||
class ERR_PlayerMedicalRules
|
||||
{
|
||||
static float AllowedDamage(float damage, float health, float maximum, float destroyedThreshold)
|
||||
{
|
||||
if (damage <= 0) return damage;
|
||||
float floor = maximum * Math.Clamp(destroyedThreshold + 0.01, 0.01, 1);
|
||||
return Math.Min(damage, Math.Max(0, health - floor));
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_CharacterDamageManagerComponent
|
||||
{
|
||||
protected bool m_ERRRevivablePlayer;
|
||||
protected bool m_ERRKnockoutPending;
|
||||
|
||||
void ERR_EnableCasualtyProtection()
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
m_ERRRevivablePlayer = true;
|
||||
SetPermitUnconsciousness(true, true);
|
||||
}
|
||||
bool ERR_IsRevivablePlayer() { return m_ERRRevivablePlayer; }
|
||||
|
||||
// Queue outside hitzone callbacks: changing another hitzone in them can re-enter
|
||||
// the native damage pipeline. The lethal damage itself is clamped synchronously.
|
||||
void ERR_RequestKnockout()
|
||||
{
|
||||
if (!Replication.IsServer() || !m_ERRRevivablePlayer || m_ERRKnockoutPending) return;
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(GetOwner().FindComponent(CharacterControllerComponent));
|
||||
if (controller && controller.IsUnconscious()) return;
|
||||
m_ERRKnockoutPending = true;
|
||||
GetGame().GetCallqueue().CallLater(ERR_ApplyKnockout, 0, false);
|
||||
}
|
||||
protected void ERR_ApplyKnockout()
|
||||
{
|
||||
m_ERRKnockoutPending = false;
|
||||
if (!m_ERRRevivablePlayer || !ERR_Actors.Alive(GetOwner())) return;
|
||||
ForceUnconsciousness();
|
||||
ERR_HQ.Event("PLAYER CASUALTY", "Lethal injury prevented; prisoner remains alive and can receive medical treatment.");
|
||||
}
|
||||
override void Kill(notnull Instigator instigator)
|
||||
{
|
||||
if (!m_ERRRevivablePlayer) { super.Kill(instigator); return; }
|
||||
ERR_RequestKnockout();
|
||||
}
|
||||
override protected bool IsIndefinitelyUnconscious(bool onlyPlayers = true)
|
||||
{
|
||||
if (m_ERRRevivablePlayer) return false;
|
||||
return super.IsIndefinitelyUnconscious(onlyPlayers);
|
||||
}
|
||||
bool ERR_TreatmentInProgress()
|
||||
{
|
||||
if (m_aBeingHealedGroup && !m_aBeingHealedGroup.IsEmpty()) return true;
|
||||
array<ref SCR_PersistentDamageEffect> effects = GetAllPersistentEffectsOfType(SCR_DotDamageEffect, true);
|
||||
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.
|
||||
if (dot && dot.GetDamageType() == EDamageType.HEALING && dot.GetDPS() < 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_HitZone
|
||||
{
|
||||
override float ComputeEffectiveDamage(notnull BaseDamageContext damageContext, bool isDOT)
|
||||
{
|
||||
float damage = super.ComputeEffectiveDamage(damageContext, isDOT);
|
||||
if (!Replication.IsServer() || damage <= 0 || SCR_CharacterResilienceHitZone.Cast(this)) return damage;
|
||||
SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());
|
||||
if (!manager || !manager.ERR_IsRevivablePlayer()) return damage;
|
||||
float allowed = ERR_PlayerMedicalRules.AllowedDamage(damage, GetHealth(), GetMaxHealth(), GetDamageStateThreshold(EDamageState.DESTROYED));
|
||||
if (allowed < damage) manager.ERR_RequestKnockout();
|
||||
return allowed;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Observation only: unconsciousness never consumes a life or ends a round here.
|
||||
// Observe recovery, retaining native treatment and the same living casualty.
|
||||
class ERR_RecoveryRules
|
||||
{
|
||||
// 0 absent/dead, 1 reserved or awaiting handoff, 2 conscious, 3 incapacitated.
|
||||
@@ -13,6 +13,20 @@ class ERR_RecoveryRules
|
||||
{
|
||||
return conscious == 0 && incapacitated > 0;
|
||||
}
|
||||
static bool Stalled(int conscious, int incapacitated, int reservedRescuers, int recovering, bool connected)
|
||||
{
|
||||
return connected && conscious == 0 && incapacitated > 0 && reservedRescuers == 0 && recovering == 0;
|
||||
}
|
||||
static bool DefeatDue(float since, float now)
|
||||
{
|
||||
return since >= 0 && now >= since && now - since >= 30000;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_RecoverySample
|
||||
{
|
||||
float Blood;
|
||||
float Resilience;
|
||||
}
|
||||
|
||||
class ERR_RecoveryService
|
||||
@@ -22,6 +36,8 @@ class ERR_RecoveryService
|
||||
protected int m_Conscious;
|
||||
protected int m_Incapacitated;
|
||||
protected int m_Reserved;
|
||||
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 ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
||||
@@ -30,11 +46,13 @@ 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); }
|
||||
|
||||
void Tick()
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
||||
int conscious, incapacitated, reserved;
|
||||
int conscious, incapacitated, reserved, reservedRescuers, recovering;
|
||||
map<IEntity, ref ERR_RecoverySample> nextSamples = new map<IEntity, ref ERR_RecoverySample>();
|
||||
array<ref ERR_PlayerLife> lives = {};
|
||||
m_Mode.Players().Participants(lives);
|
||||
foreach (ERR_PlayerLife life : lives)
|
||||
@@ -42,19 +60,40 @@ class ERR_RecoveryService
|
||||
IEntity controlled = null;
|
||||
if (life.Connected) controlled = GetGame().GetPlayerManager().GetPlayerControlledEntity(life.ConnectionId);
|
||||
int kind = ERR_RecoveryRules.SurvivorKind(ERR_Actors.Alive(life.Actor), life.Connected, controlled == life.Actor, ERR_Actors.CanFight(life.Actor));
|
||||
if (kind == 1) reserved++;
|
||||
if (kind == 1)
|
||||
{
|
||||
reserved++;
|
||||
if (ERR_Actors.CanFight(life.Actor)) reservedRescuers++;
|
||||
else incapacitated++;
|
||||
}
|
||||
else if (kind == 2) conscious++;
|
||||
else if (kind == 3) incapacitated++;
|
||||
if (kind == 0 || kind == 2) continue;
|
||||
SCR_CharacterDamageManagerComponent medical = SCR_CharacterDamageManagerComponent.Cast(life.Actor.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!medical) continue;
|
||||
ERR_RecoverySample sample = new ERR_RecoverySample();
|
||||
if (medical.GetBloodHitZone()) sample.Blood = medical.GetBloodHitZone().GetHealth();
|
||||
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++;
|
||||
nextSamples.Insert(life.Actor, sample);
|
||||
}
|
||||
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;
|
||||
string message;
|
||||
if (ERR_RecoveryRules.NeedsRescuer(conscious, incapacitated))
|
||||
{
|
||||
message = "No conscious prisoner is currently available to help. Natural recovery and treatment can still continue.";
|
||||
if (reserved > 0) message += " A surviving prisoner's reconnection may provide help.";
|
||||
message += " An administrator can abandon this round with /esc mission abandon.";
|
||||
message = "No conscious prisoner is currently available to help.";
|
||||
if (recovering > 0) message += " Treatment or recovery is progressing; 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.";
|
||||
}
|
||||
Publish(message);
|
||||
}
|
||||
@@ -69,6 +108,7 @@ class ERR_RecoveryService
|
||||
if (!Replication.IsServer() || m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
m_Conscious = 0; m_Incapacitated = 0; m_Reserved = 0;
|
||||
m_StalledSince = -1; m_Samples.Clear();
|
||||
Publish("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
m_Started = true;
|
||||
#ifdef WORKBENCH
|
||||
string historicalFixture;
|
||||
if (System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||
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")
|
||||
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 != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && 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();
|
||||
|
||||
@@ -9,15 +9,16 @@ class ERR_Settings
|
||||
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "stingerChanceOneIn", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
||||
return {"keepWeaponOnUnconscious", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
||||
}
|
||||
|
||||
static bool Valid(string key, int value)
|
||||
{
|
||||
if (key == "keepWeaponOnUnconscious") return value == 0 || value == 1;
|
||||
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
|
||||
if (key == "convoyCount") return value >= 0 && value <= 20;
|
||||
if (key == "automaticConvoyEnabled") return value == 0 || value == 1;
|
||||
if (key == "stingerChanceOneIn") return value >= 0 && value <= 10000;
|
||||
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent") return value >= 0 && value <= 100;
|
||||
if (key == "reportIntervalSeconds") return value >= 5 && value <= 60;
|
||||
if (key == "supportSquadLimit") return value >= 0 && value <= 8;
|
||||
if (key == "responseRadiusMetres") return value >= 500 && value <= 5000;
|
||||
@@ -50,7 +51,7 @@ class ERR_Settings
|
||||
// Saved modes are distinct from validated concrete gameplay values.
|
||||
static bool Randomizable(string key)
|
||||
{
|
||||
return key == "heliTimerMinutes" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent" || key == "radioDefenceSeconds" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile";
|
||||
return key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "heliTimerMinutes" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent" || key == "radioDefenceSeconds" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile";
|
||||
}
|
||||
static bool ValidSelection(string key, int value)
|
||||
{
|
||||
@@ -72,7 +73,7 @@ class ERR_Settings
|
||||
else if (key == "reportIntervalSeconds") { minimum = 5; maximum = 60; }
|
||||
else if (key == "supportSquadLimit") maximum = 8;
|
||||
else if (key == "responseRadiusMetres") { minimum = 500; maximum = 5000; }
|
||||
else if (key == "aiIntelChancePercent" || key == "intelRevealChancePercent") maximum = 100;
|
||||
else if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent") maximum = 100;
|
||||
else if (key == "radioDefenceSeconds") { minimum = 30; maximum = 1800; }
|
||||
else if (key == "startTimeMinutes") maximum = 1439;
|
||||
else if (key == "timeMultiplier") { minimum = 1; maximum = 24; }
|
||||
@@ -96,10 +97,12 @@ class ERR_Settings
|
||||
|
||||
static int AddedDefault(string key)
|
||||
{
|
||||
if (key == "keepWeaponOnUnconscious") return 1;
|
||||
if (key == "heliTimerMinutes") return 15;
|
||||
if (key == "convoyCount") return 3;
|
||||
if (key == "automaticConvoyEnabled") return 1;
|
||||
if (key == "stingerChanceOneIn") return 15;
|
||||
if (key == "antiAirChancePercent") return 7;
|
||||
if (key == "antiPersonnelMineChancePercent") return 60;
|
||||
if (key == "startTimeMinutes") return 840;
|
||||
if (key == "timeMultiplier") return 1;
|
||||
if (key == "startDateCode" || key == "weatherProfile") return 0;
|
||||
@@ -109,6 +112,7 @@ class ERR_Settings
|
||||
void ERR_Settings(string path = PATH)
|
||||
{
|
||||
m_Path = path;
|
||||
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
|
||||
m_Values.Set("reportIntervalSeconds", 10);
|
||||
m_Values.Set("supportSquadLimit", 4);
|
||||
m_Values.Set("responseRadiusMetres", 2000);
|
||||
@@ -119,7 +123,8 @@ class ERR_Settings
|
||||
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
|
||||
m_Values.Set("startDateCode", AddedDefault("startDateCode"));
|
||||
m_Values.Set("weatherProfile", AddedDefault("weatherProfile"));
|
||||
m_Values.Set("stingerChanceOneIn", AddedDefault("stingerChanceOneIn"));
|
||||
m_Values.Set("antiAirChancePercent", AddedDefault("antiAirChancePercent"));
|
||||
m_Values.Set("antiPersonnelMineChancePercent", AddedDefault("antiPersonnelMineChancePercent"));
|
||||
m_Values.Set("automaticConvoyEnabled", AddedDefault("automaticConvoyEnabled"));
|
||||
m_Values.Set("convoyCount", AddedDefault("convoyCount"));
|
||||
m_Values.Set("heliTimerMinutes", AddedDefault("heliTimerMinutes"));
|
||||
@@ -142,7 +147,18 @@ class ERR_Settings
|
||||
values = new map<string, int>();
|
||||
foreach (string key : Keys())
|
||||
{
|
||||
// Only newly introduced keys may be absent in an older valid file.
|
||||
// Migrate the former one-in-N setting to the nearest integer percent.
|
||||
if (key == "antiAirChancePercent" && !json.DoesKeyExist(key) && json.DoesKeyExist("stingerChanceOneIn"))
|
||||
{
|
||||
float oldNumber;
|
||||
if (!json.ReadValue("stingerChanceOneIn", oldNumber)) return false;
|
||||
int oldChance = Math.Round(oldNumber);
|
||||
if (oldNumber != oldChance || oldChance < 0 || oldChance > 10000) return false;
|
||||
int percent = 0;
|
||||
if (oldChance > 0) percent = Math.Round(100.0 / oldChance);
|
||||
values.Set(key, percent); continue;
|
||||
}
|
||||
// Only newly introduced keys may be absent in an older valid file.
|
||||
if (!json.DoesKeyExist(key) && AddedDefault(key) >= 0)
|
||||
{
|
||||
values.Set(key, AddedDefault(key));
|
||||
|
||||
@@ -57,7 +57,7 @@ class ERR_SiteDefinition
|
||||
ResourceName Prefab;
|
||||
[Attribute("0 0 0", UIWidgets.Coords)]
|
||||
vector Position;
|
||||
[Attribute("0", UIWidgets.CheckBox, "Choose dry clear terrain once per round (prison, radio or mortar)")]
|
||||
[Attribute("0", UIWidgets.CheckBox, "Choose dry clear terrain once per round (prison, radio, mortar, hospital, depot or camp)")]
|
||||
bool RandomClear;
|
||||
[Attribute("0", UIWidgets.EditBox, "Composition heading in degrees")]
|
||||
float Heading;
|
||||
@@ -75,6 +75,12 @@ class ERR_SiteDefinition
|
||||
[Attribute()] ref array<ref ERR_SitePatrolPoint> Patrol;
|
||||
[Attribute("0", UIWidgets.CheckBox, "Populate a finite squad from the faction catalog") ]
|
||||
bool Guarded;
|
||||
[Attribute("0", UIWidgets.EditBox, "Permanent defenders (0 uses the full faction group; otherwise 1-8)")]
|
||||
int GuardCount;
|
||||
[Attribute("0", UIWidgets.EditBox, "Separate mobile soldiers HQ may send hunting (0-8); defenders remain at home")]
|
||||
int ResponseCount;
|
||||
[Attribute("-12 0 0", UIWidgets.Coords, "Mobile patrol assembly offset")]
|
||||
vector ResponseOffset;
|
||||
[Attribute("20 0 0", UIWidgets.Coords)]
|
||||
vector GuardOffset;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Finite inventory container", "et")]
|
||||
@@ -117,10 +123,14 @@ class ERR_ScenarioDefinition
|
||||
if (!site || site.Id.IsEmpty() || ids.Contains(site.Id) || site.Prefab.IsEmpty()) return false;
|
||||
if (site.RequireAirfield && site.Kind != ERR_LocationKind.EXTRACTION) return false;
|
||||
if (site.Kind == ERR_LocationKind.RADIO && !site.Guarded && site.Guards.IsEmpty()) return false;
|
||||
if (site.RandomClear && site.Kind != ERR_LocationKind.PRISON && site.Kind != ERR_LocationKind.RADIO && site.Kind != ERR_LocationKind.MORTAR) return false;
|
||||
if (site.RandomClear && site.Kind != ERR_LocationKind.PRISON && site.Kind != ERR_LocationKind.RADIO && site.Kind != ERR_LocationKind.MORTAR && site.Kind != ERR_LocationKind.HOSPITAL && site.Kind != ERR_LocationKind.OUTPOST && site.Kind != ERR_LocationKind.DEPOT) return false;
|
||||
if (site.Label.IsEmpty() || site.Label.Length() > 64) return false;
|
||||
if (!ERR_SiteDiscoveryRules.ValidRadius(site.Kind, site.DiscoverRadius)) return false;
|
||||
bool optionalSite = site.Kind == ERR_LocationKind.DEPOT || site.Kind == ERR_LocationKind.OUTPOST;
|
||||
bool optionalSite = site.Kind == ERR_LocationKind.DEPOT || site.Kind == ERR_LocationKind.OUTPOST || site.Kind == ERR_LocationKind.HOSPITAL;
|
||||
if (site.GuardCount < 0 || site.GuardCount > 8 || site.ResponseCount < 0 || site.ResponseCount > 8) return false;
|
||||
if ((site.GuardCount > 0 || site.ResponseCount > 0) && (!optionalSite || !site.Guarded)) return false;
|
||||
if (site.ResponseOffset.Length() > 40 || Math.AbsFloat(site.ResponseOffset[1]) > 0.01) return false;
|
||||
if (site.ResponseCount > 0) groups++;
|
||||
if (!optionalSite && site.RevealOnEscape) return false;
|
||||
if (site.Transport)
|
||||
{
|
||||
@@ -137,7 +147,7 @@ class ERR_ScenarioDefinition
|
||||
foreach (ERR_SiteProp prop : site.Props)
|
||||
if (!prop || prop.Prefab.IsEmpty() || prop.Offset.Length() > 40) return false;
|
||||
}
|
||||
if (site.Kind < ERR_LocationKind.PRISON || site.Kind > ERR_LocationKind.MORTAR) return false;
|
||||
if (site.Kind < ERR_LocationKind.PRISON || site.Kind > ERR_LocationKind.HOSPITAL) return false;
|
||||
ids.Insert(site.Id);
|
||||
if (site.Kind == ERR_LocationKind.PRISON) prisons++;
|
||||
if (site.Kind == ERR_LocationKind.RADIO) radios++;
|
||||
@@ -181,6 +191,6 @@ class ERR_ScenarioDefinition
|
||||
sources++;
|
||||
}
|
||||
}
|
||||
return prisons == 1 && radios == 1 && extractions == 1 && groups <= 8 && items <= 256 && props <= 64 && transports <= 4 && sources >= MinimumRadioSources && maps >= 1;
|
||||
return prisons == 1 && radios == 1 && extractions == 1 && groups <= 24 && items <= 512 && props <= 128 && transports <= 4 && sources >= MinimumRadioSources && maps >= 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ class ERR_SiteDiscoveryRules
|
||||
{
|
||||
if (radius < 0 || radius > 60) return false;
|
||||
if (kind == ERR_LocationKind.RADIO) return radius <= RadioRadiusLimit();
|
||||
if (kind == ERR_LocationKind.DEPOT || kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.MORTAR) return true;
|
||||
if (kind == ERR_LocationKind.DEPOT || kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.MORTAR || kind == ERR_LocationKind.HOSPITAL) return true;
|
||||
return (kind == ERR_LocationKind.PRISON || kind == ERR_LocationKind.EXTRACTION) && radius == 0;
|
||||
}
|
||||
static bool InRange(float radius, float horizontalDistance, float verticalDistance)
|
||||
|
||||
@@ -13,6 +13,7 @@ class ERR_SiteRuntime
|
||||
ERR_LocationComponent Location;
|
||||
IEntity Container;
|
||||
SCR_AIGroup Group;
|
||||
ref ERR_SiteRuntime Responders;
|
||||
string ExpectedGuardFaction;
|
||||
int InitialGuardCount;
|
||||
ref ERR_PrisonPatrol PrisonPatrol;
|
||||
@@ -87,7 +88,7 @@ class ERR_SiteService
|
||||
{
|
||||
if (!group) return null;
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
if (site.Group == group) return site;
|
||||
if (site.Group == group || (site.Responders && site.Responders.Group == group)) return site;
|
||||
return null;
|
||||
}
|
||||
ERR_SiteRuntime Find(string id)
|
||||
@@ -109,6 +110,17 @@ class ERR_SiteService
|
||||
PrintFormat("Reintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
|
||||
return !m_Obstructed;
|
||||
}
|
||||
protected bool GeneratedCacheClearance(vector position)
|
||||
{
|
||||
// Match the physical terrain survey. An entity's broad bounding box may
|
||||
// overlap an empty volume near a tree or ruin without blocking the cache.
|
||||
vector ground = ERR_Actors.Ground(position);
|
||||
TraceBox trace = new TraceBox();
|
||||
trace.Start = ground + "0 0.12 0"; trace.End = ground + "0 2 0";
|
||||
trace.Mins = "-1.3 0 -1.3"; trace.Maxs = "1.3 0.1 1.3";
|
||||
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
return GetGame().GetWorld().TraceMove(trace, null) >= 1;
|
||||
}
|
||||
protected bool BackpackClearance(vector position)
|
||||
{
|
||||
vector floor = ERR_Actors.Ground(position);
|
||||
@@ -293,6 +305,7 @@ class ERR_SiteService
|
||||
vector containerPosition = position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading);
|
||||
bool clearContainer;
|
||||
if (spec.Kind == ERR_LocationKind.PRISON) clearContainer = BackpackClearance(containerPosition);
|
||||
else if (spec.PlacementResolved) clearContainer = GeneratedCacheClearance(containerPosition);
|
||||
else clearContainer = CacheClearance(containerPosition);
|
||||
if (!clearContainer) return Fail("obstructed cache position: " + spec.Id);
|
||||
site.Container = Spawn(spec.Container, position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading));
|
||||
@@ -334,11 +347,13 @@ class ERR_SiteService
|
||||
vector propPosition = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(prop.Offset, spec.Heading));
|
||||
// Radio furniture intentionally surrounds/supports its own elevated radio.
|
||||
// Generated camp terrain was checked before any of these objects existed.
|
||||
if (spec.Kind != ERR_LocationKind.RADIO && !CacheClearance(propPosition)) return Fail("obstructed scenery position: " + spec.Id);
|
||||
if (!spec.PlacementResolved && spec.Kind != ERR_LocationKind.RADIO && !CacheClearance(propPosition)) return Fail("obstructed scenery position: " + spec.Id);
|
||||
propPosition[1] = propPosition[1] + prop.Offset[1];
|
||||
IEntity scenery = ERR_Actors.Spawn(prop.Prefab, propPosition, prop.Angles + Vector(spec.Heading, 0, 0));
|
||||
if (!scenery) return Fail("scenery prefab: " + spec.Id);
|
||||
m_Entities.Insert(scenery); site.Props.Insert(scenery);
|
||||
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (aiWorld) aiWorld.RequestNavmeshRebuildEntity(scenery);
|
||||
}
|
||||
}
|
||||
if (!CreateTransport(site, position)) return false;
|
||||
@@ -363,6 +378,12 @@ class ERR_SiteService
|
||||
if (!ERR_PrisonPatrol.Configure(site.Group, groupPrefab, m_Mode.EnemyFaction())) return Fail("prison patrol roster: " + spec.Id);
|
||||
site.PrisonPatrol = new ERR_PrisonPatrol();
|
||||
}
|
||||
else if (spec.GuardCount > 0)
|
||||
{
|
||||
site.Group = SCR_AIGroup.Cast(Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", guardPosition));
|
||||
Faction faction = GetGame().GetFactionManager().GetFactionByKey(site.ExpectedGuardFaction);
|
||||
if (!ERR_SiteSquads.Configure(site.Group, groupPrefab, faction, spec.GuardCount)) return Fail("site defender roster: " + spec.Id);
|
||||
}
|
||||
else site.Group = SCR_AIGroup.Cast(Spawn(groupPrefab, guardPosition));
|
||||
if (!site.Group) return Fail("guard group resource: " + spec.Id);
|
||||
if (!site.Group.m_aUnitPrefabSlots || site.Group.m_aUnitPrefabSlots.IsEmpty() || !site.Group.GetSpawnImmediately()) return Fail("guard profile must request an initial physical squad: " + spec.Id);
|
||||
@@ -370,6 +391,7 @@ class ERR_SiteService
|
||||
foreach (ResourceName member : site.Group.m_aUnitPrefabSlots)
|
||||
if (member.IsEmpty()) return Fail("guard profile has an empty member resource: " + spec.Id);
|
||||
if (!CreateGuardOrders(site, guardPosition)) return Fail("guard initial orders: " + spec.Id);
|
||||
if (!CreateResponders(site, groupPrefab)) return Fail("site mobile roster: " + spec.Id);
|
||||
}
|
||||
PrintFormat("Reintroduced [sites]: authored %1 at %2", spec.Id, position);
|
||||
}
|
||||
@@ -452,7 +474,9 @@ class ERR_SiteService
|
||||
if (!GetGame()) return;
|
||||
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!world) return;
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
array<ERR_SiteRuntime> pendingGroups = {};
|
||||
foreach (ERR_SiteRuntime owner : m_Sites) { pendingGroups.Insert(owner); if (owner.Responders) pendingGroups.Insert(owner.Responders); }
|
||||
foreach (ERR_SiteRuntime site : pendingGroups)
|
||||
{
|
||||
if (!site.Group || site.InitialGuardCount <= 0) continue;
|
||||
// Retire this instance's initial spawn intent as well as its current queue
|
||||
@@ -482,6 +506,12 @@ class ERR_SiteService
|
||||
ERR_PrepareResult guards = InitialGuards(site);
|
||||
if (guards == ERR_PrepareResult.FAILED) return m_State;
|
||||
if (guards == ERR_PrepareResult.PENDING) pendingGuards++;
|
||||
if (site.Responders)
|
||||
{
|
||||
ERR_PrepareResult mobile = InitialGuards(site.Responders);
|
||||
if (mobile == ERR_PrepareResult.FAILED) return m_State;
|
||||
if (mobile == ERR_PrepareResult.PENDING) pendingGuards++;
|
||||
}
|
||||
}
|
||||
string firstRejected;
|
||||
foreach (ERR_StockPlacement placement : m_Placements)
|
||||
@@ -528,6 +558,7 @@ class ERR_SiteService
|
||||
}
|
||||
m_Mode.Forces().Register(site.Definition.Id, kind, site.Group.GetOrigin(), m_Mode.ForceLiving(site.Group), route, site.Group, GetGame().GetWorld().GetWorldTime());
|
||||
m_Mode.RegisterGroup(site.Group);
|
||||
ActivateResponders(site);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -588,4 +619,32 @@ class ERR_SiteService
|
||||
if (m_State == ERR_PrepareResult.PENDING) CancelInitialGuardRequests();
|
||||
StopPatrols();
|
||||
}
|
||||
protected bool CreateResponders(ERR_SiteRuntime site, ResourceName profile)
|
||||
{
|
||||
ERR_SiteDefinition spec = site.Definition;
|
||||
if (spec.ResponseCount == 0) return true;
|
||||
ERR_SiteRuntime mobile = new ERR_SiteRuntime();
|
||||
site.Responders = mobile;
|
||||
mobile.Definition = spec; mobile.Location = site.Location;
|
||||
mobile.ExpectedGuardFaction = site.ExpectedGuardFaction;
|
||||
vector position = ERR_Actors.Ground(spec.Position + ERR_CampTerrain.Rotate(spec.ResponseOffset, spec.Heading));
|
||||
mobile.Group = SCR_AIGroup.Cast(Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", position));
|
||||
mobile.InitialGuardCount = spec.ResponseCount;
|
||||
if (!ERR_SiteSquads.Configure(mobile.Group, profile, site.Group.GetFaction(), spec.ResponseCount)) return false;
|
||||
// Local guard duty resumes when an HQ search expires. HQ inserts its delivered
|
||||
// search ahead of this waypoint; only this mobile group is eligible to leave.
|
||||
AIWaypoint home = AIWaypoint.Cast(Spawn(ERR_Resources.DefendWaypoint(), position));
|
||||
if (!home) return false;
|
||||
home.SetCompletionRadius(25); mobile.Group.AddWaypoint(home);
|
||||
return true;
|
||||
}
|
||||
protected void ActivateResponders(ERR_SiteRuntime site)
|
||||
{
|
||||
if (!site.Responders || !site.Responders.Group) return;
|
||||
SCR_AIGroup group = site.Responders.Group;
|
||||
m_Mode.Forces().Register(site.Definition.Id + "-responders", ERR_ForceKind.PATROL, group.GetOrigin(), m_Mode.ForceLiving(group), null, group, GetGame().GetWorld().GetWorldTime());
|
||||
m_Mode.RegisterGroup(group);
|
||||
ERR_HQ.Event("SITE GARRISON", string.Format("%1 (%2) at %3: %4 permanent defenders stay at the site; %5 mobile soldiers may receive HQ hunting orders; faction=%6", site.Definition.Label, site.Definition.Id, site.Location.Position(), m_Mode.ForceLiving(site.Group), m_Mode.ForceLiving(group), site.ExpectedGuardFaction));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Finite role-separated site rosters. A mobile team never borrows the permanent
|
||||
// defenders, and a defeated team is not recreated by this owner.
|
||||
class ERR_SiteSquads
|
||||
{
|
||||
static array<ResourceName> Roster(array<ResourceName> source, int count)
|
||||
{
|
||||
array<ResourceName> result = {};
|
||||
if (!source || source.IsEmpty() || count < 1 || count > 8) return result;
|
||||
foreach (ResourceName member : source) if (member.IsEmpty()) return result;
|
||||
for (int index = 0; index < count; index++) result.Insert(source[index % source.Count()]);
|
||||
return result;
|
||||
}
|
||||
static bool Configure(SCR_AIGroup group, ResourceName profile, Faction faction, int count)
|
||||
{
|
||||
if (!Replication.IsServer() || !group || !faction || group.GetAgentsCount() != 0) return false;
|
||||
Resource resource = Resource.Load(profile);
|
||||
if (!resource || !resource.IsValid()) return false;
|
||||
IEntitySource source = resource.GetResource().ToEntitySource();
|
||||
array<ResourceName> roster = {};
|
||||
if (!source || !source.Get("m_aUnitPrefabSlots", roster)) return false;
|
||||
group.m_aUnitPrefabSlots = Roster(roster, count);
|
||||
if (group.m_aUnitPrefabSlots.Count() != count) return false;
|
||||
group.SetFaction(faction);
|
||||
group.SetSpawnImmediately(true);
|
||||
group.SetPermanentLOD(0); group.ActivateAI();
|
||||
group.RequestSpawn(count);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#ifdef WORKBENCH
|
||||
// The local native runner has one real player. A physical stand-in tests camera
|
||||
// tracking; canonical multiplayer target selection is tested separately.
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
protected IEntity m_ERRCameraProbeTarget;
|
||||
void ERR_ProbeCameraTarget(IEntity target) { m_ERRCameraProbeTarget = target; }
|
||||
override IEntity ERR_ObservedEntity()
|
||||
{
|
||||
string outcome;
|
||||
if (m_ERRCameraProbeTarget && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "unconscious-camera") return m_ERRCameraProbeTarget;
|
||||
return super.ERR_ObservedEntity();
|
||||
}
|
||||
bool ERR_ProbeCameraActive()
|
||||
{
|
||||
return m_ERRObserverCamera && GetGame().GetCameraManager().CurrentCamera() == m_ERRObserverCamera;
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERRCameraStage = -2;
|
||||
protected int m_ERRCameraDeadline;
|
||||
protected IEntity m_ERRCameraWeapon;
|
||||
protected float m_ERRCameraWait;
|
||||
protected IEntity m_ERRCameraBody, m_ERRCameraTarget;
|
||||
protected SCR_CharacterDamageManagerComponent m_ERRCameraDamage;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "unconscious-camera") return;
|
||||
m_ERRCameraDeadline = System.GetTickCount() + 180000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_CameraProbeTick, 500, true);
|
||||
}
|
||||
protected bool ERR_CameraCheck(bool ok, string message)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: unconscious camera " + message); return true; }
|
||||
Print("ERR SMOKE FAIL: unconscious camera " + message, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_CameraProbeTick); return false;
|
||||
}
|
||||
protected void ERR_CameraKnockout()
|
||||
{
|
||||
m_ERRCameraDamage.ForceUnconsciousness();
|
||||
array<ref SCR_PersistentDamageEffect> effects = m_ERRCameraDamage.GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect);
|
||||
foreach (SCR_PersistentDamageEffect effect : effects) m_ERRCameraDamage.TerminateDamageEffect(effect);
|
||||
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
||||
}
|
||||
protected void ERR_CameraProbeTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERRCameraDeadline || !PreparationError().IsEmpty()) { ERR_CameraCheck(false, "deadline/preparation " + PreparationError()); return; }
|
||||
if (!Active()) return;
|
||||
SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!pc || !pc.GetControlledEntity()) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
if (now < m_ERRCameraWait) return;
|
||||
if (m_ERRCameraStage == -2)
|
||||
{
|
||||
if (!ERR_CameraCheck(Settings().Get("keepWeaponOnUnconscious") == 1, "weapon retention defaults to enabled")) return;
|
||||
m_ERRCameraBody = pc.GetControlledEntity();
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_ERRCameraBody.FindComponent(InventoryStorageManagerComponent));
|
||||
m_ERRCameraWeapon = ERR_Actors.Spawn("{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et", m_ERRCameraBody.GetOrigin()); Own(m_ERRCameraWeapon);
|
||||
if (!ERR_CameraCheck(m_ERRCameraWeapon && inventory && inventory.TryInsertItem(m_ERRCameraWeapon, EStoragePurpose.PURPOSE_WEAPON_PROXY), "one physical rifle granted to the player")) return;
|
||||
m_ERRCameraStage = -1; m_ERRCameraWait = now + 1500; return;
|
||||
}
|
||||
SCR_CharacterControllerComponent character = SCR_CharacterControllerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterControllerComponent));
|
||||
InventoryStorageManagerComponent playerInventory = character.GetInventoryStorageManager();
|
||||
if (m_ERRCameraStage == -1)
|
||||
{
|
||||
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(m_ERRCameraWeapon.FindComponent(BaseWeaponComponent));
|
||||
if (!ERR_CameraCheck(playerInventory.Contains(m_ERRCameraWeapon) && character.SelectWeapon(weapon), "rifle selected with native equip animation")) return;
|
||||
m_ERRCameraStage = 0; m_ERRCameraWait = now + 2000; return;
|
||||
}
|
||||
if (m_ERRCameraStage == 0)
|
||||
{
|
||||
if (!ERR_CameraCheck(character.GetWeaponManagerComponent().GetCurrentWeapon() && character.GetWeaponManagerComponent().GetCurrentWeapon().GetOwner() == m_ERRCameraWeapon, "original rifle actually in hand before knockout")) return;
|
||||
m_ERRCameraBody = pc.GetControlledEntity();
|
||||
m_ERRCameraDamage = SCR_CharacterDamageManagerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!ERR_CameraCheck(m_ERRCameraDamage && m_ERRCameraDamage.ERR_IsRevivablePlayer(), "admitted player retains treatable body")) return;
|
||||
ERR_CameraKnockout(); m_ERRCameraStage = 1; m_ERRCameraWait = now + 3000; return;
|
||||
}
|
||||
if (m_ERRCameraStage == 1)
|
||||
{
|
||||
if (!ERR_CameraCheck(pc.ERR_ObserverState() == ERR_ObserverReason.UNCONSCIOUS && pc.ERR_ProbeCameraActive(), "native knockout automatically opens the manual third-person camera")) return;
|
||||
if (!ERR_CameraCheck(pc.GetControlledEntity() == m_ERRCameraBody && ERR_Actors.Alive(m_ERRCameraBody) && !ERR_Actors.CanFight(m_ERRCameraBody), "possession and living unconscious body unchanged")) return;
|
||||
if (!ERR_CameraCheck(!pc.ERR_ObservedEntity() && vector.DistanceXZ(pc.ERR_ObserverAnchor(), m_ERRCameraBody.GetOrigin()) < 1, "no teammate falls back to the casualty body")) return;
|
||||
if (!ERR_CameraCheck(playerInventory.Contains(m_ERRCameraWeapon), "enabled retention keeps the exact rifle with the unconscious player")) return;
|
||||
vector point = ERR_Actors.Ground(m_ERRCameraBody.GetOrigin() + "25 0 25");
|
||||
m_ERRCameraTarget = ERR_Actors.Spawn(Factions().PrisonerPrefab, point); Own(m_ERRCameraTarget);
|
||||
if (!ERR_CameraCheck(m_ERRCameraTarget != null, "physical follow-target stand-in created")) return;
|
||||
pc.ERR_ProbeCameraTarget(m_ERRCameraTarget);
|
||||
m_ERRCameraStage = 2; m_ERRCameraWait = now + 1500; return;
|
||||
}
|
||||
if (m_ERRCameraStage == 2)
|
||||
{
|
||||
vector camera = GetGame().GetCameraManager().CurrentCamera().GetOrigin();
|
||||
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;
|
||||
}
|
||||
if (m_ERRCameraStage == 3)
|
||||
{
|
||||
if (!ERR_CameraCheck(vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), m_ERRCameraTarget.GetOrigin()) < 13, "camera tracks a moving target")) 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;
|
||||
}
|
||||
if (m_ERRCameraStage == 4)
|
||||
{
|
||||
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 (!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;
|
||||
}
|
||||
if (!ERR_CameraCheck(pc.ERR_ProbeCameraActive(), "second knockout reopens camera without stale target")) return;
|
||||
if (!ERR_CameraCheck(!playerInventory.Contains(m_ERRCameraWeapon), "disabled retention leaves normal native weapon drop on ground")) return;
|
||||
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||
m_ERRCameraDamage.FullHeal();
|
||||
GetGame().GetCallqueue().Remove(ERR_CameraProbeTick);
|
||||
Print("ERR SMOKE COMPLETE: unconscious spectator camera, physical tracking, retained treatment body, recovery and repeated knockout; target stand-in, not multiplayer proof");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,112 @@
|
||||
class ERR_UnconsciousWeaponRules
|
||||
{
|
||||
static bool Firearm(int kind)
|
||||
{
|
||||
return kind == EWeaponType.WT_RIFLE || kind == EWeaponType.WT_SNIPERRIFLE || kind == EWeaponType.WT_MACHINEGUN
|
||||
|| kind == EWeaponType.WT_HANDGUN || kind == EWeaponType.WT_GRENADELAUNCHER || kind == EWeaponType.WT_ROCKETLAUNCHER;
|
||||
}
|
||||
}
|
||||
|
||||
// Retain the actual item, including its magazine/attachments. Never spawn a
|
||||
// replacement and never take an item that another inventory has acquired.
|
||||
modded class SCR_CharacterControllerComponent
|
||||
{
|
||||
protected IEntity m_ERRRetainedWeapon;
|
||||
protected ref ERR_EnemyEquipmentCallback m_ERRWeaponInsertion;
|
||||
protected int m_ERRWeaponInsertAttempts, m_ERRWeaponEquipAttempts;
|
||||
protected bool m_ERRWeaponWasDropped;
|
||||
|
||||
protected bool ERR_WeaponOwnerCurrent()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return Replication.IsServer() && mode && mode.Active() && mode.Players() && mode.Players().CurrentActor(GetOwner()) && ERR_Actors.Alive(GetOwner());
|
||||
}
|
||||
protected void ERR_RememberWeapon(IEntity weapon, bool dropped = false)
|
||||
{
|
||||
if (m_ERRRetainedWeapon || !weapon || !ERR_WeaponOwnerCurrent() || ERR_GameMode.Get().Settings().Get("keepWeaponOnUnconscious") != 1) return;
|
||||
BaseWeaponComponent component = BaseWeaponComponent.Cast(weapon.FindComponent(BaseWeaponComponent));
|
||||
if (!component || !ERR_UnconsciousWeaponRules.Firearm(component.GetWeaponType())) return;
|
||||
m_ERRRetainedWeapon = weapon; m_ERRWeaponWasDropped = dropped;
|
||||
m_ERRWeaponInsertAttempts = 0; m_ERRWeaponEquipAttempts = 0; m_ERRWeaponInsertion = null;
|
||||
GetGame().GetCallqueue().CallLater(ERR_KeepWeaponTick, 100, true);
|
||||
}
|
||||
bool ERR_RetainsWeapon(IEntity weapon)
|
||||
{
|
||||
return weapon && weapon == m_ERRRetainedWeapon && ERR_WeaponOwnerCurrent() && IsUnconscious();
|
||||
}
|
||||
override void OnLifeStateChanged(ECharacterLifeState previousLifeState, ECharacterLifeState newLifeState, bool isJIP)
|
||||
{
|
||||
if (newLifeState == ECharacterLifeState.INCAPACITATED && GetWeaponManagerComponent())
|
||||
{
|
||||
BaseWeaponComponent weapon = GetWeaponManagerComponent().GetCurrentWeapon();
|
||||
if (weapon) ERR_RememberWeapon(weapon.GetOwner());
|
||||
}
|
||||
super.OnLifeStateChanged(previousLifeState, newLifeState, isJIP);
|
||||
}
|
||||
override protected void OnWeaponDropped(IEntity pWeaponEntity, WeaponSlotComponent pWeaponSlot)
|
||||
{
|
||||
super.OnWeaponDropped(pWeaponEntity, pWeaponSlot);
|
||||
if (!IsUnconscious() && GetLifeState() != ECharacterLifeState.INCAPACITATED) return;
|
||||
ERR_RememberWeapon(pWeaponEntity, true);
|
||||
if (pWeaponEntity == m_ERRRetainedWeapon) m_ERRWeaponWasDropped = true;
|
||||
}
|
||||
protected void ERR_ForgetWeapon()
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(ERR_KeepWeaponTick);
|
||||
m_ERRRetainedWeapon = null; m_ERRWeaponInsertion = null;
|
||||
}
|
||||
protected void ERR_KeepWeaponTick()
|
||||
{
|
||||
if (!m_ERRRetainedWeapon || !ERR_WeaponOwnerCurrent()) { ERR_ForgetWeapon(); return; }
|
||||
InventoryStorageManagerComponent inventory = GetInventoryStorageManager();
|
||||
if (!inventory) { ERR_ForgetWeapon(); return; }
|
||||
if (!inventory.Contains(m_ERRRetainedWeapon))
|
||||
{
|
||||
if (m_ERRWeaponInsertion)
|
||||
{
|
||||
if (!m_ERRWeaponInsertion.Finished) return;
|
||||
// One accepted asynchronous insertion is never submitted a second time.
|
||||
if (!m_ERRWeaponInsertion.Succeeded) ERR_HQ.Event("PLAYER WEAPON", "Weapon retention insertion failed; original gun remains available on the ground.");
|
||||
ERR_ForgetWeapon(); return;
|
||||
}
|
||||
InventoryItemComponent item = InventoryItemComponent.Cast(m_ERRRetainedWeapon.FindComponent(InventoryItemComponent));
|
||||
if (!m_ERRWeaponWasDropped || !item || item.GetParentSlot() || vector.Distance(GetOwner().GetOrigin(), m_ERRRetainedWeapon.GetOrigin()) > 8 || ++m_ERRWeaponInsertAttempts > 10)
|
||||
{ ERR_ForgetWeapon(); return; }
|
||||
ref ERR_EnemyEquipmentCallback operation = new ERR_EnemyEquipmentCallback();
|
||||
if (inventory.TryInsertItem(m_ERRRetainedWeapon, EStoragePurpose.PURPOSE_WEAPON_PROXY, operation)) m_ERRWeaponInsertion = operation;
|
||||
return;
|
||||
}
|
||||
if (IsUnconscious()) return;
|
||||
if (++m_ERRWeaponEquipAttempts > 30 || CompartmentAccessComponent.GetVehicleIn(GetOwner())) { ERR_ForgetWeapon(); return; }
|
||||
BaseWeaponComponent current = GetWeaponManagerComponent().GetCurrentWeapon();
|
||||
if (current && current.GetOwner() == m_ERRRetainedWeapon) { ERR_ForgetWeapon(); return; }
|
||||
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(m_ERRRetainedWeapon.FindComponent(BaseWeaponComponent));
|
||||
if (!weapon || !SelectWeapon(weapon)) return;
|
||||
RplComponent rpl = RplComponent.Cast(m_ERRRetainedWeapon.FindComponent(RplComponent));
|
||||
if (rpl) Rpc(ERR_EquipRetainedWeapon, rpl.Id());
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
protected void ERR_EquipRetainedWeapon(RplId id)
|
||||
{
|
||||
if (GetOwner() != SCR_PlayerController.GetLocalControlledEntity() || !ERR_Actors.CanFight(GetOwner())) return;
|
||||
RplComponent rpl = RplComponent.Cast(Replication.FindItem(id));
|
||||
if (!rpl || !GetInventoryStorageManager() || !GetInventoryStorageManager().Contains(rpl.GetEntity())) return;
|
||||
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(rpl.GetEntity().FindComponent(BaseWeaponComponent));
|
||||
if (weapon) SelectWeapon(weapon);
|
||||
}
|
||||
void ~SCR_CharacterControllerComponent()
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_KeepWeaponTick);
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_CharacterInventoryStorageComponent
|
||||
{
|
||||
override bool OnOverrideCanRemoveItem() { return true; }
|
||||
override bool CanRemoveItem(IEntity item)
|
||||
{
|
||||
SCR_CharacterControllerComponent controller = SCR_CharacterControllerComponent.Cast(GetOwner().FindComponent(SCR_CharacterControllerComponent));
|
||||
if (controller && controller.ERR_RetainsWeapon(item)) return false;
|
||||
return super.CanRemoveItem(item);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ class ERR_AirAssaultTeam
|
||||
}
|
||||
class ERR_AirDropGeometry
|
||||
{
|
||||
static string LastFailure;
|
||||
static float Along(vector position, vector center, vector direction)
|
||||
{ vector offset = position - center;
|
||||
offset[1] = 0;
|
||||
@@ -47,17 +48,19 @@ class ERR_AirDropGeometry
|
||||
}
|
||||
static bool LandingLine(vector center, vector direction)
|
||||
{
|
||||
LastFailure = "clear";
|
||||
for (int step = -4; step <= 5; step++)
|
||||
{
|
||||
vector point = ERR_Actors.Ground(center + direction * (step * 50));
|
||||
if (point[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) || !ERR_HQRoadIntercept.Standable(point)) return false;
|
||||
if (point[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) { LastFailure = "water/shore"; return false; }
|
||||
if (!ERR_HQRoadIntercept.Standable(point)) { LastFailure = "landing ground not standable"; return false; }
|
||||
TraceBox trace = new TraceBox();
|
||||
trace.Start = point + "0 3 0";
|
||||
trace.End = point + "0 190 0";
|
||||
trace.Mins = "-4 -1 -4";
|
||||
trace.Maxs = "4 1 4";
|
||||
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) return false;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) { LastFailure = "obstructed descent column"; return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -254,16 +257,28 @@ class ERR_AirAssaultLoad
|
||||
radial[1] = 0;
|
||||
float heading = Math.Atan2(radial[0], radial[2]);
|
||||
bool found;
|
||||
for (int attempt = 0; attempt < 12; attempt++)
|
||||
int surveyed;
|
||||
map<string, int> rejected = new map<string, int>();
|
||||
// Several distances and both runway directions provide alternatives around
|
||||
// woodland while retaining every landing/descent safety check.
|
||||
for (int attempt = 0; attempt < 72; attempt++)
|
||||
{
|
||||
float angle = heading + attempt * Math.PI / 6;
|
||||
int bearing = attempt % 12;
|
||||
int ring = Math.Floor(attempt / 24.0);
|
||||
float radius = 300 + ring * 200;
|
||||
float angle = heading + bearing * Math.PI / 6;
|
||||
radial = Vector(Math.Sin(angle), 0, Math.Cos(angle));
|
||||
m_Center = ERR_Actors.Ground(order.Target + radial * 450);
|
||||
m_Center = ERR_Actors.Ground(order.Target + radial * radius);
|
||||
m_Direction = Vector(-radial[2], 0, radial[0]);
|
||||
if (ERR_AirDropGeometry.LandingLine(m_Center, m_Direction)) { found = true;
|
||||
break;
|
||||
}
|
||||
if ((attempt % 24) >= 12) m_Direction = radial;
|
||||
surveyed++;
|
||||
if (ERR_AirDropGeometry.LandingLine(m_Center, m_Direction)) { found = true; break; }
|
||||
int count; rejected.Find(ERR_AirDropGeometry.LastFailure, count);
|
||||
rejected.Set(ERR_AirDropGeometry.LastFailure, count + 1);
|
||||
}
|
||||
string summary;
|
||||
foreach (string reason, int failures : rejected) summary += reason + "=" + failures.ToString() + "; ";
|
||||
ERR_HQ.Event("AIR DROP SURVEY", string.Format("target=%1 checked=%2 accepted=%3 center=%4; %5", order.Target, surveyed, found, m_Center, summary));
|
||||
if (!found) return false;
|
||||
m_RunStart = m_Center - m_Direction * 350;
|
||||
m_RunEnd = m_Center + m_Direction * 450;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
class ERR_BodyRecoveryJob
|
||||
{
|
||||
int Id;
|
||||
int Repaths;
|
||||
int Reassignments;
|
||||
bool Approaching;
|
||||
float ProgressAt;
|
||||
float BestDistance = 1e20;
|
||||
ref array<SCR_AIGroup> BlockedTeams = {};
|
||||
IEntity Body;
|
||||
vector Position;
|
||||
SCR_AIGroup Team;
|
||||
@@ -38,6 +45,7 @@ class ERR_BodyRecovery
|
||||
{
|
||||
if (m_Stopped || !report || !report.Target || m_ReportedBodies.Contains(report.Target) || m_Jobs.Count() >= 128 || !m_Mode.Evidence().Find(report.Target)) return;
|
||||
ERR_BodyRecoveryJob job = new ERR_BodyRecoveryJob();
|
||||
job.Id = m_Jobs.Count() + 1;
|
||||
job.Body = report.Target;
|
||||
job.Position = report.Position;
|
||||
m_ReportedBodies.Insert(report.Target);
|
||||
@@ -49,6 +57,7 @@ class ERR_BodyRecovery
|
||||
foreach (ERR_BodyRecoveryJob previous : m_Jobs)
|
||||
{
|
||||
if (previous == job || previous.State != 4 || !previous.Team || previous.Team.GetPlayerCount(true) > 0 || previous.Team.GetFaction() != m_Mode.EnemyFaction() || m_Mode.ForceLiving(previous.Team) == 0) continue;
|
||||
if (job.BlockedTeams.Contains(previous.Team)) continue;
|
||||
job.Team = previous.Team;
|
||||
previous.Team = null;
|
||||
job.State = 1;
|
||||
@@ -178,6 +187,7 @@ class ERR_BodyRecovery
|
||||
}
|
||||
if (job.State == 2)
|
||||
{
|
||||
if (!TrackProgress(job, now)) continue;
|
||||
SCR_AIGroupUtilityComponent utility = job.Team.GetGroupUtilityComponent();
|
||||
if (utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty()) { job.SearchAt = -1;
|
||||
continue;
|
||||
@@ -193,7 +203,7 @@ class ERR_BodyRecovery
|
||||
float nearest = 1e20;
|
||||
foreach (AIAgent nearbyAgent : agents)
|
||||
if (nearbyAgent && nearbyAgent.GetControlledEntity()) nearest = Math.Min(nearest, vector.Distance(nearbyAgent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()));
|
||||
ERR_HQ.Event("BODY SEARCH PROGRESS", "nearest searcher=" + nearest.ToString() + "m; physical inspection required");
|
||||
ERR_HQ.Event("BODY SEARCH PROGRESS", string.Format("job=%1 team=%2 body=%3 nearest searcher=%4m; replans=%5; physical inspection required", job.Id, job.Team.GetOrigin(), job.Body.GetOrigin(), nearest, job.Repaths));
|
||||
}
|
||||
if (!searcher) { job.SearchAt = -1;
|
||||
continue;
|
||||
@@ -218,6 +228,51 @@ class ERR_BodyRecovery
|
||||
}
|
||||
}
|
||||
}
|
||||
protected bool TrackProgress(ERR_BodyRecoveryJob job, float now)
|
||||
{
|
||||
array<AIAgent> agents = {}; job.Team.GetAgents(agents);
|
||||
float nearest = 1e20;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) nearest = Math.Min(nearest, vector.Distance(agent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()));
|
||||
SCR_AIGroupUtilityComponent utility = job.Team.GetGroupUtilityComponent();
|
||||
bool combat = utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty();
|
||||
if (job.ProgressAt == 0 || nearest < job.BestDistance - 10 || combat || nearest <= 3)
|
||||
{ job.ProgressAt = now; job.BestDistance = nearest; }
|
||||
if (job.Approaching && job.Waypoint && job.Waypoint.IsWithinCompletionRadius(job.Team))
|
||||
{
|
||||
RemoveWaypoint(job); job.Approaching = false;
|
||||
job.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), job.Position));
|
||||
if (job.Waypoint) { job.Waypoint.SetCompletionRadius(1.5); job.Team.AddWaypointAt(job.Waypoint, 0); }
|
||||
}
|
||||
if (now - job.ProgressAt < 120000) return true;
|
||||
job.ProgressAt = now;
|
||||
if (job.Repaths >= 3)
|
||||
{
|
||||
ERR_HQ.Event("BODY SEARCH STALLED", string.Format("job=%1 team=%2 body=%3 distance=%4m; three native replans exhausted", job.Id, job.Team.GetOrigin(), job.Body.GetOrigin(), nearest));
|
||||
if (job.Reassignments >= 2) { Finish(job, false); return false; }
|
||||
RemoveWaypoint(job); m_HQ.ReleaseRecovery(job.Team);
|
||||
job.BlockedTeams.Insert(job.Team); job.Team = null;
|
||||
job.Reassignments++; job.Repaths = 0; job.State = 0; job.Approaching = false;
|
||||
job.BestDistance = 1e20; job.RetryAt = now + 30000; job.SearchAt = -1;
|
||||
return false;
|
||||
}
|
||||
job.Repaths++;
|
||||
vector target = job.Position;
|
||||
// Vary the final approach instead of teleporting a team or pretending the
|
||||
// body was searched. The original body remains the only inspection target.
|
||||
for (int attempt = 0; attempt < 8; attempt++)
|
||||
{
|
||||
float angle = (job.Repaths * 90 + attempt * 45) * Math.DEG2RAD;
|
||||
vector approach = ERR_Actors.Ground(target + Vector(Math.Sin(angle), 0, Math.Cos(angle)) * 60);
|
||||
if (!ERR_HQRoadIntercept.Standable(approach)) continue;
|
||||
target = approach; job.Approaching = true; break;
|
||||
}
|
||||
RemoveWaypoint(job);
|
||||
job.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), target));
|
||||
if (job.Waypoint) { job.Waypoint.SetCompletionRadius(8); job.Team.AddWaypointAt(job.Waypoint, 0); }
|
||||
ERR_HQ.Event("BODY SEARCH REPATH", string.Format("job=%1 attempt=%2 body=%3 approach=%4 distance=%5m; no progress for 120 seconds", job.Id, job.Repaths, job.Body.GetOrigin(), target, nearest));
|
||||
return true;
|
||||
}
|
||||
void Stop() { m_Stopped = true;
|
||||
foreach (ERR_BodyRecoveryJob job : m_Jobs) { RemoveWaypoint(job);
|
||||
job.Order = null;
|
||||
@@ -267,7 +322,6 @@ modded class ERR_HQ
|
||||
if (assignedTask && assignedTask.SecuritySite == id && GetGame().GetWorld().GetWorldTime() < assignedTask.ExpiresAt) secured = true;
|
||||
}
|
||||
if (secured) continue;
|
||||
ERR_HQ.Event("INTEL COMPROMISE", "missing issued document concerns " + id + "; defensive reinforcement requested");
|
||||
ERR_HQUnit best;
|
||||
float distance = 1e20;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
@@ -279,9 +333,10 @@ modded class ERR_HQ
|
||||
distance = candidate;
|
||||
}
|
||||
}
|
||||
if (!best) continue;
|
||||
if (!best) { ERR_HQ.Event("SITE SECURITY WAIT", "site=" + id + ": no eligible idle infantry with a fresh status report within 2500m; no dispatch"); continue; }
|
||||
ERR_HQRoute route = Downlink(best.Group, "SITE SECURITY", "secure potentially compromised site " + id, 0);
|
||||
if (!route) continue;
|
||||
if (!route) { ERR_HQ.Event("SITE SECURITY WAIT", "site=" + id + ": selected group has no radio route; no dispatch"); continue; }
|
||||
ERR_HQ.Event("SITE SECURITY QUEUED", "site=" + id + "; group=" + best.Id.ToString() + "; distance=" + distance.ToString() + "m; awaiting radio delivery");
|
||||
ERR_HQTask task = new ERR_HQTask();
|
||||
task.SecuritySite = id;
|
||||
task.Role = ERR_HQPursuitRole.SEARCH;
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
// 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)
|
||||
static bool Owned(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;
|
||||
if (!group || !actor || !ERR_Actors.Alive(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 && mode.IsMissionEnemy(group.GetFaction()) && !ERR_EnemyGunnerSkill.CapturedGroup(mode, group);
|
||||
}
|
||||
static bool Eligible(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||
{
|
||||
return Owned(mode, group, actor) && ERR_Actors.CanFight(actor);
|
||||
}
|
||||
static EAISkill Floor(EAISkill skill)
|
||||
{
|
||||
return EAISkill.CYLON;
|
||||
@@ -70,66 +74,23 @@ class ERR_EnemyEquipmentCallback : ScriptedInventoryOperationCallback
|
||||
|
||||
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;
|
||||
ref ERR_EnemyLauncherGrant Launcher = new ERR_EnemyLauncherGrant();
|
||||
ref ERR_EnemyMineGrant Mines = new ERR_EnemyMineGrant();
|
||||
int SkillChecks;
|
||||
bool Rolled;
|
||||
bool Selected;
|
||||
bool Submitted;
|
||||
bool Complete;
|
||||
bool Released;
|
||||
ref ERR_EnemyEquipmentCallback Operation;
|
||||
void Check(ERR_GameMode mode)
|
||||
{
|
||||
Launcher.Check(mode, Group, Actor);
|
||||
Mines.Check(mode, Group, Actor);
|
||||
}
|
||||
|
||||
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;
|
||||
Launcher.Stop(); Mines.Stop();
|
||||
Released = true;
|
||||
if (Optics) Optics.Stop();
|
||||
if (Combat) Combat.ERR_ReleaseEnemySkill(mode);
|
||||
@@ -205,7 +166,10 @@ class ERR_EnemyEquipment
|
||||
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; }
|
||||
if (!ERR_EnemyEquipmentRules.Owned(m_Mode, entry.Group, entry.Actor)) { entry.Stop(m_Mode); continue; }
|
||||
// Incapacitation suspends work on the existing entry; it must not permanently
|
||||
// remove the skill binding or recreate completed equipment grants on recovery.
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, entry.Group, entry.Actor)) continue;
|
||||
// Combat may arrive after the group's initial membership notification.
|
||||
if (!entry.Combat && entry.SkillChecks < 5)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
// Finite mission-owned inventory grants. No client spawns, rerolls or replenishment.
|
||||
class ERR_EnemyLoadoutRules
|
||||
{
|
||||
static bool Selected(int percent, int roll) { return percent >= 0 && percent <= 100 && roll >= 0 && roll < 100 && roll < percent; }
|
||||
static bool AntiAirName(ResourceName prefab)
|
||||
{
|
||||
string path = prefab; path.ToLower();
|
||||
return path.Contains("/launchers/") && !path.Contains("_base.et")
|
||||
&& (path.Contains("stinger") || path.Contains("fim92") || path.Contains("igla") || path.Contains("strela") || path.Contains("manpad"));
|
||||
}
|
||||
}
|
||||
class ERR_EnemyLoadoutCatalog
|
||||
{
|
||||
static const ResourceName FIM = "{3DF204EEE0534EF2}Prefabs/Weapons/Launchers/FIM92_Stinger/Launcher_FIM92_Stinger.et";
|
||||
static const ResourceName IGLA = "{962C400354E4BD6C}Prefabs/Weapons/Launchers/9K38_IGLA/Launcher_9K38_IGLA.et";
|
||||
static const ResourceName MINE = "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et";
|
||||
protected static ERR_GameMode s_Mode;
|
||||
protected static ref map<string, ResourceName> s_Launchers = new map<string, ResourceName>();
|
||||
static bool Available(ResourceName prefab)
|
||||
{
|
||||
if (prefab.IsEmpty()) return false;
|
||||
Resource resource = Resource.Load(prefab);
|
||||
return resource && resource.IsValid();
|
||||
}
|
||||
static ResourceName Launcher(ERR_GameMode mode, Faction faction)
|
||||
{
|
||||
if (!faction) return FIM;
|
||||
if (s_Mode != mode) { s_Mode = mode; s_Launchers.Clear(); }
|
||||
string key = faction.GetFactionKey();
|
||||
if (s_Launchers.Contains(key)) return s_Launchers.Get(key);
|
||||
ResourceName preferred;
|
||||
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||
if (definition)
|
||||
foreach (ERR_ContentOption option : definition.Options)
|
||||
if (option.NativeFactionKey == key && !option.AntiAirLauncher.IsEmpty()) preferred = option.AntiAirLauncher;
|
||||
if (preferred.IsEmpty())
|
||||
{
|
||||
SCR_Faction nativeFaction = SCR_Faction.Cast(faction);
|
||||
SCR_EntityCatalog catalog;
|
||||
if (nativeFaction) catalog = nativeFaction.GetFactionEntityCatalogOfType(EEntityCatalogType.ITEM, false);
|
||||
array<SCR_EntityCatalogEntry> items = {};
|
||||
if (catalog) catalog.GetEntityList(items);
|
||||
int limit = Math.Min(items.Count(), 4096);
|
||||
for (int i = 0; i < limit; i++)
|
||||
{
|
||||
if (!items[i] || !items[i].IsEnabled()) continue;
|
||||
ResourceName candidate = items[i].GetPrefab();
|
||||
if (ERR_EnemyLoadoutRules.AntiAirName(candidate) && Available(candidate)) { preferred = candidate; break; }
|
||||
}
|
||||
}
|
||||
// WCS supplies these native faction AA soldiers, even if the item catalog is absent.
|
||||
if (preferred.IsEmpty() && (key == "USSR" || key == "FIA")) preferred = IGLA;
|
||||
if (!Available(preferred)) preferred = FIM;
|
||||
s_Launchers.Set(key, preferred);
|
||||
ERR_HQ.Event("AA PROFILE", "faction=" + key + "; preferred=" + preferred + "; fallback=" + FIM);
|
||||
return preferred;
|
||||
}
|
||||
static bool Loaded(IEntity item)
|
||||
{
|
||||
if (!item) return false;
|
||||
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(item.FindComponent(BaseWeaponComponent));
|
||||
if (!weapon || weapon.GetWeaponType() != EWeaponType.WT_ROCKETLAUNCHER) return false;
|
||||
array<BaseMuzzleComponent> muzzles = {}; weapon.GetMuzzlesList(muzzles);
|
||||
if (muzzles.IsEmpty() || muzzles.Count() > 8) return false;
|
||||
foreach (BaseMuzzleComponent muzzle : muzzles)
|
||||
{
|
||||
if (!muzzle) continue;
|
||||
for (int barrel = 0; barrel < Math.Min(muzzle.GetBarrelsCount(), 8); barrel++) if (muzzle.IsBarrelChambered(barrel)) return true;
|
||||
BaseMagazineComponent magazine = muzzle.GetMagazine();
|
||||
if (magazine && magazine.GetAmmoCount() > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static IEntity Find(ResourceName prefab, array<IEntity> items, array<IEntity> excluded)
|
||||
{
|
||||
foreach (IEntity item : items)
|
||||
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == prefab && (!excluded || !excluded.Contains(item))) return item;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
class ERR_EnemyLauncherGrant
|
||||
{
|
||||
protected bool m_Rolled;
|
||||
protected bool m_Finished;
|
||||
protected bool m_Fallback;
|
||||
protected bool m_Removing;
|
||||
protected int m_Checks;
|
||||
protected ResourceName m_Prefab;
|
||||
protected IEntity m_Issued;
|
||||
protected ref array<IEntity> m_Before = {};
|
||||
protected ref ERR_EnemyEquipmentCallback m_Operation;
|
||||
protected ResourceName Preferred(ERR_GameMode mode, SCR_AIGroup group) { return ERR_EnemyLoadoutCatalog.Launcher(mode, group.GetFaction()); }
|
||||
protected void Finish(IEntity actor, string reason)
|
||||
{
|
||||
m_Finished = true;
|
||||
ERR_HQ.Event("AA EQUIPMENT", string.Format("actor=%1 position=%2 prefab=%3 %4", actor, actor.GetOrigin(), m_Prefab, reason));
|
||||
}
|
||||
protected void Fallback(IEntity actor, string reason)
|
||||
{
|
||||
if (m_Fallback || m_Prefab == ERR_EnemyLoadoutCatalog.FIM) { Finish(actor, "FIM-92 unavailable/unusable: " + reason); return; }
|
||||
ERR_HQ.Event("AA FALLBACK", string.Format("actor=%1 rejected=%2 reason=%3; trying FIM-92", actor, m_Prefab, reason));
|
||||
m_Fallback = true; m_Removing = false; m_Prefab = ERR_EnemyLoadoutCatalog.FIM; m_Operation = null; m_Issued = null; m_Checks = 0;
|
||||
}
|
||||
void Check(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||
{
|
||||
if (m_Finished) return;
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(mode, group, actor)) { m_Finished = true; return; }
|
||||
if (!m_Rolled)
|
||||
{
|
||||
m_Rolled = true;
|
||||
if (!ERR_EnemyLoadoutRules.Selected(mode.Settings().Runtime("antiAirChancePercent"), Math.RandomInt(0, 100))) { m_Finished = true; return; }
|
||||
m_Prefab = Preferred(mode, group);
|
||||
}
|
||||
if (++m_Checks > 10) { Finish(actor, "bounded startup expired; unresolved operation quarantined, no duplicate grant"); return; }
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));
|
||||
if (!inventory || !manager) return;
|
||||
array<IEntity> items = {}; inventory.GetItems(items);
|
||||
array<IEntity> weapons = {}; manager.GetWeaponsList(weapons);
|
||||
if (items.Count() > 512 || weapons.Count() > 16) { Finish(actor, "inventory exceeds inspection budget"); return; }
|
||||
if (m_Removing)
|
||||
{
|
||||
if (!m_Operation || !m_Operation.Finished) return;
|
||||
if (m_Operation.Succeeded && (!m_Issued || !items.Contains(m_Issued))) Fallback(actor, "own unusable replacement removed");
|
||||
else Finish(actor, "could not confirm removal of unusable replacement; no duplicate grant");
|
||||
return;
|
||||
}
|
||||
foreach (IEntity existing : weapons)
|
||||
if (existing && existing.GetPrefabData() && (ERR_EnemyLoadoutRules.AntiAirName(existing.GetPrefabData().GetPrefabName()) || existing.GetPrefabData().GetPrefabName() == m_Prefab) && ERR_EnemyLoadoutCatalog.Loaded(existing))
|
||||
{ Finish(actor, "loaded anti-air weapon physically equipped; preserve it without replenishment"); return; }
|
||||
if (m_Operation)
|
||||
{
|
||||
if (!m_Operation.Finished) return;
|
||||
m_Issued = ERR_EnemyLoadoutCatalog.Find(m_Prefab, items, m_Before);
|
||||
if (!m_Issued)
|
||||
{
|
||||
if (!m_Operation.Succeeded) Fallback(actor, "native insertion failed");
|
||||
else Finish(actor, "completed insertion no longer present; may have been looted, no replacement");
|
||||
return;
|
||||
}
|
||||
// Delete only this newly issued, still-owned unusable item, never an authored weapon.
|
||||
if (m_Fallback || m_Prefab == ERR_EnemyLoadoutCatalog.FIM) { Finish(actor, "FIM-92 inserted but not loaded/equipped; inspect mod compatibility"); return; }
|
||||
m_Removing = true; m_Checks = 0; m_Operation = new ERR_EnemyEquipmentCallback();
|
||||
if (!inventory.TryDeleteItem(m_Issued, m_Operation)) Finish(actor, "unusable item removal refused; fallback withheld");
|
||||
return;
|
||||
}
|
||||
if (!ERR_EnemyLoadoutCatalog.Available(m_Prefab)) { Fallback(actor, "resource unavailable"); return; }
|
||||
m_Before.Clear(); foreach (IEntity before : items) m_Before.Insert(before);
|
||||
m_Operation = new ERR_EnemyEquipmentCallback();
|
||||
if (!inventory.TrySpawnPrefabToStorage(m_Prefab, null, -1, EStoragePurpose.PURPOSE_WEAPON_PROXY, m_Operation)) Fallback(actor, "native weapon slot insertion refused");
|
||||
}
|
||||
void Stop() { m_Finished = true; }
|
||||
}
|
||||
class ERR_EnemyMineGrant
|
||||
{
|
||||
protected bool m_Rolled;
|
||||
protected bool m_Finished;
|
||||
protected int m_Checks;
|
||||
protected int m_Remaining;
|
||||
protected ref ERR_EnemyEquipmentCallback m_Operation;
|
||||
void Check(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||
{
|
||||
if (m_Finished) return;
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(mode, group, actor)) { m_Finished = true; return; }
|
||||
if (!m_Rolled)
|
||||
{
|
||||
m_Rolled = true;
|
||||
if (!ERR_EnemyLoadoutRules.Selected(mode.Settings().Runtime("antiPersonnelMineChancePercent"), Math.RandomInt(0, 100))) { m_Finished = true; return; }
|
||||
m_Remaining = 2;
|
||||
}
|
||||
if (++m_Checks > 10) { m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", "startup deadline; pending native operation will not be duplicated"); return; }
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return;
|
||||
if (m_Operation)
|
||||
{
|
||||
if (!m_Operation.Finished) return;
|
||||
if (!m_Operation.Succeeded) { m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", "inventory insertion failed; no loose mines spawned"); return; }
|
||||
m_Operation = null;
|
||||
if (m_Remaining == 0) { m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", string.Format("actor=%1 position=%2 finite two-mine allowance issued", actor, actor.GetOrigin())); return; }
|
||||
}
|
||||
array<IEntity> items = {}; inventory.GetItems(items);
|
||||
int present;
|
||||
foreach (IEntity item : items) if (item && item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)) present++;
|
||||
m_Remaining = Math.Min(m_Remaining, 2 - present);
|
||||
if (m_Remaining <= 0) { m_Finished = true; return; }
|
||||
m_Remaining--; // An accepted grant consumes its allowance even if later dropped or looted.
|
||||
m_Operation = new ERR_EnemyEquipmentCallback();
|
||||
if (!inventory.TrySpawnPrefabToStorage(ERR_EnemyLoadoutCatalog.MINE, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, m_Operation))
|
||||
{ m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", "no inventory capacity; finite grant refused"); }
|
||||
}
|
||||
void Stop() { m_Finished = true; }
|
||||
}
|
||||
@@ -92,6 +92,7 @@ class ERR_HQ : ERR_CommandService
|
||||
protected ref ERR_EnemyEquipment m_Equipment;
|
||||
protected ref ERR_HunterService m_Hunters;
|
||||
protected ref ERR_ObjectiveDefence m_ObjectiveDefence;
|
||||
protected ref ERR_TownGarrisons m_Towns;
|
||||
protected ref array<SCR_AICombatComponent> m_SpecialSkills = {};
|
||||
ERR_HunterService Hunters() { return m_Hunters; }
|
||||
protected ref ERR_ScoutSystem m_Scouts;
|
||||
@@ -136,6 +137,7 @@ class ERR_HQ : ERR_CommandService
|
||||
m_Equipment = new ERR_EnemyEquipment(mode);
|
||||
m_Hunters = new ERR_HunterService(mode, null);
|
||||
m_ObjectiveDefence = new ERR_ObjectiveDefence(mode, this);
|
||||
m_Towns = new ERR_TownGarrisons(mode);
|
||||
m_Scouts = new ERR_ScoutSystem(mode);
|
||||
m_Recon = new ERR_AirReconSystem(mode);
|
||||
m_AirAssault = new ERR_AirAssaultSystem(this, mode);
|
||||
@@ -232,10 +234,15 @@ class ERR_HQ : ERR_CommandService
|
||||
{
|
||||
if (m_Units.IsEmpty() || !m_Mode.Forces()) return false;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
ERR_MortarBattery support = m_Mortars.FindGroup(unit.Group);
|
||||
// Failed optional support is terminal, not an outstanding force admission.
|
||||
if (support && (support.Stopped() || !support.SpawnFailure.IsEmpty())) continue;
|
||||
if (unit.Group && !m_Mode.Forces().FindGroup(unit.Group)) return false;
|
||||
}
|
||||
array<ref ERR_MortarBattery> batteries = {}; m_Mortars.Batteries(batteries);
|
||||
foreach (ERR_MortarBattery battery : batteries)
|
||||
if (battery && !battery.Stopped() && !battery.StartupActivated) return false;
|
||||
if (battery && !battery.Stopped() && battery.SpawnFailure.IsEmpty() && !battery.StartupActivated) return false;
|
||||
return true;
|
||||
}
|
||||
protected void AutomaticConvoy(float now)
|
||||
@@ -419,7 +426,10 @@ class ERR_HQ : ERR_CommandService
|
||||
ERR_HQJournal Journal() { return m_Journal; }
|
||||
string Status()
|
||||
{
|
||||
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + string.Format("; %1 remembered tracks, %2 swept cells; aggression=%3/30 identified bodies=%4; ", m_Memory.Count(), m_Coverage.Count(), m_Watch.Aggression(), m_Watch.IdentifiedBodies()) + m_Hunters.Status() + "; " + m_Mode.Forces().Status() + "; " + m_Mortars.Status() + "; " + m_Logistics.Status() + "; " + m_Resupply.Status() + "; " + m_Recon.Status();
|
||||
return string.Format("Enemy HQ: %1 registered groups; %2 received sightings; %3 assigned responses.", m_Units.Count(), m_Contacts.Count(), Assigned())
|
||||
+ string.Format("\nHQ memory: %1 tracked reports; %2 searched areas.\nAggression: %3 of 30, based on %4 identified bodies (one level per three bodies).", m_Memory.Count(), m_Coverage.Count(), m_Watch.Aggression(), m_Watch.IdentifiedBodies())
|
||||
+ "\n" + m_Hunters.Status() + "\n" + m_Mode.Forces().Status() + "\n" + m_Mortars.Status()
|
||||
+ "\n" + m_Logistics.Status() + "\n" + m_Resupply.Status() + "\n" + m_Recon.Status();
|
||||
}
|
||||
|
||||
protected void BindPhysical(ERR_HQUnit unit)
|
||||
@@ -481,8 +491,8 @@ class ERR_HQ : ERR_CommandService
|
||||
unit.HomeId = home.Definition.Id;
|
||||
// Objective security is permanent; it can report contacts but cannot be tasked
|
||||
// with searches, reinforcements, resupply or courier work away from its site.
|
||||
unit.ReservedForSupport = ERR_ObjectiveDefence.Objective(home.Definition.Kind);
|
||||
unit.HomeGuard = !home.Definition.Patrol || home.Definition.Patrol.IsEmpty();
|
||||
unit.ReservedForSupport = group == home.Group && ERR_ObjectiveDefence.Objective(home.Definition.Kind);
|
||||
unit.HomeGuard = group == home.Group && (!home.Definition.Patrol || home.Definition.Patrol.IsEmpty());
|
||||
}
|
||||
int phase = unit.Id % 5;
|
||||
unit.NextReport = GetGame().GetWorld().GetWorldTime() + phase * 1000;
|
||||
@@ -493,7 +503,14 @@ class ERR_HQ : ERR_CommandService
|
||||
unit.ReservedForSupport = true;
|
||||
BindSpecialSkills(group);
|
||||
}
|
||||
else m_Equipment.RegisterGroup(group, unit.Id, !m_Mortars || !m_Mortars.FindGroup(group));
|
||||
else
|
||||
{
|
||||
int equipmentRadioId = unit.Id;
|
||||
// Widely separated HQ hunters each need a physical manpack uplink.
|
||||
if (m_Hunters && m_Hunters.Contains(group)) equipmentRadioId = 1;
|
||||
if (m_Towns && m_Towns.Contains(group)) equipmentRadioId = 1;
|
||||
m_Equipment.RegisterGroup(group, equipmentRadioId, !m_Mortars || !m_Mortars.FindGroup(group));
|
||||
}
|
||||
BindPhysical(unit);
|
||||
unit.NextStatus = GetGame().GetWorld().GetWorldTime() + 10000 + phase * 1000;
|
||||
unit.AmmoReporting.Initialize(unit.Id, GetGame().GetWorld().GetWorldTime());
|
||||
@@ -1215,6 +1232,7 @@ class ERR_HQ : ERR_CommandService
|
||||
}
|
||||
m_Hunters.Tick(now);
|
||||
m_ObjectiveDefence.Tick(now);
|
||||
m_Towns.Tick(now);
|
||||
m_Convoys.Tick(now);
|
||||
AutomaticConvoy(now);
|
||||
m_Bodies.TickBudget(now);
|
||||
@@ -1268,6 +1286,7 @@ class ERR_HQ : ERR_CommandService
|
||||
override void Stop()
|
||||
{
|
||||
m_Hunters.Stop(); m_ObjectiveDefence.Stop();
|
||||
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);
|
||||
|
||||
@@ -10,8 +10,9 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
||||
if (section != "hq") return "";
|
||||
if (option == "interval") return "reportIntervalSeconds";
|
||||
if (option == "squads") return "supportSquadLimit";
|
||||
if (option == "mines") return "antiPersonnelMineChancePercent";
|
||||
if (option == "radius") return "responseRadiusMetres";
|
||||
if (option == "stinger") return "stingerChanceOneIn";
|
||||
if (option == "aa" || option == "stinger") return "antiAirChancePercent";
|
||||
return "";
|
||||
}
|
||||
override int JournalSequence()
|
||||
@@ -29,9 +30,9 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
||||
// reroll registered soldiers or remove/replenish an issued physical launcher.
|
||||
protected void StingerSetting(ERR_GameMode mode, array<string> parts, array<string> replies)
|
||||
{
|
||||
string key = "stingerChanceOneIn";
|
||||
string key = "antiAirChancePercent";
|
||||
string effect = "Only eligible actors which have not rolled yet use this chance; no reroll, removal or replenishment of existing equipment.";
|
||||
string range = "0 off, 1 every eligible soldier, 2..10000 one in N";
|
||||
string range = "0..100 percent (0 off, 100 every eligible soldier); faction MANPADS preferred, FIM-92 fallback";
|
||||
if (parts.Count() == 3 && parts[2] == "show")
|
||||
{ replies.Insert(key + "=" + mode.Settings().Get(key).ToString() + ". " + effect); return; }
|
||||
int value;
|
||||
@@ -124,7 +125,7 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
||||
replies.Insert("/esc convoys show|set 0..20|random | hq convoy count show|set N (default three; next round)");
|
||||
replies.Insert("/esc hq show | hq memory | hq log show|next|set on|off | hq convoy show|start");
|
||||
replies.Insert("/esc hq interval|squads|radius|stinger show|set N | hq convoy auto show|set 0|1|help");
|
||||
replies.Insert("Stinger: 0 off, 1 every eligible soldier, 2..10000 one in N; applies only before each actor rolls.");
|
||||
replies.Insert("Stinger: 0..100 percent (0 off, 100 every eligible soldier); faction MANPADS preferred, FIM-92 fallback; applies only before each actor rolls.");
|
||||
replies.Insert("Convoy auto: default 1, next round, finite fleet. /esc hq heli show | hq heli timer set 1..1440 | hq heli return");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ class ERR_HQJournal
|
||||
return;
|
||||
float seconds = GetGame().GetWorld().GetWorldTime() / 1000;
|
||||
string line = string.Format("#%1 t=%2s M%3 %4 %5", ++m_Sequence, Math.Round(seconds * 10) / 10.0, messageId, kind, message) + " [session=" + m_Session + "]";
|
||||
// Capture server-local wall time once, so history keeps the event time
|
||||
// when a client opens the menu later or resumes a paused journal.
|
||||
line = ERR_JournalText.Entry(m_Sequence, SCR_DateTimeHelper.GetTimeLocal(), kind, message, messageId) + "\n" + line;
|
||||
if (m_Recent.Count() >= 300)
|
||||
m_Recent.Remove(0);
|
||||
m_Recent.Insert(line);
|
||||
@@ -84,7 +87,7 @@ class ERR_HQJournal
|
||||
if (!file)
|
||||
{
|
||||
m_RetryAt = now + 30000;
|
||||
PrintFormat("Escapists AI [HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR);
|
||||
PrintFormat("Co10 Escape Reintroduced [AI / HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
foreach (string line : m_Pending)
|
||||
|
||||
@@ -18,7 +18,7 @@ class ERR_HQStatusReport
|
||||
class ERR_HQWatchRules
|
||||
{
|
||||
static int Aggression(int identifiedBodies) { return Math.Clamp(identifiedBodies / 3, 0, 30); }
|
||||
static int Hunters(int aggression) { return (Math.Clamp(aggression, 0, 30) + 1) / 2; }
|
||||
static int Hunters(int aggression) { return 3 + (Math.Clamp(aggression, 0, 30) * 12 + 29) / 30; }
|
||||
static bool Valid(ERR_HQStatusReport report, float now)
|
||||
{
|
||||
if (!report || report.SourceId.IsEmpty() || report.SourceId.Length() > 96 || report.MessageId <= 0)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
||||
// crew, vehicle teleports, health restoration, or ammunition replacement.
|
||||
class ERR_HunterCrewRecovery
|
||||
{
|
||||
protected ERR_HunterVehicle m_Unit;
|
||||
protected ref SCR_AIVehicleBehavior m_Boarding;
|
||||
protected IEntity m_BoardingActor;
|
||||
protected float m_NextAttempt;
|
||||
protected string m_LastReason;
|
||||
protected bool m_Stopped;
|
||||
string Reason;
|
||||
void ERR_HunterCrewRecovery(ERR_HunterVehicle unit) { m_Unit = unit; }
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
if (m_Boarding) m_Boarding.Complete();
|
||||
m_Boarding = null; m_BoardingActor = null;
|
||||
}
|
||||
bool Lost(ERR_GameMode mode)
|
||||
{
|
||||
if (!m_Unit.Group || !m_Unit.Entity) { Reason = "group or vehicle removed"; return true; }
|
||||
if (!ERR_Actors.Alive(m_Unit.Entity)) { Reason = "vehicle destroyed"; return true; }
|
||||
if (m_Unit.Group.GetFaction() != mode.EnemyFaction()) { Reason = "crew faction changed"; return true; }
|
||||
foreach (IEntity member : m_Unit.Members)
|
||||
if (ERR_Actors.Alive(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == m_Unit.Group) return false;
|
||||
Reason = "no living original crew remain in the group";
|
||||
return true;
|
||||
}
|
||||
protected void Note(string reason)
|
||||
{
|
||||
Reason = reason;
|
||||
if (reason == m_LastReason) return;
|
||||
m_LastReason = reason;
|
||||
ERR_HQ.Event("HUNTER CREW", m_Unit.Source + ": " + reason);
|
||||
}
|
||||
void Update(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Unit || m_Unit.Released || Lost(mode)) return;
|
||||
// Activate before admission: distant native compartment-spawn requests need
|
||||
// live agents to finish. Do not wait for seated readiness to activate them.
|
||||
m_Unit.Group.SetPermanentLOD(0); m_Unit.Group.ActivateAI();
|
||||
array<AIAgent> agents = {}; m_Unit.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents)
|
||||
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) { agent.SetPermanentLOD(0); agent.ActivateAI(); }
|
||||
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.CrewValid(mode))
|
||||
{
|
||||
if (m_Boarding) m_Boarding.Complete();
|
||||
m_Boarding = null; m_BoardingActor = null;
|
||||
Note("driver seated and conscious; patrol can continue"); return;
|
||||
}
|
||||
if (m_Unit.Seats.IsEmpty() || !m_Unit.Seats[0]) { Note("waiting for native driver compartment"); return; }
|
||||
BaseCompartmentSlot seat = m_Unit.Seats[0];
|
||||
if (!ERR_Actors.Alive(m_Unit.Driver) || ERR_Actors.Group(m_Unit.Driver) != m_Unit.Group)
|
||||
{
|
||||
foreach (IEntity survivor : m_Unit.Members)
|
||||
if (ERR_Actors.CanFight(survivor) && !ERR_Actors.Player(survivor) && ERR_Actors.Group(survivor) == m_Unit.Group)
|
||||
{ m_Unit.Driver = survivor; break; }
|
||||
}
|
||||
if (!ERR_Actors.CanFight(m_Unit.Driver)) { Note("driver incapacitated; retaining vehicle and destination while awaiting recovery"); return; }
|
||||
if (seat.GetOccupant() && seat.GetOccupant() != m_Unit.Driver)
|
||||
{ Note("driver seat occupied; original survivor waiting, no occupant forcibly removed"); return; }
|
||||
SCR_ChimeraAIAgent driver = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Unit.Driver));
|
||||
if (!driver || !driver.m_UtilityComponent) { Note("waiting for original driver's AI utility"); return; }
|
||||
array<ref AIActionBase> actions = {}; driver.m_UtilityComponent.GetActions(actions);
|
||||
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();
|
||||
m_BoardingActor = m_Unit.Driver;
|
||||
IEntity aboard = CompartmentAccessComponent.GetVehicleIn(m_Unit.Driver);
|
||||
if (aboard)
|
||||
{
|
||||
m_Boarding = new SCR_AIGetOutVehicle(driver.m_UtilityComponent, null, aboard, priority: 1000);
|
||||
Note("original survivor leaving another seat before taking the driver seat");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Boarding = new SCR_AIGetInVehicle(driver.m_UtilityComponent, null, m_Unit.Entity, seat, EAICompartmentType.Pilot, 1000);
|
||||
Note("original driver ordered to walk to the vehicle and board");
|
||||
}
|
||||
driver.m_UtilityComponent.AddAction(m_Boarding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Deployment geometry only. Pursuit destinations always come from delivered HQ reports.
|
||||
class ERR_HunterOpeningRules
|
||||
{
|
||||
static bool Spread(vector center, vector first, vector second)
|
||||
{
|
||||
vector a = first - center; a[1] = 0;
|
||||
vector b = second - center; b[1] = 0;
|
||||
return vector.DistanceXZ(center, first) >= 600 && vector.DistanceXZ(center, second) >= 600
|
||||
&& vector.Dot(a, b) <= 0; // At least ninety degrees between actual road positions.
|
||||
}
|
||||
static bool FreshOrder(float observed, float queued, float now, float nextContact, bool pendingContact)
|
||||
{
|
||||
return !pendingContact && observed >= 0 && observed > queued && now >= nextContact;
|
||||
}
|
||||
static bool Select(vector center, array<vector> candidates, array<int> selected)
|
||||
{
|
||||
selected.Clear();
|
||||
if (candidates.Count() > 64) return false;
|
||||
for (int i = 0; i < candidates.Count(); i++)
|
||||
for (int j = i + 1; j < candidates.Count(); j++)
|
||||
{
|
||||
if (!Spread(center, candidates[i], candidates[j])) continue;
|
||||
for (int k = j + 1; k < candidates.Count(); k++)
|
||||
if (Spread(center, candidates[i], candidates[k]) && Spread(center, candidates[j], candidates[k]))
|
||||
{ selected.Insert(i); selected.Insert(j); selected.Insert(k); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
class ERR_HunterOpeningPlan
|
||||
{
|
||||
vector Center;
|
||||
ref array<vector> Positions = {};
|
||||
ref array<vector> Angles = {};
|
||||
protected ref array<vector> m_Candidates = {};
|
||||
protected ref array<vector> m_Angles = {};
|
||||
protected int m_Scanned;
|
||||
protected bool m_Done;
|
||||
protected float m_Heading;
|
||||
void ERR_HunterOpeningPlan(vector center) { Center = center; m_Heading = Math.RandomFloat(0, 360); }
|
||||
bool Ready() { return Positions.Count() == 3; }
|
||||
bool Done() { return m_Done; }
|
||||
static bool AwayFromPlayers(vector position, array<IEntity> players)
|
||||
{
|
||||
foreach (IEntity player : players) if (player && vector.DistanceXZ(player.GetOrigin(), position) < 400) return false;
|
||||
return true;
|
||||
}
|
||||
void Step(array<IEntity> players)
|
||||
{
|
||||
if (m_Done) return;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
// Eight bounded road/footprint queries per HQ second; four rings, sixteen bearings.
|
||||
for (int work = 0; work < 8 && m_Scanned < 64; work++)
|
||||
{
|
||||
int candidate = m_Scanned++;
|
||||
int ring = candidate / 16;
|
||||
int sector = candidate % 16;
|
||||
float radius = 800 + ring * 450;
|
||||
float bearing = (m_Heading + sector * 22.5) * Math.DEG2RAD;
|
||||
vector seed = Center + Vector(Math.Sin(bearing) * radius, 0, Math.Cos(bearing) * radius);
|
||||
vector position, angles;
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles, 300)) continue;
|
||||
if (vector.DistanceXZ(Center, position) < 600 || !AwayFromPlayers(position, players)) continue;
|
||||
if (!ERR_SupplyConvoyRules.Separated(position, m_Candidates, 100) || !placement.Clear(position, angles[0])) continue;
|
||||
m_Candidates.Insert(position); m_Angles.Insert(angles);
|
||||
}
|
||||
array<int> selected = {};
|
||||
if (ERR_HunterOpeningRules.Select(Center, m_Candidates, selected))
|
||||
{
|
||||
foreach (int index : selected) { Positions.Insert(m_Candidates[index]); Angles.Insert(m_Angles[index]); }
|
||||
m_Done = true;
|
||||
ERR_HQ.Event("HUNTER OPENING", "three clear road starts reserved at least 90 degrees apart; center=" + Center.ToString());
|
||||
}
|
||||
else if (m_Scanned == 64)
|
||||
{
|
||||
m_Done = true;
|
||||
ERR_HQ.Event("HUNTER OPENING FAILED", "no three safe, widely separated road starts in 64 candidates; unsafe spawns withheld");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,23 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||
ref ERR_HQRoute Downlink;
|
||||
vector Destination;
|
||||
int SourceMessage;
|
||||
float LastContact = -1;
|
||||
float NextContactOrder;
|
||||
ref ERR_HunterCrewRecovery Recovery;
|
||||
float NextMotionLog;
|
||||
float ProgressAt;
|
||||
vector ProgressPosition;
|
||||
int Repaths;
|
||||
override bool CrewValid(ERR_GameMode mode)
|
||||
{
|
||||
return Group && Entity && ERR_Actors.Alive(Entity) && Group.GetFaction() == mode.EnemyFaction()
|
||||
&& Members.Contains(Driver) && ERR_Actors.CanFight(Driver) && !ERR_Actors.Player(Driver)
|
||||
&& ERR_Actors.Group(Driver) == Group && Seats.Count() > 0 && Seats[0].GetOccupant() == Driver;
|
||||
}
|
||||
override void Release(ERR_GameMode mode)
|
||||
{
|
||||
if (Released) return;
|
||||
if (Recovery) Recovery.Stop();
|
||||
if (!mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { super.Release(mode); return; }
|
||||
Released = true; Downlink = null; ClearOrder();
|
||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||
@@ -33,7 +47,13 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
protected ref array<ref ERR_HunterVehicle> m_Hunters = {};
|
||||
protected float m_NextHunter;
|
||||
protected float m_NextSpawn;
|
||||
protected int m_Target;
|
||||
protected int m_Target = 3;
|
||||
protected ref ERR_HunterOpeningPlan m_Opening;
|
||||
bool Contains(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Group == group) return true;
|
||||
return false;
|
||||
}
|
||||
int Deployed() { return m_Hunters.Count(); }
|
||||
int ActiveHunters()
|
||||
{
|
||||
@@ -41,12 +61,22 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Released) count++;
|
||||
return count;
|
||||
}
|
||||
override string Status() { return string.Format("aggression UAZs: %1 active, %2/%3 deployed allowance (maximum 15)", ActiveHunters(), Deployed(), m_Target); }
|
||||
override string Status() { return string.Format("HQ UAZ patrols: %1 active; %2 deployed from a total allowance of %3 (maximum 15). Lost vehicles are not replaced.", ActiveHunters(), Deployed(), m_Target); }
|
||||
protected bool StartHunter(ERR_HQ hq, float now)
|
||||
{
|
||||
if (m_Hunters.Count() >= m_Target || m_Hunters.Count() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
||||
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
if (m_Hunters.Count() < 3)
|
||||
{
|
||||
if (!m_Opening || !m_Opening.Ready()) return false;
|
||||
int slot = m_Hunters.Count();
|
||||
vector openingPosition = m_Opening.Positions[slot];
|
||||
vector openingAngles = m_Opening.Angles[slot];
|
||||
if (!ERR_HunterOpeningPlan.AwayFromPlayers(openingPosition, players) || !placement.Clear(openingPosition, openingAngles[0]))
|
||||
{ ERR_HQ.Event("HUNTER WAIT", "reserved opening road occupied; no unsafe spawn"); return false; }
|
||||
return SpawnHunter(hq, now, openingPosition, openingAngles, "prison-opening");
|
||||
}
|
||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At((i + m_Hunters.Count()) % m_Mode.Sites().Count());
|
||||
@@ -58,25 +88,31 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
bool nearPlayer;
|
||||
foreach (IEntity player : players) if (player && vector.DistanceXZ(player.GetOrigin(), position) < 400) nearPlayer = true;
|
||||
if (nearPlayer) continue;
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
||||
unit.Deadline = now + 30000; m_Hunters.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(0), position, angles));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
|
||||
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||
if (movement) movement.SetCruiseSpeed(45);
|
||||
ERR_HQ.Event("HUNTER SPAWN", string.Format("%1 reason=identified bodies %2 aggression=%3/30 allowance=%4/15 base=%5 position=%6 crew=%7", unit.Source, hq.Watch().IdentifiedBodies(), hq.Watch().Aggression(), m_Target, site.Definition.Id, position, unit.Members.Count()));
|
||||
return true;
|
||||
return SpawnHunter(hq, now, position, angles, site.Definition.Id);
|
||||
}
|
||||
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
||||
return false;
|
||||
}
|
||||
protected bool SpawnHunter(ERR_HQ hq, float now, vector position, vector angles, string source)
|
||||
{
|
||||
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
||||
unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(0), position, angles));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||
{ unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
|
||||
unit.Recovery = new ERR_HunterCrewRecovery(unit);
|
||||
unit.Recovery.Update(m_Mode, now);
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
|
||||
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||
if (movement) movement.SetCruiseSpeed(45);
|
||||
ERR_HQ.Event("HUNTER SPAWN", string.Format("%1 identified bodies=%2 aggression=%3/30 allowance=%4/15 base=%5 position=%6 crew=%7", unit.Source, hq.Watch().IdentifiedBodies(), hq.Watch().Aggression(), m_Target, source, position, unit.Members.Count()));
|
||||
return true;
|
||||
}
|
||||
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
if (!unit.CrewValid(m_Mode)) return false;
|
||||
@@ -97,8 +133,6 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
}
|
||||
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||
{
|
||||
if (now < unit.NextOrder || unit.Downlink) return;
|
||||
unit.NextOrder = now + 30000;
|
||||
array<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
|
||||
ERR_HQBelief best;
|
||||
foreach (ERR_HQReport report : reports)
|
||||
@@ -106,12 +140,18 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
ERR_HQBelief belief = hq.Memory().Get(report.Target);
|
||||
if (belief && belief.Actionable() && (!best || belief.ObservedAt > best.ObservedAt)) best = belief;
|
||||
}
|
||||
bool freshContact;
|
||||
if (best) freshContact = ERR_HunterOpeningRules.FreshOrder(best.ObservedAt, unit.LastContact, now, unit.NextContactOrder, unit.Downlink && unit.SourceMessage > 0);
|
||||
// A first/new delivered sighting preempts routine patrol, including an in-flight patrol order.
|
||||
if (freshContact && unit.Downlink && unit.SourceMessage == 0) unit.Downlink = null;
|
||||
if (unit.Downlink || (!freshContact && now < unit.NextOrder)) return;
|
||||
unit.NextOrder = now + 30000;
|
||||
vector destination, angles;
|
||||
string reason = "road patrol between friendly objectives";
|
||||
unit.SourceMessage = 0;
|
||||
if (best)
|
||||
{
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 500)) 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
|
||||
@@ -128,6 +168,11 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
}
|
||||
unit.Destination = destination;
|
||||
unit.Downlink = hq.Downlink(unit.Group, "HUNTER", unit.Source + " " + reason + " destination=" + destination.ToString(), unit.SourceMessage);
|
||||
if (best)
|
||||
{
|
||||
unit.NextContactOrder = now + 5000;
|
||||
if (unit.Downlink) unit.LastContact = best.ObservedAt;
|
||||
}
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
@@ -135,18 +180,46 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
m_NextHunter = now + 1000;
|
||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq) return;
|
||||
m_Target = hq.Watch().HunterTarget();
|
||||
if (now >= m_NextSpawn && Deployed() < m_Target) { m_NextSpawn = now + 30000; StartHunter(hq, now); }
|
||||
if (!m_Opening)
|
||||
{
|
||||
array<IEntity> openingPlayers = {}; ERR_Actors.Players(openingPlayers);
|
||||
vector center = m_Mode.PrisonPosition(); vector sum; int count;
|
||||
foreach (IEntity player : openingPlayers) if (player) { sum += player.GetOrigin(); count++; }
|
||||
if (count > 0) center = sum / count;
|
||||
m_Opening = new ERR_HunterOpeningPlan(center);
|
||||
}
|
||||
if (!m_Opening.Done())
|
||||
{
|
||||
array<IEntity> currentPlayers = {}; ERR_Actors.Players(currentPlayers);
|
||||
m_Opening.Step(currentPlayers);
|
||||
}
|
||||
if (now >= m_NextSpawn && Deployed() < m_Target)
|
||||
{
|
||||
m_NextSpawn = now + 30000;
|
||||
if (Deployed() < 3) m_NextSpawn = now + 1000;
|
||||
StartHunter(hq, now);
|
||||
}
|
||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||
{
|
||||
if (unit.Released) continue;
|
||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) { unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + " player captured vehicle/crew; orders removed"); continue; }
|
||||
if (unit.Recovery && unit.Recovery.Lost(m_Mode))
|
||||
{
|
||||
string loss = unit.Recovery.Reason;
|
||||
unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + loss + "; surviving actors and vehicle retained"); continue;
|
||||
}
|
||||
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||
if (!unit.Registered)
|
||||
{
|
||||
if (AdmitHunter(unit, hq, now)) continue;
|
||||
if (now >= unit.Deadline) { InspectReadiness(unit, now); unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native readiness timed out"); }
|
||||
continue;
|
||||
}
|
||||
if (!unit.CrewValid(m_Mode)) { unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + " vehicle or original crew no longer operational; no replacement spawn"); continue; }
|
||||
// A temporary unconscious/dismounted driver suspends driving without
|
||||
// throwing away the delivered destination or the original physical crew.
|
||||
if (!unit.CrewValid(m_Mode)) continue;
|
||||
ObserveMovement(unit, now);
|
||||
QueueDestination(unit, hq, now);
|
||||
if (unit.Downlink)
|
||||
{
|
||||
int delivered = unit.Downlink.Advance(now, hq.Journal());
|
||||
@@ -157,14 +230,42 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
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); }
|
||||
else { ERR_HQ.Event("HUNTER ORDER FAILED", unit.Source + ": move waypoint could not be created"); unit.Downlink = null; continue; }
|
||||
ERR_HQ.Event("HUNTER ORDER", unit.Source + " radio delivered; destination=" + unit.Destination.ToString(), unit.SourceMessage);
|
||||
}
|
||||
unit.Downlink = null;
|
||||
}
|
||||
}
|
||||
QueueDestination(unit, hq, now);
|
||||
}
|
||||
}
|
||||
protected void ObserveMovement(ERR_HunterVehicle unit, float now)
|
||||
{
|
||||
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();
|
||||
bool travelling = unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
||||
if (!travelling || combat) unit.ProgressAt = now;
|
||||
if (now >= unit.NextMotionLog)
|
||||
{
|
||||
unit.NextMotionLog = now + 30000;
|
||||
float speed;
|
||||
if (unit.Entity.GetPhysics()) speed = unit.Entity.GetPhysics().GetVelocity().Length();
|
||||
vector deliveredDestination;
|
||||
if (unit.Waypoint) deliveredDestination = unit.Waypoint.GetOrigin();
|
||||
ERR_HQ.Event("HUNTER MOVEMENT", string.Format("%1 position=%2 speed=%3m/s delivered destination=%4 travelling=%5 combat=%6 no progress=%7s replans=%8", unit.Source, position, speed, deliveredDestination, travelling, combat, (now - unit.ProgressAt) / 1000, unit.Repaths));
|
||||
}
|
||||
if (!travelling || combat || now - unit.ProgressAt < 60000 || unit.Repaths >= 3) return;
|
||||
unit.ProgressAt = now; unit.Repaths++;
|
||||
// Destination can already contain a newer order awaiting radio delivery.
|
||||
// Retry only the waypoint the crew has actually received.
|
||||
vector retryDestination = unit.Waypoint.GetOrigin();
|
||||
unit.ClearOrder();
|
||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination));
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
|
||||
@@ -23,6 +23,7 @@ class ERR_MortarLogistics
|
||||
protected ERR_SiteRuntime m_Site;
|
||||
protected IEntity m_Cache;
|
||||
protected SCR_AIGroup m_Group;
|
||||
protected ERR_SiteRuntime m_CrewSite;
|
||||
protected ERR_MortarBattery m_Battery;
|
||||
protected IEntity m_OriginalGun;
|
||||
protected SCR_AIGroup m_OriginalBatteryGroup;
|
||||
@@ -88,6 +89,15 @@ class ERR_MortarLogistics
|
||||
foreach (IEntity item : items)
|
||||
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL) shells.Insert(item);
|
||||
}
|
||||
static ERR_SiteRuntime CourierSource(ERR_SiteRuntime site)
|
||||
{
|
||||
if (!site) return null;
|
||||
if (site.Responders) return site.Responders;
|
||||
// Historical sites explicitly authored a roaming patrol. A permanent-only
|
||||
// garrison is never silently substituted when a mobile team is unavailable.
|
||||
if (site.PatrolCycle) return site;
|
||||
return null;
|
||||
}
|
||||
bool Prepare()
|
||||
{
|
||||
if (!Replication.IsServer() || m_Started || m_Stopped) return false;
|
||||
@@ -98,8 +108,10 @@ class ERR_MortarLogistics
|
||||
// Production uses the existing guard outpost; no replacement car/cache is created.
|
||||
m_Site = scenario.Sites().Find("everon-supply-2");
|
||||
if (!m_Site) m_Site = scenario.Sites().Find("everon-outpost-1");
|
||||
if (!m_Site || !m_Site.Group || !m_Site.PatrolCycle || !scenario.Sites().Owns(m_Site.Container)) return false;
|
||||
m_Cache = m_Site.Container; m_Group = m_Site.Group;
|
||||
if (!m_Site || !scenario.Sites().Owns(m_Site.Container)) return false;
|
||||
m_CrewSite = CourierSource(m_Site);
|
||||
if (!m_CrewSite || !m_CrewSite.Group) { Print("Co10 Escape Reintroduced [mortar supplies]: source needs a mobile team; permanent defenders cannot be borrowed", LogLevel.ERROR); return false; }
|
||||
m_Cache = m_Site.Container; m_Group = m_CrewSite.Group;
|
||||
array<ref ERR_MortarBattery> batteries = {}; m_HQ.Mortars().Batteries(batteries);
|
||||
if (batteries.IsEmpty()) return false;
|
||||
m_Battery = batteries[0]; m_OriginalGun = m_Battery.Gun; m_OriginalBatteryGroup = m_Battery.Group;
|
||||
@@ -112,7 +124,7 @@ class ERR_MortarLogistics
|
||||
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||
m_OriginalMembers.Insert(agent.GetControlledEntity());
|
||||
}
|
||||
if (m_OriginalMembers.Count() != m_Site.InitialGuardCount) return false;
|
||||
if (m_OriginalMembers.Count() != m_CrewSite.InitialGuardCount) return false;
|
||||
m_InitialDeadline = GetGame().GetWorld().GetWorldTime() + 15000;
|
||||
// SiteService owns this existing cache and its contents through rollback/world teardown.
|
||||
// This bounded startup allocation is never reopened, including partial failure/theft.
|
||||
@@ -151,7 +163,7 @@ class ERR_MortarLogistics
|
||||
}
|
||||
protected bool OriginalGroup()
|
||||
{
|
||||
if (!m_Site || m_Site.Group != m_Group || m_Site.Container != m_Cache || !m_Group || !m_Cache || m_Group.GetPlayerCount(true) > 0 || m_Group.GetFaction() != m_Mode.EnemyFaction()) return false;
|
||||
if (!m_Site || CourierSource(m_Site) != m_CrewSite || !m_CrewSite || m_CrewSite.Group != m_Group || m_Site.Container != m_Cache || !m_Group || !m_Cache || m_Group.GetPlayerCount(true) > 0 || m_Group.GetFaction() != m_Mode.EnemyFaction()) return false;
|
||||
array<AIAgent> agents = {}; m_Group.GetAgents(agents); bool living;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@ class ERR_MortarReadiness
|
||||
protected ERR_MortarBattery m_Battery;
|
||||
protected ref array<IEntity> m_Members = {};
|
||||
protected ref array<ref ERR_MortarInitialItem> m_Operations = {};
|
||||
protected float m_StartedAt = -1;
|
||||
protected bool m_Allocated;
|
||||
protected bool m_Ready;
|
||||
protected bool m_Stopped;
|
||||
@@ -106,7 +107,10 @@ class ERR_MortarReadiness
|
||||
if (!mode || !group || !m_Battery.Gun || group.GetFaction() != mode.EnemyFaction() || group.GetPlayerCount(true) > 0) return Fail("initial support group/gun/faction changed");
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Battery.Gun.FindComponent(DamageManagerComponent));
|
||||
if (!damage || damage.IsDestroyed()) return Fail("initial support gun missing damage manager or destroyed");
|
||||
bool expired = now < m_Battery.SpawnedAt || now - m_Battery.SpawnedAt >= 90000;
|
||||
// HQ is paused in prison. Time before the first eligible initialization poll
|
||||
// must not consume the finite native expansion/inventory deadline.
|
||||
if (m_StartedAt < 0) m_StartedAt = now;
|
||||
bool expired = now < m_StartedAt || now - m_StartedAt >= 90000;
|
||||
array<AIAgent> agents = {}; group.GetAgents(agents);
|
||||
array<IEntity> physical = {};
|
||||
int gunners, operators;
|
||||
|
||||
@@ -354,7 +354,11 @@ class ERR_MortarSystem
|
||||
if (battery.StartupActivated && battery.Operational()) ready++;
|
||||
shells += battery.CountShells();
|
||||
}
|
||||
return string.Format("mortars %1/%2; HE shells %3", ready, m_Batteries.Count(), shells);
|
||||
string text = string.Format("Mortars: %1 of %2 operational; %3 HE shells in inventory.", ready, m_Batteries.Count(), shells);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.Phase() <= ERR_Phase.PRISON) text += " Crews activate after the prison gate is unlocked.";
|
||||
else if (ready < m_Batteries.Count()) text += " Other batteries are still preparing or unavailable; check the journal.";
|
||||
return text;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ modded class ERR_GameMode
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "mortar-station") return;
|
||||
m_ERRStationDeadline = System.GetTickCount() + 180000;
|
||||
m_ERRStationDeadline = System.GetTickCount() + 300000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_StationProbeTick, 500, true);
|
||||
}
|
||||
protected bool ERR_StationCheck(bool condition, string detail)
|
||||
@@ -35,6 +35,7 @@ modded class ERR_GameMode
|
||||
float now = GetWorld().GetWorldTime();
|
||||
if (m_ERRStationStage == 0)
|
||||
{
|
||||
if (now < 110000) return; // Reproduce a real session spending over 90 seconds in prison.
|
||||
array<ref ERR_MortarBattery> batteries = {}; ERR_MortarSystem.Get().Batteries(batteries);
|
||||
if (batteries.IsEmpty()) return;
|
||||
m_ERRStationBattery = batteries[0];
|
||||
@@ -45,6 +46,12 @@ modded class ERR_GameMode
|
||||
ERR_MortarStation station = m_ERRStationBattery.ERR_StationProbe();
|
||||
if (m_ERRStationStage == 1)
|
||||
{
|
||||
array<ref ERR_MortarBattery> initial = {}; ERR_MortarSystem.Get().Batteries(initial);
|
||||
foreach (ERR_MortarBattery battery : initial)
|
||||
{
|
||||
if (!ERR_StationCheck(battery.SpawnFailure.IsEmpty(), "delayed prison release preserves mortar initialization deadline")) return;
|
||||
if (!battery.StartupActivated) return;
|
||||
}
|
||||
if (!m_ERRStationBattery.StartupActivated || !station) return;
|
||||
m_ERRStationGunner = station.Gunner();
|
||||
if (!m_ERRStationGunner || CompartmentAccessComponent.GetVehicleIn(m_ERRStationGunner) != m_ERRStationBattery.Gun) return;
|
||||
|
||||
@@ -35,7 +35,7 @@ class ERR_NativeScoutProbe
|
||||
if (matches < 1 || matches != post.Magazines) return false;
|
||||
for (int other = 0; other < index; other++)
|
||||
if (vector.DistanceXZ(post.Position, scouts.At(other).Position) < 900) return false;
|
||||
PrintFormat("Escapists [scout probe]: PASS post=%1 height=%2 full magazines=%3 physical manpack, return fire, reserved", index + 1, post.Position[1], matches);
|
||||
PrintFormat("Co10 Escape Reintroduced [scout probe]: PASS post=%1 height=%2 full magazines=%3 physical manpack, return fire, reserved", index + 1, post.Position[1], matches);
|
||||
}
|
||||
ERR_HQReport report = new ERR_HQReport(); report.RoadStatus = "on road";
|
||||
ERR_HQReport snapshot = report.Snapshot(); report.RoadStatus = "off road";
|
||||
@@ -47,7 +47,7 @@ class ERR_NativeScoutProbe
|
||||
if (!after.HasVelocity || after.Velocity != "1 0 0" || after.TravelDirection() != "E") return false;
|
||||
after.ObservedAt = 42000; ERR_ScoutMotion.Estimate(after, before);
|
||||
if (after.HasVelocity) return false;
|
||||
Print("Escapists [scout probe]: PASS observed motion, direction, stale-sighting refusal and immutable road snapshot");
|
||||
Print("Co10 Escape Reintroduced [scout probe]: PASS observed motion, direction, stale-sighting refusal and immutable road snapshot");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_NativeTownService : ERR_TownGarrisons
|
||||
{
|
||||
ref array<vector> Blockers = {};
|
||||
ERR_TownGarrison Selected;
|
||||
vector Approach;
|
||||
protected override void Players(out array<vector> positions)
|
||||
{
|
||||
super.Players(positions);
|
||||
foreach (vector blocker : Blockers) positions.Insert(blocker);
|
||||
}
|
||||
bool Select(int count)
|
||||
{
|
||||
if (m_Towns.IsEmpty())
|
||||
{
|
||||
vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(low - "0 5000 0", high + "0 5000 0", Remember, Settlement, EQueryEntitiesFlags.ALL);
|
||||
}
|
||||
foreach (ERR_TownGarrison town : m_Towns)
|
||||
{
|
||||
if (town.Finished) continue;
|
||||
for (int i = 0; i < 24; i++)
|
||||
{
|
||||
float angle = i * 15 * Math.DEG2RAD;
|
||||
vector point = ERR_Actors.Ground(town.Centre + Vector(Math.Cos(angle) * 690, 0, Math.Sin(angle) * 690));
|
||||
if (!m_Placement.Clear(point, null)) continue;
|
||||
Selected = town; town.Count = count; Approach = point; return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Selected || now < m_Next) return;
|
||||
m_Next = now + 2000;
|
||||
array<vector> players = {}; Players(players);
|
||||
Attempt(Selected, players, now);
|
||||
}
|
||||
bool BirthsSafe()
|
||||
{
|
||||
if (!Selected || !Selected.Admitted || m_Mode.ForceLiving(Selected.Group) != Selected.Count) return false;
|
||||
array<vector> players = {}; Players(players);
|
||||
array<AIAgent> agents = {}; Selected.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !agent.GetControlledEntity() || !ERR_TownSpawnRules.Safe(agent.GetControlledEntity().GetOrigin(), players)) return false;
|
||||
return true;
|
||||
}
|
||||
void ClearSquad()
|
||||
{
|
||||
ERR_Actors.DeleteSpawned(Selected.Group); Selected.Group = null;
|
||||
}
|
||||
}
|
||||
modded class ERR_HQ
|
||||
{
|
||||
void ERR_ProbeTowns(ERR_TownGarrisons service) { m_Towns.Stop(); m_Towns = service; }
|
||||
bool ERR_ProbeTownLinked(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == group) return !unit.HomeGuard && !unit.ReservedForSupport && unit.Eligible(m_Mode) && unit.StatusSampler.Bound();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected ref ERR_NativeTownService m_ERRTownProbe;
|
||||
protected int m_ERRTownStage, m_ERRTownDeadline;
|
||||
protected float m_ERRTownWait;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "town-garrisons") return;
|
||||
m_ERRTownDeadline = System.GetTickCount() + 240000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_TownProbeTick, 250, true);
|
||||
}
|
||||
protected bool ERR_TownCheck(bool ok, string message)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: towns " + message); return true; }
|
||||
Print("ERR SMOKE FAIL: towns " + message, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_TownProbeTick); return false;
|
||||
}
|
||||
protected void ERR_TownProbeTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERRTownDeadline || !PreparationError().IsEmpty()) { ERR_TownCheck(false, "startup/test deadline: " + PreparationError()); return; }
|
||||
if (!Active() || !GetGame().GetPlayerController()) return;
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity();
|
||||
if (!player) return;
|
||||
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
if (now < m_ERRTownWait) return;
|
||||
if (m_ERRTownStage == 0)
|
||||
{
|
||||
m_ERRTownProbe = new ERR_NativeTownService(this); hq.ERR_ProbeTowns(m_ERRTownProbe);
|
||||
if (!ERR_TownCheck(m_ERRTownProbe.Select(3), "found native named settlement with clear approach")) return;
|
||||
if (!ERR_TownCheck(UnlockPrison(player), "normal prison release")) return;
|
||||
player.SetOrigin(m_ERRTownProbe.Approach);
|
||||
// A second teammate inside the town must veto the entire spawn, even
|
||||
// while the approaching player satisfies the activation trigger.
|
||||
m_ERRTownProbe.Blockers.Insert(m_ERRTownProbe.Selected.Centre);
|
||||
m_ERRTownWait = now + 6000; m_ERRTownStage = 1; return;
|
||||
}
|
||||
if (m_ERRTownStage == 1)
|
||||
{
|
||||
if (!ERR_TownCheck(!m_ERRTownProbe.Selected.Admitted && (!m_ERRTownProbe.Selected.Group || m_ERRTownProbe.Selected.Group.GetAgentsCount() == 0), "nearby second teammate blocks all native character creation")) return;
|
||||
m_ERRTownProbe.Blockers.Clear(); m_ERRTownStage = 2; return;
|
||||
}
|
||||
if (m_ERRTownStage == 2 || m_ERRTownStage == 4)
|
||||
{
|
||||
if (!m_ERRTownProbe.Selected.Admitted) return;
|
||||
if (!ERR_TownCheck(m_ERRTownProbe.BirthsSafe(), string.Format("%1 physical soldiers all at least 700 m from player", m_ERRTownProbe.Selected.Count))) return;
|
||||
if (!ERR_TownCheck(hq.ERR_ProbeTownLinked(m_ERRTownProbe.Selected.Group), "squad bound to VirtualHQ reporting and available for hunt orders")) return;
|
||||
if (!ERR_TownCheck(m_ERRTownProbe.Selected.Group.m_aUnitPrefabSlots.IsEmpty(), "no native expansion slots or delayed replacement soldiers")) return;
|
||||
m_ERRTownProbe.ClearSquad(); m_ERRTownWait = now + 5000; m_ERRTownStage++; return;
|
||||
}
|
||||
if (m_ERRTownStage == 3)
|
||||
{
|
||||
if (!ERR_TownCheck(!m_ERRTownProbe.Selected.Group && m_ERRTownProbe.Selected.Finished, "eliminated three-man squad is not replaced")) return;
|
||||
if (!ERR_TownCheck(m_ERRTownProbe.Select(10), "second distinct native town selected")) return;
|
||||
player.SetOrigin(m_ERRTownProbe.Approach); m_ERRTownStage = 4; return;
|
||||
}
|
||||
if (!ERR_TownCheck(!m_ERRTownProbe.Selected.Group && m_ERRTownProbe.Selected.Finished, "eliminated ten-man squad is not replaced")) return;
|
||||
m_ERRTownProbe.Stop();
|
||||
GetGame().GetCallqueue().Remove(ERR_TownProbeTick);
|
||||
Print("ERR SMOKE COMPLETE: native towns, 3 and 10 soldiers, split-player exclusion, HQ integration and no replacement waves");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -24,7 +24,7 @@ class ERR_ObjectiveDefence
|
||||
static bool Objective(int kind)
|
||||
{
|
||||
return kind == ERR_LocationKind.RADIO || kind == ERR_LocationKind.DEPOT
|
||||
|| kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.EXTRACTION;
|
||||
|| kind == ERR_LocationKind.HOSPITAL || kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.EXTRACTION;
|
||||
}
|
||||
protected void Initialize()
|
||||
{
|
||||
|
||||
@@ -40,9 +40,9 @@ class ERR_ReconAirports
|
||||
m_bComplete = m_vCursor[2] >= m_vMax[2];
|
||||
if (m_bComplete)
|
||||
{
|
||||
PrintFormat("Escapists AI [air recon]: airport search finished; airports=%1", m_Positions.Count());
|
||||
PrintFormat("Co10 Escape Reintroduced [AI / air recon]: airport search finished; airports=%1", m_Positions.Count());
|
||||
for (int airportIndex = 0; airportIndex < m_Positions.Count(); airportIndex++)
|
||||
PrintFormat("Escapists AI [air recon]: airport=%1 centre=%2 runway/tower samples=%3", airportIndex, m_Positions[airportIndex], m_PositionSamples[airportIndex]);
|
||||
PrintFormat("Co10 Escape Reintroduced [AI / air recon]: airport=%1 centre=%2 runway/tower samples=%3", airportIndex, m_Positions[airportIndex], m_PositionSamples[airportIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class ERR_ReconAirports
|
||||
}
|
||||
m_Positions.Insert(entity.GetOrigin());
|
||||
m_PositionSamples.Insert(1);
|
||||
PrintFormat("Escapists AI [air recon]: airport structure=%1 position=%2", entity.GetPrefabData().GetPrefabName(), entity.GetOrigin());
|
||||
PrintFormat("Co10 Escape Reintroduced [AI / air recon]: airport structure=%1 position=%2", entity.GetPrefabData().GetPrefabName(), entity.GetOrigin());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class ERR_ScoutSystem
|
||||
if (ERR_ScoutTerrain.Separated(point, m_Posts, m_Definition) && ERR_ScoutTerrain.Survey(point, m_IslandHigh, heading))
|
||||
{
|
||||
ERR_ScoutPost post = new ERR_ScoutPost(); post.Position = point; post.Heading = heading; m_Posts.Insert(post);
|
||||
PrintFormat("Escapists [scout survey]: post=%1 position=%2 heading=%3", m_Posts.Count(), point, heading);
|
||||
PrintFormat("Co10 Escape Reintroduced [scout survey]: post=%1 position=%2 heading=%3", m_Posts.Count(), point, heading);
|
||||
}
|
||||
if (System.GetTickCount() - tick >= 6) return ERR_PrepareResult.PENDING;
|
||||
}
|
||||
@@ -218,7 +218,7 @@ class ERR_ScoutSystem
|
||||
record.Pinned = true;
|
||||
m_Mode.RegisterGroup(post.Group);
|
||||
post.Ready = true;
|
||||
PrintFormat("Escapists [scout ready]: position=%1 backpack magazines=%2", post.Position, post.Magazines);
|
||||
PrintFormat("Co10 Escape Reintroduced [scout ready]: position=%1 backpack magazines=%2", post.Position, post.Magazines);
|
||||
return ERR_PrepareResult.READY;
|
||||
}
|
||||
ERR_PrepareResult Prepare(ERR_ScenarioDefinition definition)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
// Town activation is only a population trigger, never an observation for HQ.
|
||||
class ERR_TownSpawnRules
|
||||
{
|
||||
static const float DISTANCE = 700;
|
||||
static const float TOWN_RADIUS = 200;
|
||||
static bool Safe(vector position, array<vector> players, float minimum = 700)
|
||||
{
|
||||
if (!players || players.IsEmpty() || !ERR_ForceRules.FiniteVector(position)) return false;
|
||||
foreach (vector player : players)
|
||||
if (!ERR_ForceRules.FiniteVector(player) || vector.DistanceXZ(position, player) < minimum) return false;
|
||||
return true;
|
||||
}
|
||||
static bool Approached(vector centre, array<vector> players)
|
||||
{
|
||||
if (!players || !ERR_ForceRules.FiniteVector(centre)) return false;
|
||||
foreach (vector player : players)
|
||||
if (ERR_ForceRules.FiniteVector(player) && vector.DistanceXZ(centre, player) <= DISTANCE) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_TownGarrison
|
||||
{
|
||||
vector Centre;
|
||||
string Id;
|
||||
string Name;
|
||||
int Count;
|
||||
bool Finished;
|
||||
bool Admitted;
|
||||
float NextLog;
|
||||
SCR_AIGroup Group;
|
||||
AIWaypoint Hold;
|
||||
}
|
||||
|
||||
// One finite squad per named settlement per round. Empty native groups have no
|
||||
// queued slots: each character is created synchronously after a fresh distance
|
||||
// check, so native expansion cannot introduce late soldiers beside a player.
|
||||
class ERR_TownGarrisons
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref array<ref ERR_TownGarrison> m_Towns = {};
|
||||
protected ref ERR_SpawnPlacement m_Placement = new ERR_SpawnPlacement();
|
||||
protected float m_Next;
|
||||
protected int m_Cursor;
|
||||
protected bool m_Stopped;
|
||||
void ERR_TownGarrisons(ERR_GameMode mode) { m_Mode = mode; }
|
||||
|
||||
protected void Players(out array<vector> positions)
|
||||
{
|
||||
positions.Clear();
|
||||
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
// Include unconscious and dead controlled bodies: do not spawn beside a
|
||||
// teammate just because that teammate temporarily cannot fight.
|
||||
if (actor) positions.Insert(actor.GetOrigin());
|
||||
}
|
||||
}
|
||||
protected bool Settlement(IEntity entity)
|
||||
{
|
||||
if (!entity) return false;
|
||||
MapDescriptorComponent descriptor = MapDescriptorComponent.Cast(entity.FindComponent(MapDescriptorComponent));
|
||||
if (!descriptor) return false;
|
||||
int type = descriptor.GetBaseType();
|
||||
return type == EMapDescriptorType.MDT_NAME_CITY || type == EMapDescriptorType.MDT_NAME_TOWN
|
||||
|| type == EMapDescriptorType.MDT_NAME_VILLAGE || type == EMapDescriptorType.MDT_NAME_SETTLEMENT;
|
||||
}
|
||||
protected bool Remember(IEntity entity)
|
||||
{
|
||||
vector centre = entity.GetOrigin();
|
||||
foreach (ERR_TownGarrison known : m_Towns)
|
||||
if (vector.DistanceXZ(known.Centre, centre) < 50) return true;
|
||||
if (m_Towns.Count() >= 256) return false;
|
||||
ERR_TownGarrison town = new ERR_TownGarrison();
|
||||
town.Centre = centre; town.Count = Math.RandomIntInclusive(3, 10);
|
||||
town.Id = "town-" + m_Towns.Count().ToString();
|
||||
town.Name = entity.GetName(); if (town.Name.IsEmpty()) town.Name = town.Id;
|
||||
m_Towns.Insert(town);
|
||||
return true;
|
||||
}
|
||||
protected void Discover(array<vector> players)
|
||||
{
|
||||
foreach (vector player : players)
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(player - "700 5000 700", player + "700 5000 700", Remember, Settlement, EQueryEntitiesFlags.ALL);
|
||||
}
|
||||
bool Contains(SCR_AIGroup group)
|
||||
{
|
||||
if (!group) return false;
|
||||
foreach (ERR_TownGarrison town : m_Towns) if (town.Group == group && town.Admitted) return true;
|
||||
return false;
|
||||
}
|
||||
protected void Waiting(ERR_TownGarrison town, float now, string reason)
|
||||
{
|
||||
if (now < town.NextLog) return;
|
||||
town.NextLog = now + 60000;
|
||||
ERR_HQ.Event("TOWN GARRISON", string.Format("%1 at %2: waiting to place %3 soldiers; %4. No nearby soldiers spawned.", town.Name, town.Centre, town.Count, reason));
|
||||
}
|
||||
protected bool Plan(ERR_TownGarrison town, array<vector> players, out array<vector> positions)
|
||||
{
|
||||
positions.Clear();
|
||||
AIPathfindingComponent path = AIPathfindingComponent.Cast(town.Group.FindComponent(AIPathfindingComponent));
|
||||
if (!path) return false;
|
||||
NavmeshWorldComponent navmesh = path.GetNavmeshComponent();
|
||||
int first = Math.RandomInt(0, 24);
|
||||
for (int ring = 0; ring < 6; ring++)
|
||||
for (int direction = 0; direction < 24; direction++)
|
||||
{
|
||||
float angle = ((first + direction) * 15) * Math.DEG2RAD;
|
||||
float radius = Math.Min(ring * 40, 185);
|
||||
vector anchor = ERR_Actors.Ground(town.Centre + Vector(Math.Cos(angle) * radius, 0, Math.Sin(angle) * radius));
|
||||
if (!ERR_TownSpawnRules.Safe(anchor, players, 710)) continue;
|
||||
if (navmesh && !navmesh.IsTileLoaded(anchor)) { navmesh.LoadTileIn(anchor); continue; }
|
||||
positions.Clear();
|
||||
map<int, vector> reserved = new map<int, vector>();
|
||||
for (int slot = 0; slot < town.Count; slot++)
|
||||
{
|
||||
int column = slot % 4;
|
||||
int row = slot / 4;
|
||||
vector point = ERR_Actors.Ground(anchor + Vector(column * 3 - 4.5, 0, row * 3 - 3));
|
||||
vector navigable;
|
||||
if (!path.GetClosestPositionOnNavmesh(point, "3 3 3", navigable)) break;
|
||||
point = ERR_Actors.Ground(navigable);
|
||||
if (vector.DistanceXZ(point, town.Centre) > ERR_TownSpawnRules.TOWN_RADIUS || !ERR_TownSpawnRules.Safe(point, players, 702) || !m_Placement.Clear(point, reserved)) break;
|
||||
positions.Insert(point); reserved.Set(slot, point);
|
||||
}
|
||||
if (positions.Count() == town.Count) return true;
|
||||
}
|
||||
positions.Clear(); return false;
|
||||
}
|
||||
protected void Attempt(ERR_TownGarrison town, array<vector> players, float now)
|
||||
{
|
||||
if (town.Finished || !ERR_TownSpawnRules.Approached(town.Centre, players)) return;
|
||||
if (m_Mode.Forces().Count() >= 250) { Waiting(town, now, "mission force budget is full"); return; }
|
||||
if (!town.Group)
|
||||
{
|
||||
town.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", town.Centre));
|
||||
if (!town.Group) { town.Finished = true; return; }
|
||||
m_Mode.Own(town.Group);
|
||||
town.Group.SetFaction(m_Mode.EnemyFaction());
|
||||
town.Group.SetSpawnImmediately(false); town.Group.SetDeleteWhenEmpty(false);
|
||||
town.Group.SetPermanentLOD(0);
|
||||
}
|
||||
array<vector> positions = {};
|
||||
if (!Plan(town, players, positions)) { Waiting(town, now, "no clear navigable town position at least 700 m from every player"); return; }
|
||||
Resource profile = Resource.Load(m_Mode.FactionSelection().Choice("enemy").Group);
|
||||
array<ResourceName> roster = {};
|
||||
IEntitySource source;
|
||||
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
|
||||
if (!source || !source.Get("m_aUnitPrefabSlots", roster) || roster.IsEmpty()) { Fail(town, "enemy faction has no infantry roster"); return; }
|
||||
// Load every resource before the final checks, not part-way through creation.
|
||||
array<ref Resource> loaded = {};
|
||||
foreach (ResourceName prefab : roster)
|
||||
{
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) { Fail(town, "enemy soldier prefab could not be loaded"); return; }
|
||||
loaded.Insert(resource);
|
||||
}
|
||||
Players(players);
|
||||
foreach (vector planned : positions)
|
||||
if (!ERR_TownSpawnRules.Safe(planned, players)) { Waiting(town, now, "a player approached during preparation"); return; }
|
||||
town.Finished = true;
|
||||
float nearest = 1e20;
|
||||
for (int index = 0; index < positions.Count(); index++)
|
||||
{
|
||||
Players(players);
|
||||
if (!ERR_TownSpawnRules.Safe(positions[index], players)) { Fail(town, "player entered the exclusion area during creation"); return; }
|
||||
IEntity actor = ERR_Actors.Spawn(roster[index % roster.Count()], positions[index]);
|
||||
if (!actor) { Fail(town, "native soldier creation failed"); return; }
|
||||
m_Mode.Own(actor);
|
||||
if (!ERR_TownSpawnRules.Safe(actor.GetOrigin(), players) || !ERR_Actors.SetFaction(actor, m_Mode.EnemyFaction()) || !town.Group.AddAIEntityToGroup(actor))
|
||||
{ ERR_Actors.DeleteSpawned(actor); Fail(town, "native soldier placement or group admission failed"); return; }
|
||||
foreach (vector observer : players) nearest = Math.Min(nearest, vector.DistanceXZ(actor.GetOrigin(), observer));
|
||||
}
|
||||
town.Group.SetOrigin(positions[0]);
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(town.Id, ERR_ForceKind.GARRISON, town.Centre, town.Count, null, town.Group, now);
|
||||
if (!record) { Fail(town, "force ledger refused the squad"); return; }
|
||||
town.Admitted = true;
|
||||
town.Hold = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), positions[0]));
|
||||
if (town.Hold) { m_Mode.Own(town.Hold); town.Hold.SetCompletionRadius(60); town.Group.AddWaypoint(town.Hold); }
|
||||
m_Mode.RegisterGroup(town.Group); town.Group.ActivateAI();
|
||||
ERR_HQ.Event("TOWN GARRISON", string.Format("%1 at %2: spawned %3 enemy soldiers; nearest player at creation %4 m. All report to VirtualHQ and are available for orders. One squad this round; no replacements.", town.Name, town.Centre, town.Count, Math.Round(nearest)));
|
||||
}
|
||||
protected void Fail(ERR_TownGarrison town, string reason)
|
||||
{
|
||||
town.Finished = true;
|
||||
ERR_Actors.DeleteSpawned(town.Group); town.Group = null;
|
||||
ERR_HQ.Event("TOWN GARRISON", town.Name + ": creation cancelled: " + reason + ". No replacement wave.");
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode.Active() || m_Mode.Phase() <= ERR_Phase.PRISON || now < m_Next) return;
|
||||
m_Next = now + 2000;
|
||||
array<vector> players = {}; Players(players);
|
||||
if (players.IsEmpty()) return;
|
||||
Discover(players);
|
||||
// At most one finite squad per update; round-robin avoids a blocked town
|
||||
// preventing other settlements from activating for separated teammates.
|
||||
for (int checked = 0; checked < m_Towns.Count(); checked++)
|
||||
{
|
||||
m_Cursor = m_Cursor % m_Towns.Count();
|
||||
ERR_TownGarrison town = m_Towns[m_Cursor++];
|
||||
if (town.Finished || !ERR_TownSpawnRules.Approached(town.Centre, players)) continue;
|
||||
Attempt(town, players, now); break;
|
||||
}
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
foreach (ERR_TownGarrison town : m_Towns)
|
||||
{
|
||||
if (town.Hold) { if (town.Group) town.Group.RemoveWaypoint(town.Hold); ERR_Actors.DeleteSpawned(town.Hold); town.Hold = null; }
|
||||
// Admitted units remain under normal mission/body ownership until teardown.
|
||||
if (!town.Admitted) { ERR_Actors.DeleteSpawned(town.Group); town.Group = null; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ class ERR_IntelDiscoveryText
|
||||
static string Type(int kind, string id)
|
||||
{
|
||||
if (kind == ERR_LocationKind.RADIO) return "radio station";
|
||||
if (kind == ERR_LocationKind.HOSPITAL) return "field hospital";
|
||||
if (kind == ERR_LocationKind.OUTPOST) return "outpost";
|
||||
if (kind == ERR_LocationKind.MORTAR) return "mortar site";
|
||||
if (id.Contains("medical")) return "medical stash";
|
||||
|
||||
Reference in New Issue
Block a user