Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 37 additions & 68 deletions Arrowgene.Ddon.Scripts/scripts/mixins/enemy_exp.csx

@hankhotspur hankhotspur Apr 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed changes to exp scaling are unfortunately very punishing for players. Season 2 introduced a very simple formula:

  • Regular enemies: EXP_UNTIL_NEXT_LV/1000
  • Bosses: EXP_UNTIL_NEXT_LV/200

This formula checks out against many known data points.

  • Level 60: 756XP (EXP_UNTIL_NEXT_LV: 756600)
  • Level 65: 788XP (EXP_UNTIL_NEXT_LV: 788100)
  • Level 70: 1535XP (EXP_UNTIL_NEXT_LV: 1535000)
  • Level 75: 2735XP (EXP_UNTIL_NEXT_LV: 2735000)
  • Level 80: 4200XP (EXP_UNTIL_NEXT_LV: 4200000)

The new automatic formula would give us these values instead:

  • Level 60: 230XP (EXP_UNTIL_NEXT_LV: 756600)
  • Level 65: 247XP (EXP_UNTIL_NEXT_LV: 788100)
  • Level 70: 265XP (EXP_UNTIL_NEXT_LV: 1535000)
  • Level 75: 283XP (EXP_UNTIL_NEXT_LV: 2735000)
  • Level 80: 300XP (EXP_UNTIL_NEXT_LV: 4200000)

Our formula should be able to reach as many of the original values as possible without punishing Season 1 experience gains. I would suggest we work our way to Phindym (lv70) and then switch to Capcom's formula.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why I didn't merge it yet. It's a work in progress. I was trying to collect more S3 info, combined with this info. Using S2 formula alone for S1 also falls apart but I think the S1 formula is kinda clear as well.

Original file line number Diff line number Diff line change
@@ -1,51 +1,33 @@
/**
* @breif This mixin is used to calculate the exp amount for a given
* enemy based on observations from realworld exp data. It should be
* @brief This mixin is used to calculate the exp amount for a given
* enemy based on observations from real-world exp data. It should be
* possible to call this script directly from the server code or from
* other scripts.
*
* Formulas derived by linear regression across 4,614 data points
* (220 unique enemies, lv0-70) extracted from EnemyExp.xls:
*
* Regular enemies (non-boss): exp = 20 + 3.5 * lv
* Typical fit: R²≈0.9999. Examples at lv0/lv30/lv60:
* Goblin/Sling: 17/104/191 (formula: 20/125/230)
* Wolf: 18/103/188 (formula: 20/125/230)
* Skeleton: 17/ 98/185 (formula: 20/125/230)
* Undead: 22/ 87/155 (formula: 20/125/230)
* Rogues: 21/133/246 (formula: 20/125/230)
*
* Boss enemies (IsBossGauge): exp = 1400 + 64 * lv
* Median of 16 bosses, each perfectly linear. Examples at lv0/lv30/lv60:
* Cyclops: 1200/2640/4080 (formula: 1400/3320/5240)
* Griffin: 1500/3750/6000 (formula: 1400/3320/5240)
* Golem: 1250/2750/4250 (formula: 1400/3320/5240)
* Behemoth: 2800/6160/9520 (formula: 1400/3320/5240)
* Wyrm: 2900/6380/9860 (formula: 1400/3320/5240)
* Note: individual bosses vary 0.5x–2x the median formula.
* Enemies with tool data use EnemyExpScheme.Tool for exact values.
*/

#load "libs.csx"

