# ADR-129: Move Treasure Scoring from Platform to Story Layer

## Status: PROPOSED

## Date: 2026-02-12

## Context

TreasureTrait currently lives in `packages/world-model/src/traits/treasure/` (platform level). The `ScoringEventProcessor` in `packages/stdlib/src/services/scoring/` imports TreasureTrait and uses it to detect treasures via a "dynamic treasure mode" that listens to domain events (`if.event.taken`, `if.event.put_in`).

This arrangement has three problems:

### 1. TreasureTrait is story-specific, not platform-generic

The trait's fields (`treasureId`, `treasureValue`, `trophyCaseValue`) map directly to Mainframe Zork's MDL concepts (OFVAL and OTVAL). The `trophyCaseValue` field assumes a "trophy case" mechanic that is specific to Zork-style treasure hunts. A mystery game, a horror game, or even a different treasure-hunt game might score entirely differently.

A platform trait should represent a universal IF concept (like OpenableTrait, ContainerTrait, IdentityTrait). "Item worth N points when placed in a specific container" is not universal — it's a game design choice.

### 2. ScoringEventProcessor misuses the event system

Sharpee's events are domain events (semantic messages), not a pub/sub mechanism. The ScoringEventProcessor registers handlers on `if.event.taken` and `if.event.put_in` to react to actions after the fact. This is architecturally wrong:

- Domain events describe what happened — they are not hooks for side effects
- Scoring is a mutation (changing the score) that belongs in the action execution flow
- The handler approach creates hidden coupling: the taking action doesn't know scoring happens, and the scoring code doesn't know which action triggered it

The correct Sharpee pattern is interceptors on the relevant actions. The taking action's execution flow should include scoring logic, not a disconnected event listener.

### 3. The thief depends on TreasureTrait in story code anyway

The thief's deposit logic filters by `trophyCaseValue > 0` to decide which items to deposit at his lair (matching MDL's `OTVAL > 0` check). This logic is in `stories/dungeo/src/npcs/thief/thief-helpers.ts` — story code reaching into a platform trait. If the trait lived in the story, this dependency would be local and obvious.

## Decision

### Move TreasureTrait to the Dungeo story

**From:** `packages/world-model/src/traits/treasure/treasureTrait.ts`
**To:** `stories/dungeo/src/traits/treasure-trait.ts`

The trait definition, its config interface, and its type constant all move. The re-export in `stories/dungeo/src/traits/index.ts` (currently importing from `@sharpee/world-model`) becomes a local export.

### Remove ScoringEventProcessor's treasure-specific code

The `dynamicTreasureMode`, `handleTaken()`, and `handlePutIn()` methods that import TreasureTrait are removed from the stdlib class. The generic parts of ScoringEventProcessor (room visits, sequences, goals, unlocks) may remain in stdlib if they prove useful to other stories, or can be evaluated independently.

### Replace with action interceptors in Dungeo

Treasure scoring moves into the action execution flow via interceptors:

- **Taking interceptor**: When a player takes an item with TreasureTrait, the interceptor's `postExecute` awards `treasureValue` points (if not already scored).
- **Putting interceptor**: When a player puts an item with TreasureTrait into the trophy case, the interceptor's `postExecute` awards `trophyCaseValue` points (if not already scored).

These interceptors live in `stories/dungeo/src/interceptors/` and use the story's own scoring service (`IScoringService.addPoints()`). They participate in the action's four-phase flow rather than reacting to domain events after the fact.

### Thief deposit logic stays as-is

The thief's `depositTreasures()` helper already filters by `trophyCaseValue > 0` from TreasureTrait. With the trait now local to the story, this is a clean same-package dependency.

## Consequences

### Positive

- **Correct layer separation**: Platform owns universal IF concepts; stories own game-specific mechanics
- **Correct event usage**: Scoring happens in the action flow, not via domain event side effects
- **Thief logic simplified**: TreasureTrait and thief behavior are co-located in the same story package
- **Other stories are free**: A future story can define its own scoring traits and mechanics without inheriting Zork's OFVAL/OTVAL model

### Negative

- **Reversal of prior decision**: TreasureTrait was deliberately promoted to platform level. This reverses that without a prior ADR to reference — the original decision was made implicitly.
- **ScoringEventProcessor loses convenience**: Story authors who want Zork-style treasure scoring can't get it "for free" from stdlib. They must wire their own interceptors. (This is acceptable — each story's scoring is unique enough to warrant explicit wiring.)
- **Migration work**: All region files that import TreasureTrait from `@sharpee/world-model` must update their imports to the local story trait. The re-export in `stories/dungeo/src/traits/index.ts` already exists, so most files import from there and won't change.

### Migration Scope

Files that import TreasureTrait from `@sharpee/world-model` directly:

- `packages/world-model/src/traits/treasure/` — delete
- `packages/world-model/src/traits/all-traits.ts` — remove registration
- `packages/world-model/src/traits/implementations.ts` — remove export
- `packages/stdlib/src/services/scoring/scoring-event-processor.ts` — remove TreasureTrait import and dynamic treasure code
- `stories/dungeo/src/traits/index.ts` — change from re-export to local export

Files that import from `stories/dungeo/src/traits/` — no change needed (already local).

## Alternatives Considered

### Keep TreasureTrait in platform, fix only the event handler pattern

Move scoring into interceptors but leave the trait in world-model. This fixes problem #2 but not #1 or #3. A half-measure that leaves a story-specific concept in the platform.

### Make TreasureTrait generic (rename to ScoringTrait)

Abstract away the Zork-specific field names (`treasureValue` → `takeScore`, `trophyCaseValue` → `containerScore`). This papers over the problem — the shape is still "points for taking + points for placing in container," which is still a Zork-specific game design pattern, not a universal IF concept.

## References

- ADR-051: Four-Phase Action Pattern
- ADR-090: Entity-Centric Action Dispatch (capability interceptors)
- MDL source: `docs/dungeon-81/mdlzork_810722/act1.254` (OFVAL/OTVAL treasure values)
