Author reusable mission sites with finite scavenging stock

This commit is contained in:
2026-09-19 02:40:52 +02:00
parent 92fd23f274
commit 35daefa860
96 changed files with 10626 additions and 130 deletions
@@ -58,7 +58,7 @@ modded class ERR_HQ
unit.Group.GetAgents(agents);
if (agents.Count() != 6) return false;
}
return infantry == 3;
return infantry == 4;
}
}
@@ -74,7 +74,7 @@ class ERR_NativeRadioProbe
if (m_Step == 0)
{
if (!hq.ERR_ProbeSquadsReady()) return 0;
Print("ERR SMOKE PASS: three physical six-person squads each have a manpack operator");
Print("ERR SMOKE PASS: four physical six-person squads each have a manpack operator");
m_Route = hq.ERR_ProbeRoute();
if (!m_Route || m_Route.Nodes.IsEmpty()) return -1;
m_Carrier = m_Route.Nodes[0].Carrier;
@@ -176,7 +176,7 @@ modded class ERR_GameMode
if (!ERR_Check(Phase() == ERR_Phase.RADIO_SEARCH, "lock action advances mission")) return;
foreach (IEntity tracked : m_OwnedEntities)
{
if (tracked && tracked.GetPrefabData() && tracked.GetPrefabData().GetPrefabName() == ERR_FactionCatalog.IntelFolder()) m_ERRCacheFolders.Insert(tracked);
if (tracked && tracked.GetPrefabData() && tracked.GetPrefabData().GetPrefabName() == ERR_FactionCatalog.ObjectiveDocument()) m_ERRCacheFolders.Insert(tracked);
}
if (!ERR_Check(m_ERRCacheFolders.Count() == 2, "two guaranteed physical cache documents")) return;
if (!ERR_Check(m_Settings.Set("aiIntelChancePercent", 0), "test does not rely on random intel")) return;
@@ -186,8 +186,11 @@ modded class ERR_GameMode
else if (m_ERRSmokeStep == 1 || m_ERRSmokeStep == 2)
{
if (m_ERRSmokeStep == 2 && !ERR_Check(m_ERRGate && m_ERRGate.IsOpen() && m_ERRGate.CanCharacterPass(0.6), "unlocked gate physically permits passage")) return;
ERR_MovePlayer(Sites().Find("everon-supply-1").Container.GetOrigin() + "0 0 2");
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_InventoryStorageManagerComponent));
if (!ERR_Check(inventory && inventory.TryInsertItem(m_ERRCacheFolders[m_ERRSmokeStep - 1]), "authored cache document recovered into inventory")) return;
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(Sites().Find("everon-supply-1").Container.FindComponent(InventoryStorageManagerComponent));
BaseInventoryStorageComponent destination = inventory.FindStorageForItem(m_ERRCacheFolders[m_ERRSmokeStep - 1]);
if (!ERR_Check(destination && source.TryMoveItemToStorage(m_ERRCacheFolders[m_ERRSmokeStep - 1], destination), "authored cache document recovered into inventory")) return;
m_ERRSmokeStep++;
m_ERRSmokeWait = now + 2500;
}
+52
View File
@@ -0,0 +1,52 @@
ERR_ScenarioDefinition {
Sites {
ERR_SiteDefinition {
Id "everon-prison-1"
Kind 0
Prefab "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
Position 6090 0 7160
Guards "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
GuardOffset 35 0 0
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
ContainerOffset -4 0 0
Stock {
ERR_SiteStock { Prefab "{C0F7DD85A86B2900}Prefabs/Weapons/Handguns/PM/Handgun_PM.et" Count 5 }
ERR_SiteStock { Prefab "{8B853CDD11BA916E}Prefabs/Weapons/Magazines/Magazine_9x18_PM_8rnd_Ball.et" Count 8 }
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
}
}
ERR_SiteDefinition {
Id "everon-supply-1"
Kind 4
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
Position 6150 0 7050
Guards "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
GuardOffset 18 0 0
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
ContainerOffset 0 0 0
Stock {
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 8 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 16 }
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
}
RevealOnEscape 1
}
ERR_SiteDefinition {
Id "everon-radio-1"
Kind 1
Prefab "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
Position 6500 0 7200
Guards "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
GuardOffset 30 0 0
}
ERR_SiteDefinition {
Id "everon-extraction-1"
Kind 2
Prefab "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
Position 7000 0 7500
Guards "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
GuardOffset 30 0 0
}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
GenericEntity {
components {
ERR_LocationComponent "{C17CA14398A1CD7E}" { m_Id "everon-extraction-1" m_Kind 2 }
ERR_LocationComponent "{C17CA14398A1CD7E}" { m_Kind 2 }
RplComponent "{0BF5BAAA5FBFEECE}" { Streamable Disabled }
}
}
+1 -3
View File
@@ -4,9 +4,7 @@ ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
m_GameOverScreenConfig SCR_GameOverScreenConfig "{56DEC6462FFDD82D}" : "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf" {}
}
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
m_Prison "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
m_Radio "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
m_Extraction "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
m_Definition "{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"
}
SCR_RespawnSystemComponent "{56B2B4793051E7C9}" {
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
@@ -0,0 +1,5 @@
GenericEntity : "{6D56FED1E55A8F84}Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et" {
components {
ERR_DocumentComponent "{E4598D0BE2C251A3}" {}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
+1
View File
@@ -1,5 +1,6 @@
GenericEntity {
components {
ERR_LocationComponent "{3B60CBEB3C04333C}" { m_Kind 0 }
Hierarchy "{30ADADE77D1EC3B8}" {}
RplComponent "{7FA5168C6FBD0C22}" {}
}
+1 -1
View File
@@ -2,7 +2,7 @@ GenericEntity {
components {
MeshObject "{111C06CB740ED195}" { Object "{77C9FCF3FB1246E9}Assets/Items/Equipment/Radios/Radio_R107M/Radio_R107M_01_item.xob" }
RigidBody "{4C174A44D07B91BA}" { ModelGeometry 1 Static 1 }
ERR_LocationComponent "{C19CA6CC83470879}" { m_Id "everon-radio-1" m_Kind 1 }
ERR_LocationComponent "{C19CA6CC83470879}" { m_Kind 1 }
RplComponent "{54ED7463161A5622}" { Streamable Disabled }
ActionsManagerComponent "{B081EC0768636EAB}" {
ActionContexts { UserActionContext "{C1A75A5D8B7EF1E4}" { ContextName "ERR_Radio" Radius 2 } }
+28
View File
@@ -0,0 +1,28 @@
GenericEntity : "{EBBC859CCD94D96D}Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et" {
components {
MeshObject "{6530845A09309E37}" {
Object "{819ED3266EE5113B}Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/EquipmentBoxWooden_Weapon_01.xob"
Materials {
MaterialAssignClass "{6530845A093091BB}" {
SourceMaterial "EquipmentBoxWooden_Weapon_01"
AssignedMaterial "{7D2E831AFA5CF50D}Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/Data/EquipmentBoxWooden_Weapon_01_USSR.emat"
}
MaterialAssignClass "{6530845A093091B2}" {
SourceMaterial "EquipmentBoxWooden_Weapon_01_Decal"
}
}
}
SCR_UniversalInventoryStorageComponent "{6530845A09302DE0}" {
MaxCumulativeVolume 120000
MaxItemSize 120 40 40
m_fMaxWeight 80
Attributes SCR_ItemAttributeCollection "{6530845A09302DFF}" {
ItemDisplayName SCR_InventoryUIInfo "{6530845A09302D03}" {
Name "Supply cache"
Description "Limited equipment shared between prisoners."
}
}
}
InventoryStorageManagerComponent "{18A1D42A468F584F}" {}
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
+6
View File
@@ -0,0 +1,6 @@
GenericEntity {
components {
ERR_LocationComponent "{8E7CFEA92C1811F0}" { m_Kind 4 }
RplComponent "{98A4A31435A9CA04}" { Streamable Disabled }
}
}
@@ -0,0 +1,9 @@
MetaFileClass {
Name "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -30,6 +30,8 @@ modded class ERR_LocationComponent
{
if (Kind() == ERR_LocationKind.RADIO) label = "Radio station";
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Extraction helicopter";
else if (Kind() == ERR_LocationKind.DEPOT) label = "Supply cache";
else if (Kind() == ERR_LocationKind.PRISON) label = "Prison";
else label = Id();
}
m_ERRMarker.SetCustomText(label);
@@ -11,6 +11,11 @@ class ERR_DiscoveryService
SCR_PrefabNamePredicate predicate = new SCR_PrefabNamePredicate();
predicate.prefabName = ERR_FactionCatalog.IntelFolder();
inventory.FindItems(folders, predicate, EStoragePurpose.PURPOSE_ANY);
array<IEntity> objectives = {};
predicate.prefabName = ERR_FactionCatalog.ObjectiveDocument();
inventory.FindItems(objectives, predicate, EStoragePurpose.PURPOSE_ANY);
foreach (IEntity objective : objectives)
if (!folders.Contains(objective)) folders.Insert(objective);
}
void Tick(float now)
@@ -32,9 +37,15 @@ class ERR_DiscoveryService
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
array<ERR_LocationComponent> eligible = {};
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent));
foreach (ERR_LocationComponent location : locations)
{
if (location && location.Kind() != ERR_LocationKind.PRISON && location.Kind() != ERR_LocationKind.EXTRACTION && location.Knowledge() < 2) eligible.Insert(location);
if (!location || location.Knowledge() >= 2) continue;
if (document)
{
if (location.Id() == document.TargetId()) eligible.Insert(location);
}
else if (location.Kind() != ERR_LocationKind.PRISON && location.Kind() != ERR_LocationKind.EXTRACTION) eligible.Insert(location);
}
if (eligible.IsEmpty()) continue;
ERR_LocationComponent chosen = eligible.GetRandomElement();
@@ -0,0 +1,8 @@
[ComponentEditorProps(category: "Rereintroduced", description: "Physical document identifying one authored objective")]
class ERR_DocumentComponentClass : ScriptComponentClass {}
class ERR_DocumentComponent : ScriptComponent
{
protected string m_TargetId;
void SetTarget(string id) { if (Replication.IsServer()) m_TargetId = id; }
string TargetId() { return m_TargetId; }
}
@@ -1,6 +1,7 @@
// Vanilla-only catalog. Optional factions will supply separate catalogs.
class ERR_FactionCatalog
{
static ResourceName ObjectiveDocument() { return "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et"; }
static ResourceName Prisoner() { return "{2F912ED6E399FF47}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et"; }
static ResourceName Patrol() { return "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"; }
static ResourceName Helicopter() { return "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"; }
+23 -3
View File
@@ -1,7 +1,7 @@
class ERR_GameModeClass : SCR_BaseGameModeClass {}
class ERR_GameMode : SCR_BaseGameMode
{
[Attribute("6090 0 7160", UIWidgets.Coords, "Prison position; validated by scenario author")]
[RplProp(), Attribute("6090 0 7160", UIWidgets.Coords, "Prison position; configured from authored scenario")]
protected vector m_PrisonPosition;
[Attribute("6090 0 7160", UIWidgets.Coords, "Centre of extraction boundary")]
protected vector m_EscapeCenter;
@@ -58,9 +58,22 @@ class ERR_GameMode : SCR_BaseGameMode
bool Active() { return m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
protected ERR_CommandService CreateCommandService() { return null; }
void ConfigureStart(vector position)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING) return;
m_PrisonPosition = position;
m_EscapeCenter = position;
Replication.BumpMe();
}
ERR_SiteService Sites()
{
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
if (!scenario) return null;
return scenario.Sites();
}
void RegisterLocation(ERR_LocationComponent location)
{
if (m_Locations.Contains(location)) return;
if (!location || location.Id().IsEmpty() || m_Locations.Contains(location)) return;
foreach (ERR_LocationComponent existing : m_Locations)
{
if (existing.Id() == location.Id())
@@ -161,7 +174,14 @@ class ERR_GameMode : SCR_BaseGameMode
if (m_Phase != ERR_Phase.PREPARING) return;
// Scenario content is created through a separate authoring component.
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
if (!scenario || !scenario.Prepare())
ERR_PrepareResult prepared = ERR_PrepareResult.FAILED;
if (scenario) prepared = scenario.Prepare();
if (prepared == ERR_PrepareResult.PENDING)
{
GetGame().GetCallqueue().CallLater(StartScenario, 250, false);
return;
}
if (prepared == ERR_PrepareResult.FAILED)
{
Print("Rereintroduced: scenario preparation failed; spawning remains disabled", LogLevel.ERROR);
return;
+21 -2
View File
@@ -4,9 +4,9 @@ enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT }
class ERR_LocationComponentClass : ScriptComponentClass {}
class ERR_LocationComponent : ScriptComponent
{
[Attribute("", UIWidgets.EditBox, "Stable scenario location ID")]
[RplProp(onRplName: "IdentityChanged"), Attribute("", UIWidgets.EditBox, "Stable scenario location ID")]
protected string m_Id;
[Attribute("0", UIWidgets.ComboBox, "Location kind", enums: ParamEnumArray.FromEnum(ERR_LocationKind))]
[RplProp(), Attribute("0", UIWidgets.ComboBox, "Location kind", enums: ParamEnumArray.FromEnum(ERR_LocationKind))]
protected ERR_LocationKind m_Kind;
[RplProp()]
protected int m_Knowledge;
@@ -16,6 +16,25 @@ class ERR_LocationComponent : ScriptComponent
int Knowledge() { return m_Knowledge; }
vector Position() { return GetOwner().GetOrigin(); }
bool Configure(string id, ERR_LocationKind kind)
{
if (!Replication.IsServer() || id.IsEmpty()) return false;
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || mode.Phase() != ERR_Phase.PREPARING) return false;
mode.UnregisterLocation(this);
m_Id = id;
m_Kind = kind;
mode.RegisterLocation(this);
Replication.BumpMe();
return true;
}
protected void IdentityChanged()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode) return;
mode.UnregisterLocation(this);
mode.RegisterLocation(this);
}
bool Discover(int roll, int chance)
{
if (!Replication.IsServer() || m_Knowledge >= 2) return false;
@@ -73,8 +73,15 @@ modded class ERR_GameMode
ERR_Actors.Players(players, true);
if (players.Count() != 1 || Phase() != ERR_Phase.PRISON) return;
m_ERRBasePlayer = players[0];
if (!ERR_NativeDefinitionProbe.Run()) return;
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
if (!ERR_BaseCheck(Objectives() && Objectives().Current() && Objectives().Current().GetTaskName() == "Escape the prison", "native prison objective exists")) return;
ERR_SiteRuntime cache = Sites().Find("everon-supply-1");
if (!ERR_BaseCheck(Sites().Count() == 4 && cache && cache.Container && vector.DistanceXZ(cache.Container.GetOrigin(), PrisonPosition()) > 100, "authored supply cache is a separate scavenging location")) return;
array<IEntity> stock = {};
InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)).GetItems(stock);
PrintFormat("ERR BASE STOCK: cache items=%1", stock.Count());
if (!ERR_BaseCheck(stock.Count() >= 38, "cache holds finite physical equipment and objective documents")) return;
if (!ERR_BaseCheck(Command() == null, "no companion HQ or support controller loaded")) return;
if (!ERR_BaseCheck(vector.DistanceXZ(m_ERRBasePlayer.GetOrigin(), PrisonPosition()) < 5, "prisoner spawned in authored prison")) return;
foreach (ERR_LocationComponent location : m_Locations)
@@ -84,7 +91,7 @@ modded class ERR_GameMode
{
if (!entity) continue;
if (!lock) lock = ERR_BaseLock(entity);
if (entity.GetPrefabData() && entity.GetPrefabData().GetPrefabName() == ERR_FactionCatalog.IntelFolder()) m_ERRBaseDocuments.Insert(entity);
if (entity.GetPrefabData() && entity.GetPrefabData().GetPrefabName() == ERR_FactionCatalog.ObjectiveDocument()) m_ERRBaseDocuments.Insert(entity);
}
if (!ERR_BaseCheck(lock && m_ERRBaseRadio && m_ERRBaseDocuments.Count() == 2 && ERR_Actors.Alive(m_ExtractionVehicle), "gate, clues, radio and helicopter created by core")) return;
ERR_BaseMove(lock.GetOwner().GetOrigin() + "1 0 1");
@@ -98,14 +105,21 @@ modded class ERR_GameMode
}
else if (m_ERRBaseStep == 1 || m_ERRBaseStep == 2)
{
ERR_BaseMove(Sites().Find("everon-supply-1").Container.GetOrigin() + "0 0 2");
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(SCR_InventoryStorageManagerComponent));
if (!ERR_BaseCheck(inventory && inventory.TryInsertItem(m_ERRBaseDocuments[m_ERRBaseStep - 1]), "physical clue recovered")) return;
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(Sites().Find("everon-supply-1").Container.FindComponent(InventoryStorageManagerComponent));
BaseInventoryStorageComponent destination = inventory.FindStorageForItem(m_ERRBaseDocuments[m_ERRBaseStep - 1]);
if (!ERR_BaseCheck(destination && source.TryMoveItemToStorage(m_ERRBaseDocuments[m_ERRBaseStep - 1], destination), "physical clue recovered")) return;
m_ERRBaseStep++;
m_ERRBaseWait = now + 2500;
}
else if (m_ERRBaseStep == 3)
{
if (!ERR_BaseCheck(Objectives().Current().GetTaskDescription().Contains("identify the radio station on your map. Reach"), "objective guidance reacts to discovered radio")) return;
ERR_SiteRuntime cache = Sites().Find("everon-supply-1");
array<IEntity> stock = {};
InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)).GetItems(stock);
foreach (IEntity item : stock) if (item) SCR_EntityHelper.DeleteEntityAndChildren(item);
if (!ERR_BaseCheck(m_ERRBaseRadio.Knowledge() == 2, "documents identify radio without Intel addon")) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "2 0 0");
m_ERRBaseStep = 30;
@@ -119,6 +133,11 @@ modded class ERR_GameMode
}
else if (m_ERRBaseStep == 31)
{
ERR_SiteRuntime cache = Sites().Find("everon-supply-1");
array<IEntity> stock = {};
InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)).GetItems(stock);
if (!ERR_BaseCheck(stock.IsEmpty() && Sites().Prepare() == ERR_PrepareResult.READY, "depleted stock stays empty when preparation is queried again")) return;
ERR_Actors.DeleteSpawned(cache.Container);
if (!ERR_BaseCheck(DefenceOccupied() && DefenceSecondsRemaining() > 0 && DefenceSecondsRemaining() < 30 && Objectives().Current().GetTaskDescription().Contains("seconds remaining"), "native objective reports advancing radio progress")) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "90 0 0");
m_ERRBaseStep = 32;
@@ -126,6 +145,7 @@ modded class ERR_GameMode
}
else if (m_ERRBaseStep == 32)
{
if (!ERR_BaseCheck(!Sites().Find("everon-supply-1").Container, "lost supply cache is not replaced")) return;
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30 && Objectives().Current().GetTaskDescription().Contains("interrupted"), "objective reports interruption and reset")) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "2 0 0");
m_ERRBaseStep = 4;
@@ -0,0 +1,36 @@
#ifdef WORKBENCH
class ERR_NativeDefinitionProbe
{
protected static bool Check(bool condition, string name)
{
if (condition) { Print("ERR SMOKE PASS: definition " + name); return true; }
Print("ERR SMOKE FAIL: definition " + name, LogLevel.ERROR);
return false;
}
static bool Run()
{
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
if (!Check(definition && definition.Valid(), "authored configuration resolves")) return false;
ERR_SiteDefinition cache = definition.Find("everon-supply-1");
if (!cache) return false;
cache.Id = "everon-prison-1";
if (!Check(!definition.Valid(), "duplicate site identity rejected")) return false;
cache.Id = "everon-supply-1";
ERR_SiteStock documents = cache.Stock[3];
documents.TargetId = "missing-radio";
if (!Check(!definition.Valid(), "dangling document target rejected")) return false;
documents.TargetId = "everon-prison-1";
if (!Check(!definition.Valid(), "non-radio document target rejected")) return false;
documents.TargetId = "everon-radio-1";
documents.Count = 1;
if (!Check(!definition.Valid(), "insufficient guaranteed radio clues rejected")) return false;
documents.Count = 65;
if (!Check(!definition.Valid(), "unbounded stock rejected")) return false;
documents.Count = 2;
cache.RevealOnEscape = false;
if (!Check(!definition.Valid(), "unguaranteed clue route rejected")) return false;
cache.RevealOnEscape = true;
return Check(definition.Valid(), "valid configuration restored after isolated mutations");
}
}
#endif
@@ -43,7 +43,7 @@ class ERR_Objectives
{
ERR_Phase phase = m_Mode.Phase();
if (phase == ERR_Phase.PRISON)
return "Collect supplies and the field map inside the prison. Pick the gate lock, then get clear of the guards.";
return "Search the supply box inside the prison for weapons and a field map. Pick the gate lock, then get clear of the guards.";
if (phase == ERR_Phase.RADIO_SEARCH)
{
array<ERR_LocationComponent> locations = {};
@@ -51,7 +51,7 @@ class ERR_Objectives
foreach (ERR_LocationComponent location : locations)
if (location.Kind() == ERR_LocationKind.RADIO && location.Knowledge() == 2)
return "The recovered documents identify the radio station on your map. Reach the station and tune its radio to start the transmission.";
return "Recover the two documents on the guard's table outside the prison gate. Carry them to identify the radio station on your map.";
return "Search the supply cache marked on your map. Its finite stock contains weapons, ammunition, medical supplies and radio documents. Carry the documents to identify the radio station.";
}
if (phase == ERR_Phase.RADIO_DEFENCE)
{
@@ -19,9 +19,9 @@ modded class SCR_PlayerController
if (!hints) return;
string message;
if (mode.Phase() == ERR_Phase.PRISON)
message = "Find the supplies and field map inside the prison. Pick the gate lock to begin your escape.";
message = "Search the supply box inside the prison for weapons and a field map. Pick the gate lock to begin your escape.";
else if (mode.Phase() == ERR_Phase.RADIO_SEARCH)
message = "Recover the two documents on the guard's table just outside the gate. They identify the radio station on your map. Enemy soldiers may carry more intel.";
message = "The nearby supply cache is marked on your map. Scavenge its equipment and carry the radio documents to identify the radio station. Enemy soldiers may carry more intel.";
else if (mode.Phase() == ERR_Phase.RADIO_DEFENCE)
message = "Keep a conscious survivor within 60 metres of the radio until the transmission finishes. Leaving the area resets the defence timer.";
else if (mode.Phase() == ERR_Phase.EXTRACTION)
@@ -1,104 +1,52 @@
[ComponentEditorProps(category: "Rereintroduced", description: "Everon escape composition and population")]
[ComponentEditorProps(category: "Rereintroduced", description: "Authored scenario definition and finite population lifecycle")]
class ERR_ScenarioComponentClass : ScriptComponentClass {}
class ERR_ScenarioComponent : ScriptComponent
{
[Attribute("", UIWidgets.ResourceNamePicker, "Prison composition", "et")]
protected ResourceName m_Prison;
[Attribute("", UIWidgets.ResourceNamePicker, "Radio station", "et")]
protected ResourceName m_Radio;
[Attribute("", UIWidgets.ResourceNamePicker, "Extraction marker", "et")]
protected ResourceName m_Extraction;
[Attribute("6500 0 7200", UIWidgets.Coords, "Radio site")]
protected vector m_RadioPosition;
[Attribute("7000 0 7500", UIWidgets.Coords, "Extraction helicopter site")]
protected vector m_ExtractionPosition;
protected ref array<IEntity> m_Spawned = {};
[Attribute("", UIWidgets.ResourceNamePicker, "Scenario site definitions", "conf")]
protected ResourceName m_Definition;
protected ref ERR_SiteService m_Sites;
protected bool m_Started;
protected bool m_Finished;
protected bool m_Failed;
bool Prepare()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || !m_Spawned.IsEmpty()) return false;
vector start = mode.PrisonPosition();
vector radioPosition = ERR_Actors.Ground(m_RadioPosition);
vector extractionPosition = ERR_Actors.Ground(m_ExtractionPosition);
if (start[1] < 1 || radioPosition[1] < 1 || extractionPosition[1] < 1) return false;
IEntity prison = Spawn(m_Prison, start);
IEntity radio = Spawn(m_Radio, radioPosition);
IEntity extraction = Spawn(m_Extraction, extractionPosition);
Vehicle helicopter = Vehicle.Cast(Spawn(ERR_FactionCatalog.Helicopter(), ERR_Actors.Ground(extractionPosition + "12 0 0")));
if (!prison || !radio || !extraction || !helicopter) { Rollback(); return false; }
mode.SetExtractionVehicle(helicopter);
// Keep equipment above vegetation and visible to newly spawned prisoners.
IEntity supplyTable = Spawn(ERR_Supplies.IntelTable(), ERR_Actors.Ground(start + "-4 0 0"));
if (!supplyTable) { Rollback(); return false; }
vector supplyLow, supplyHigh;
supplyTable.GetWorldBounds(supplyLow, supplyHigh);
for (int i = 0; i < 5; i++)
if (!Spawn(ERR_Supplies.Pistol(), TablePoint(supplyLow, supplyHigh, (i + 1) / 6.0, 0.3))) { Rollback(); return false; }
for (int i = 0; i < 8; i++)
if (!Spawn(ERR_Supplies.Magazine(), TablePoint(supplyLow, supplyHigh, (i + 1) / 9.0, 0.65))) { Rollback(); return false; }
if (!Spawn(FieldMap(), TablePoint(supplyLow, supplyHigh, 0.5, 0.9))) { Rollback(); return false; }
// Objective documents are guaranteed; random carried intel remains optional.
vector cachePosition = ERR_Actors.Ground(start + "0 0 -12");
IEntity table = Spawn(ERR_Supplies.IntelTable(), cachePosition);
if (!table) { Rollback(); return false; }
vector lower, upper;
table.GetWorldBounds(lower, upper);
for (int i = 0; i < 2; i++)
{
vector documentPosition = cachePosition + Vector(-0.2 + i * 0.4, 0, 0);
documentPosition[1] = upper[1] + 0.05;
if (!Spawn(ERR_FactionCatalog.IntelFolder(), documentPosition)) { Rollback(); return false; }
}
for (int i = 0; i < 3; i++)
{
vector centre = start + "35 0 0";
if (i == 1) centre = radioPosition + "30 0 0";
if (i == 2) centre = extractionPosition + "30 0 0";
SCR_AIGroup group = SCR_AIGroup.Cast(Spawn(ERR_FactionCatalog.Patrol(), ERR_Actors.Ground(centre)));
if (!group) { Rollback(); return false; }
AIWaypoint defend = AIWaypoint.Cast(Spawn(ERR_FactionCatalog.DefendWaypoint(), ERR_Actors.Ground(centre)));
if (!defend) { Rollback(); return false; }
group.AddWaypoint(defend);
GetGame().GetCallqueue().CallLater(RegisterGroup, 2000, false, group);
}
if (!PrepareAdditionalForces()) { Rollback(); return false; }
foreach (IEntity entity : m_Spawned) mode.Own(entity);
return true;
}
protected bool PrepareAdditionalForces() { return true; }
protected vector TablePoint(vector lower, vector upper, float along, float across)
{
float x = along;
float z = across;
if (upper[2] - lower[2] > upper[0] - lower[0]) { x = across; z = along; }
return Vector(lower[0] + (upper[0] - lower[0]) * x, upper[1] + 0.05, lower[2] + (upper[2] - lower[2]) * z);
}
protected ResourceName FieldMap() { return ERR_Supplies.Map(); }
protected IEntity Spawn(ResourceName resource, vector position)
{
IEntity entity = ERR_Actors.Spawn(resource, position);
if (entity) m_Spawned.Insert(entity);
return entity;
}
protected void RegisterGroup(SCR_AIGroup group)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && mode.Active() && group) mode.RegisterGroup(group);
}
protected void Rollback()
{
GetGame().GetCallqueue().Remove(RegisterGroup);
foreach (IEntity entity : m_Spawned) ERR_Actors.DeleteSpawned(entity);
m_Spawned.Clear();
}
protected override void OnDelete(IEntity owner)
{
if (GetGame()) GetGame().GetCallqueue().Remove(RegisterGroup);
super.OnDelete(owner);
}
ERR_SiteService Sites() { return m_Sites; }
ERR_PrepareResult Prepare()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || m_Failed) return ERR_PrepareResult.FAILED;
if (m_Finished) return ERR_PrepareResult.READY;
if (!m_Started)
{
m_Started = true;
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
m_Sites = new ERR_SiteService(mode);
if (!m_Sites.Begin(definition, FieldMap())) { m_Failed = true; return ERR_PrepareResult.FAILED; }
}
ERR_PrepareResult result = m_Sites.Prepare();
if (result != ERR_PrepareResult.READY) return result;
if (!PrepareAdditionalForces())
{
m_Failed = true;
m_Sites.Rollback();
return ERR_PrepareResult.FAILED;
}
mode.OnPhaseChanged().Insert(m_Sites.PhaseChanged);
mode.OnStop().Insert(m_Sites.Stop);
GetGame().GetCallqueue().CallLater(ActivateSites, 2000, false);
m_Finished = true;
return ERR_PrepareResult.READY;
}
protected void ActivateSites()
{
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && mode.Active() && m_Sites) m_Sites.Activate();
}
protected bool PrepareAdditionalForces() { return true; }
protected ResourceName FieldMap() { return ERR_Supplies.Map(); }
protected override void OnDelete(IEntity owner)
{
if (GetGame()) GetGame().GetCallqueue().Remove(ActivateSites);
if (m_Sites) m_Sites.Stop();
super.OnDelete(owner);
}
}
@@ -0,0 +1,85 @@
// Authored scenario data. These definitions contain no runtime entities.
[BaseContainerProps()]
class ERR_SiteStock
{
[Attribute("", UIWidgets.ResourceNamePicker, "Physical inventory item", "et")]
ResourceName Prefab;
[Attribute("1", UIWidgets.EditBox, "Finite initial count")]
int Count;
[Attribute("", UIWidgets.EditBox, "Objective document's destination location ID")]
string TargetId;
}
[BaseContainerProps()]
class ERR_SiteDefinition
{
[Attribute()]
string Id;
[Attribute("0", UIWidgets.ComboBox, "Location kind", enums: ParamEnumArray.FromEnum(ERR_LocationKind))]
ERR_LocationKind Kind;
[Attribute("", UIWidgets.ResourceNamePicker, "Successor site composition", "et")]
ResourceName Prefab;
[Attribute("0 0 0", UIWidgets.Coords)]
vector Position;
[Attribute("", UIWidgets.ResourceNamePicker, "Optional physical guard group", "et")]
ResourceName Guards;
[Attribute("20 0 0", UIWidgets.Coords)]
vector GuardOffset;
[Attribute("", UIWidgets.ResourceNamePicker, "Finite inventory container", "et")]
ResourceName Container;
[Attribute("0 0 0", UIWidgets.Coords)]
vector ContainerOffset;
[Attribute()]
ref array<ref ERR_SiteStock> Stock;
[Attribute("0", UIWidgets.CheckBox, "Show this supply location after prison escape")]
bool RevealOnEscape;
}
[BaseContainerProps()]
class ERR_ScenarioDefinition
{
[Attribute()]
ref array<ref ERR_SiteDefinition> Sites;
ERR_SiteDefinition Find(string id)
{
foreach (ERR_SiteDefinition site : Sites)
if (site && site.Id == id) return site;
return null;
}
bool Valid()
{
if (!Sites || Sites.IsEmpty() || Sites.Count() > 32) return false;
set<string> ids = new set<string>();
int prisons, radios, extractions, groups, items, documents, maps;
foreach (ERR_SiteDefinition site : Sites)
{
if (!site || site.Id.IsEmpty() || ids.Contains(site.Id) || site.Prefab.IsEmpty()) return false;
if (site.Kind < ERR_LocationKind.PRISON || site.Kind > ERR_LocationKind.DEPOT) return false;
ids.Insert(site.Id);
if (site.Kind == ERR_LocationKind.PRISON) prisons++;
if (site.Kind == ERR_LocationKind.RADIO) radios++;
if (site.Kind == ERR_LocationKind.EXTRACTION) extractions++;
if (!site.Guards.IsEmpty()) groups++;
if (!site.Stock) continue;
foreach (ERR_SiteStock stock : site.Stock)
{
if (!stock || stock.Prefab.IsEmpty() || stock.Count < 1 || stock.Count > 64 || site.Container.IsEmpty()) return false;
items += stock.Count;
if (stock.Prefab == ERR_FactionCatalog.ObjectiveDocument())
{
if (stock.TargetId.IsEmpty()) return false;
if (site.RevealOnEscape || site.Kind == ERR_LocationKind.PRISON) documents += stock.Count;
}
if (site.Kind == ERR_LocationKind.PRISON && stock.Prefab == ERR_Supplies.Map()) maps += stock.Count;
if (!stock.TargetId.IsEmpty())
{
ERR_SiteDefinition target = Find(stock.TargetId);
if (!target || target.Kind != ERR_LocationKind.RADIO || stock.Prefab != ERR_FactionCatalog.ObjectiveDocument()) return false;
}
}
}
return prisons == 1 && radios == 1 && extractions == 1 && groups <= 8 && items <= 256 && documents >= 2 && maps >= 1;
}
}
@@ -0,0 +1,193 @@
enum ERR_PrepareResult { FAILED = -1, PENDING, READY }
class ERR_StockPlacement
{
IEntity Item;
InventoryStorageManagerComponent Inventory;
bool Accepted;
}
class ERR_SiteRuntime
{
ref ERR_SiteDefinition Definition;
ERR_LocationComponent Location;
IEntity Container;
SCR_AIGroup Group;
}
// One owner for finite physical site contents. No population or stock refill tick.
class ERR_SiteService
{
protected ERR_GameMode m_Mode;
protected ref array<ref ERR_SiteRuntime> m_Sites = {};
protected ref array<IEntity> m_Entities = {};
protected ref array<ref ERR_StockPlacement> m_Placements = {};
protected ERR_PrepareResult m_State = ERR_PrepareResult.PENDING;
protected float m_Deadline;
protected bool m_Begun;
protected bool m_Stopped;
protected bool m_Activated;
protected bool m_Obstructed;
void ERR_SiteService(ERR_GameMode mode) { m_Mode = mode; }
int Count() { return m_Sites.Count(); }
ERR_SiteRuntime Find(string id)
{
foreach (ERR_SiteRuntime site : m_Sites)
if (site.Definition.Id == id) return site;
return null;
}
protected bool CheckClearance(IEntity entity)
{
if (entity && entity.GetVObject()) m_Obstructed = true;
return !m_Obstructed;
}
protected bool CacheClearance(vector position)
{
m_Obstructed = false;
vector ground = ERR_Actors.Ground(position);
GetGame().GetWorld().QueryEntitiesByAABB(ground + "-1.3 0.1 -1.3", ground + "1.3 2 1.3", CheckClearance);
PrintFormat("Rereintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
return !m_Obstructed;
}
protected IEntity Spawn(ResourceName resource, vector position)
{
IEntity entity = ERR_Actors.Spawn(resource, ERR_Actors.Ground(position));
if (entity) m_Entities.Insert(entity);
return entity;
}
protected bool Fail(string reason)
{
Print("Rereintroduced [sites]: preparation failed: " + reason, LogLevel.ERROR);
Rollback();
m_State = ERR_PrepareResult.FAILED;
return false;
}
bool Begin(ERR_ScenarioDefinition definition, ResourceName fieldMap)
{
if (!Replication.IsServer() || m_Begun || m_Stopped) return false;
m_Begun = true;
if (!definition || !definition.Valid()) return Fail("invalid scenario definition");
m_Deadline = GetGame().GetWorld().GetWorldTime() + 15000;
foreach (ERR_SiteDefinition spec : definition.Sites)
{
vector position = ERR_Actors.Ground(spec.Position);
if (position[1] < 1) return Fail("site below sea level: " + spec.Id);
IEntity root = Spawn(spec.Prefab, position);
if (!root) return Fail("site composition: " + spec.Id);
ERR_SiteRuntime site = new ERR_SiteRuntime();
site.Definition = spec;
site.Location = ERR_LocationComponent.Cast(root.FindComponent(ERR_LocationComponent));
if (!site.Location || !site.Location.Configure(spec.Id, spec.Kind)) return Fail("location identity: " + spec.Id);
m_Sites.Insert(site);
if (spec.Kind == ERR_LocationKind.PRISON) m_Mode.ConfigureStart(position);
if (spec.Kind == ERR_LocationKind.EXTRACTION)
{
Vehicle helicopter = Vehicle.Cast(Spawn(ERR_FactionCatalog.Helicopter(), position + "12 0 0"));
if (!helicopter) return Fail("extraction vehicle");
m_Mode.SetExtractionVehicle(helicopter);
}
if (!spec.Container.IsEmpty())
{
if (!CacheClearance(position + spec.ContainerOffset)) return Fail("obstructed cache position: " + spec.Id);
site.Container = Spawn(spec.Container, position + spec.ContainerOffset);
if (!site.Container) return Fail("container: " + spec.Id);
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(site.Container.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return Fail("container inventory: " + spec.Id);
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(site.Container.FindComponent(BaseInventoryStorageComponent));
if (!storage) return Fail("container storage: " + spec.Id);
inventory.AddStorage(storage);
if (spec.Stock)
{
foreach (ERR_SiteStock stock : spec.Stock)
{
ResourceName resource = stock.Prefab;
if (resource == ERR_Supplies.Map()) resource = fieldMap;
for (int i = 0; i < stock.Count; i++)
{
// Created exactly once; insertion retries always use this same entity.
IEntity item = Spawn(resource, site.Container.GetOrigin());
if (!item) return Fail("stock resource: " + spec.Id);
if (!stock.TargetId.IsEmpty())
{
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(item.FindComponent(ERR_DocumentComponent));
if (!document) return Fail("targeted document component");
document.SetTarget(stock.TargetId);
}
ERR_StockPlacement placement = new ERR_StockPlacement();
placement.Item = item;
placement.Inventory = inventory;
m_Placements.Insert(placement);
}
}
}
}
if (!spec.Guards.IsEmpty())
{
vector guardPosition = position + spec.GuardOffset;
site.Group = SCR_AIGroup.Cast(Spawn(spec.Guards, guardPosition));
AIWaypoint waypoint = AIWaypoint.Cast(Spawn(ERR_FactionCatalog.DefendWaypoint(), guardPosition));
if (!site.Group || !waypoint) return Fail("guard population: " + spec.Id);
site.Group.AddWaypoint(waypoint);
}
PrintFormat("Rereintroduced [sites]: authored %1 at %2", spec.Id, position);
}
return true;
}
ERR_PrepareResult Prepare()
{
if (!Replication.IsServer() || !m_Begun || m_Stopped) return ERR_PrepareResult.FAILED;
if (m_State != ERR_PrepareResult.PENDING) return m_State;
int pending, accepted;
string firstRejected;
foreach (ERR_StockPlacement placement : m_Placements)
{
if (!placement.Item || !placement.Inventory) { Fail("stock disappeared during initialization"); return m_State; }
array<IEntity> items = {};
placement.Inventory.GetItems(items);
if (items.Contains(placement.Item)) continue;
pending++;
if (firstRejected.IsEmpty() && placement.Item.GetPrefabData()) firstRejected = placement.Item.GetPrefabData().GetPrefabName();
if (placement.Accepted) accepted++;
if (!placement.Accepted) placement.Accepted = placement.Inventory.TryInsertItem(placement.Item);
}
if (pending)
{
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) Fail(string.Format("physical stock insertion timed out: pending=%1 accepted=%2 sample=%3", pending, accepted, firstRejected));
return m_State;
}
foreach (IEntity entity : m_Entities) m_Mode.Own(entity);
m_State = ERR_PrepareResult.READY;
PrintFormat("Rereintroduced [sites]: %1 sites and %2 physical stock items ready", m_Sites.Count(), m_Placements.Count());
m_Placements.Clear();
return m_State;
}
void Activate()
{
if (m_State != ERR_PrepareResult.READY || m_Stopped || m_Activated) return;
m_Activated = true;
foreach (ERR_SiteRuntime site : m_Sites)
{
if (site.Definition.Kind == ERR_LocationKind.PRISON && site.Location) site.Location.Discover(0, 100);
if (site.Group) m_Mode.RegisterGroup(site.Group);
}
}
void PhaseChanged(ERR_Phase previous, ERR_Phase current)
{
if (!Replication.IsServer() || current != ERR_Phase.RADIO_SEARCH) return;
foreach (ERR_SiteRuntime site : m_Sites)
if (site.Definition.RevealOnEscape && site.Location) site.Location.Discover(0, 100);
}
void Rollback()
{
// Children/items are listed after parents; delete them first.
for (int i = m_Entities.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_Entities[i]);
m_Entities.Clear();
m_Placements.Clear();
m_Sites.Clear();
}
void Stop() { m_Stopped = true; }
}
@@ -5,6 +5,23 @@ class ERR_ValidateBaseWorldPlugin : WorldEditorPlugin
{
string play;
WorldEditor editor = Workbench.GetModule(WorldEditor);
if (editor)
{
WorldEditorAPI api = editor.GetApi();
if (api)
{
float low = 10000;
float high = -10000;
for (int x = -3; x <= 3; x += 3)
for (int z = -3; z <= 3; z += 3)
{
float height = api.GetTerrainSurfaceY(6150 + x, 7050 + z);
low = Math.Min(low, height);
high = Math.Max(high, height);
}
PrintFormat("ERR SITE SURVEY: supply=6150/7050 elevationRange=%1 min=%2 max=%3", high - low, low, high);
}
}
if (editor && System.GetCLIParam("errPlay", play) && play == "1") editor.SwitchToGameMode();
}
}