1
\$\begingroup\$

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!

\$\endgroup\$
2
\$\begingroup\$

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.

\$\endgroup\$
3
  • \$\begingroup\$ 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 \$\endgroup\$ – GrayedFox Nov 14 '20 at 16:50
  • 1
    \$\begingroup\$ @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). \$\endgroup\$ – Kevin Reid Nov 14 '20 at 16:59
  • \$\begingroup\$ Derp. Was trying that and forgot the decrement the count by one, hence getting some unexpected results. Thank you again! \$\endgroup\$ – GrayedFox Nov 14 '20 at 17:13
0
\$\begingroup\$

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;
}
```
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.