from crimson.perks.runtime.hook_types import WorldDtStepclass ReflexBoostDtHook(WorldDtStep): def apply(self, world: WorldState, dt: float) -> float: # Check if any player has active Reflex Boost reflex_timer = max( player.reflex_boost_timer for player in world.players ) if reflex_timer <= 0: return dt # Scale down time (slow motion) return dt * 0.3
from crimson.perks.runtime.player_tick_context import PlayerTickContextdef angry_reloader_tick(ctx: PlayerTickContext) -> None: """Angry Reloader: +25% speed while reloading.""" if not perk_active(ctx.player, PerkId.ANGRY_RELOADER): return if ctx.player.weapon.reload_active: # Speed boost already applied in movement code pass
The canonical registry in src/crimson/perks/runtime/manifest.py:
src/crimson/perks/runtime/manifest.py
from crimson.perks.impl import ( instant_winner, final_revenge, evil_eyes_effect, # ... all perk modules)# Parity-critical dispatch orderPERK_HOOKS_IN_ORDER: list[PerkHooks] = [ instant_winner.HOOKS, final_revenge.HOOKS, evil_eyes_effect.HOOKS, # ... all perk hooks in native order]# Derived registries (preserve order)PERK_APPLY_HANDLERS: dict[PerkId, ApplyHandler] = { hook.perk_id: hook.apply_handler for hook in PERK_HOOKS_IN_ORDER if hook.apply_handler is not None}WORLD_DT_STEPS: list[WorldDtStep] = [ hook.world_dt_step for hook in PERK_HOOKS_IN_ORDER if hook.world_dt_step is not None]PLAYER_DEATH_HOOKS: list[PlayerDeathHook] = [ hook.player_death_hook for hook in PERK_HOOKS_IN_ORDER if hook.player_death_hook is not None]
Order matters! Changing hook order affects RNG consumption and can break replay parity.
def perks_update_effects( state: GameplayState, players: list[PlayerState], creatures: CreaturePool, dt: float,) -> None: """Run all perk effects hooks.""" ctx = EffectsContext( state=state, players=players, creatures=creatures, dt=dt, rand=state.rng.rand, ) # Always run bonus timers first update_player_bonus_timers(ctx) # Run all effects steps in order for step in PERKS_UPDATE_EFFECT_STEPS: step(ctx)
# Radioactive: Damage nearby enemiesdef radioactive_effect(ctx: EffectsContext) -> None: for player in ctx.players: if not perk_active(player, PerkId.RADIOACTIVE): continue for creature in ctx.creatures.active_creatures(): dist = distance(player.pos, creature.pos) if dist < 80: creature.health -= 5.0 * ctx.dt
def test_perk_hooks_have_unique_perk_ids(): perk_ids = [hook.perk_id for hook in PERK_HOOKS_IN_ORDER] assert len(perk_ids) == len(set(perk_ids))def test_effects_steps_prefix_invariant(): # update_player_bonus_timers must be first assert PERKS_UPDATE_EFFECT_STEPS[0].__name__ == "update_player_bonus_timers"