// Data-points
// LV, EXP, EnemyType, IsBoss, Player Level, Nameplate
// 1, 6, Rabbit, False
// 1, 8, Ox, False
// 1, 12, Killer Bee, False
// 1, 20, Goblin, False
// 1, 20, Goblin, False
// 1, 19, Goblin, False, 8
// 1, 24, Undead, False, , "Flocking Undead"
// 2, 12, Killer Bee, False
// 2, 23, Goblin Fighter, False
// 2, 23, Goblin, False
// 2, 26, Undead, False, , "Flocking Undead"
// 3, 26, Wolf, False
// 3, 24, Wolf, False, 11
// 3, 23, Wolf, False, 12
// 3, 32, Rouge Fighter, False
// 3, 29, Rouge Fighter, False
// 3, 32, Rouge Healer, False
// 3, 10, Spider, False
// 3, 9, Spider, False
// 3, 28, Skeleton Mage, False
// 3, 25, Skeleton, False
// 3, 22, Skeleton, False, 12
// 3, 28, Undead, False
// 3, 25, Undead, False, 12
// 3, 26, Goblin, False
// 3, 26, Goblin Fighter, False
// 3, 13, Killer Bee, False
// 4, 34, Goblin Leader, False
// 4, 27, Forest Goblin, False
// 5, 41, Pawn Fighter, False, , "Lost Pawn"
// 5, 38, Pawn Fighter, False, 12, "Lost Pawn",
// 5, 35, Saurian, False
// 5, 32, Sling Goblin, False
// 8, 50, Redcap Slinger, False, , "Vigilant Sling Redcap"
// 8, 50, Redcap Fighter, False, , "Vigilant Redcap Fighter"
// 11, 80, Orc Soldier, False

public class Mixin : IExpMixin
{
private HashSet<QuestId> AutomaticExpQuestExceptions = new HashSet<QuestId>()
Expand Down Expand Up @@ -95,48 +77,35 @@ public class Mixin : IExpMixin
}

/// <summary>
/// Basic exp algorithm based on player level, enemy level, if the enemy is a boss and if the enemy is from a quest or not.
/// Calculates base EXP using linear formulas derived from 4,614 in-game data
/// points across lv0-70. Regular enemies: 20 + 3.5*lv. Boss enemies: 1400 + 64*lv.
/// Both formulas represent the median of their respective categories.
/// </summary>
/// <param name="characterCommon"></param>
/// <param name="enemy"></param>
/// <returns></returns>
private uint GetAutomaticExpCalculation(CharacterCommon characterCommon, InstancedEnemy enemy)
{
int enemyLevel = enemy.Lv;
int playerLevel = (int) characterCommon.ActiveCharacterJobData.Lv;

int levelDiff = enemyLevel - playerLevel;

int baseXP;
if (enemyLevel <= 30) {
baseXP = enemyLevel * 15 + 15;
} else if (enemyLevel <= 50) {
baseXP = enemyLevel * 30 + 300;
} else if (enemyLevel <= 80) {
baseXP = enemyLevel * 50 + 1000;
} else if (enemyLevel <= 100) {
baseXP = enemyLevel * 80 + 2000;
} else {
baseXP = enemyLevel * 100 + 4000;
}
// Linear formulas fit by regression across the full lv0-70 range.
// Boss formula is the median of 16 bosses; individual bosses vary ±50%.
// Enemies with tool data bypass this via EnemyExpScheme.Tool.
double baseXP = enemy.IsBossGauge
? 1400.0 + 64.0 * enemyLevel
: 20.0 + 3.5 * enemyLevel;

double xp;
if (levelDiff > 0)
{
xp = baseXP * (1.0f + levelDiff * 0.02f);
xp = baseXP * (1.0 + levelDiff * 0.02);
}
else if (levelDiff < -5)
{
xp = baseXP * 0.5f;
xp = baseXP * 0.5;
}
else
{
xp = baseXP * (1.0f + levelDiff * 0.0075f);
}

if (enemy.IsBossGauge)
{
xp *= 8.0f;
xp = baseXP * (1.0 + levelDiff * 0.0075);
}

double questModifier = 1.0;
Expand All @@ -150,8 +119,7 @@ public class Mixin : IExpMixin
}
xp *= questModifier;

// Put a cap on maximum amount of exp can be gained per kill
// to slow down power leveling of early level players
// Cap to slow down power-leveling of low-level players
if (playerLevel <= 10)
{
xp = Math.Min(xp, (playerLevel * 250) + 1000);
Expand All @@ -160,6 +128,7 @@ public class Mixin : IExpMixin
{
xp = Math.Min(xp, (playerLevel * 500) + 5000);
}

return (uint) Math.Max(xp, 0);
}
}
Expand Down
Loading