Custom curves
Customize the level progression formula with presets or custom code.
By default, the XP plugin uses a quadratic formula to determine how much XP each level requires:
XP(level) = 5 * level² + 50 * level + 100This means early levels are quick to earn and later levels require progressively more XP. You can replace this with any of the four built-in presets or supply your own formula via code.
Preset curve types
All presets support an optional maxLevel cap to prevent progression beyond a certain level.
Quadratic
Formula: XP = a * level² + b * level + c
import { config } from '@robojs/xp'
// Steeper progression for large guilds
await config.set(guildId, {
levels: {
type: 'quadratic',
params: { a: 10, b: 100, c: 200 },
maxLevel: 100
}
})
// Gentler progression for small guilds
await config.set(guildId, {
levels: {
type: 'quadratic',
params: { a: 2, b: 20, c: 50 }
}
})import { config } from '@robojs/xp'
// Steeper progression for large guilds
await config.set(guildId, {
levels: {
type: 'quadratic',
params: { a: 10, b: 100, c: 200 },
maxLevel: 100
}
})
// Gentler progression for small guilds
await config.set(guildId, {
levels: {
type: 'quadratic',
params: { a: 2, b: 20, c: 50 }
}
})Higher a values make late-game progression steeper. Adjust b and c to tune early levels.
Linear
Formula: XP = level * xpPerLevel
await config.set(guildId, {
levels: {
type: 'linear',
params: { xpPerLevel: 100 }
}
})await config.set(guildId, {
levels: {
type: 'linear',
params: { xpPerLevel: 100 }
}
})Every level requires the same amount of XP. Good for reputation systems, currencies, or non-competitive tracks.
Exponential
Formula: XP = multiplier * base^level
await config.set(guildId, {
levels: {
type: 'exponential',
params: { base: 2, multiplier: 100 },
maxLevel: 50
}
})await config.set(guildId, {
levels: {
type: 'exponential',
params: { base: 2, multiplier: 100 },
maxLevel: 50
}
})Exponential growth gets steep fast. Always set a maxLevel to prevent overflow at high levels.
Lookup table
Define exact XP thresholds per level with an array.
await config.set(guildId, {
levels: {
type: 'lookup',
params: { thresholds: [0, 100, 250, 500, 1000, 2000, 5000, 10000] }
}
})await config.set(guildId, {
levels: {
type: 'lookup',
params: { thresholds: [0, 100, 250, 500, 1000, 2000, 5000, 10000] }
}
})The array index is the level, and the value is the total XP required. maxLevel defaults to thresholds.length - 1 when not specified. Thresholds must be sorted in ascending order.
This gives pixel-perfect control over each level's requirement. Useful for seasonal passes and event-specific tuning.
Dynamic curves with getCurve
For logic that can't be expressed with static presets, define a getCurve callback in your plugin config. This has the highest precedence in the resolution chain:
getCurve callback → guild preset → default quadraticimport type { PluginOptions } from '@robojs/xp'
export default {
levels: {
getCurve: (guildId, storeId) => {
if (storeId === 'reputation') {
return {
xpForLevel: (level) => level * 500,
levelFromXp: (xp) => Math.floor(xp / 500)
}
}
return null // Fall through to guild preset or default
}
}
} satisfies PluginOptionsexport default {
levels: {
getCurve: (guildId, storeId) => {
if (storeId === 'reputation') {
return {
xpForLevel: (level) => level * 500,
levelFromXp: (xp) => Math.floor(xp / 500)
}
}
return null // Fall through to guild preset or default
}
}
}Return null to fall through to the next level in the resolution chain. The callback can be synchronous or asynchronous.
Use cases for getCurve
Different curves per store:
import type { PluginOptions } from '@robojs/xp'
export default {
levels: {
getCurve: (guildId, storeId) => {
if (storeId === 'coins') {
return {
xpForLevel: (level) => level * 50,
levelFromXp: (xp) => Math.floor(xp / 50),
maxLevel: 999
}
}
return null
}
}
} satisfies PluginOptionsexport default {
levels: {
getCurve: (guildId, storeId) => {
if (storeId === 'coins') {
return {
xpForLevel: (level) => level * 50,
levelFromXp: (xp) => Math.floor(xp / 50),
maxLevel: 999
}
}
return null
}
}
}Dynamic curves based on guild size:
import { getClient } from '@robojs/discordjs'
import type { PluginOptions } from '@robojs/xp'
export default {
levels: {
getCurve: async (guildId) => {
const client = getClient()
const guild = await client.guilds.fetch(guildId)
if (guild.memberCount > 1000) {
return {
xpForLevel: (level) => 10 * level * level + 100 * level + 200,
levelFromXp: (xp) => Math.floor((-100 + Math.sqrt(10000 + 40 * (xp - 200))) / 20)
}
}
return null
}
}
} satisfies PluginOptionsimport { getClient } from '@robojs/discordjs'
export default {
levels: {
getCurve: async (guildId) => {
const client = getClient()
const guild = await client.guilds.fetch(guildId)
if (guild.memberCount > 1000) {
return {
xpForLevel: (level) => 10 * level * level + 100 * level + 200,
levelFromXp: (xp) => Math.floor((-100 + Math.sqrt(10000 + 40 * (xp - 200))) / 20)
}
}
return null
}
}
}LevelCurve interface
Custom curves must implement this interface:
interface LevelCurve {
xpForLevel: (level: number) => number // Total XP needed to reach this level
levelFromXp: (totalXp: number) => number // Level for a given total XP (inverse)
maxLevel?: number // Optional level cap
}levelFromXp must be the mathematical inverse of xpForLevel. Incorrect inverses cause level calculation errors.
Resolution and caching
Curves resolve lazily on first XP operation per (guildId, storeId) pair and are cached in-memory. Subsequent operations use the cached curve with sub-millisecond lookups.
Cache is invalidated when guild config changes. If you update a getCurve callback, restart the Robo to clear the cache.
Default curve reference
| Level | XP for level | Total XP |
|---|---|---|
| 1 | 155 | 155 |
| 5 | 475 | 1,675 |
| 10 | 1,100 | 5,675 |
| 20 | 3,100 | 25,175 |
| 50 | 15,100 | 260,675 |
| 100 | 55,100 | 1,838,175 |
