Skip to content

refactor: move award application and progression choice resolution into the library#159

Merged
QMalcolm merged 3 commits into
mainfrom
qmalcolm--refactor-award-progression-into-library
Jul 21, 2026
Merged

refactor: move award application and progression choice resolution into the library#159
QMalcolm merged 3 commits into
mainfrom
qmalcolm--refactor-award-progression-into-library

Conversation

@QMalcolm

@QMalcolm QMalcolm commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes #131.

What

Business logic that lived in the JSON server moves into the library, in three commits:

  1. Characters.apply_award/4 — owns award lookup, value_type interpretation ("integer" / "next_level_xp"), effect construction, and pending-choice-slot recompute (awards can level a character up, so the returned character always has consistent slots). Vocabulary gains the reserved "award" type ID, value_type joins the structural metadata keys (documented in the Loader moduledoc), and it's removed from dnd_5e_srd's custom_metadata_keys where it was parked while the server owned the interpretation. apply_award! and compute_next_level_xp! are deleted from the server.

  2. Extract Characters.Advancementcharacters.ex sits at its code-health size threshold, so the advancement area gets its own module before growing. Characters.apply_award/4 stays the public entry point.

  3. Characters.resolve_progression_choice/4,5 — selection progressions now validate against the pending_choices/3 entry, the single source of truth for valid options (already-selected exclusion and slot caps included), instead of the server's parallel re-derivation. cap_resolved_for_slot, consume_slot, load_progression_target!, parse_effect_target!, and apply_inventory_addition! are deleted from the server; both handlers are now load / call library / save / serialize plus error-message formatting. next_progression_decision/3 becomes public as the canonical decision constructor.

Behavior notes (details in commit messages)

  • Over-resolving a selection progression (nothing pending) is now rejected with {:no_pending_choice, id}; the old path would accept resolutions beyond required_count. No client or test relied on that.
  • An explicit JSON null award value is now treated as "compute the value" rather than applied verbatim (the old behavior stored null as an effect value — a latent evaluator crash).
  • Value progressions stay deliberately unvalidated against pending state, matching old behavior (documented on the function).

Tests

17 new library tests in test/characters/apply_award_test.exs and test/characters/resolve_progression_choice_test.exs (new files — characters_test.exs is at its size threshold). All 382 existing tests pass unchanged; credo and dialyzer clean.

Summary by Bito

  • Introduced character advancement features including award application and progression choice resolution.
  • Added `ExTTRPGDev.Characters.Advancement` module to handle the implementation of advancement logic.
  • Updated `ExTTRPGDev.Characters` to delegate advancement operations to the new module.
  • Added support for a new reserved concept type 'award' in the rule system loader.
  • Updated `.gitignore` to include Dialyzer PLT cache directory.

QMalcolm added 3 commits July 20, 2026 21:03
Part of #131.

Award semantics lived in the JSON server: apply_award!/3 interpreted
value_type ("integer" / "next_level_xp") and built effects, and
compute_next_level_xp!/3 owned the level-up XP computation. Per the
library/config boundary, value_type and its values are structural
vocabulary — the library's own language for interpreting award
concepts — so only the JSON server could apply awards, and any second
frontend would have to re-implement the interpretation.

Characters.apply_award/4 now owns the pipeline: award lookup, value
resolution, effect construction, and pending-choice-slot recompute
(an award can change the character's level and thereby unlock slots,
so the returned character is always internally consistent). The server
handler shrinks to load / call / save / serialize plus error-message
formatting, and apply_award! / compute_next_level_xp! are deleted.

Vocabulary gains the reserved "award" type ID and "value_type" joins
the structural metadata keys (documented in the Loader moduledoc);
value_type is accordingly removed from dnd_5e_srd's
custom_metadata_keys, where it had been parked while the server owned
the interpretation.

Tests live in a new test/characters/apply_award_test.exs rather than
characters_test.exs, which is at its code-health size threshold.

One deliberate edge-case change: a request with an explicit JSON null
value is now treated as "compute the value" (nil is the library's
compute sentinel) rather than being applied verbatim. The old behavior
applied null as an effect value for next_level_xp awards — a latent
crash at evaluation time — so the new interpretation is strictly
safer, and no test or client sends explicit nulls.

next_level_xp with an explicit value still applies the explicit value
unvalidated, matching the old server behavior.
Part of #131.

characters.ex sits at the code-health size threshold (~1000 lines),
and the next change — moving progression choice resolution out of the
CLI server — would push it well past. Award application is the start
of a cohesive "advancement" area (awards now, progression choice
resolution next), so it moves to a dedicated Characters.Advancement
module before that lands.

Characters.apply_award/4 remains the public entry point, delegating to
the new module, so callers and the documented API are unchanged. The
implementation is moved verbatim; Advancement reaches back into
Characters only through its public functions (xp_to_next_level,
compute_pending_choice_slots).
…rary

Closes #131.

The characters.resolve_choice progression handler re-implemented the
library's own choice pipeline in the transport layer: already-selected
filtering, slot capping (cap_resolved_for_slot), option rejection, and
choice-number derivation — all logic that selection_progression_choices
and pending_choices/3 already compute, drifting copies waiting to
disagree.

Characters.resolve_progression_choice/4,5 (implemented in
Characters.Advancement) now owns resolution. Selection progressions
validate against the pending_choices/3 entry for the progression, which
is the single source of truth for valid options (already-selected
exclusion and slot-level caps included); the decision is recorded via
next_progression_decision — made public for this, as it is the
canonical constructor any frontend needs — the pending slot is
consumed, and the concept added to typed inventory. Value progressions
record the method label and contribute the integer value to the
progression's effect_target.

Two consequences of validating against pending_choices rather than the
server's re-derivation:

- Over-resolution (resolving a selection progression with nothing
  pending) is now rejected with {:no_pending_choice, id}; the old
  server path only filtered already-selected options and would accept
  an extra resolution beyond required_count. No client or test relied
  on that.
- Value progressions remain deliberately unvalidated against pending
  state, matching old behavior (documented on the function).

The server handler shrinks to load / call / save / serialize plus
error formatting, and cap_resolved_for_slot, consume_slot (moved into
the library), load_progression_target!, parse_effect_target!, and
apply_inventory_addition! are deleted. validate_concept_selection!
stays: the scoped sub-choice handlers still use it against
Serializer-computed option lists, which are outside this issue's scope.
@QMalcolm
QMalcolm merged commit 541f9ba into main Jul 21, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: move award application and progression choice resolution into the library

1 participant