lib/rule_systems/abilities/modifier_calculation.ex

defmodule ExTTRPGDev.RuleSystems.Abilities.ModifierCalculation do
  alias ExTTRPGDev.RuleSystems.Abilities.ModifierCalculation

  @moduledoc """
  Module handling calculation of ability modifiers based on ability scores
  """

  defstruct [:steps, :mapping]

  defmodule Step do
    @moduledoc """
    A step is generally part of a series of step which taken in order
    can calculate from the modifier from the starting ability score
    """
    defstruct [:order, :method, :value]
  end

  defmodule Mapping do
    @moduledoc """
    Defines direct mapping of ability scores to their modifer values
    """
    defstruct [:ability_value, :modifier_value]
  end

  @doc """
  Takes in a ModifierCalculation struct and a score, and calculates the scores modifier

  ## Examples

      iex> modifier_for_score(%ModifierCalculation{steps: [%Step{}, %Step{}, ...]}, 13)
      1
      modifier_for_score(%ModifierCalculation{mapping: [%Mapping{}, %Mapping, ...], 6)
      -2

  """
  def modifier_for_score(
        %ModifierCalculation{steps: [%ModifierCalculation.Step{} | _tail] = steps, mapping: nil},
        score
      ) do
    steps
    |> ordered_steps()
    |> Enum.reduce(score, fn step, acc -> ModifierCalculation.modify_score_by_step(step, acc) end)
  end

  def modifier_for_score(
        %ModifierCalculation{
          steps: nil,
          mapping: [%ModifierCalculation.Mapping{} | _tail] = mappings
        },
        score
      ) do
    mappings
    |> map_mappings()
    |> Map.get(score)
  end

  defp map_mappings([%ModifierCalculation.Mapping{} | _tail] = mappings) do
    mappings
    |> Enum.reduce(%{}, fn mapping, acc ->
      Map.put(acc, Map.get(mapping, :ability_value), Map.get(mapping, :modifier_value))
    end)
  end

  def modify_score_by_step(%ModifierCalculation.Step{} = step, score) do
    case step do
      %ModifierCalculation.Step{order: _, method: "add", value: value} ->
        score + value

      %ModifierCalculation.Step{order: _, method: "subtract", value: value} ->
        score - value

      %ModifierCalculation.Step{order: _, method: "multiply", value: value} ->
        score * value

      %ModifierCalculation.Step{order: _, method: "divide", value: value} ->
        score / value

      %ModifierCalculation.Step{order: _, method: "round_down", value: nil} ->
        Float.floor(score) |> Kernel.trunc()

      %ModifierCalculation.Step{order: _, method: "round_up", value: nil} ->
        Float.ceil(score) |> Kernel.trunc()
    end
  end

  defp ordered_steps([%ModifierCalculation.Step{} | _tail] = steps) do
    steps
    |> Enum.sort(fn a, b -> Map.get(a, :order) <= Map.get(b, :order) end)
  end
end