Add a stat to the damage formula¶
Programmer · 60 min · C++
Result: a new character stat, Critical Damage, that adds to the critical-hit multiplier. A character with 0.5 Critical Damage lands crits at ×1.75 instead of ×1.25.
Designers: try a skill first
Adding a new stat to the formula needs C++; there is no editor-only route. But crit damage
itself needs none: a skill whose Bonus is
XenoCore.SkillBonus.CriticalDamage already adds that many percent to the crit multiplier. See
Add a skill. Hand this page to a programmer only when equipment or effects must
carry a stat that does not exist yet.
Before you start¶
- A working C++ build of the project. See Troubleshooting: build and editor.
- Stats and damage: the formula and what each stat means.
- Set a character's statline: how stats reach a character.
How the formula is put together¶
Two C++ classes own the maths, both in Plugins/GameFeatures/XenoCore/Source/XenoCoreRuntime:
| Class | File | Job |
|---|---|---|
UXenoCoreStatSet |
Public/AbilitySystem/Attributes/XenoCoreStatSet.h |
Holds every stat as a gameplay attribute (set by gameplay effects): Strength, Ether, Agility, the defences, Critical Rate, Block Rate, Weight, weapon attack, level, Talent Gauge |
UXenoCoreDamageExecution |
Public/AbilitySystem/Executions/XenoCoreDamageExecution.h |
Reads those stats and computes the damage |
The execution runs inside Battle/Attributes/GE_XenoCore_Damage, through the Blueprint child
BP_XenoCoreDamageExecution (which only sets the level-difference curve). Two formulas are used:
- Party attacker:
floor((stat + weapon) × art × (1 − def) × mods × lvl × block), with the target's defence as a fraction capped at Max Defence Fraction (0.9). - Enemy attacker:
floor(max(stat − def, 0) × art × mods × lvl × block), with the target's defence as flat points.
UXenoCoreDamageExecution::GetCombatSide decides the side by what the attacker is (an
AXenoCoreEnemyCharacter, a character with a member definition,
then its team), never by whether a
player is driving it. AI companions and the leader use the same formula.
block is Block Multiplier on a blocked hit, 0 by default, so a block negates the hit
(0.5 restores XC1's halving), and 1 otherwise.
mods holds the crit multiplier (and any first-strike bonus), floored at Minimum Modifiers
(0.1). That is where the new stat goes.
Two routes¶
Both classes are exported, so a C++ class in your module can derive from either. What the
formula lacks is a hook: Execute_Implementation computes the whole result in one function, and
no term can be overridden on its own.
| Route | What you do | Cost |
|---|---|---|
| Edit the formula in place (the steps below) | Add the attribute to UXenoCoreStatSet and a few lines to UXenoCoreDamageExecution |
Small, but it edits plugin source. Keep it as a patch and reapply it after template updates |
| Keep it in your module | Put the attribute on your own attribute set (a child of UXenoCoreStatSet or of ULyraAttributeSet) granted in your ability sets, and derive your own execution from UXenoCoreDamageExecution, overriding Execute_Implementation |
Nothing of XenoCore's is edited, but your override must reproduce the whole formula, and GE_XenoCore_Damage must use your execution (a copy of the effect, or BP_XenoCoreDamageExecution reparented) |
For one small term such as this one, the in-place edit is less to maintain. Choose the second route when you are replacing the formula outright.
Steps¶
1. Add the attribute¶
In XenoCoreStatSet.h, add an accessor beside the others:
ATTRIBUTE_ACCESSORS(UXenoCoreStatSet, CriticalDamage);
and the attribute itself in the private: block, following the existing pattern:
// Added to the crit multiplier. 0 = no bonus.
UPROPERTY(BlueprintReadOnly, Category = "XenoCore|Stats", Meta = (AllowPrivateAccess = true))
FGameplayAttributeData CriticalDamage;
In XenoCoreStatSet.cpp, initialise it in the constructor list with CriticalDamage(0.0f).
You do not need to add a clamp. ClampAttribute sends every attribute it does not name to its last
branch, which keeps the value at zero or above.
2. Capture it in the execution¶
In XenoCoreDamageExecution.cpp, inside the FStatics struct, add a capture definition. Build it
from the public accessor, as the others are; the capture macro does not compile against the set's
private members:
FGameplayEffectAttributeCaptureDefinition CriticalDamageDef;
// in FStatics():
CriticalDamageDef = { UXenoCoreStatSet::GetCriticalDamageAttribute(), ESrc::Source, true };
ESrc::Source with true snapshots the attacker's value when the attack starts, which is how every
other attacker stat is captured. Then register it in the constructor:
RelevantAttributesToCapture.Add(XenoCoreDamage::Statics().CriticalDamageDef);
3. Use it¶
Find the critical-hit block in Execute_Implementation (it sets Modifiers to
CriticalMultiplierVsReacting or CriticalMultiplier, then adds the Critical Hit Plus skill
bonus). Read the new stat and add it after those lines, still inside the block:
float CritBonus = 0.0f;
ExecutionParams.AttemptCalculateCapturedAttributeMagnitude(
XenoCoreDamage::Statics().CriticalDamageDef, Params, CritBonus);
Modifiers += CritBonus;
Add it to the UE_LOG(LogXenoDamage, Verbose, ...) line as well, so you can see it.
4. Build¶
- Close the editor completely. A new
UPROPERTYcannot go through Live Coding. - Build the
LyraEditortarget, then check the end of the build log, not only the exit code. - Open the editor.
5. Give a character some¶
A stat that nothing sets is always 0. Give the player some with a test effect first:
- In
MyGame/Battle, right-click and choose Blueprint Class, pickGameplayEffect, and name itGE_MyGame_CritDamageTest. - Open it and set Duration Policy to Infinite.
- Click + beside Modifiers. Set Attribute to
XenoCoreStatSet.CriticalDamage, Modifier Op to Add, and the Scalable Float Magnitude Value to0.5. - Click Compile, then Save.
Then give it a permanent home:
- Per character: add a modifier to that character's statline effect. See Set a character's statline.
- From equipment: see Add a stat to equipment, section "A brand-new stat". Equipment needs no code for it: one row on the stats effect.
6. Show it on the character sheet (optional)¶
The character sheet and equipment screen read one table,
UXenoCoreInventoryStatics::GetStatDefinitions() in Private/Inventory/XenoCoreInventoryStatics.cpp.
Add a Row(...) for your attribute. Rows need a XenoCore.Stat.* tag only if an item can grant the
stat; pass FGameplayTag() otherwise. Set the last flag (character sheet) to true. The table is
built once per process, so this needs a full rebuild and a fresh editor.
Test it¶
- Press Play in
Maps/L_XenoCore_Gym. - In the console, enter
Log LogXenoDamage Verbose. - Enter
AbilitySystem.Effect.Apply GE_MyGame_CritDamageTest. - Fight a minion until CRITICAL HIT! appears and the damage number turns gold.
- In the Output Log, the damage line for that hit reads
mods=1.75.
A character without critical rate never crits. If none appear, give the test effect a Critical Rate
modifier too (Add 1.0 for a crit every hit).
If it doesn't work¶
Fails silently: the stat reads 0 on the attacker
The character has no UXenoCoreStatSet at all, or nothing sets the new attribute. Stats arrive
only through gameplay effects. Check the attribute with showdebug abilitysystem.
| Symptom | Fix |
|---|---|
| The editor compiles but the stat is missing from the attribute list | Live Coding patched the code but not the reflection data. Close the editor, confirm UnrealEditor.exe is no longer running, and do a full build. |
Crits read mods=1.25 with the test effect applied |
The capture was added to FStatics but not registered in RelevantAttributesToCapture, or the read sits outside the critical-hit block. |
| No crits at all | The character has no Critical Rate. Add a Critical Rate modifier to the test effect. |
| The stat does not show on the character sheet | Step 6 needs a full rebuild and a fresh editor; the table is built once per process. |
Going further¶
- Stats and damage.
- Add a stat to equipment.
- Subclass a mechanic: setting up your own C++ module for the second route.