Link to Pastebin
. I put this together in Sublime Text and this forum isn't maintaining the styling. I did try to add code tags where appropriate, but I'm exhausted.
This is going to be a very long post, apologies in advance. I also want to preface this with as much love as I can. This genre continues to be one of my favorites, and I have really been enjoying my time modding it. I only ask all of this so I can help contribute to the growth and survival of this genre, and thus, Divinity itself. As always, I appreciate any time Larian can take out of their schedule to discuss or read this feedback. Know that I make this feedback as a source of ideas to draw on not only for future Divinity 2 patches, but for the future of the engine/editor in general.
Also, I only use the word Iterator on some of these calls to denote a loop. You guys seem to view an Iteration as an async process in some spots, so choose whatever internal definition you need. I am simply using it to make it explicit that I am doing a loop.
First, I want to point to a post I made a few weeks back, http://larian.com/forums/ubbthreads.php?ubb=showflat&Number=628376#Post628376.
I will be re-hashing some of the points made in this post as I have more perspective and can better articulate what I feel is required. That being said, here we go.Script
I will be providing proposed Osiris Script APIs in each section, but here I want to specifically address the absence of two very important methods.
String.Split() and String.SubStr()
These are important because it lets us create naming schemas. For example:
With String.Split, I could split something like this up into a projectile (LightningBolt) and a potency (75). This, combined with the suggested ApplyDamage calls, is incredibly powerful. I could achieve similar things with String.SubStr, but it would be more unwiedly. Thus, I propose the following Queries.
String -> StringSplit((STRING)_String, (STRING)_Delimiter, (INTEGER)_Index, [OUT](STRING)_Result)
String -> StringSplitIterator((STRING)_String, (STRING)_Delimiter, [OUT](STRING)_Result)
String -> StringSub((STRING)_String, (INTEGER)_Start, (INTEGER)_End, [OUT](STRING)_Result)
I would imagine these are both pretty simple implementations as they likely map to very simple underlying workflows.
To make my case stronger, here is a usecase I would actually use, right now.
CharacterUsedSkillOnTarget(_Caster, (CHARACTERGUID)_Target, _Skill)
StringContains(_Skill, "_FAKE_", 1)
StringSplit(_Skill, "_", 4, _ModifierGroup)
StringSplitIterator(_ModifierGroup, "-", _Status)
StringConcatenate("ASC_", _Status, _Modifier)
StringSplit(_Skill, "_", 5, _SkillName)
StringConcatenate(_Modifier, "_" , _First)
StringConcatenate(_First, _SkillName, _RicochetToUse)
CharacterUseSkill(_Caster, _RicochetToUse, _Target, 1, 1);
This lets me designate skills as "Fake", and then based on criteria hotswap them with another skill, allowing me to dynamically change skills on the fly. The problem is, because I don't have a String.Split() call, I have to perform a number of lookup calls (One for status, One for Skills to map to, and possibly one for Animation not listed here. There might be more as well as I get further into this) I know it isn't easy to read, but it's a common structure I can use that would be efficient as hell both in performance and long-term resource usage as I expect some of these lists to grow very, very large.Runes
Stats -> Extra Properties -> Self:OnEquip
Runes either need to support OnEquip and have the act of socketing them trigger OnEquip, or there needs to be an event specific to runes (like OnSocket) that fires upon
- Socketing a rune to an equipped item
- Equipping an item with a rune
- Unsocketing a rune on an equipped item
- Unequipping an item with a rune
Ideally runes would just be metadata to equipment. Would make this very simple and intuitive.
The use case for this is drastically expanded itemization. Statuses are regarded by many as the best hook between an item and a script, and for good reason. Being able to have runes that grant OnEquip statuses would be huge.
Script -> OnRuneSocketedInItem((CHARACTERGUID)_Socketer, (ITEMGUID)_Rune, (ITEMGUID)_Item), (STRING)_ItemSlot)
Script -> GetItemRuneIterator((ITEMGUID)_Item, [OUT](ITEMGUID)_Rune)
Script -> GetItemRuneAtSocket((ITEMGUID)_Item, (INTEGER)_Socket, [OUT](ITEMGUID)_Rune)
Script -> RemoveItemRuneAtSocket((ITEMGUID)_Item, (INTEGER)_Socket)
Script -> AddItemRuneAtSocket((ITEMGUID)_Item, (INTEGER)_Socket)
The above assumes Runes would be treated as stand-alone instances of an Item Template. If Larian would prefer the approach of simply adding metadata to an Item Instance (I suspect this will be easier), see below as I have another issue to bring up with Item metadata.
The usecase for this I think is very simple. Being able to query and interact with an object in the game should not be something we are lacking. I think Runes have a lot of potential as customizable progression, but their current state as stat stones is causing them to not realize their full potential. Being able to query this data will let us do far more things, such as allowing Runes to add proc effects to gear. (I'm ignoring Tags for now because that's going to be it's own thing)Items
Stats -> Tags
Script -> Item/Tag interaction
See the Tags section below.
Script -> GetItemPropertyIterator((ITEMGUID)_Item, [OUT](STRING)_StatName)
Script -> GetItemProperty((ITEMGUID)_Item, (STRING)_StatName, [OUT](INTEGER)_Amount)
Script -> GetItemPropertyStatusIterator((ITEMGUID)_Item, (STRING)_Status, (STRING)_Trigger)
Script -> GetItemPropertyStatus((ITEMGUID)_Item, (STRING)_Status, (STRING)_Trigger)
_Trigger defined as one of the following: ["OnEquip", "OnHit", "OnAttacked", "OnBlock", etc] //Optional but would be nice
Script -> SetItemLevel((ITEMGUID)_Item, (INTEGER)_Level)
Script -> LevelUpItem((ITEMGUID)_Item)
Script -> SetItemProperty((ITEMGUID)_Item, (STRING)_Property, (INTEGER)_Amount)
Script -> SetItemPropertyStatus((ITEMGUID)_Item, (STRING)_Status, (STRING)_Trigger)
This is more what I think will be worthwhile in doing. Assuming Larian wants to treat Runes as metadata, calls like this make more sense. Ultimately, I don't think modders will care too much about what a rune does (Unless it's to add or remove one), so long as we can flag a weapon with a rune in some way that we can query. As of right now, finding out what an item does is very difficult due to the random generation.
Also, I think having a way to interact with item level in script would be a nice quality of life change, especially in Divinity 2 where level and tier are vastly more defined than Divinity 1.Combat
Stats -> Statuses
Statuses are currently incapable of critical hits. That being said, if you guys implement the ApplyDamage calls, this ultimately won't matter (but would still be nice!).
This also applies to Healing.
Script -> OnObjectBeginAttack((GUIDSTRING)_Defender, (GUIDSTRING)_AttackerOwner, (GUIDSTRING)_Attacker)
We currently have a way to determine when someone attacked something, and when a skill was used. We have statuses to hook into when skills hit, so we are missing a way to determine when an attack is started. This is important because it gives us a hook that we can use to override an attack with something else. There are a ton of uses for this, from status effects to replacing what a "basic attack" does.
Script -> CharacterReceivedDamage((GUIDSTRING)_Source, (CHARACTERGUID)_Target, (INTEGER)_Damage, (STRING)_DamageType)
Rimevan has already commented on this one, just re-listing it to keep it in everyones mind. This call was especially neglected, originally only listing _Target.
Script -> CharacterVitalityChanged((CHARACTERGUID)_Target, (REAL)_Percentage, (INTEGER)_Amount)
Bugfix, imo. Percentage should be a REAL, not an Integer. Also, RecievedDamage returns an exact amount, but VitalityChanged does not. This seems arbitrarily limiting.
Script -> CharacterRecievedHealing((GUIDSTRING)_Source, (CHARACTERGUID)_Target, (INTEGER)_Amount[, (STRING)_ArmorType])
Making this consistent with RecievedDamage. This call doesn't exist, but it would really be nice as VitalityChanged alone isn't enough design space due to it being an inaccurate percentage. What sets this apart from VitalityChanged is this is specifically a result of someone healing something, not an arbitrary change in health.
Script -> CharacterArmorRecievedDamage((CHARACTERGUID)_Target, (GUIDSTRING)_Source, (INTEGER)_Damage, (STRING)_DamageType, (STRING)_ArmorType)
We currently have no way to know if armor was damaged. Following suit with _ArmorType
Script -> GetStatusDuration((GUIDSTRING)_Target, (STRING)_Status, [OUT](REAL)_Duration)
Script -> GetSurfaceGroundLifetime((GUIDSTRING)_Target, [OUT](REAL)_Lifetime)
Script -> GetSurfaceCloudLifetime((GUIDSTRING)_Target, [OUT](REAL)_Lifetime)
Script -> GetSurfaceGroundSize((GUIDSTRING)_Target, [OUT](INTEGER)_Size)
Script -> GetSurfaceCloudSize((GUIDSTRING)_Target, [OUT](INTEGER)_Size)
For DoT playstyles, these calls would be amazing. Being able to create playstyles around feeding a surface or status and then exploding it based on size and/or duration sounds like a fun playstyle. Note that I am aware Shouts can consume surfaces with increasing potency of effects, but there is no way to Query for the "scale" of that effect, so there is no way to create an instance of remote damage with it.
Script -> ApplyDamage((GUIDSTRING)_Source, (GUIDSTRING)_Target, (INTEGER)_Damage, (STRING)_DamageType[, (INTEGER)_DamageRange, (STRING)_AbilityType), (INTEGER)_CanCriticalHit, (INTEGER)_CanMiss])
Script -> ApplyDamageMod((GUIDSTRING)_Source, (GUIDSTRING)_Target, (INTEGER)_DamageMod, (STRING)_DamageType[, (INTEGER)_DamageRange, (STRING)_AbilityType), (INTEGER)_CanCriticalHit, (INTEGER)_CanMiss])
Script -> ApplyDamageWeaponMod((GUIDSTRING)_Source, (GUIDSTRING)_Target, (INTEGER)_DamageMod, (STRING)_DamageType[, (INTEGER)_DamageRange, (STRING)_AbilityType), (INTEGER)_CanCriticalHit, (INTEGER)_CanMiss])
Script -> ApplyDamagePercent((GUIDSTRING)_Target, (GUIDSTRING)_Source, (REAL)_Percent, (STRING)_DamageType)
Rehashed this greatly from the forum post and expanded on it now that I know what's going on. These 4 calls will be integral for more advanced effects and I already have a ton of use cases for these (Hybrid Damage, Damage Over Time, Scaling Damage). Ultimately, the ideal world to me is one where all damage calculation can happen in script and be variable.
I specifically am trying to not include a "Skill" definition here because I want these damage attributes to be variable, not static.
The last one is more to give us a percentile option that follows the semantics of dealing damage (It's dealt TO something, BY something). I wouldn't expect this one to scale, only to attribute the damage properly.
Script -> ApplyHeal((GUIDSTRING)_Source, (GUIDSTRING)_Target, (INTEGER)_Heal[, (INTEGER)_CanCriticalHit, (STRING)_ArmorType]);
Script -> ApplyHealMod((GUIDSTRING)_Source, (GUIDSTRING)_Target, (INTEGER)_HealMod[, (INTEGER)_CanCriticalHit, (STRING)_ArmorType]);
Script -> ApplyPercentHeal((GUIDSTRING)_Source, (GUIDSTRING)_Target, (REAL)_Percent[, (STRING)_ArmorType]);
Same semantics as above. A flat heal, one that reflects the heal table, and one that is capable of percentile. I want these specifically so I can modify values dynamically, which I cannot do with something like REGENERATION. Is _ArmorType isn't possible, then just expand these 3 methods into 9, with each batch targeting health/armor/magic.
Script -> ApplyStatus((GUIDSTRING)_Source, (GUIDSTRING)_Target, (STRING)_Status, (REAL)_Duration)
Script -> AddStatusDuration((GUIDSTRING)_Target, (STRING)_Status, (REAL)_Duration)
Script -> AddSurfaceGroundDuration((GUIDSTRING)_Target, (REAL)_Duration)
Script -> AddSurfaceCloudDuration((GUIDSTRING)_Target, (REAL)_Duration)
Script -> CharacterSetCooldown((CHARACTERGUID)_Character, (STRING)_Skill, (INTEGER)_Cooldown)
Script -> CharacterDied((GUIDSTRING)_Source, (CHARACTERGUID)_Target)
Script -> CharacterDie((GUIDSTRING)_Source, (CHARACTERGUID)_Target, (INTEGER)_GenerateTreasure, (STRING)_DeathType)
Adding a source to all these calls. (I specifically added Died/Die for Executioner interaction and it's like). Also added two new ones to further expand combat. AddStatusDuration to let us better manipulate statuses and a way to influence individual cooldowns.Tags
Tags have so much potential, and giving love to them would be the second thing I most want from this post. Tag support is single handedly destroying the (imo) true solution to bridging the gap between the game and script.
Stats -> Weapons/Armor/Etc
Tag support doesn't exist here, despite there being a tag field. I have heard rumor that this was planned, and likely didn't make it in time. If the editor is going to see patches, seeing this patched would be amazing, especially if...
Stats -> Runes
Socketing tagged runes should cause the item they were slotted in to receive the tag. This would let us key so many effects off gear without having to rely on hacky status solutions, and I would also imagine tags are far, far lighter weight.
Script -> GetObjectTagIterator((GUIDSTRING)_Object, [OUT](STRING)_Tag)
This single query used in conjunction with ItemEquipped/ItemUnEquipped and proper tag support would be huge. We could build proc tables and make combat so much more dynamic, like a rune that causes you to cast a fireball at anyone who is FireLash'd (as an example).Compatibility
Rather than re-hash what has already been discussed about this topic, I am instead going to link to http://larian.com/forums/ubbthreads.php?ubb=showflat&Main=77048&Number=626850#Post626850
With the advent of Add-ons, mod compatibility is now something that needs to be given more of a spotlight, otherwise we will have to rely on 3rd party support which will segment the modding community further and cause installing mods to become more difficult.Abilities
Polymorph -> data.txt
Polymorph has no entry in data.txt to adjust the bonuses it provides like every other ability.
Alright, so I've been meaning to bring this up for awhile. It seems very limiting that damage modifiers are only attached to abilities, and not as stand-alone stats. We could do so much more with progression if the idea of Pyrokinetic and +%Fire Damage were separated into separate values. I hate that these two ideas are coupled. It really limits design space, makes itemization more difficult, and is abrasive to modding. I want to be able to give players bonuses without allowing them to increase their skill in something. If we had this, modders would be able to customize what attributes did very easily for very little cost.
The Benefits of Rendering from a List
Abilities, Civics, and Weapon Abilities should be rendered from a String Array internally (or perhaps some ENUM array), and that ENUM Array should be exposed to us. Most of the framework for this is in place already. Abilities are referenced via String in Requirements, Queries, and most other places I have seen. We just need a way to push and pop entries.
If you could just do this one thing, it would allow modders to add custom abilities and hook into the existing framework you have built around them. It likely wouldn't be THAT simple, but I also think this would be a huge gain for little work.
AddAbilityDefinition((STRING)_AbilityName, (STRING)_AbilityDescription, (STRING)_Category)
OnAbilityValueChange((CHARACTERGUID)_Learner, (STRING)_Ability, (INTEGER)_Value)
Elemental Affinity -> data.txt
Elemental Affinity has no entry in data.txt to adjust the bonus it provides like most other talents.
The Benefits of Rendering from a List
Similar to Abilities, Talents need to be rendered from some keyed list that we can modify. Fields that interact with Talents are, again, strings. It honestly feels like Larian was moving both Talents and Abilities towards being moddable, and honestly it looks like it's so close to being complete. All we need is, assuming Talents are rendered from a list, the following:
AddTalent((STRING)_TalentName, (STRING)_TalentDescription, (STRING)_LearnEvent)
OnTalentEvent((CHARACTERGUID)_Learner, (STRING)_LearnEvent, (INTEGER)_WasRemoved)
If you made it this far, then all I can say is thank you for reading. I tried to only include what I thought was specifically limiting design space and seemed fairly straight-forward to address given the current systems in the game. Feel free to question any of this or add your own.