To help this discussion, I've made a reference implementation for what changes would have to be made in DFU, and what a mod using these changes would look like
The DFU changes are here: https://github.com/KABoissonneault/dagg ... 6ca1507f8d
The reference mod is here: https://github.com/KABoissonneault/DFU- ... Sprites.cs
In this implementation, I tried creating a new type of "class" enemy called "Druid", which is like a Mage, except with brown robes. Its enemy id is 147.
In DFU, I found that there's really only two components we need to create to define our new enemy: a MobileEnemy, and a DFCareer.
MobileEnemy
Mods can currently add new MobileEnemy instances to EnemyBasics.Enemies, due to it being public and mutable. Adding values to an array is not too convenient, but a temporary conversion to and from a "List<MobileEnemy>" does the trick. The new value will appear just fine in "GameObjectHelper.EnemyDict", and "SetupDemoEnemy.ApplyEnemySettings" will find the modded MobileEnemy just fine.
There's two systems in DFU that access EnemyBasics.Enemies as an array directly: "EnemyMotor.MakeEnemyHostileToAttacker" and "SoulBound.GetEnchantmentSettings". I'm pretty sure those systems only work for the 42 first enemies, since the enemy ID would match the array index. So mods should only add values at the end, and shouldn't expect those two systems to work on custom enemies.
Now, when creating an enemy, SetupDemoEnemy will try to set the DFCareer of the EnemyEntity, using an EntityType of either EnemyMonster or EnemyClass (ie: humans with a class). If the ID is between 0 and 42, it's an EnemyMonster, if the ID is between 128 and 146 it's an EnemyClass, otherwise it's an error. But then, how should it know whether our Druid (ID=147) is a monster or a class?
For the reference implementation, I decided that for every 256 IDs, the first 128 would be monsters, and the last 128 would be classes. So, 0-127 are monsters, 128-255 are classes, 256-383 are monsters again, and 384-511 are classes again, etc. This doesn't change classic IDs, and doesn't really limit how many IDs mods can use.
Next step in the SetupDemoEnemy process: setting the enemy career on the EnemyEntity.
DFCareer
EnemyEntity.SetEnemyCareer currently has two ways to get the DFCareer template. For monsters, it loads MONSTER.BSA, then loads the career for our enemy id. For classes, it loads the "CLASS<id>.CFG" for the class id, which is the enemy id minus 128. Of course, this won't work for our arbitrary enemy IDs.
In the reference implementation, I added a public static Dictionary in DaggerfallEntity, "CustomCareerTemplates". The key is the enemy id, and the value is the DFCareer template for that enemy type. Any system can easily detect whether an enemy id corresponds to a custom enemy by using this collection.
Quests
I'm not sure if there's an official way to add to Quests-Foes.txt without overriding the entire file, but in my case, I just wrote this
Code: Select all
QuestMachine.Instance.FoesTable.AddIntoTable(new string[] { "147, Druid" });
Text Manager
This is minor, but the enemy's name is sometimes user-facing (ex: "You see a Druid."), and as a result, goes through the TextManager, in order to support localization. The approach taken by the manager only works for the classic enemies, and will break when given another enemy ID. For my reference implementation, I use the DaggerfallEntity.CustomCareerTemplates dictionary to detect a custom enemy, and use the career name in that case.
Mobile Sprites
This is the part that I blocked on. Personally, I don't find it worth adding new enemy types if players can't distinguish them from their appearance. I've investigated two approaches: new Textures, and palette swaps.
For new textures, I tried adding new textures named 512_0-0.png, 512_0-1.png, ... through my mod, and set 512 as the MobileEnemy's MaleTexture and FemaleTexture. This failed pretty hard, and from what I've gathered so far, this would take a lot of work, as the system would need to know if the sprites have Ranged, Idle, and Casting animations. I abandoned this approach f or now.
For my Druid, and for many other "new enemies" I had in mind, simply being able to change some colors on the existing sprites is enough. My Druid sprite just takes TEXTURE.486 and changes the robe color (18 colors total). I saw that while TextureFile loads the ART_PAL.COL palette by default, its palette can be replaced before its pixels are accessed. I thought of adding a new "palette override" field to MobileEnemy, and have DaggerfallMobileUnit use this palette to generate its material in "AssignMeshAndMaterial". Again, I didn't test this approach, as I didn't find a "clean" way to do it.
Conclusion
As can be seen in the reference implementation, there's not much DFU has to add to support new enemies. Sure, not everything would work properly, such as soul traps for custom monsters, but I don't think these limitations would stop the feature from being useful. However, I wouldn't consider this feature "version 0.1 ready" until I figure some way to let players customize the appearance of the enemy.
I'm curious what existing discussions people might have had on this topic, and any comments people would have on the current implementation. I went for the "minimal change" approach, given the state of the project, which means not everything is as clean as I would have wanted it to be. The #1 priority should be not breaking anything, of course.