How do I calculate/weight a diminishing damage formula based on N targets?

I'm making a game and had a look at the questions about calculating damage/diminishing returns here and here but I haven't quite been able to come up with a logarithmic function that satisfies this goal - which is probably super obvious to those mathematically minded of you (apologies if it is!).

The goal is:

• deal half of the damage total to the first target
• deal a diminishing amount of damage for each target beyond the first
• do not deal more damage than is input into the function
• bonus: create a linear downward curve

An example (what I've got so far):

fn = (amount, count) => {
let d = amount;
let r = 0.5;
let a = [];

for (i = 0; i < count; i++) {
a.push(d * r);
r -= r/(count-i);
}

a.push(a.reduce((acc, cur) => acc + cur)); //push the total combined damage output as last result of array
return a;
}


You can probably spot the problem! Given 300 damage and 3 targets (fn(300, 3)), this outputs the following: [150, 100, 50, 300] - which is great. The last number is just the total amount of damage being dealt, which with these inputs matches the amount fed into the function.

Given 4 targets and 400 damage, the problem is exposed with the output being: [200, 150, 100, 50, 500] - the damage output is 100 more than the input amount.

This is not a game breaking problem for me and it's also not that important that the downward curve be linear, but if there is a way to achieve that result that would be awesome and I would greatly appreciate any input!

Thanks for your time!

2 Answers

If you want a linear decrease in damage, then the total damage can be described as the area of a right triangle formed by a sloped line graph: the height is the max damage, the length is the number of targets, and the hypotenuse corresponds to the sequence of damage numbers seen.

The area of a right triangle is w × h / 2. So solving that gives our first intermediate result: h = 2 × totalDamage / numberOfTargets. Then, we can pick damages evenly along that sequence (just a linear interpolation from zero):

damageToTarget[i] = h × ((i + 0.5) / numberOfTargets)

The "+ 0.5" is needed in order to sample the line at the right points to sum up to exactly the total and not get any of the real-number-math equivalent of off-by-one errors.

Test program, in Python, and its output:

from __future__ import division

def damages(total, count):
scale = 2.0 * total / count
return [scale * (i + 0.5) / count for i in range(0, count)]

def test_case(total, count):
d = damages(total, count)
print(total, d, sum(d))

test_case(300, 3)
test_case(400, 4)
test_case(10, 10)
test_case(65, 1)

(300, [33.333333333333336, 100.0, 166.66666666666666], 300.0)
(400, [25.0, 75.0, 125.0, 175.0], 400.0)
(10, [0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5, 1.7, 1.9], 10.0)
(65, [65.0], 65.0)


A completely different strategy, which works for arbitrary damage curves, is this:

1. Pick some numbers that make a curve you like the shape of.
2. Sum them up.
3. Multiply each of them by totalDamage / sum.

That is, just “correct the error so it adds up right”. Note that this means that you don't have to pick the numbers based on the total damage: they can have any scale at all, or even be a handmade list of constants, and they will be adjusted to fit.

All of the above strategies will sometimes give you fractions. If that's a problem, I suggest the strategy of, for each target's damage value:

1. Round it up or down as you see fit.
2. Subtract the rounded value from the old value; this gives you the "leftover" damage.
3. Add the leftover to the next target's damage value, before you perform step 1 for it.

This should always end up with zero leftover on the last item (approximately, due to floating point error) so the damage total will still be correct.

• Thank you! This is great. I've marked this as correct as it is very very close to the desired output and achieves the goal of not surpassing the height/total damage - however it doesn't guarantee that the first damage amount is half of the total (and work down from there). Still fiddling in my browser now trying to achieve that result - that goal is somewhat important to be honest! Thanks again for your input and speedy reply – GrayedFox Nov 14 '20 at 16:50
• @GrayedFox Whoops, I missed that requirement. You could do that by simply assigning half the damage to the first target, then use either of the algorithms I wrote on the remaining half (count - 1, damage / 2). – Kevin Reid Nov 14 '20 at 16:59
• Derp. Was trying that and forgot the decrement the count by one, hence getting some unexpected results. Thank you again! – GrayedFox Nov 14 '20 at 17:13

This is the working/final method in JS just in case future eyeballs. Please see/upvote the accepted answer (which this is based on) accordingly :)

fn = (amount, count) => {
// ensure full damage is dealt if only one target
if (count <= 1) return [amount];

count--;
let h = 2 * (amount / 2) / count;
let a = [];

for (i = 0; i < count; i++) {
a.unshift(h * (i + 0.5) / count);
}

// using unshift to populate array from beginning to avoid reversing/remembering to traverse it backwards later on
a.unshift(amount/2);

a.push(a.reduce((acc, cur) => acc + cur));

return a;
}
$$$$
`