# E2.M2 — XpAwardAndLevelEngine

## Summary

| Field | Value |
|--------|--------|
| **Module ID** | E2.M2 |
| **Epic** | [Epic 2 — Skills and Progression Framework](../epics/epic_02_skills_and_progression.md) |
| **Stage target** | Prototype |
| **Status** | In Progress (see [dependency register](module_dependency_register.md)) |

## Implementation snapshot

**Linear (label `E2.M2`):** [Epic 2 project](https://linear.app/neon-sprawl/project/epic-2-classless-skill-and-progression-framework-5200f84bf8b6) — **Slice 2:** [NEO-37](https://linear.app/neon-sprawl/issue/NEO-37) (progression read snapshot) → [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) (apply grant + allowlist + level-up) → [NEO-39](https://linear.app/neon-sprawl/issue/NEO-39) (data-driven `LevelCurve` + CI); [NEO-40](https://linear.app/neon-sprawl/issue/NEO-40) (telemetry hook sites) after NEO-38, parallel with NEO-39. **Slice 3 integration** (multi-label where noted): [NEO-41](https://linear.app/neon-sprawl/issue/NEO-41), [NEO-42](https://linear.app/neon-sprawl/issue/NEO-42), [NEO-43](https://linear.app/neon-sprawl/issue/NEO-43), [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44).

**NEO-37 landed:** versioned **`GET /game/players/{id}/skill-progression`** ([implementation plan](../../plans/NEO-37-implementation-plan.md)) — `SkillProgressionSnapshotApi` / `SkillProgressionSnapshotDtos` under `server/NeonSprawl.Server/Game/Skills/`; one row per `ISkillDefinitionRegistry` skill; **`404`** when the player has no position (same gate as **`GET …/cooldown-snapshot`**). Manual QA **[`NEO-37.md`](../../manual-qa/NEO-37.md)**; **[server README — Skill progression snapshot (NEO-37)](../../../server/README.md#skill-progression-snapshot-neo-37)**.

**NEO-38 landed:** versioned **`POST /game/players/{id}/skill-progression`** ([implementation plan](../../plans/NEO-38-implementation-plan.md)) — applies one grant per request with **`ISkillDefinitionRegistry`** + **`allowedXpSourceKinds`** validation, stable deny **`reasonCode`** values, dual persistence (**`IPlayerSkillProgressionStore`**: in-memory / Postgres + [`V003__player_skill_progression.sql`](../../../server/db/migrations/V003__player_skill_progression.sql)), GET merges stored XP with the registry; success payload includes **`levelUps`** (see README for multi-step semantics). Bruno **`bruno/neon-sprawl-server/skill-progression/`** (GET + POST + deny); manual QA **[`NEO-38.md`](../../manual-qa/NEO-38.md)**; **[server README — Skill progression grant (NEO-38)](../../../server/README.md#skill-progression-grant-neo-38)**.

**NEO-39 landed:** data-driven **`LevelCurve`** at `content/skills/prototype_level_curve.json` with schema `content/schemas/level-curve.schema.json`; CI validation extended in `scripts/validate_content.py`; server boot now fail-fast loads curve content and uses it for XP→level resolution in skill progression GET/POST paths (no code edit required for threshold tuning).

**NEO-40 landed:** comment-only **telemetry hook sites** in `SkillProgressionSnapshotApi` (`POST …/skill-progression`) for future catalog events **`xp_grant`** and **`level_up`** ([implementation plan](../../plans/NEO-40-implementation-plan.md)); manual QA **[`NEO-40.md`](../../manual-qa/NEO-40.md)**.

**NEO-41 landed:** prototype **gather → skill XP** — successful **`POST …/interact`** when interactable **`kind`** is **`resource_node`** grants **`salvage`** XP (**`sourceKind: activity`**, **10** XP per interact) via shared **`SkillProgressionGrantOperations`** ([implementation plan](../../plans/NEO-41-implementation-plan.md)); manual QA **[`NEO-41.md`](../../manual-qa/NEO-41.md)**; **[server README — Interaction (gather)](../../../server/README.md#interaction-neo-9)**.

**NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** + **`RefineSkillXpConstants`** + test **`RefineActivityDeniedRegistryWebApplicationFactory`** — craft/refine success is intended to award **`refine`** XP (**`sourceKind: activity`**, **10** XP) via the same **`SkillProgressionGrantOperations`** stack as NEO-38 ([implementation plan](../../plans/NEO-42-implementation-plan.md)); manual QA **[`NEO-42.md`](../../manual-qa/NEO-42.md)**; **[server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42)**. **E3.M2** must invoke **`GrantOnSuccessfulCraftOrRefine`** on real **`CraftResult`** success for end-to-end behavior (no craft route in repo at this slice).

**NEO-43 landed (prep):** **`MissionRewardSkillXpGrant`** for scripted **`mission_reward`** skill XP ([implementation plan](../../plans/NEO-43-implementation-plan.md)); manual QA **[`NEO-43.md`](../../manual-qa/NEO-43.md)**; full quest hand-in awaits **E7.M2**.

**Still backlog within E2.M2 / Slice 3:** **NEO-44** (gig XP from combat, Epic 5 — not skill XP via E2.M2). **NEO-42** end-to-end craft wiring awaits **E3.M2** success handler.

**E2.M3:** [MasteryAndPerkUnlocks](E2_M3_MasteryAndPerkUnlocks.md) re-evaluates perk eligibility on skill **level-up** after grants ([NEO-45](https://linear.app/neon-sprawl/issue/NEO-45) → [NEO-49](https://linear.app/neon-sprawl/issue/NEO-49)).

Keep this paragraph and [documentation tracking](documentation_and_implementation_alignment.md) in sync as stories merge.

## Purpose

Central server-authoritative engine for **non-combat skill** progression: applying XP grants, resolving levels against data-driven curves, and emitting level-up events. **Gathering** and **crafting** integrate as callers for **skill** XP. **Combat encounters** advance **gigs** (gig XP), not `SkillDef` lines. **Mission rewards** may call this API for **skill** XP when the payout lists a skill grant. Callers are not hard dependencies for implementing the engine core (see Epic 2 Slice 2 vs Slice 3).

## Responsibilities

- Apply `XpGrantEvent` (or equivalent) per skill with validated **`skillId`** from [E2.M1](E2_M1_SkillDefinitionRegistry.md). Reject grants when **`sourceKind`** is not in the target `SkillDef.allowedXpSourceKinds`.
- Resolve experience against `LevelCurve` / threshold tables.
- Emit `LevelUpEvent` when thresholds are crossed.
- Support data-driven curves; **reload policy:** [Data reload and telemetry operations policy](data_and_ops_policy.md#content-and-gameplay-data-reload) (boot load default; optional dev reload).

## Key contracts

| Contract | Role |
|----------|------|
| `XpGrantEvent` | Structured XP award: **`skillId`** (must exist in registry), **amount**, **`sourceKind`** (must appear in that skill’s **`allowedXpSourceKinds`**), optional modifiers. |
| `LevelCurve` | Thresholds or formula per skill or global policy. |
| `LevelUpEvent` | Notification for UI, unlocks, and downstream systems. |

## Module dependencies

- **E2.M1** — [SkillDefinitionRegistry](E2_M1_SkillDefinitionRegistry.md): `SkillDef`, categories, **allowedXpSourceKinds**, and validation of skill IDs.

## Dependents (by design)

- **E5.M1** — CombatRulesEngine: **gig XP** from combat (not `SkillDef` XP via E2.M2).
- **E3.M1 / E3.M2** — Gathering and crafting award XP when wired in Slice 3.
- **E7.M2** — RewardAndUnlockRouter routes quest rewards including XP through E2.M2.
- **E2.M3 / E2.M4** — Mastery and pacing layers build on level/XP state (pre-production).

## Related implementation slices

- **Epic 2 Slice 2 — XP engine (E2.M2 core)**: implement contracts and tests without requiring Epic 3 or 5 modules.
- **Epic 2 Slice 3 — XP award integration**: E3.M1 / E3.M2 call the skill XP API with **`sourceKind`: `activity`** (where allowed per skill); E7.M2 / mission rewards use **`mission_reward`** (etc.) as configured; combat resolves **gig** progression separately from E2.M2.

## Risks and telemetry

- Runaway leveling in one skill: diminishing returns per slice; monitor distribution via E9.M2 when available.
- Telemetry: `xp_grant`, `level_up`, time-to-first-level-up once real activity fires.

## Source anchors

- Master plan: [`neon_sprawl_vision.plan.md`](../../../neon_sprawl_vision.plan.md) — Core Epic Map, Epic 2.
- [Module dependency register](module_dependency_register.md)
