diff --git a/Assets/Game/Addons/UnityConsole/Console/Scripts/DefaultCommands.cs b/Assets/Game/Addons/UnityConsole/Console/Scripts/DefaultCommands.cs index 5318d9ce5a..4ae3746c5b 100644 --- a/Assets/Game/Addons/UnityConsole/Console/Scripts/DefaultCommands.cs +++ b/Assets/Game/Addons/UnityConsole/Console/Scripts/DefaultCommands.cs @@ -443,7 +443,7 @@ public static string Execute(params string[] args) if (!int.TryParse(args[0], out id)) return "Invalid mobile ID."; - if (!Enum.IsDefined(typeof(MobileTypes), id)) + if (!Enum.IsDefined(typeof(MobileTypes), id) && DaggerfallEntity.GetCustomCareerTemplate(id) == null) return "Invalid mobile ID."; int team = 0; diff --git a/Assets/Scripts/Game/Entities/DaggerfallEntity.cs b/Assets/Scripts/Game/Entities/DaggerfallEntity.cs index 10987d14ab..2ac23916f7 100644 --- a/Assets/Scripts/Game/Entities/DaggerfallEntity.cs +++ b/Assets/Scripts/Game/Entities/DaggerfallEntity.cs @@ -929,6 +929,38 @@ public static DFCareer GetMonsterCareerTemplate(MonsterCareers career) return monsterFile.GetMonsterClass((int)career); } + /// + /// Allows mods to register a DFCareer template for IDs outside of the values in MobileTypes + /// + static readonly Dictionary CustomCareerTemplates = new Dictionary(); + + /// + /// Gets the career template for a custom (ie: mod-provided) enemy type + /// + /// ID, as defined in EnemyBasics.Enemies + /// The custom DFCareer template registered for this id, or null + public static DFCareer GetCustomCareerTemplate(int enemyId) + { + if (!CustomCareerTemplates.TryGetValue(enemyId, out DFCareer value)) + { + return null; + } + + return value; + } + + /// + /// Sets the career template for a custom (ie: mod-provided) enemy type. + /// + /// ID, as defined in EnemyBasics.Enemies + /// The custom DFCareer template to register + public static void RegisterCustomCareerTemplate(int enemyId, DFCareer career) + { + // Use indexer so that mods can overwrite previous values added by mods + // ex: mod 1 provides new enemies, mod 2 rebalances them + CustomCareerTemplates[enemyId] = career; + } + public static SoundClips GetRaceGenderAttackSound(Races race, Genders gender, bool isPlayerAttack = false) { // Check for racial override attack sound for player only diff --git a/Assets/Scripts/Game/Entities/EnemyEntity.cs b/Assets/Scripts/Game/Entities/EnemyEntity.cs index 59da632ccf..2fc257f230 100644 --- a/Assets/Scripts/Game/Entities/EnemyEntity.cs +++ b/Assets/Scripts/Game/Entities/EnemyEntity.cs @@ -246,7 +246,33 @@ public override void ClearConstantEffects() /// public void SetEnemyCareer(MobileEnemy mobileEnemy, EntityTypes entityType) { - if (entityType == EntityTypes.EnemyMonster) + // Try custom career first + career = GetCustomCareerTemplate(mobileEnemy.ID); + + if (career != null) + { + // Custom enemy + careerIndex = mobileEnemy.ID; + stats.SetPermanentFromCareer(career); + + if (entityType == EntityTypes.EnemyMonster) + { + // Default like a monster + level = mobileEnemy.Level; + maxHealth = Random.Range(mobileEnemy.MinHealth, mobileEnemy.MaxHealth + 1); + for (int i = 0; i < ArmorValues.Length; i++) + { + ArmorValues[i] = (sbyte)(mobileEnemy.ArmorValue * 5); + } + } + else + { + // Default like a class enemy + level = GameManager.Instance.PlayerEntity.Level; + maxHealth = FormulaHelper.RollEnemyClassMaxHealth(level, career.HitPointsPerLevel); + } + } + else if (entityType == EntityTypes.EnemyMonster) { careerIndex = mobileEnemy.ID; career = GetMonsterCareerTemplate((MonsterCareers)careerIndex); diff --git a/Assets/Scripts/Game/SetupDemoEnemy.cs b/Assets/Scripts/Game/SetupDemoEnemy.cs index de54e93be1..5a92a0cdf0 100644 --- a/Assets/Scripts/Game/SetupDemoEnemy.cs +++ b/Assets/Scripts/Game/SetupDemoEnemy.cs @@ -167,6 +167,23 @@ public void ApplyEnemySettings(MobileGender gender) entityBehaviour.EntityType = EntityTypes.EnemyClass; entity.SetEnemyCareer(mobileEnemy, entityBehaviour.EntityType); } + else if (DaggerfallEntity.GetCustomCareerTemplate(enemyIndex) != null) + { + // For custom enemies, we use the 7th bit to tell whether a class or monster was intended + // 0-127 is monster + // 128-255 is class + // 256-383 is monster again + // etc + if ((enemyIndex & 128) != 0) + { + entityBehaviour.EntityType = EntityTypes.EnemyClass; + } + else + { + entityBehaviour.EntityType = EntityTypes.EnemyMonster; + } + entity.SetEnemyCareer(mobileEnemy, entityBehaviour.EntityType); + } else { entityBehaviour.EntityType = EntityTypes.None; diff --git a/Assets/Scripts/Game/TextManager.cs b/Assets/Scripts/Game/TextManager.cs index 840837870d..249d382db0 100644 --- a/Assets/Scripts/Game/TextManager.cs +++ b/Assets/Scripts/Game/TextManager.cs @@ -18,6 +18,7 @@ using UnityEngine.Localization.Settings; using Wenzil.Console; using DaggerfallWorkshop.Game.UserInterface; +using DaggerfallWorkshop.Game.Entity; using UnityEngine.Localization.Tables; namespace DaggerfallWorkshop.Game @@ -331,15 +332,31 @@ public string[] GetLocalizedTextList(string key, TextCollections collection = Te /// /// Gets display name of an enemy from their ID. /// - /// ID of enemy. Valid IDs are 0-42 and 128-146. + /// ID of enemy. Valid IDs are 0-42 and 128-146, or values registered in Daggerfallentity.CustomCareerTemplates /// Name of enemy from localization if found, or exception if not found. public string GetLocalizedEnemyName(int enemyID) { - string[] enemyNames = GetLocalizedTextList("enemyNames", exception:true); - if (enemyID < 128) - return enemyNames[enemyID]; + if (Enum.IsDefined(typeof(MobileTypes), (MobileTypes)enemyID)) + { + string[] enemyNames = GetLocalizedTextList("enemyNames", exception: true); + if (enemyID < 128) + return enemyNames[enemyID]; + else + return enemyNames[43 + enemyID - 128]; + } + // Handle custom enemies else - return enemyNames[43 + enemyID - 128]; + { + // TODO: maybe go through TextProvider so mods can try to offer localization? + DaggerfallConnect.DFCareer career = DaggerfallEntity.GetCustomCareerTemplate(enemyID); + if (career == null) + { + Debug.LogError($"Enemy ID '{enemyID}' did not have a registered custom career template"); + return "(invalid enemy)"; + } + + return career.Name; + } } #endregion