Packages

Pure-Elixir tabletop RPG game support and rules modeled on the System Reference Document (SRD) 5.2 with opt-in support for some 5.1 rules.

Current section

Files

Jump to
srd_5e lib srd rules hitpoints.ex
Raw

lib/srd/rules/hitpoints.ex

defmodule Srd.Rules.Hitpoints do
@moduledoc """
Hit point rules.
Applies damage and healing to a hit point pool, accounting for temporary hit
points and clamping to the range `0..max_hp`. Each operation reports an outcome
describing the transition, such as dropping to 0 or dying outright.
"""
@enforce_keys [:hp, :max_hp]
defstruct [:hp, :max_hp, temp_hp: 0, outcome: nil]
@typedoc """
The outcome of the most recent operation on a hit point pool:
* `:ok` - hit points remain above 0
* `:downed` - damage dropped the pool from above 0 to 0
* `:hit_while_down` - damage was taken while already at 0
* `:dead` - instant death, when the damage past 0 is at least the maximum
* `:recovered` - healing brought the pool from 0 back above 0
"""
@type outcome :: :ok | :downed | :hit_while_down | :dead | :recovered
@typedoc """
A hit point pool:
* `:hp` - current hit points (0..max_hp)
* `:max_hp` - the hit point maximum
* `:temp_hp` - temporary hit points, a separate buffer that absorbs damage first
* `:outcome` - the result of the most recent `damage/2` or `heal/2`, or `nil`
"""
@type t :: %__MODULE__{
hp: non_neg_integer(),
max_hp: pos_integer(),
temp_hp: non_neg_integer(),
outcome: outcome() | nil
}
@doc """
Build a hit point pool.
"""
@spec new(non_neg_integer(), pos_integer(), non_neg_integer()) :: t()
def new(hp, max_hp, temp_hp \\ 0) do
%__MODULE__{hp: hp, max_hp: max_hp, temp_hp: temp_hp, outcome: nil}
end
@doc """
Apply damage. Temporary hit points absorb it first and current hit points floor
at 0. Damage that leaves the pool at 0 with an excess of at least the maximum
is instant death.
"""
@spec damage(t(), non_neg_integer()) :: t()
def damage(%__MODULE__{} = pool, amount) when is_integer(amount) and amount >= 0 do
was_down = pool.hp == 0
absorbed = min(pool.temp_hp, amount)
remaining = amount - absorbed
new_hp = pool.hp - remaining
outcome =
cond do
remaining == 0 -> :ok
new_hp > 0 -> :ok
remaining - pool.hp >= pool.max_hp -> :dead
was_down -> :hit_while_down
true -> :downed
end
%{pool | hp: max(new_hp, 0), temp_hp: pool.temp_hp - absorbed, outcome: outcome}
end
@doc """
Apply healing, up to the maximum. Temporary hit points are unaffected.
"""
@spec heal(t(), non_neg_integer()) :: t()
def heal(%__MODULE__{} = pool, amount) when is_integer(amount) and amount >= 0 do
new_hp = min(pool.hp + amount, pool.max_hp)
outcome = if pool.hp == 0 and new_hp > 0, do: :recovered, else: :ok
%{pool | hp: new_hp, outcome: outcome}
end
end