User guide All recipes Tutorial Function reference Open playground

Cairn 2e — Blood Elk horns vs armored PC

At the table

Cairn (2e) combat is swingy and armor matters. Attacks automatically hit; the attacker rolls weapon damage, the defender subtracts Armor, and whatever is left comes off HP.

This recipe models one hit from a Blood Elk (horns d8) against a typical front-line PC:

StatValue
HP4
STR11 (for critical damage saves)
Armor2 (1 worn + 1 shield)

Damage steps:

  1. Roll d8, subtract 2 Armor (results below 0 count as 0).
  2. Subtract damage from HP.
  3. If HP lands on exactly 0, the PC takes a Scar keyed to how much HP that hit removed (after armor). Here that only happens on 4 damage—Scar entry #4 (Broken Limb).
  4. If damage would push HP below 0, the leftover comes off STR instead. The PC immediately rolls a STR save (d20 ≤ STR after the loss; 1 always succeeds, 20 always fails). Failing means Critical Damage—out of the fight and dying without aid. A Blood Elk that inflicts Critical Damage is especially nasty: it gores the victim (fiction + Warden fallout).

So on a single elk charge you might see: the horns glance off armor (no HP loss), hit protection chipped away (HP loss that reads more like guard fatigue than lasting injury), a Scar at 0 HP, STR loss but still fighting after a passed save, or Critical Damage after a failed save.

Try it

In the playground, open this recipe in the playground and click Run.

The script

HP = 4
ARMOR = 2
STR = 11

Scale = (
    scale()
    .step("NO_EFFECT")
    .step("HIT_PROTECTION_LOSS")
    .step("SCAR")
    .step("STR_DOWN_OK")
    .step("CRITICAL_DAMAGE")
)

def damage(horn):
    d = horn - ARMOR
    if d < 0:
        return 0
    return d

def blood_elk_hit(horn, save):
    dmg = damage(horn)
    if dmg == 0:
        return "NO_EFFECT"
    if dmg < HP:
        return "HIT_PROTECTION_LOSS"
    if dmg == HP:
        return "SCAR"
    overflow = dmg - HP
    new_str = STR - overflow
    if save == 1:
        return "STR_DOWN_OK"
    if save == 20:
        return "CRITICAL_DAMAGE"
    if save <= new_str:
        return "STR_DOWN_OK"
    return "CRITICAL_DAMAGE"

out = joint_classify(d(8), d(20), Scale, blood_elk_hit)
output("blood_elk_horns", out)
output("p_protection_loss_or_worse", out.p_at_least("HIT_PROTECTION_LOSS"))
output("p_scar_or_worse", out.p_at_least("SCAR"))
output("p_critical_damage", out.p_at_least("CRITICAL_DAMAGE"))

blood_elk_horns · Outcomes

outcome%fracX/160
NO_EFFECT25.01/440
HIT_PROTECTION_LOSS37.53/860
SCAR12.51/820
STR_DOWN_OK11.919/16019
CRITICAL_DAMAGE13.121/16021

p_protection_loss_or_worse · Prob

outcome%fracX/160
p_protection_loss_or_worse75.03/4120

p_scar_or_worse · Prob

outcome%fracX/160
p_scar_or_worse37.53/860

p_critical_damage · Prob

outcome%fracX/160
p_critical_damage13.121/16021