diff --git a/server/NeonSprawl.Server/Game/Combat/CombatOperations.cs b/server/NeonSprawl.Server/Game/Combat/CombatOperations.cs
index 8de24f2..af6119c 100644
--- a/server/NeonSprawl.Server/Game/Combat/CombatOperations.cs
+++ b/server/NeonSprawl.Server/Game/Combat/CombatOperations.cs
@@ -8,7 +8,7 @@ namespace NeonSprawl.Server.Game.Combat;
public static class CombatOperations
{
// --- Reserved telemetry hook sites (NEO-84): no emit until future modules ---
- // encounter_start — future E5.M3 encounter spawn/engage path (not wired in Slice 1).
+ // encounter_start — E5.M3 anchor: EncounterProgressOperations.TryActivateOnFirstEngagement (NEO-109).
// player_death — future player HP / death resolution (player HP out of scope in Slice 1).
///
/// Resolves one ability against one prototype combat target using catalog damage and the health store.
diff --git a/server/NeonSprawl.Server/Game/Encounters/EncounterCombatWiring.cs b/server/NeonSprawl.Server/Game/Encounters/EncounterCombatWiring.cs
index 3072c58..a5a036b 100644
--- a/server/NeonSprawl.Server/Game/Encounters/EncounterCombatWiring.cs
+++ b/server/NeonSprawl.Server/Game/Encounters/EncounterCombatWiring.cs
@@ -88,10 +88,8 @@ public static class EncounterCombatWiring
playerId,
encounterId,
completion.ReasonCode);
- return;
}
- // --- Telemetry hook site (NEO-109): future E9.M1 catalog event `encounter_complete` ---
- // TODO(E9.M1): catalog emit — once per player+encounter when grants commit (NEO-106 cast path).
+ // encounter_complete / reward_attribution hooks: EncounterCompletionOperations.TryCompleteAndGrant (NEO-109).
}
}
diff --git a/server/NeonSprawl.Server/Game/Encounters/EncounterCompleteEvent.cs b/server/NeonSprawl.Server/Game/Encounters/EncounterCompleteEvent.cs
index 3b4e238..c48e4c4 100644
--- a/server/NeonSprawl.Server/Game/Encounters/EncounterCompleteEvent.cs
+++ b/server/NeonSprawl.Server/Game/Encounters/EncounterCompleteEvent.cs
@@ -4,6 +4,8 @@ namespace NeonSprawl.Server.Game.Encounters;
/// Internal completion payload returned on successful encounter grant (NEO-105).
/// Persisted via on success (NEO-107).
/// E7.M2 quest-credit routing consumes this record (hook stub on NEO-107).
+/// NEO-109 telemetry: encounter_complete / reward_attribution hook sites in
+/// .
///
public readonly record struct EncounterCompleteEvent(
string PlayerId,
diff --git a/server/NeonSprawl.Server/Game/Encounters/EncounterCompletionOperations.cs b/server/NeonSprawl.Server/Game/Encounters/EncounterCompletionOperations.cs
index 7bb2136..5e621f4 100644
--- a/server/NeonSprawl.Server/Game/Encounters/EncounterCompletionOperations.cs
+++ b/server/NeonSprawl.Server/Game/Encounters/EncounterCompletionOperations.cs
@@ -5,6 +5,8 @@ namespace NeonSprawl.Server.Game.Encounters;
///
/// Applies encounter reward-table grants and marks completion idempotently (NEO-105).
/// Combat defeat wiring: (NEO-106).
+/// NEO-109 telemetry hook sites: (encounter_complete, reward_attribution),
+/// (encounter_complete_denied).
///
public static class EncounterCompletionOperations
{
@@ -122,7 +124,16 @@ public static class EncounterCompletionOperations
// TODO(E7.M2): reward_delivery — QuestRewardBundle router consumes EncounterCompleteEvent
// (playerId, encounterId, idempotencyKey, grantedItemSummary) → idempotent RewardDeliveryEvent.
// Prototype quest credit: contract_handoff_token item loot + this event record; full router: E7.M2.
- // TODO(E9.M1): catalog emit — encounter_complete on successful grant commit (NEO-109).
+
+ // --- Telemetry hook site (NEO-109): future E9.M1 catalog event `encounter_complete` ---
+ // TODO(E9.M1): catalog emit — once per player+encounter on successful grant + completion mark commit.
+ // Planned payload: playerId, encounterId, rewardTableId, completedAt, idempotencyKey, grantedItems summary
+ // (EncounterCompleteEvent). No ingest or ILogger here (comments-only). EncounterCombatWiring delegates here.
+
+ // --- Telemetry hook site (NEO-109): future E9.M1 catalog event `reward_attribution` ---
+ // TODO(E9.M1): catalog emit — once per successful completion (batch attribution, not per TryAddStack).
+ // Planned payload: playerId, encounterId, rewardTableId, grantedItems[] (EncounterGrantApplied batch).
+ // Inventory layer still emits item_created per stack via PlayerInventoryOperations (NEO-56).
return new EncounterCompletionResult(
Success: true,
diff --git a/server/NeonSprawl.Server/Game/Encounters/EncounterCompletionResult.cs b/server/NeonSprawl.Server/Game/Encounters/EncounterCompletionResult.cs
index a8c66f5..de2b0f4 100644
--- a/server/NeonSprawl.Server/Game/Encounters/EncounterCompletionResult.cs
+++ b/server/NeonSprawl.Server/Game/Encounters/EncounterCompletionResult.cs
@@ -3,6 +3,7 @@ namespace NeonSprawl.Server.Game.Encounters;
///
/// Server-internal encounter completion resolution envelope (NEO-105).
/// Combat wiring promotes via NEO-106; HTTP DTOs via NEO-108.
+/// NEO-109 telemetry hook sites live in .
///
/// Item grants on success; empty when denied.
/// Completion payload on success; null when denied.
diff --git a/server/NeonSprawl.Server/Game/Encounters/EncounterProgressOperations.cs b/server/NeonSprawl.Server/Game/Encounters/EncounterProgressOperations.cs
index 10128e5..19707ec 100644
--- a/server/NeonSprawl.Server/Game/Encounters/EncounterProgressOperations.cs
+++ b/server/NeonSprawl.Server/Game/Encounters/EncounterProgressOperations.cs
@@ -3,6 +3,7 @@ namespace NeonSprawl.Server.Game.Encounters;
///
/// Encounter activation and defeat-all progress orchestration (NEO-104).
/// Combat cast wiring: (NEO-106).
+/// NEO-109 telemetry hook site: (encounter_start).
///
public static class EncounterProgressOperations
{
@@ -63,7 +64,10 @@ public static class EncounterProgressOperations
}
// --- Telemetry hook site (NEO-109): future E9.M1 catalog event `encounter_start` ---
- // TODO(E9.M1): catalog emit — on first activation for player+encounter (NEO-106 wires cast path).
+ // TODO(E9.M1): catalog emit — when TryActivate returns true (first activation for player+encounter).
+ // Planned payload: playerId, encounterId, npcInstanceId (engagement target). No ingest or ILogger here
+ // (comments-only). EncounterCombatWiring calls this on first damaging hit vs a required NPC (NEO-106).
+
return progressStore.TryActivate(playerId, encounterId);
}
diff --git a/server/README.md b/server/README.md
index 06d6bb5..f189617 100644
--- a/server/README.md
+++ b/server/README.md
@@ -206,13 +206,26 @@ Prototype: defeat all three E5.M2 NPCs via cast → row **`prototype_combat_pock
curl -sS -i "http://localhost:5253/game/players/dev-local-1/encounter-progress"
```
+## Encounter telemetry hooks (NEO-109)
+
+Comment-only hook sites for Epic 5 **Slice 3** catalog events ([epic_05 Slice 3](../../docs/decomposition/epics/epic_05_pve_combat.md#epic-5-slice-3)). **`TODO(E9.M1)`** — no production ingest. **`POST …/ability-cast`** and **`EncounterCombatWiring`** delegate to the engines below (no duplicate API-layer hooks).
+
+| Event | Anchor |
+|-------|--------|
+| **`encounter_start`** | **`EncounterProgressOperations.TryActivateOnFirstEngagement`** — emit when **`TryActivate`** returns true (first engagement). Activates NEO-84 reserved **`encounter_start`** pointer in **`CombatOperations`**. |
+| **`encounter_complete`** | **`EncounterCompletionOperations.TryCompleteAndGrant`** — once per successful grant + completion mark commit. |
+| **`reward_attribution`** | Same success path — once per completion (batch **`grantedItems`**); per-stack **`item_created`** remains in **`PlayerInventoryOperations`** ([NEO-56](#player-inventory-neo-54-store-neo-55-http)). |
+| **`encounter_complete_denied`** | **`EncounterCompletionOperations.Deny`** — structured deny + **`EncounterCompletionReasonCodes`**. |
+
+Plan: [NEO-109 implementation plan](../../docs/plans/NEO-109-implementation-plan.md).
+
## Encounter combat wiring (NEO-106)
On every successful **`POST …/ability-cast`** accept, **`EncounterCombatWiring.TryProcessCastOutcome`** (best-effort; does not change cast JSON):
| Cast outcome | Encounter effect |
|--------------|------------------|
-| **`damageDealt > 0`** vs a required NPC | **`EncounterProgressOperations.TryActivateOnFirstEngagement`** — starts **`prototype_combat_pocket`** progress (`encounter_start` hook site in NEO-104). |
+| **`damageDealt > 0`** vs a required NPC | **`EncounterProgressOperations.TryActivateOnFirstEngagement`** — starts **`prototype_combat_pocket`** progress (**`encounter_start`** hook site, [NEO-109](#encounter-telemetry-hooks-neo-109)). |
| **`targetDefeated: true`** | **`TryMarkTargetDefeated`**; when all three E5.M2 NPC ids defeated, **`EncounterCompletionOperations.TryCompleteAndGrant`**. |
| Inventory full on completion | Defeat progress retained; completion/grants fail closed (retry when bag has space). |
@@ -363,7 +376,7 @@ Comment-only hook sites in **`CombatOperations.TryResolve`** for future E9.M1 ca
- **`ability_used`** — on every successful resolve (zero-damage abilities included).
- **`enemy_defeat`** — only when lethal damage drives **`after.Defeated`** to **`true`** (re-hit on defeated targets denies at **`target_defeated`** before damage).
-- **Reserved (comments only):** **`encounter_start`** (E5.M3 encounters).
+- **Reserved (comments only):** **`encounter_start`** — E5.M3 anchor: [Encounter telemetry hooks (NEO-109)](#encounter-telemetry-hooks-neo-109).
- **`player_death`** — only when lethal NPC damage drives player **`currentHp`** to **0** (comment hook in NEO-95; full event deferred).
E1.M4 cast funnel events **`ability_cast_requested`** / **`ability_cast_denied`** remain in **`AbilityCastApi`** ([NEO-30](#ability-cast-neo-31-neo-82)). E9.M1 ingest should correlate route **`playerId`** with engine payload fields at accept. Plan: [NEO-84 implementation plan](../../docs/plans/NEO-84-implementation-plan.md).