From 6353d384cc9aa13b52041cd56baa1e3bd4a92c9a Mon Sep 17 00:00:00 2001 From: ToallaNova-O2D Date: Fri, 29 May 2026 05:59:47 +0000 Subject: [PATCH] feat: Add modular rebel loadout system and species mod support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config-driven weapon categories, utility archetypes, and item pools replace hardcoded rebel soldier loadout logic. New helper functions (GetUtilityPool, GetWeaponForTier, EquipItemOnUnit, RollRebelWeaponTier, RollLegacyLoadout, RollRebelCharacterTemplate) provide clean separation of concerns. Character template configurability enables species mod integration (PlayableAdvent, PlayableAliens) via soft dependency — templates are validated at runtime, missing mods fall back silently. Uses SYNC_RAND_STATIC for all random rolls (network sync). Uses TechResearchedOrHasHQInventoryItem pattern (upstream PR #1959). Legacy loadout system preserved as automatic fallback when config is empty. --- LongWarOfTheChosen/Config/XComLW_Overhaul.ini | 85 ++++ .../Src/LW_Overhaul/Classes/Utilities_LW.uc | 426 ++++++++++++++---- 2 files changed, 420 insertions(+), 91 deletions(-) diff --git a/LongWarOfTheChosen/Config/XComLW_Overhaul.ini b/LongWarOfTheChosen/Config/XComLW_Overhaul.ini index 864321e57..910f7bab7 100644 --- a/LongWarOfTheChosen/Config/XComLW_Overhaul.ini +++ b/LongWarOfTheChosen/Config/XComLW_Overhaul.ini @@ -2751,6 +2751,91 @@ LOW_INFILTRATION_MODIFIER_ON_REFLEX_ACTIONS[1]=0.1 LOW_INFILTRATION_MODIFIER_ON_REFLEX_ACTIONS[2]=0.5 LOW_INFILTRATION_MODIFIER_ON_REFLEX_ACTIONS[3]=1.0 +; ============================================================================= +; MODULAR REBEL LOADOUT SYSTEM — Config-Driven Weapon & Utility Setup +; ============================================================================= +; +; Weapon Categories: Each entry defines a weapon type with 4 tech tiers. +; When rebels spawn, the system rolls a random category, then picks the +; weapon matching the highest researched tier. +; +; Utility Archetypes: Define slot combinations (offensive/defensive/utility). +; Each archetype names two pools; one item is drawn from each pool at random. +; +; Item Pools: Categorized lists of items. Rebels draw from these based on +; their rolled archetype. +; +; REBEL_ALWAYS_EQUIP: Items that every rebel soldier always receives. +; +; If these arrays are empty, the LEGACY loadout system is used automatically +; (loadout string lookup via "RebelSoldier" + tier + weapon type). +; ============================================================================= + +; --- Weapon categories (one random category per rebel) --- ++REBEL_WEAPON_CATEGORIES=(CategoryName="Rifle", Tier1Weapon="AssaultRifle_CV", Tier2Weapon="AssaultRifle_LS", Tier3Weapon="AssaultRifle_MG", Tier4Weapon="AssaultRifle_CG") ++REBEL_WEAPON_CATEGORIES=(CategoryName="SMG", Tier1Weapon="SMG_CV", Tier2Weapon="SMG_LS", Tier3Weapon="SMG_MG", Tier4Weapon="SMG_CG") ++REBEL_WEAPON_CATEGORIES=(CategoryName="Shotgun", Tier1Weapon="Shotgun_CV", Tier2Weapon="Shotgun_LS", Tier3Weapon="Shotgun_MG", Tier4Weapon="Shotgun_CG") + +; --- Offensive items pool --- ++REBEL_OFFENSIVE_ITEMS="Frag" ++REBEL_OFFENSIVE_ITEMS="AdvGrenade" + +; --- Defensive items pool --- ++REBEL_DEFENSIVE_ITEMS="Flashbang" ++REBEL_DEFENSIVE_ITEMS="SmokeGrenade" ++REBEL_DEFENSIVE_ITEMS="SmokeGrenadeMk2" + +; --- Utility items pool --- ++REBEL_UTILITY_ITEMS="Medikit" ++REBEL_UTILITY_ITEMS="NanoMedikit" + +; --- Items every rebel always gets --- +; +REBEL_ALWAYS_EQUIP="Frag" + +; --- Utility archetypes (one random archetype per rebel) --- +; Each archetype names two slot pools. One item is drawn from each pool. ++REBEL_UTILITY_ARCHETYPES=(ArchetypeName="OffDef", Slot1Pool="Offensive", Slot2Pool="Defensive") ++REBEL_UTILITY_ARCHETYPES=(ArchetypeName="OffOff", Slot1Pool="Offensive", Slot2Pool="Offensive") ++REBEL_UTILITY_ARCHETYPES=(ArchetypeName="DefDef", Slot1Pool="Defensive", Slot2Pool="Defensive") ++REBEL_UTILITY_ARCHETYPES=(ArchetypeName="OffUtil", Slot1Pool="Offensive", Slot2Pool="Utility") ++REBEL_UTILITY_ARCHETYPES=(ArchetypeName="DefUtil", Slot1Pool="Defensive", Slot2Pool="Utility") + +; ============================================================================= +; CHARACTER TEMPLATE CONFIGURABILITY — Species Mod Integration +; ============================================================================= +; +; REBEL_CHARACTER_TEMPLATES: If populated, rebel soldiers randomly spawn as +; one of these character templates instead of the default RebelSoldierProxy. +; This enables species mods (PlayableAdvent, PlayableAliens, etc.) to add +; non-human rebels without code changes — just config. +; +; SOFT DEPENDENCY: Templates are validated at runtime. If a mod is not +; installed, the missing template is silently skipped and the default +; proxy is used instead. No crashes, no hard dependency. +; +; HOW TO USE: +; 1. Install the species mod (e.g. PlayableAdvent from Steam Workshop) +; 2. Uncomment the relevant lines below +; 3. Rebels will randomly spawn as those species +; +; EXAMPLES (uncomment to enable): +; --- PlayableAdvent mod templates --- +; +REBEL_CHARACTER_TEMPLATES="AdvTrooperMP" +; +REBEL_CHARACTER_TEMPLATES="AdvCaptainMP" +; +REBEL_CHARACTER_TEMPLATES="AdvShieldBearerMP" +; +REBEL_CHARACTER_TEMPLATES="AdvStunLancerMP" +; +; --- PlayableAliens mod templates --- +; +REBEL_CHARACTER_TEMPLATES="SectoidPlayerCharacter" +; +REBEL_CHARACTER_TEMPLATES="ViperPlayerCharacter" +; +REBEL_CHARACTER_TEMPLATES="MutonPlayerCharacter" +; +; --- Mixed example (both mods) --- +; +REBEL_CHARACTER_TEMPLATES="AdvTrooperMP" +; +REBEL_CHARACTER_TEMPLATES="SectoidPlayerCharacter" +; +REBEL_CHARACTER_TEMPLATES="ViperPlayerCharacter" + + [LW_Overhaul.SeqAct_ResetCivilians] +CharacterTemplatesToSwap=Rebel +CharacterTemplatesToSwap=Soldier_VIP diff --git a/LongWarOfTheChosen/Src/LW_Overhaul/Classes/Utilities_LW.uc b/LongWarOfTheChosen/Src/LW_Overhaul/Classes/Utilities_LW.uc index dc8186f55..b4a7cb74b 100644 --- a/LongWarOfTheChosen/Src/LW_Overhaul/Classes/Utilities_LW.uc +++ b/LongWarOfTheChosen/Src/LW_Overhaul/Classes/Utilities_LW.uc @@ -19,6 +19,25 @@ struct MissionEnemyCountOffset var int OffsetAmount; }; +// === Modular Rebel Loadout System structs === +// Defines a weapon category with template names for each tech tier. +struct RebelWeaponCategoryEntry +{ + var name CategoryName; // e.g. 'Rifle', 'SMG', 'Shotgun' + var name Tier1Weapon; // e.g. 'AssaultRifle_CV' + var name Tier2Weapon; // e.g. 'AssaultRifle_LS' + var name Tier3Weapon; // e.g. 'AssaultRifle_MG' + var name Tier4Weapon; // e.g. 'AssaultRifle_CG' +}; + +// Defines a utility slot combination archetype. +struct RebelUtilityArchetype +{ + var name ArchetypeName; // e.g. 'OffDef', 'OffOff', 'DefDef' + var name Slot1Pool; // 'Offensive', 'Defensive', or 'Utility' + var name Slot2Pool; // 'Offensive', 'Defensive', or 'Utility' +}; + var config array REFLEX_ACTION_CHANCE_YELLOW; var config array REFLEX_ACTION_CHANCE_GREEN; var config float REFLEX_ACTION_CHANCE_REDUCTION; @@ -33,6 +52,20 @@ var config array OverrideMissionEnemyCounts; var config array OverrideMissionEnemyCountOffsets; var config bool bDisableKillXPCap; +// === Modular Rebel Loadout System config === +var config array REBEL_WEAPON_CATEGORIES; +var config array REBEL_OFFENSIVE_ITEMS; +var config array REBEL_DEFENSIVE_ITEMS; +var config array REBEL_UTILITY_ITEMS; +var config array REBEL_ALWAYS_EQUIP; +var config array REBEL_UTILITY_ARCHETYPES; + +// === Character Template Configurability === +// List of character template names that rebel soldiers can spawn as. +// If empty, default RebelSoldierProxy/M2/M3 templates are used. +// Soft dependency: validates templates at runtime, falls back if not found. +var config array REBEL_CHARACTER_TEMPLATES; + const CA_FAILURE_RISK_MARKER = "CovertActionRisk_Failure"; const OffensiveReflexAction = 'OffensiveReflexActionPoint_LW'; @@ -276,7 +309,6 @@ function static bool GetSpawnTileNearTile(out TTile Tile, int FirstRange, int Se return false; } - function static RemoveInvalidTiles(out array Tiles) { local XComWorldData WorldData; @@ -457,15 +489,82 @@ static function array GetCompleteDefaultLoadout(XComGameSta return completedefaultloadout; } -// Create a soldier proxy for the given rebel, set them as on-mission, and give them a loadout. All done within the -// provided game state. -function static XComGameState_Unit CreateRebelSoldier(StateObjectReference RebelRef, StateObjectReference OutpostRef, XComGameState NewGameState, optional name Loadout) +// ============================================================================= +// MODULAR REBEL LOADOUT SYSTEM — Helper Functions +// ============================================================================= + +/// Returns the appropriate item pool array for the given pool name. +/// Valid pool names: 'Offensive', 'Defensive', 'Utility' +static function array GetUtilityPool(name PoolName) { - local XComGameState_LWOutpost Outpost; - local XComGameState_Unit Proxy; - local name TemplateName; - local int LaserChance, MagChance, CoilChance, iRand; - local string LoadoutStr; + local array EmptyArray; + + if (PoolName == 'Offensive') + return default.REBEL_OFFENSIVE_ITEMS; + else if (PoolName == 'Defensive') + return default.REBEL_DEFENSIVE_ITEMS; + else if (PoolName == 'Utility') + return default.REBEL_UTILITY_ITEMS; + + `Redscreen("Utilities_LW.GetUtilityPool: Unknown pool name:" @ PoolName); + return EmptyArray; +} + +/// Returns the weapon template name for the given category and tier. +static function name GetWeaponForTier(RebelWeaponCategoryEntry Category, int Tier) +{ + switch(Tier) + { + case 4: return Category.Tier4Weapon; + case 3: return Category.Tier3Weapon; + case 2: return Category.Tier2Weapon; + default: return Category.Tier1Weapon; + } +} + +/// Creates and equips an item on a unit by template name. +/// Handles weapon appearance transfer for primary/secondary weapons. +static function EquipItemOnUnit(XComGameState_Unit Unit, name ItemTemplateName, XComGameState ModifyGameState) +{ + local X2ItemTemplateManager ItemMgr; + local X2EquipmentTemplate EquipTemplate; + local XComGameState_Item NewItem; + + if (ItemTemplateName == '') + { + `Redscreen("Utilities_LW.EquipItemOnUnit: Empty item template name!"); + return; + } + + ItemMgr = class'X2ItemTemplateManager'.static.GetItemTemplateManager(); + EquipTemplate = X2EquipmentTemplate(ItemMgr.FindItemTemplate(ItemTemplateName)); + + if (EquipTemplate == none) + { + `Redscreen("Utilities_LW.EquipItemOnUnit: Template not found:" @ ItemTemplateName); + return; + } + + NewItem = EquipTemplate.CreateInstanceFromTemplate(ModifyGameState); + + // Transfer weapon appearance from the soldier (same as original ApplyLoadout) + if (EquipTemplate.InventorySlot == eInvSlot_PrimaryWeapon || EquipTemplate.InventorySlot == eInvSlot_SecondaryWeapon) + { + NewItem.WeaponAppearance.iWeaponTint = Unit.kAppearance.iWeaponTint; + NewItem.WeaponAppearance.nmWeaponPattern = Unit.kAppearance.nmWeaponPattern; + } + + Unit.AddItemToInventory(NewItem, EquipTemplate.InventorySlot, ModifyGameState); + ModifyGameState.AddStateObject(NewItem); +} + +/// Rolls the weapon tier based on researched techs and rebel level. +/// Returns 1 (conventional), 2 (laser), 3 (magnetic), or 4 (coilgun). +/// Uses configurable tech names via TechResearchedOrHasHQInventoryItem (upstream PR #1959). +static function int RollRebelWeaponTier(XComGameState_LWOutpost Outpost, StateObjectReference RebelRef) +{ + local int LaserChance, MagChance, CoilChance; + local int RebelLevel; local bool AdvancedLasersResearched; local bool MagnetizedWeaponsResearched; @@ -477,110 +576,256 @@ function static XComGameState_Unit CreateRebelSoldier(StateObjectReference Rebel local bool HeavyPlasmaResearched; local bool PlasmaSniperResearched; - Outpost = XComGameState_LWOutpost(`XCOMHISTORY.GetGameStateForObjectID(OutpostRef.ObjectID)); - switch(Outpost.GetRebelLevel(RebelRef)) - { - case 0: - TemplateName = 'RebelSoldierProxy'; - break; - case 1: - TemplateName = 'RebelSoldierProxyM2'; - break; - case 2: - TemplateName = 'RebelSoldierProxyM3'; - break; - default: - `Redscreen("CreateRebelSoldier: Unsupported rebel level " $ Outpost.GetRebelLevel(RebelRef)); - TemplateName = 'RebelSoldierProxy'; - } + RebelLevel = Outpost.GetRebelLevel(RebelRef); - Proxy = CreateRebelProxy(RebelRef, OutpostRef, TemplateName, true, NewGameState); - Proxy.SetSoldierClassTemplate('LWS_RebelSoldier'); + // Cache tech research status using configurable tech names (PR #1959 pattern) + AdvancedLasersResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.AdvancedLasersTechName); + MagnetizedWeaponsResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.MagnetizedWeaponsTechName); + GaussWeaponsResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.GaussWeaponsTechName); + CoilgunsResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.CoilgunsTechName); + AdvancedCoilgunsResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.AdvancedCoilgunsTechName); + PlasmaRifleResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.PlasmaRifleTechName); + AlloyCannonResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.AlloyCannonTechName); + HeavyPlasmaResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.HeavyPlasmaTechName); + PlasmaSniperResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.PlasmaSniperTechName); LaserChance = 0; MagChance = 0; CoilChance = 0; - if (Loadout == '') + // --- Laser tier chances --- + if (MagnetizedWeaponsResearched && AdvancedLasersResearched) { - AdvancedLasersResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.AdvancedLasersTechName); - MagnetizedWeaponsResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.MagnetizedWeaponsTechName); - GaussWeaponsResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.GaussWeaponsTechName); - CoilgunsResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.CoilgunsTechName); - AdvancedCoilgunsResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.AdvancedCoilgunsTechName); - PlasmaRifleResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.PlasmaRifleTechName); - AlloyCannonResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.AlloyCannonTechName); - HeavyPlasmaResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.HeavyPlasmaTechName); - PlasmaSniperResearched = class'X2DownloadableContentInfo_LongWarOfTheChosen'.static.TechResearchedOrHasHQInventoryItem(class'X2DownloadableContentInfo_LongWarOfTheChosen'.default.PlasmaSniperTechName); + LaserChance += (20 + 10 * (RebelLevel + 1)); // 30/40/50 + } + if (GaussWeaponsResearched && AdvancedLasersResearched) + { + LaserChance += (20 + 10 * (RebelLevel + 1)); // 60/80/100 + } - LoadoutStr = "RebelSoldier"; - if (MagnetizedWeaponsResearched && AdvancedLasersResearched) - { - LaserChance += (20 + 10 * (Outpost.GetRebelLevel(RebelRef) + 1)); // 30/40/50 - } - if (GaussWeaponsResearched && AdvancedLasersResearched) - { - LaserChance += (20 + 10 * (Outpost.GetRebelLevel(RebelRef) + 1)); // 60/80/100 - } - if (CoilgunsResearched && GaussWeaponsResearched) - { - if (AdvancedLasersResearched) - { - LaserChance = 100; - } - MagChance += (20 + 10 * (Outpost.GetRebelLevel(RebelRef) + 1)); // 30/40/50 - } - if (AdvancedCoilgunsResearched && GaussWeaponsResearched) - { - MagChance += (20 + 10 * (Outpost.GetRebelLevel(RebelRef) + 1)); // 60/80/100 - } - if (PlasmaRifleResearched) - { - if (GaussWeaponsResearched) - { - MagChance = 100; - } - CoilChance += (20 + 10 * (Outpost.GetRebelLevel(RebelRef) + 1)); // 30/40/50 - } - // All plasma weapon related research - if (HeavyPlasmaResearched && - AlloyCannonResearched && - PlasmaSniperResearched && - AdvancedCoilgunsResearched) + // --- Magnetic tier chances --- + if (CoilgunsResearched && GaussWeaponsResearched) + { + if (AdvancedLasersResearched) { - CoilChance += (20 + 10 * (Outpost.GetRebelLevel(RebelRef) + 1)); // 60/80/100 + LaserChance = 100; } + MagChance += (20 + 10 * (RebelLevel + 1)); // 30/40/50 + } + if (AdvancedCoilgunsResearched && GaussWeaponsResearched) + { + MagChance += (20 + 10 * (RebelLevel + 1)); // 60/80/100 + } - if (`SYNC_RAND_STATIC(100) < CoilChance) + // --- Coilgun tier chances --- + if (PlasmaRifleResearched) + { + if (GaussWeaponsResearched) { - LoadoutStr $= "4"; + MagChance = 100; } - else if (`SYNC_RAND_STATIC(100) < MagChance) + CoilChance += (20 + 10 * (RebelLevel + 1)); // 30/40/50 + } + // All plasma weapon related research + if (HeavyPlasmaResearched && + AlloyCannonResearched && + PlasmaSniperResearched && + AdvancedCoilgunsResearched) + { + CoilChance += (20 + 10 * (RebelLevel + 1)); // 60/80/100 + } + + // Descending roll (preserved from original — each tier uses independent roll) + if (`SYNC_RAND_STATIC(100) < CoilChance) + return 4; + else if (`SYNC_RAND_STATIC(100) < MagChance) + return 3; + else if (`SYNC_RAND_STATIC(100) < LaserChance) + return 2; + + return 1; +} + +/// Fallback: Rolls a legacy loadout name for backward compatibility. +/// Used when REBEL_WEAPON_CATEGORIES config is empty. +static function name RollLegacyLoadout(XComGameState_LWOutpost Outpost, StateObjectReference RebelRef) +{ + local string LoadoutStr; + local int WeaponTier, iRand; + + LoadoutStr = "RebelSoldier"; + WeaponTier = RollRebelWeaponTier(Outpost, RebelRef); + + if (WeaponTier >= 4) + LoadoutStr $= "4"; + else if (WeaponTier >= 3) + LoadoutStr $= "3"; + else if (WeaponTier >= 2) + LoadoutStr $= "2"; + + iRand = `SYNC_RAND_STATIC(100); + if (iRand < 20) + LoadoutStr $= "SMG"; + else if (iRand < 40) + LoadoutStr $= "Shotgun"; + + return name(LoadoutStr); +} + +/// Rolls a character template from REBEL_CHARACTER_TEMPLATES config array. +/// Returns '' (empty name) if the array is empty or the rolled template is +/// not found, signaling that the default RebelSoldierProxy should be used. +/// Soft dependency: missing mod templates are silently skipped. +static function name RollRebelCharacterTemplate() +{ + local X2CharacterTemplateManager CharTemplateMgr; + local X2CharacterTemplate CharTemplate; + local int RollIdx; + local name RolledName; + + if (default.REBEL_CHARACTER_TEMPLATES.Length == 0) + return ''; + + CharTemplateMgr = class'X2CharacterTemplateManager'.static.GetCharacterTemplateManager(); + + RollIdx = `SYNC_RAND_STATIC(default.REBEL_CHARACTER_TEMPLATES.Length); + RolledName = name(default.REBEL_CHARACTER_TEMPLATES[RollIdx]); + + CharTemplate = CharTemplateMgr.FindCharacterTemplate(RolledName); + if (CharTemplate == none) + { + `Redscreen("Utilities_LW.RollRebelCharacterTemplate: Template not found:" @ RolledName + @ "- mod may not be installed. Falling back to default proxy."); + return ''; + } + + return RolledName; +} + +// ============================================================================= +// CreateRebelSoldier — MODULAR VERSION +// ============================================================================= +// Creates a soldier proxy for the given rebel, sets them as on-mission, +// and gives them a loadout. +// +// Character template selection: +// If REBEL_CHARACTER_TEMPLATES is configured, one entry is rolled at random. +// This enables species mods (PlayableAdvent, PlayableAliens, etc.) to add +// non-human rebels via config. Falls back to default proxy if not installed. +// +// Loadout assignment: +// For custom character templates, uses the template's own DefaultLoadout. +// For default proxies: uses modular system if configured, else legacy. +// +// See: XComLW_Overhaul.ini [LW_Overhaul.Utilities_LW] for config defaults. +// ============================================================================= +function static XComGameState_Unit CreateRebelSoldier(StateObjectReference RebelRef, StateObjectReference OutpostRef, XComGameState NewGameState, optional name Loadout) +{ + local XComGameState_LWOutpost Outpost; + local XComGameState_Unit Proxy; + local Name TemplateName; + local name RolledCharTemplate; + local bool bUsingCustomTemplate; + local int WeaponTier; + local int CategoryIdx, ArchetypeIdx, ItemIdx; + local RebelWeaponCategoryEntry SelectedCategory; + local RebelUtilityArchetype SelectedArchetype; + local name WeaponTemplate; + local array Pool; + + Outpost = XComGameState_LWOutpost(`XCOMHISTORY.GetGameStateForObjectID(OutpostRef.ObjectID)); + + // === DETERMINE CHARACTER TEMPLATE === + RolledCharTemplate = RollRebelCharacterTemplate(); + bUsingCustomTemplate = (RolledCharTemplate != ''); + + if (bUsingCustomTemplate) + { + TemplateName = RolledCharTemplate; + `LWTrace("CreateRebelSoldier: Using custom character template:" @ TemplateName); + } + else + { + switch(Outpost.GetRebelLevel(RebelRef)) { - LoadoutStr $= "3"; + case 0: + TemplateName = 'RebelSoldierProxy'; + break; + case 1: + TemplateName = 'RebelSoldierProxyM2'; + break; + case 2: + TemplateName = 'RebelSoldierProxyM3'; + break; + default: + `Redscreen("CreateRebelSoldier: Unsupported rebel level" @ Outpost.GetRebelLevel(RebelRef)); + TemplateName = 'RebelSoldierProxy'; } - else + } + + Proxy = CreateRebelProxy(RebelRef, OutpostRef, TemplateName, true, NewGameState); + + if (!bUsingCustomTemplate) + { + Proxy.SetSoldierClassTemplate('LWS_RebelSoldier'); + } + + // === LOADOUT ASSIGNMENT === + if (bUsingCustomTemplate) + { + // Custom character templates use their own DefaultLoadout. + `LWTrace("Rebel Loadout (custom template):" @ TemplateName); + } + else if (Loadout != '') + { + ApplyLoadout(Proxy, Loadout, NewGameState); + `LWTrace("Rebel Loadout (forced):" @ Loadout); + } + else if (default.REBEL_WEAPON_CATEGORIES.Length > 0 && default.REBEL_UTILITY_ARCHETYPES.Length > 0) + { + // NEW MODULAR LOADOUT SYSTEM + WeaponTier = RollRebelWeaponTier(Outpost, RebelRef); + + CategoryIdx = `SYNC_RAND_STATIC(default.REBEL_WEAPON_CATEGORIES.Length); + SelectedCategory = default.REBEL_WEAPON_CATEGORIES[CategoryIdx]; + + WeaponTemplate = GetWeaponForTier(SelectedCategory, WeaponTier); + EquipItemOnUnit(Proxy, WeaponTemplate, NewGameState); + + ArchetypeIdx = `SYNC_RAND_STATIC(default.REBEL_UTILITY_ARCHETYPES.Length); + SelectedArchetype = default.REBEL_UTILITY_ARCHETYPES[ArchetypeIdx]; + + Pool = GetUtilityPool(SelectedArchetype.Slot1Pool); + if (Pool.Length > 0) { - if (`SYNC_RAND_STATIC(100) < LaserChance) - { - LoadoutStr $= "2"; - } + ItemIdx = `SYNC_RAND_STATIC(Pool.Length); + EquipItemOnUnit(Proxy, Pool[ItemIdx], NewGameState); } - iRand = `SYNC_RAND_STATIC(100); - if (iRand < 20) + + Pool = GetUtilityPool(SelectedArchetype.Slot2Pool); + if (Pool.Length > 0) { - LoadoutStr $= "SMG"; + ItemIdx = `SYNC_RAND_STATIC(Pool.Length); + EquipItemOnUnit(Proxy, Pool[ItemIdx], NewGameState); } - else if (iRand < 40) + + for (ItemIdx = 0; ItemIdx < default.REBEL_ALWAYS_EQUIP.Length; ItemIdx++) { - LoadoutStr $= "Shotgun"; + EquipItemOnUnit(Proxy, default.REBEL_ALWAYS_EQUIP[ItemIdx], NewGameState); } - //`LWTRACE ("Rebel Loadout" @ LoadoutStr); - Loadout = name(LoadOutStr); + `LWTrace("Rebel Loadout:" @ SelectedCategory.CategoryName + @ "T" $ WeaponTier + @ WeaponTemplate + @ SelectedArchetype.ArchetypeName); + } + else + { + // LEGACY FALLBACK + `LWTrace("Rebel Loadout: Legacy fallback (config arrays empty)"); + Loadout = RollLegacyLoadout(Outpost, RebelRef); + ApplyLoadout(Proxy, Loadout, NewGameState); } - - ApplyLoadout(Proxy, Loadout, NewGameState); return Proxy; } @@ -828,7 +1073,6 @@ static function int GetPreviousAlertLevel(XComGameState_Unit UnitState) `LWTrace("Current Turn is" @ string(CurrentTurn)); - // Walk backwards through history for this unit until we find a state in which this unit wasn't in // its current alert to see what the previous alert level was. while (UnitState != none && (UnitState.GetCurrentStat(eStat_AlertLevel) == CurrentAlert || CurrentTurn == PreviousTeam))