Skip to content

Curve tables

Designer · reference · No code

XenoCore keeps its level-driven numbers in five curve tables in Battle/Attributes/. Each row is a curve: the X axis is a level (or a level difference) and the value is read at that X. Nothing in C++ holds these numbers, so balancing is a data job.

Table X axis What reads it What it controls
CT_XenoCore_PartyStatlines Character level Each sample character's own stats effect (GE_XenoCore_BaseStats_Kael ... _Tamsin) That character's Max HP, Strength, Ether, Agility: rows <Name>.<Stat>, read at ×1.0
CT_XenoCore_BaseStats Character level The stock members' stats effects (GE_XenoCore_BaseStats_Shulk, _Party_*) Their Max HP, Strength, Ether, Agility: shared rows with a coefficient per character
CT_XenoCore_EnemyStats Enemy level The enemy stats effects (GE_XenoCore_BaseStats_Minion_Melee, _Minion_Skirmisher, _Minion_Bulwark, _Minion_Brute, GE_XenoCore_BaseStats_Dummy; the other types scale the same rows by a multiplier on their effect), and the enemy's Experience Level Scale Enemy Max HP, Strength, Ether, Agility, defences; how kill EXP grows with the enemy's level
CT_XenoCore_RewardLevelDiff Party level minus enemy level Game/BP_XenoCore_Progression › Reward Multiplier Curve EXP multiplier for a kill
CT_XenoCore_DamageLevelDiff Attacker level minus target level Battle/Attributes/BP_XenoCoreDamageExecution › Level Difference Curve Damage multiplier by level gap

EXP needed per level is not a curve table

It is the Data Table Game/DT_XenoCore_ExperienceCurves, one row per curve, picked per character. See Tune levelling.

How a table is read

There are two ways, and they fail differently.

Stats tables (BaseStats, EnemyStats). A stats Gameplay Effect has one modifier per attribute. Each modifier's magnitude is a scalable float: a number multiplied by a curve-table row evaluated at the effect's level. The character's level is passed as the effect level, so the row is read at that level. The effect is picked per character by the pawn's Stats Effect property (XenoCore › Combatant on party pawns, XenoCore › Enemy on enemies).

Fails silently: the number in the table is not always the number in the game

A scalable float multiplies its curve by its own coefficient. If the coefficient is not 1.0, the game uses a different number from the one you see in the table, and nothing says so. The shipped minion types use this on purpose: the melee minion reads its rows at 1.0, and the Brute (1.45), Bulwark (1.15) and Skirmisher (0.9) scale the same rows. Before you rebalance a row, open the effect that reads it and check the coefficient on each modifier.

Lookup tables (RewardLevelDiff, DamageLevelDiff). C++ looks the row up by name on the table assigned in a Blueprint (Reward Multiplier Curve, Level Difference Curve) and evaluates it at X.

If… You get Logged?
The table property is empty Reward and damage multipliers read 1.0. No
The table is set but the row name is wrong Same fallbacks as above Yes, a warning naming the row

Fails silently: an empty curve property

If Reward Multiplier Curve is cleared, every kill pays its flat reward. If Level Difference Curve is cleared, level gaps stop affecting damage. Neither logs anything. Keep a table assigned, even if you flatten its values.

The rows

Row names must match exactly: C++ or the stats effects refer to them by name.

CT_XenoCore_BaseStats

Shulk.MaxHealth
Shulk.Strength
Shulk.Ether
Shulk.Agility

One set of rows, read by every stock party member's stats effect (GE_XenoCore_BaseStats_Shulk and the eleven _Party_* effects). Members differ only through their own stats effect: each _Party_* effect multiplies the same rows by its own coefficient per stat (between 0.8 and 1.2 when last measured), so the number in the table is not the number that member gets.

Scheduled for renaming

These row names are placeholders and will be renamed before release. When that happens every stats effect that reads them is updated in the same change. If you add your own character, add your own rows (for example MyHero.Strength) rather than editing these.

Physical and Ether Defence, critical rate and block rate are not in this table: party members have none of their own, and all of it comes from equipment.

CT_XenoCore_EnemyStats

Minion_Melee.MaxHealth      Minion_Melee.Strength     Minion_Melee.Ether
Minion_Melee.Agility        Minion_Melee.PhysicalDefence  Minion_Melee.EtherDefence
Minion_Melee.CharacterLevel
Minion_Ranged.*   Minion_Siege.*   Minion_Super.*   (same seven stats each)
Minion.ExperienceScale

Enemy stats grow linearly with level. The melee minion's HP and Strength follow an XC1 field enemy's line (Max HP 900 + 100 × (level − 1), Strength 69 + 10 × (level − 1)). Only the Minion_Melee.* rows are read by a shipped stats effect: all seven sample enemy types read them, through their effect's multiplier. The Minion_Ranged, Minion_Siege and Minion_Super rows are not read by anything. *.CharacterLevel is not read by any effect.

Minion.ExperienceScale is read by the minion's Experience Level Scale (XenoCore › Enemy): the kill's Experience Reward is multiplied by this row at the enemy's level, so a same-level kill is worth about the same share of a level all the way up. An enemy with the property empty pays a flat reward.

Defence means different things on the two sides

On an enemy, PhysicalDefence and EtherDefence are fractions (0 to 0.9): damage is multiplied by 1 − defence. On a party member the same attributes are flat points subtracted from the attacker's stat. Author enemy defence rows as fractions. The execution clamps a fraction to Max Defence Fraction (default 0.9).

The level cap

The level cap is not any table's length: it is MaxCharacterLevel on UXenoCoreStatSet, default 99. It is a config value, so you can raise it without touching C++. Add this to your project's Config/DefaultGame.ini:

[/Script/XenoCoreRuntime.XenoCoreStatSet]
MaxCharacterLevel=120

Then extend the stats rows to the new cap (past its last key a stats curve holds its final value), and lengthen every row of the Data Table DT_XenoCore_ExperienceCurves. A character whose EXP row is shorter than the cap stops levelling at the end of the list; validating the table warns about a row shorter than the cap.

CT_XenoCore_RewardLevelDiff

Reward.LevelDiffMultiplier

X is party level minus enemy level. A positive X (you out-level the enemy) should give a multiplier below 1; a negative X above 1. Keys run from −20 to +20; outside that the curve's end values hold.

CT_XenoCore_DamageLevelDiff

Damage.LevelDiff.Physical
Damage.LevelDiff.Ether

X is attacker level minus target level. The execution picks the Ether row for ether attacks and the Physical row otherwise, and multiplies damage by the result.

Editing a table

  1. Duplicate the table into your own folder, for example Content/MyGame/Data/.
  2. Open the duplicate. Edit values in the curve table editor, or add rows with the same naming pattern.
  3. Point the reader at your duplicate: the stats effect's modifiers, or the Blueprint property in the table above.

Keep row names when you duplicate

The lookup tables are found by row name (Reward.LevelDiffMultiplier, Damage.LevelDiff.Physical, Damage.LevelDiff.Ether). A duplicate with renamed rows falls back to the defaults above, with only a warning in the log.

The shipped tables were imported from CSV files that are not part of the template, so Reimport will not find a source. Unreal then asks whether to specify a new source file: Yes lets you pick your own CSV, No skips the reimport. Edit the tables in the editor, or export to CSV and reimport from your own file.