NEO-27: add Slice 3 telemetry hook-site documentation

Map SelectionEvent to target_changed, reserve ability_cast_requested and ability_cast_denied hook locations, and align plan/manual QA/decomposition docs with TODO(E9.M1) telemetry follow-up.
pull/52/head
VinPropane 2026-04-25 21:56:28 -04:00
parent 9894ce0d59
commit 9c054fbcf2
9 changed files with 66 additions and 8 deletions

View File

@ -111,7 +111,8 @@ Full checklist: [`docs/manual-qa/NEO-25.md`](../manual-qa/NEO-25.md).
- **`TargetSelectionClient`** emits **`selection_event(event: Dictionary)`** only when the server-acknowledged **`lockedTargetId`** changes (not on **`validity`-only** updates; those stay on **`target_state_changed`**). - **`TargetSelectionClient`** emits **`selection_event(event: Dictionary)`** only when the server-acknowledged **`lockedTargetId`** changes (not on **`validity`-only** updates; those stay on **`target_state_changed`**).
- Payload keys: **`previous`** ([String] or `null`), **`next`** (same), **`cause`** (`tab` \| `clear` \| `server_correction`), **`sequence`** (int from the new state). **`POST …/target/select`** from **`request_select_target_id`** (including the path used by Tab) uses cause **`tab`**; **`request_clear_target`** uses **`clear`**; every **`GET …/target`** completion uses **`server_correction`** (including boot sync). - Payload keys: **`previous`** ([String] or `null`), **`next`** (same), **`cause`** (`tab` \| `clear` \| `server_correction`), **`sequence`** (int from the new state). **`POST …/target/select`** from **`request_select_target_id`** (including the path used by Tab) uses cause **`tab`**; **`request_clear_target`** uses **`clear`**; every **`GET …/target`** completion uses **`server_correction`** (including boot sync).
- **Dev logging:** enable **`log_selection_events`** on the `TargetSelectionClient` node to mirror each event to the Godot **Output** (`print`). - **Slice 3 telemetry hook (NEO-27):** this selection transition maps to product event name **`target_changed`**; with **`log_selection_events`** enabled, Godot Output prints the `target_changed` token for manual verification. **TODO(E9.M1):** replace dev log with cataloged telemetry emit.
- **Ability telemetry reserves (NEO-27):** `scripts/main.gd` documents future hook sites for **`ability_cast_requested`** / **`ability_cast_denied`** (implemented in E1.M4 + E5.M1, not in this story).
- **E1.M4+:** connect to **`$TargetSelectionClient.selection_event`** (or your scenes path to that node) without reshaping the payload. - **E1.M4+:** connect to **`$TargetSelectionClient.selection_event`** (or your scenes path to that node) without reshaping the payload.
Full checklist: [`docs/manual-qa/NEO-26.md`](../docs/manual-qa/NEO-26.md). Full checklist: [`docs/manual-qa/NEO-26.md`](../docs/manual-qa/NEO-26.md).

View File

@ -200,6 +200,11 @@ func _on_authoritative_position(world: Vector3, apply_as_snap: bool) -> void:
## surface client-vs-server divergence. This is the same signal `TargetSelectionClient` ## surface client-vs-server divergence. This is the same signal `TargetSelectionClient`
## hooks for movement-driven lock refresh — we do not snap the capsule to this value ## hooks for movement-driven lock refresh — we do not snap the capsule to this value
## (that is `authoritative_position_received`'s job on boot + move rejection). ## (that is `authoritative_position_received`'s job on boot + move rejection).
##
## NEO-27 telemetry reserve (input/cast path not implemented in E1.M3):
## - `ability_cast_requested` hook will attach in E1.M4 input wiring before cast submit.
## - `ability_cast_denied` hook will attach on cast-response denial (`reasonCode`) in E1.M4+E5.M1.
## TODO(E9.M1): map both to the telemetry schema/catalog once available.
func _on_authoritative_ack_for_hud(world: Vector3) -> void: func _on_authoritative_ack_for_hud(world: Vector3) -> void:
_last_ack_world = world _last_ack_world = world
_have_last_ack = true _have_last_ack = true

