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 Answers 2

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, 2020 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, 2020 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, 2020 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\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .