Perks architecture (rewrite)¶
This document is the canonical architecture contract for perk runtime behavior in the Python rewrite.
Goals:
- Original fidelity: keep hook order and side effects aligned with native flow.
- Navigability: “open a perk file, see that perk’s runtime logic.”
- Deterministic auditability: stable RNG consumption and dispatch order for differential testing.
Package layout¶
Perk runtime code is intentionally split into three concerns:
- Perk metadata + selection state (
src/crimson/perks/*.py) ids.py,helpers.py,availability.py,selection.py,state.py- No per-perk hook ownership in this layer.
- Perk implementation ownership (
src/crimson/perks/impl/*.py) - One module per perk behavior owner.
- Each module exports exactly one
HOOKS = PerkHooks(...). - The same file holds the perk’s runtime hook functions.
- Runtime dispatch orchestration (
src/crimson/perks/runtime/*.py) - Hook contracts and contexts (
hook_types.py,*_context.py) - Dispatch entry points (
apply.py,effects.py,player_ticks.py) - Canonical registry (
manifest.py) that imports allimplowners and defines parity-critical dispatch ordering.
There are no compatibility re-export wrappers for runtime dispatch. Runtime
ownership/order is authoritative in src/crimson/perks/runtime/manifest.py.
Runtime surfaces¶
Hook shape is defined in src/crimson/perks/runtime/hook_types.py:
apply_handler: immediate on-pick logic (perk_applypath)world_dt_step: frame-dt transforms (e.g. Reflex Boosted)player_tick_steps: per-player tick hooks insideplayer_updateeffects_steps: global per-frame perk effects (perks_update_effects)player_death_hook: death-triggered behavior (e.g. Final Revenge)
PerkHooks fields are optional; each perk declares only what it owns.
Example:
Dispatch integration points¶
1) Apply-time perks¶
- Entry:
src/crimson/perks/runtime/apply.py:perk_apply - Source:
PERK_APPLY_HANDLERSderived fromPERK_HOOKS_IN_ORDER - Flow:
- Increment owner perk count (
adjust_perk_count). - Run apply handler if registered.
- Mirror
perk_countsfrom player 0 to other players.
This keeps multiplayer perk-count state deterministic and aligned with native shared-count behavior.
2) World dt hooks¶
- Entry:
src/crimson/sim/world_state.py:WorldState.step - Source:
WORLD_DT_STEPS - Runs first, before core simulation work.
3) Perk effects hooks¶
- Entry:
src/crimson/perks/runtime/effects.py:perks_update_effects - Source:
PERKS_UPDATE_EFFECT_STEPS - Called early in
WorldState.step, after aim staging and beforestate.effects.update(...). update_player_bonus_timersis always first in this sequence.
4) Player tick hooks¶
- Entry:
src/crimson/gameplay.py:player_updateviasrc/crimson/perks/runtime/player_ticks.py:apply_player_perk_ticks - Source:
PLAYER_PERK_TICK_STEPS - Runs once per player each tick.
5) Player death hooks¶
- Entry:
src/crimson/sim/world_state.py:WorldState.step - Source:
PLAYER_DEATH_HOOKS - Runs for players transitioning alive -> dead during the current step.
Ordering and RNG invariants¶
These rules are parity-critical:
PERK_HOOKS_IN_ORDERis authoritative for hook dispatch order.- Derived registries preserve this order and must not sort/reorder.
- Adding/removing/reordering hooks can change RNG draw order and differential trace behavior, even when gameplay looks similar.
- Keep perk-side RNG draws inside the perk’s own hook file unless ordering evidence requires otherwise.
- Avoid moving logic between phases (
apply_handlervseffects_stepsvsplayer_tick_steps) without native evidence.
Import boundary contracts¶
import-linter contracts enforce anti-drift boundaries in code:
crimson.perks.implmust not importselection/availability.crimson.perks.runtimemust not importselection/availability.selection/availabilitymust not importimpldirectly.
This keeps runtime ownership centralized in runtime/manifest.py and avoids
split-brain registration paths.
Anti-drift guardrails¶
Guard tests live in tests/test_feature_hook_registries.py:
- Explicit expected world-dt and death-hook wiring.
- Single runtime owner per perk (
PERK_HOOKS_IN_ORDERhas uniqueperk_id). - Derived registries are exact projections of manifest entries.
- Effects step prefix invariant (
update_player_bonus_timersfirst).
Validation command:
just check
Contributor workflow for perk changes¶
When adding or refactoring a perk runtime hook:
- Implement/update the hook function in
src/crimson/perks/impl/<perk>.py. - Update that module’s
HOOKS = PerkHooks(...). - Add/update the import + placement in
PERK_HOOKS_IN_ORDERinsrc/crimson/perks/runtime/manifest.py. - Keep deterministic behavior explicit:
- do not normalize parity-sensitive float constants.
- preserve native guard/branch structure when it affects RNG or timing.
- Add/update tests:
- scenario tests for the perk behavior.
- registry invariant tests if hook shape/order changed.
- Run
just check.
What this architecture intentionally does not do¶
- It does not try to force all perk behavior through one hook type. Some perks
are owned by other hot paths by design (
player_take_damage,creature_apply_damage, projectile systems, rendering paths). - It does not hide phase boundaries. The phase where a perk runs is part of the parity contract.
Use Perk runtime reference with this page:
re/static/perks-runtime-reference.mdanswers “where does this perk run?”- this page answers “how does perk runtime registration and dispatch work?”