View File

@ -26,6 +26,8 @@ signal target_state_changed(state: Dictionary)
## [code]cause[/code] ([code]"tab"[/code] | [code]"clear"[/code] | ## [code]cause[/code] ([code]"tab"[/code] | [code]"clear"[/code] |
## [code]"server_correction"[/code]), [code]sequence[/code] ([int], from new state). ## [code]"server_correction"[/code]), [code]sequence[/code] ([int], from new state).
## E1.M4+ may [code]connect[/code] here without reshaping. ## E1.M4+ may [code]connect[/code] here without reshaping.
## NEO-27 telemetry hook: this transition maps to product event name [code]target_changed[/code]
## (TODO(E9.M1): wire schema/catalog and ingest once telemetry contracts land).
signal selection_event(event: Dictionary) signal selection_event(event: Dictionary)
enum Phase { IDLE, GET, POST_SELECT } enum Phase { IDLE, GET, POST_SELECT }
@ -351,4 +353,6 @@ func _maybe_emit_selection_event(
} }
selection_event.emit(ev) selection_event.emit(ev)
if log_selection_events: if log_selection_events:
print("[TargetSelectionClient] SelectionEvent ", ev) # NEO-27 telemetry hook site: `target_changed` mirrors this payload shape.
# TODO(E9.M1): replace dev print with cataloged telemetry emit.
print("[TargetSelectionClient] target_changed ", ev)

View File

