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))