DEV Community

Cover image for Model a burst weapon as a timeline, not HP divided by DPS
keyboardTester.Click
keyboardTester.Click

Posted on Originally published at keyboardtester.click

Model a burst weapon as a timeline, not HP divided by DPS

Originally published on KeyboardTester.click. I maintain the site. This adaptation was prepared with AI assistance and its example arithmetic and code were independently checked.

A player’s hand on a backlit mechanical keyboard.

Illustrative photo: Matheus Bertelli / Pexels; not a gameplay measurement.

The interesting edge case in a time-to-kill calculator is often the shot that never deals damage. A miss still advances the firing pattern and consumes ammunition. Dividing health by average damage per second loses both of those events.

Here is a small JavaScript exercise with two invented weapons and a 100-HP target. A deals 34 damage, fires three-round bursts at 900 RPM within each burst, and waits 300 ms from the last round of one burst to the first of the next. B deals 26 damage at a continuous 600 RPM. There is no armor, shield or falloff.

First, write down the event contract

The first fired round is time zero. A burst gap replaces the normal shot interval at that boundary. An empty-magazine reload also replaces the boundary interval, resets the burst and ends at the next fired round. A killing shot ends the timeline immediately. These are this exercise’s rules, not universal game-engine rules.

The state needed is small: elapsed time, rounds used in the current burst, rounds used in the magazine, and remaining HP. Keep miss events in the input; do not filter them before counting rounds.

// Standalone teaching implementation for integer damage and fixed burst/reload gaps.
// A timeline of chosen events, not live weapon stats or a probability model.
function shotTimeline(gun, hits, hp = 100) {
  const { damage, rpm, burstSize = 0, burstGap = 0,
    magazine = 0, reload = 0, limbMultiplier = 0.5 } = gun;
  if (![damage, rpm, hp].every(n => Number.isFinite(n) && n > 0) ||
      !Number.isInteger(damage) || !Number.isInteger(hp) ||
      ![burstSize, magazine].every(n => Number.isSafeInteger(n) && n >= 0) ||
      ![burstGap, reload, limbMultiplier].every(n => Number.isFinite(n) && n >= 0) ||
      (burstSize === 1) || (magazine > 0 && reload <= 0) ||
      !Number.isSafeInteger(damage * limbMultiplier)) {
    throw new Error('Use valid timing and integer damage/HP inputs for this exercise');
  }
  if (!Array.isArray(hits) || hits.length < 1 || hits.length > 200 ||
      !hits.every(x => ['body', 'limb', 'miss'].includes(x))) {
    throw new Error('Provide 1–200 body, limb or miss events');
  }
  const rows = [];
  let time = 0, burstUsed = 0, magazineUsed = 0;
  for (let i = 0; i < hits.length; i++) {
    if (i > 0) {
      if (magazine > 0 && magazineUsed === magazine) {
        time += reload; // replaces the ordinary/burst gap
        magazineUsed = 0;
        burstUsed = 0;
      } else if (burstSize > 0 && burstUsed === burstSize) {
        time += burstGap;
        burstUsed = 0;
      } else {
        time += 60000 / rpm;
      }
    }
    const zone = hits[i];
    const dealt = zone === 'miss' ? 0 : damage * (zone === 'limb' ? limbMultiplier : 1);
    hp = Math.max(0, hp - dealt);
    burstUsed++;
    magazineUsed++;
    rows.push({ shot: i + 1, zone, time, dealt, hp });
    if (hp === 0) break; // never append a reload after a kill
  }
  return { killed: hp === 0, time, remainingHP: hp, rows };
}
Enter fullscreen mode Exit fullscreen mode

This deliberately restricts damage and HP to integers, including the derived limb damage. It is not an exact-decimal engine for arbitrary imported weapon statistics. Timing remains a JavaScript Number; round only when displaying it, not after each interval.

A regression fixture with an actual winner reversal

const A = { damage: 34, rpm: 900, burstSize: 3, burstGap: 300 };
const B = { damage: 26, rpm: 600 };
console.log(shotTimeline(A, ['body','body','body']).time); // 133.333...
console.log(shotTimeline(B, ['body','body','body','body']).time); // 300
console.log(shotTimeline(A, ['body','miss','body','body']).time); // 433.333...
console.log(shotTimeline(B, ['body','miss','body','body','body']).time); // 400
Enter fullscreen mode Exit fullscreen mode

All four sequences land their first shot. In the miss cases, the second fired shot misses. A’s next damage opportunity is across a burst boundary; B merely waits another ordinary interval. This is one explicit scenario, not an expected result at a shared accuracy percentage.

Two more fixtures are useful. body, body, limb, body at A’s 0.5 limb multiplier ends at 433.333… ms because the first three impacts deal only 85 damage. A three-round magazine with a 1,000 ms reload moves body, miss, body, body to 1,133.333… ms. A four-round magazine leaves it at 433.333… ms. The difference is 700 ms because reload replaces the 300 ms burst gap.

Keep model results separate from gameplay measurements

The supplied teaching implementation passed 30 assertions covering eight lethal fixtures, the 15-HP partial sequence and invalid-input cases. It does not model aim, travel time, recoil, networking, regeneration or a probability distribution of hits. A polished interface cannot create evidence for those missing variables.

You can reproduce the worked scenarios in the browser calculator or read the full burst-boundary explanation. Its separate mixed-sequence output is the appropriate place for misses; its four-region comparison table assumes all hits land in one region.

Top comments (0)