@ -100,7 +100,7 @@ flowchart LR
## Implementation snapshot ## Implementation snapshot
- **`TargetState` (prototype JSON v1 — NEO-23):** server **`GET /game/players/{id}/target`** and **`POST /game/players/{id}/target/select`** with versioned **`PlayerTargetStateResponse`** / **`TargetSelectRequest`** / **`TargetSelectResponse`**; stub registry + in-memory lock under `server/NeonSprawl.Server/Game/Targeting/`; horizontal reach + soft lock semantics — [NEO-23](../../plans/NEO-23-implementation-plan.md), [server README — Targeting](../../../server/README.md#targeting-neo-23). Godot tab-target: [NEO-24](https://linear.app/neon-sprawl/issue/NEO-24). **`InteractableDescriptor` list:** **`GET /game/world/interactables`** (NEO-25). **`SelectionEvent` (prototype):** Godot **`TargetSelectionClient.selection_event`** when **`lockedTargetId`** changes — [NEO-26](../../plans/NEO-26-implementation-plan.md). **Story backlog:** [E1M3 prototype backlog](../../plans/E1M3-prototype-backlog.md). - **`TargetState` (prototype JSON v1 — NEO-23):** server **`GET /game/players/{id}/target`** and **`POST /game/players/{id}/target/select`** with versioned **`PlayerTargetStateResponse`** / **`TargetSelectRequest`** / **`TargetSelectResponse`**; stub registry + in-memory lock under `server/NeonSprawl.Server/Game/Targeting/`; horizontal reach + soft lock semantics — [NEO-23](../../plans/NEO-23-implementation-plan.md), [server README — Targeting](../../../server/README.md#targeting-neo-23). Godot tab-target: [NEO-24](https://linear.app/neon-sprawl/issue/NEO-24). **`InteractableDescriptor` list:** **`GET /game/world/interactables`** (NEO-25). **`SelectionEvent` (prototype):** Godot **`TargetSelectionClient.selection_event`** when **`lockedTargetId`** changes — [NEO-26](../../plans/NEO-26-implementation-plan.md). **NEO-27 hook mapping:** `selection_event` maps to Slice 3 telemetry name **`target_changed`** (dev log token + TODO(E9.M1) in `client/scripts/target_selection_client.gd`); server lock mutation hook comments live in `server/NeonSprawl.Server/Game/Targeting/InMemoryPlayerTargetLockStore.cs`; ability hook reserves (`ability_cast_requested` / `ability_cast_denied`) are documented in `client/scripts/main.gd` pending E1.M4 + E5.M1 — [NEO-27](../../plans/NEO-27-implementation-plan.md). **Story backlog:** [E1M3 prototype backlog](../../plans/E1M3-prototype-backlog.md).
- **Precursor under E1.M1 (NEO-9 + NEO-25):** server **`POST /game/players/{id}/interact`** with **`InteractionRequest`** / **`InteractionResponse`**; **`GET /game/world/interactables`** (versioned descriptor list); **`HorizontalReach`** (`server/NeonSprawl.Server/Game/World/HorizontalReach.cs`); prototype registry under `server/NeonSprawl.Server/Game/Interaction/`; client **`interactables_catalog_client.gd`**, **`interactable_world_builder.gd`**, **`interaction_request_client.gd`**, **`interaction_radius_indicators.gd`** — see [NEO-9](../../plans/NEO-9-implementation-plan.md), [NEO-25](../../plans/NEO-25-implementation-plan.md), and [server README — Interaction](../../../server/README.md#interaction-neo-9). - **Precursor under E1.M1 (NEO-9 + NEO-25):** server **`POST /game/players/{id}/interact`** with **`InteractionRequest`** / **`InteractionResponse`**; **`GET /game/world/interactables`** (versioned descriptor list); **`HorizontalReach`** (`server/NeonSprawl.Server/Game/World/HorizontalReach.cs`); prototype registry under `server/NeonSprawl.Server/Game/Interaction/`; client **`interactables_catalog_client.gd`**, **`interactable_world_builder.gd`**, **`interaction_request_client.gd`**, **`interaction_radius_indicators.gd`** — see [NEO-9](../../plans/NEO-9-implementation-plan.md), [NEO-25](../../plans/NEO-25-implementation-plan.md), and [server README — Interaction](../../../server/README.md#interaction-neo-9).
- **Alignment:** [Documentation and implementation alignment](documentation_and_implementation_alignment.md). - **Alignment:** [Documentation and implementation alignment](documentation_and_implementation_alignment.md).
@ -110,7 +110,7 @@ flowchart LR
- **Occluders vs truth:** camera occlusion and pick-through ([NEO-20](https://linear.app/neon-sprawl/issue/NEO-20)) affect **presentation** and click-to-move; **server** eligibility must not depend on “what the camera showed.” - **Occluders vs truth:** camera occlusion and pick-through ([NEO-20](https://linear.app/neon-sprawl/issue/NEO-20)) affect **presentation** and click-to-move; **server** eligibility must not depend on “what the camera showed.”
- **Lag compensation** for tab-target: deferred per [client vs server authority](client_server_authority.md#what-this-doc-does-not-fix-yet); decide with combat design when E5.M1 networking MVP is scoped. - **Lag compensation** for tab-target: deferred per [client vs server authority](client_server_authority.md#what-this-doc-does-not-fix-yet); decide with combat design when E5.M1 networking MVP is scoped.
**Telemetry (Slice 3):** `target_changed`, `ability_cast_requested`, `ability_cast_denied` **TODO(E9.M1)** until event schema exists. **Telemetry (Slice 3):** `target_changed`, `ability_cast_requested`, `ability_cast_denied` hook sites are documented in client/server code (NEO-27); **TODO(E9.M1)** until event schema/catalog and ingest wiring exist.
## Linear backlog ## Linear backlog

View File

@ -48,7 +48,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
|--------|-----------------|----------|-------------------| |--------|-----------------|----------|-------------------|
| E1.M1 | Ready | Prototype milestone **Done** ([E1.M1](https://linear.app/neon-sprawl/project/e1m1-inputandmovementruntime-20eb28dd3db4)). Authoritative `PositionState` + **MoveCommand** over HTTP (JSON v1 snap); **in-memory** store by default, **Postgres** when configured ([NEO-8](../../plans/NEO-8-implementation-plan.md)); shared **NpgsqlDataSource** disposed on host shutdown ([NEO-13](../../plans/NEO-13-implementation-plan.md)); Godot sync + path-follow ([NEO-7](../../plans/NEO-7-implementation-plan.md), [NEO-11](../../plans/NEO-11-implementation-plan.md)); **InteractionRequest** + horizontal range ([NEO-9](../../plans/NEO-9-implementation-plan.md)). Follow-on: prediction/reconciliation, Slice 1 telemetry, Protobuf wire per [contracts.md](contracts.md). | [NEO-6](../../plans/NEO-6-implementation-plan.md), [NEO-7](../../plans/NEO-7-implementation-plan.md), [NEO-8](../../plans/NEO-8-implementation-plan.md), [NEO-9](../../plans/NEO-9-implementation-plan.md), [NEO-10](../../plans/NEO-10-implementation-plan.md), [NEO-11](../../plans/NEO-11-implementation-plan.md), [NEO-13](../../plans/NEO-13-implementation-plan.md); `server/NeonSprawl.Server/Game/PositionState/`, `Game/Interaction/`; [server README](../../../server/README.md) | | E1.M1 | Ready | Prototype milestone **Done** ([E1.M1](https://linear.app/neon-sprawl/project/e1m1-inputandmovementruntime-20eb28dd3db4)). Authoritative `PositionState` + **MoveCommand** over HTTP (JSON v1 snap); **in-memory** store by default, **Postgres** when configured ([NEO-8](../../plans/NEO-8-implementation-plan.md)); shared **NpgsqlDataSource** disposed on host shutdown ([NEO-13](../../plans/NEO-13-implementation-plan.md)); Godot sync + path-follow ([NEO-7](../../plans/NEO-7-implementation-plan.md), [NEO-11](../../plans/NEO-11-implementation-plan.md)); **InteractionRequest** + horizontal range ([NEO-9](../../plans/NEO-9-implementation-plan.md)). Follow-on: prediction/reconciliation, Slice 1 telemetry, Protobuf wire per [contracts.md](contracts.md). | [NEO-6](../../plans/NEO-6-implementation-plan.md), [NEO-7](../../plans/NEO-7-implementation-plan.md), [NEO-8](../../plans/NEO-8-implementation-plan.md), [NEO-9](../../plans/NEO-9-implementation-plan.md), [NEO-10](../../plans/NEO-10-implementation-plan.md), [NEO-11](../../plans/NEO-11-implementation-plan.md), [NEO-13](../../plans/NEO-13-implementation-plan.md); `server/NeonSprawl.Server/Game/PositionState/`, `Game/Interaction/`; [server README](../../../server/README.md) |
| E1.M2 | Ready | **Slice 2 prototype slice closed:** follow + `CameraState` ([NEO-15](https://linear.app/neon-sprawl/issue/NEO-15)); zoom bands ([NEO-16](https://linear.app/neon-sprawl/issue/NEO-16)); occlusion ([NEO-17](https://linear.app/neon-sprawl/issue/NEO-17)); occluder pick-through ([NEO-20](https://linear.app/neon-sprawl/issue/NEO-20)); contracts + hardening ([NEO-18](https://linear.app/neon-sprawl/issue/NEO-18)). Client-local; no server use of camera pose. | [NEO-15](../../plans/NEO-15-implementation-plan.md), [NEO-16](../../plans/NEO-16-implementation-plan.md), [NEO-17](../../plans/NEO-17-implementation-plan.md), [NEO-18](../../plans/NEO-18-implementation-plan.md), [NEO-20](../../plans/NEO-20-implementation-plan.md); `client/scripts/isometric_follow_camera.gd`, `camera_state.gd`, `zoom_band_config.gd`, `occlusion_policy.gd`, `ground_pick.gd`; [E1_M2_IsometricCameraController.md](E1_M2_IsometricCameraController.md) | | E1.M2 | Ready | **Slice 2 prototype slice closed:** follow + `CameraState` ([NEO-15](https://linear.app/neon-sprawl/issue/NEO-15)); zoom bands ([NEO-16](https://linear.app/neon-sprawl/issue/NEO-16)); occlusion ([NEO-17](https://linear.app/neon-sprawl/issue/NEO-17)); occluder pick-through ([NEO-20](https://linear.app/neon-sprawl/issue/NEO-20)); contracts + hardening ([NEO-18](https://linear.app/neon-sprawl/issue/NEO-18)). Client-local; no server use of camera pose. | [NEO-15](../../plans/NEO-15-implementation-plan.md), [NEO-16](../../plans/NEO-16-implementation-plan.md), [NEO-17](../../plans/NEO-17-implementation-plan.md), [NEO-18](../../plans/NEO-18-implementation-plan.md), [NEO-20](../../plans/NEO-20-implementation-plan.md); `client/scripts/isometric_follow_camera.gd`, `camera_state.gd`, `zoom_band_config.gd`, `occlusion_policy.gd`, `ground_pick.gd`; [E1_M2_IsometricCameraController.md](E1_M2_IsometricCameraController.md) |
| E1.M3 | In Progress | **NEO-23 landed:** JSON v1 targeting read + select (`GET`/`POST …/target`, `Game/Targeting/`, [NEO-23](../../plans/NEO-23-implementation-plan.md)) — `PlayerTargetStateResponse` / soft lock / `reasonCode` denials; wire name differs from illustrative **`TargetState`** in module doc (called out in plan + README). **NEO-24 landed:** client tab-target + lock HUD synced to server ([NEO-24](../../plans/NEO-24-implementation-plan.md)) — `TargetSelectionClient` ([`client/scripts/target_selection_client.gd`](../../../client/scripts/target_selection_client.gd)), Tab / Esc bindings, `TargetLockLabel` HUD, hybrid movement-triggered refresh via new `PositionAuthorityClient.authoritative_ack` signal (fires on every server-confirmed position, boot + `move-stream` 200) with a 250 ms cooldown; manual QA in [`docs/manual-qa/NEO-24.md`](../../manual-qa/NEO-24.md). **NEO-25 landed:** versioned **`GET /game/world/interactables`** ([NEO-25](../../plans/NEO-25-implementation-plan.md)) — `InteractablesListResponse` / `InteractablesWorldApi` in `server/NeonSprawl.Server/Game/Interaction/`; Godot `interactables_catalog_client.gd` + `interactable_world_builder.gd` (fetch-driven props + glow); [server README — Interactable descriptors (NEO-25)](../../../server/README.md#interactable-descriptors-neo-25). **NEO-26 landed:** prototype **`SelectionEvent`** — **`TargetSelectionClient.selection_event`** ([NEO-26](../../plans/NEO-26-implementation-plan.md)) when **`lockedTargetId`** changes; optional **`log_selection_events`**. **Follow-on / still open:** richer **`InteractableDescriptor`** consumers beyond list projection + existing `POST …/interact`, Slice 3 telemetry (see [E1M3 prototype backlog](../../plans/E1M3-prototype-backlog.md)). **Precursor (E1.M1):** [NEO-9](../../plans/NEO-9-implementation-plan.md) `POST …/interact`. | Linear [NEO-24](https://linear.app/neon-sprawl/issue/NEO-24)[NEO-27](https://linear.app/neon-sprawl/issue/NEO-27); [E1_M3_InteractionAndTargetingLayer.md](E1_M3_InteractionAndTargetingLayer.md); [E1M3 prototype backlog](../../plans/E1M3-prototype-backlog.md); [server README — Targeting](../../../server/README.md#targeting-neo-23), [Interactable descriptors (NEO-25)](../../../server/README.md#interactable-descriptors-neo-25), [Interaction](../../../server/README.md#interaction-neo-9) | | E1.M3 | In Progress | **NEO-23 landed:** JSON v1 targeting read + select (`GET`/`POST …/target`, `Game/Targeting/`, [NEO-23](../../plans/NEO-23-implementation-plan.md)) — `PlayerTargetStateResponse` / soft lock / `reasonCode` denials; wire name differs from illustrative **`TargetState`** in module doc (called out in plan + README). **NEO-24 landed:** client tab-target + lock HUD synced to server ([NEO-24](../../plans/NEO-24-implementation-plan.md)) — `TargetSelectionClient` ([`client/scripts/target_selection_client.gd`](../../../client/scripts/target_selection_client.gd)), Tab / Esc bindings, `TargetLockLabel` HUD, hybrid movement-triggered refresh via new `PositionAuthorityClient.authoritative_ack` signal (fires on every server-confirmed position, boot + `move-stream` 200) with a 250 ms cooldown; manual QA in [`docs/manual-qa/NEO-24.md`](../../manual-qa/NEO-24.md). **NEO-25 landed:** versioned **`GET /game/world/interactables`** ([NEO-25](../../plans/NEO-25-implementation-plan.md)) — `InteractablesListResponse` / `InteractablesWorldApi` in `server/NeonSprawl.Server/Game/Interaction/`; Godot `interactables_catalog_client.gd` + `interactable_world_builder.gd` (fetch-driven props + glow); [server README — Interactable descriptors (NEO-25)](../../../server/README.md#interactable-descriptors-neo-25). **NEO-26 landed:** prototype **`SelectionEvent`** — **`TargetSelectionClient.selection_event`** ([NEO-26](../../plans/NEO-26-implementation-plan.md)) when **`lockedTargetId`** changes; optional **`log_selection_events`**. **NEO-27 landed:** Slice 3 telemetry hook sites documented — `selection_event` maps to product `target_changed` in `target_selection_client.gd`; server lock transition hook comments in `InMemoryPlayerTargetLockStore`; reserved `ability_cast_requested` / `ability_cast_denied` comments in `client/scripts/main.gd` (all TODO(E9.M1)); see [NEO-27](../../plans/NEO-27-implementation-plan.md) and [`docs/manual-qa/NEO-27.md`](../../manual-qa/NEO-27.md). **Follow-on / still open:** richer **`InteractableDescriptor`** consumers beyond list projection + existing `POST …/interact`; production telemetry ingest/catalog after E9.M1. **Precursor (E1.M1):** [NEO-9](../../plans/NEO-9-implementation-plan.md) `POST …/interact`. | Linear [NEO-24](https://linear.app/neon-sprawl/issue/NEO-24)[NEO-27](https://linear.app/neon-sprawl/issue/NEO-27); [E1_M3_InteractionAndTargetingLayer.md](E1_M3_InteractionAndTargetingLayer.md); [E1M3 prototype backlog](../../plans/E1M3-prototype-backlog.md); [server README — Targeting](../../../server/README.md#targeting-neo-23), [Interactable descriptors (NEO-25)](../../../server/README.md#interactable-descriptors-neo-25), [Interaction](../../../server/README.md#interaction-neo-9) |
--- ---

View File

@ -0,0 +1,32 @@
# NEO-27 — Manual QA checklist
Story: [NEO-27](https://linear.app/neon-sprawl/issue/NEO-27/e1m3-slice-3-telemetry-hook-sites-target-changed)
Plan: [`docs/plans/NEO-27-implementation-plan.md`](../plans/NEO-27-implementation-plan.md)
## Preconditions
- Game server running (`server/NeonSprawl.Server`).
- Godot client opens `client/project.godot` and runs `scenes/main.tscn`.
- In the scene, `TargetSelectionClient.log_selection_events` is enabled (`true`).
## Checks
- [ ] **`target_changed` dev token appears on lock transition**
- Press `Tab` to acquire a target lock or switch lock ids.
- Verify Godot Output prints a line prefixed with `[TargetSelectionClient] target_changed`.
- Confirm payload includes `previous`, `next`, `cause`, and `sequence`.
- [ ] **No `target_changed` log on validity-only updates**
- Hold a lock, move to force `validity` changes (`ok` ↔ `out_of_range`) without lock-id change.
- Verify `target_state_changed` behavior remains, and no new `target_changed` log is printed for id-unchanged refreshes.
- [ ] **Ability telemetry reserves are documented (not implemented)**
- Confirm comment block exists in `client/scripts/main.gd` naming `ability_cast_requested` and `ability_cast_denied`.
- Confirm comments include `TODO(E9.M1)` and reference E1.M4/E5.M1 as future wiring points.
## Expected outcome
- Slice 3 telemetry hook names are explicitly present in code/docs:
- Active hook mapping: `target_changed`.
- Reserved hook names: `ability_cast_requested`, `ability_cast_denied`.
- No production telemetry pipeline is introduced in this story.

View File

@ -12,6 +12,12 @@
| **Parent context** | [Epic 1 — Core Player Runtime](https://linear.app/neon-sprawl/project/epic-1-core-player-runtime-client-controls-character-loop-66bd590cd016) · [E1M3 prototype backlog](E1M3-prototype-backlog.md) | | **Parent context** | [Epic 1 — Core Player Runtime](https://linear.app/neon-sprawl/project/epic-1-core-player-runtime-client-controls-character-loop-66bd590cd016) · [E1M3 prototype backlog](E1M3-prototype-backlog.md) |
| **Decomposition** | [Epic 1 Slice 3 — telemetry hooks](../decomposition/epics/epic_01_core_player_runtime.md#epic-1-slice-3); [E1.M3 — InteractionAndTargetingLayer](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md) | | **Decomposition** | [Epic 1 Slice 3 — telemetry hooks](../decomposition/epics/epic_01_core_player_runtime.md#epic-1-slice-3); [E1.M3 — InteractionAndTargetingLayer](../decomposition/modules/E1_M3_InteractionAndTargetingLayer.md) |
## Kickoff clarifications
- **Q1 (resolved):** Where should reserved `ability_cast_requested` / `ability_cast_denied` hook comments live for this story?
- **Answer:** Keep them in **`client/scripts/main.gd`** (no extra placeholder script in NEO-27).
- **Why no additional blocking questions:** Event names, scope, and out-of-scope were explicit in Linear + backlog docs; this was the only implementation-location decision not already settled.
## Goal, scope, and out-of-scope ## Goal, scope, and out-of-scope
**Goal:** Establish **documented hook sites** for Epic 1 Slice 3 product telemetry names: **`target_changed`** (wired to real selection transitions), plus **reserved** (no-op / comment-only) sites for **`ability_cast_requested`** and **`ability_cast_denied`** until [E1.M4](../decomposition/modules/E1_M4_AbilityInputScaffold.md) + [E5.M1](../decomposition/modules/E5_M1_CombatRulesEngine.md). All real ingest waits on **`TODO(E9.M1)`** per [E9.M1 — TelemetryEventSchema](../decomposition/modules/E9_M1_TelemetryEventSchema.md). **Goal:** Establish **documented hook sites** for Epic 1 Slice 3 product telemetry names: **`target_changed`** (wired to real selection transitions), plus **reserved** (no-op / comment-only) sites for **`ability_cast_requested`** and **`ability_cast_denied`** until [E1.M4](../decomposition/modules/E1_M4_AbilityInputScaffold.md) + [E5.M1](../decomposition/modules/E5_M1_CombatRulesEngine.md). All real ingest waits on **`TODO(E9.M1)`** per [E9.M1 — TelemetryEventSchema](../decomposition/modules/E9_M1_TelemetryEventSchema.md).
@ -48,7 +54,9 @@
## Files to add ## Files to add
**None** — hook sites are comments / optional log text on existing modules (unless we later extract a shared `TelemetryHooks` static class; out of scope for this slice). | Path | Rationale |
|------|-----------|
| `docs/manual-qa/NEO-27.md` | Ticket-level manual verification checklist for Slice 3 telemetry hook-site behavior and expectations. |
## Files to modify ## Files to modify
@ -61,19 +69,20 @@
| `client/README.md` | Short “Slice 3 telemetry (NEO-27)” pointer next to SelectionEvent section. | | `client/README.md` | Short “Slice 3 telemetry (NEO-27)” pointer next to SelectionEvent section. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E1.M3 snapshot: NEO-27 telemetry hook sites landed (per team rule). | | `docs/decomposition/modules/documentation_and_implementation_alignment.md` | E1.M3 snapshot: NEO-27 telemetry hook sites landed (per team rule). |
| `docs/decomposition/modules/E1_M3_InteractionAndTargetingLayer.md` | Optional one-line under telemetry: code hook pointers (`target_selection_client.gd`, lock store)—only if the module table still says hooks are TODO-only; keep register alignment. | | `docs/decomposition/modules/E1_M3_InteractionAndTargetingLayer.md` | Optional one-line under telemetry: code hook pointers (`target_selection_client.gd`, lock store)—only if the module table still says hooks are TODO-only; keep register alignment. |
| `docs/plans/NEO-27-implementation-plan.md` | Keep kickoff clarifications and resolved decisions synchronized with actual implementation choices. |
## Tests ## Tests
| File | Action | | File | Action |
|------|--------| |------|--------|
| `client/test/selection_event_client_test.gd` | **Change only if** new observable behavior is added (e.g. new export or signal). If implementation is comments + optional `print` format only, **no test change** — manual QA: enable `log_selection_events`, tab/clear, confirm output contains `target_changed`. | | `client/test/selection_event_client_test.gd` | **Change only if** new observable behavior is added (e.g. new export or signal). If implementation is comments + optional `print` format only, **no test change** — manual QA: enable `log_selection_events`, tab/clear, confirm output contains `target_changed`. |
| `docs/manual-qa/NEO-27.md` | Add explicit manual checks for `target_changed` log token, id-change-only emission behavior, and ability telemetry reserve documentation. |
**Server:** No new tests required for comment-only / dev-trace placeholders unless we introduce a testable abstraction (out of scope). **Server:** No new tests required for comment-only / dev-trace placeholders unless we introduce a testable abstraction (out of scope).
## Open questions / risks ## Open questions / risks
- **Ability reserve file:** If you prefer a dedicated `ability_input_placeholder.gd` for E1.M4 instead of `main.gd`, say so before implementation; plan defaults to **`main.gd`** to avoid extra nodes until E1.M4.
**Resolved (2026-04-26):** Linear no longer lists **NEO-24** as blocking NEO-27 (dependency removed on board). **Resolved (2026-04-26):** Linear no longer lists **NEO-24** as blocking NEO-27 (dependency removed on board).
**Resolved (2026-04-26):** Keep ability telemetry reserve comments in **`client/scripts/main.gd`** for NEO-27; no placeholder script yet.
**None** otherwise. **None** otherwise.

View File

@ -44,6 +44,8 @@ public sealed class InMemoryPlayerTargetLockStore : IPlayerTargetLockStore
var next = new PlayerLockRow(null, prev.Sequence + 1); var next = new PlayerLockRow(null, prev.Sequence + 1);
rows[key] = next; rows[key] = next;
// NEO-27 telemetry hook site: `target_changed` fires when lock id transitions.
// TODO(E9.M1): emit cataloged telemetry event (player id, previous/next lock, cause=clear).
return (null, next.Sequence); return (null, next.Sequence);
} }
} }
@ -67,6 +69,8 @@ public sealed class InMemoryPlayerTargetLockStore : IPlayerTargetLockStore
var next = new PlayerLockRow(targetIdLowercase, prev.Sequence + 1); var next = new PlayerLockRow(targetIdLowercase, prev.Sequence + 1);
rows[key] = next; rows[key] = next;
// NEO-27 telemetry hook site: `target_changed` fires when lock id transitions.
// TODO(E9.M1): emit cataloged telemetry event (player id, previous/next lock, cause=tab/select).
return (next.LockIdLowercase, next.Sequence); return (next.LockIdLowercase, next.Sequence);
} }
} }

View File

@ -6,6 +6,9 @@ namespace NeonSprawl.Server.Game.Targeting;
/// <summary>Maps <c>GET/POST …/target</c> selection APIs (NEO-23).</summary> /// <summary>Maps <c>GET/POST …/target</c> selection APIs (NEO-23).</summary>
public static class TargetingApi public static class TargetingApi
{ {
// NEO-27 note: these are target-selection deny reasons, not cast telemetry events.
// Future ability flow (E1.M4 + E5.M1) will hook `ability_cast_denied` on cast endpoints.
// TODO(E9.M1): map deny reason telemetry to cataloged event schema.
public const string ReasonUnknownTarget = "unknown_target"; public const string ReasonUnknownTarget = "unknown_target";
public const string ReasonOutOfRange = "out_of_range"; public const string ReasonOutOfRange = "out_of_range";