TOME currently uses two different functions to check for success in opposed roll situations, such as one skill countering another. These cases come up extensively in such things as melee attacks, spell saving throws, stealth and invisibility, detecting and disarming traps, etc. For most to-hit checks, the function checkHit compares offense (atk) verses defense (def) with the following formula:
HitChance = 50 + 2.5 * (atk - def)
This function is simple and linear, but needs hard limit checks to be robust, and is unforgiving of wide differences in offense versus defense. That is, the chance of success quickly vanishes or tops out once the difference between atk and def approaches +/-20 (subject to hard-coded limits). This is an important limitation because it allows certain combinations of npc (and player) talents to become nearly invincible. A random boss with several defense bonuses can become almost completely unhittable by a melee-based character, for example.
The second function, checkHitOld works in almost the same way, but uses an exponential function to compute success chance:
a = 1/(1 + exp(-(atk - def)/7))
b = atk/(atk + def)
HitChance = 50 * (a + b)
with limit checks to avoid singularities when atk = def. This function is smoother and more forgiving, but is computationally more expensive (not really an issue here) and starts to break down at low defense values -- it cannot tolerate atk and def <= 0. The function behaves differently in the early game than in the late game because the balance between atk and def is significantly affected by the magnitude of the values involved. Both functions require limit checks (default 5% and 95%) to keep success probabilities reasonable throughout the normal range of play.
As an alternative to these functions, I propose a new checkHit function (that I call checkHitSmooth) that combines the best features of both of the current formulas:
HitChance = 50 + 50 * (atk - def)/(abs(atk - def) + m)
m = constant > 0 (default 10)
This function is simple, both mathematically and computationally, is inherently bounded (it always gives a hit chance between 0% and 100% without limit checks needed, and scales well with all character levels. The tuning paramer, m, directly affects the slope of the line passing through (50%,0). Higher values flatten out the curve, while smaller ones steepen it near the middle. The following charts plot a comparison of the three functions (with the limit checks in place for checkHit and checkHitOld): The stability of the new function is obvious. Since it depends only on the difference between atk and def, and not their absolute values, it maintains its integrity at any level, preserving game balance from character level 1 to level 50 (or beyond). It tolerates any conceivable range of values. (Since everyone really wants to know that -1,000,000 atk versus +10,000 def has a 0.000495% chance to succeed, when m = 10.) By tolerating negative numbers, it opens up the option of having negative offensive stats, particularly in the early game where debuffs are currently limited by small combat values.
Further, the new routine (below) is a completely compatible drop-in replacement for the current checkHit function, without any other changes required in the game:
Code: Select all
-- I5 updated to smooth out hit probabilities and to be more forgiving of level differences
function _M:checkHit(atk, def, min, max, factor)
local min = min or 0
local max = max or 100
if game.player:hasQuest("tutorial-combat-stats") then
min = 0
max = 100
end --ensures predictable combat for the tutorial
local hit = 50+50*(atk-def)/(math.abs(atk-def)+10) -- tunable parameter in denominator 10> ~ 16.7% hit at -20 atk-def
hit = util.bound(hit, min, max) -- Limit checks aren't needed with the new formula. Left in for compatibility
print("checkHit(smooth)",atk,def,"=> chance to hit", hit)
return rng.percent(hit), hit
end
By changing the constant, m, checkHitSmooth can be tuned to more closely mimic either of the existing checkHit and checkHitOld functions. With m=20, it gives nearly identical results to checkHitOld, but without the deterioration at low values: I have tested this function extensively in my Infinite500 addon, which requires that a large range of values be tolerated, sometimes as much as +/-100 deep in the I.D. Replacing both existing functions with this one, I have used m=10 as the tuning parameter as a compromise between the more forgiving checkHitOld and maintaining consistency in game play with the more widely used existing (linear) checkHit function. I think this has worked out especially well, since tactical uncertainty is increased: improbable events do occur requiring you to stay on your toes and you always have a reasonable chance to land an attack.