Solution: exponential formulas

Here is one solution to the problem. The red curve represents the base damage output based on the attack stat, and the green one the damage reduction based on the character’s defense.

I’m using a cube for the attack and a power of 2.2 for the defense so final damage values keep growing exponentially.

The implementation looks like that:

static func calculate_damage(action_data, attacker, defender) -> int:
    var base_damage: float = pow(attacker.attack, 3.0) / 10.0 + 12.0 * attacker.attack + 48.0
    var damage_reduction: float = pow(defender.defense, 2.2)
    var damage: float = base_damage * action_data.damage_multiplier - damage_reduction
    return int(clamp(damage, 0.0, 99999.0))

The action’s damage multiplier gets applied before damage reduction happens, allowing powerful attacks to benefit a lot from it.