Skip to content

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

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

  1. Close the editor completely. A new UPROPERTY cannot go through Live Coding.
  2. Build the LyraEditor target, then check the end of the build log, not only the exit code.
  3. 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:

  1. In MyGame/Battle, right-click and choose Blueprint Class, pick GameplayEffect, and name it GE_MyGame_CritDamageTest.
  2. Open it and set Duration Policy to Infinite.
  3. Click + beside Modifiers. Set Attribute to XenoCoreStatSet.CriticalDamage, Modifier Op to Add, and the Scalable Float Magnitude Value to 0.5.
  4. Click Compile, then Save.

Then give it a permanent home:

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

  1. Press Play in Maps/L_XenoCore_Gym.
  2. In the console, enter Log LogXenoDamage Verbose.
  3. Enter AbilitySystem.Effect.Apply GE_MyGame_CritDamageTest.
  4. Fight a minion until CRITICAL HIT! appears and the damage number turns gold.
  5. 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