Compare commits

...

16 Commits

Author SHA1 Message Date
VinPropane f89efdce9b
Merge pull request #68 from ViPro-Technologies/docs/e2m2-linear-neo-37-44
docs: E2.M2 Linear backlog in decomposition + documentation review follow-up
2026-05-04 21:12:17 -04:00
VinPropane e2e413f42c docs: address 2026-04-30 documentation review suggestions 2026-05-04 21:09:05 -04:00
VinPropane f67bae79d8 chore: add 2026-05-04 documentation review 2026-05-04 21:06:51 -04:00
VinPropane 4236d1ec00 chore: link E2.M2 backlog to Linear NEO-37–NEO-44 2026-05-04 20:57:01 -04:00
VinPropane de43ec1e4a
Merge pull request #67 from ViPro-Technologies/NEO-36-e2m1-read-only-skill-catalog-http-projection
NEO-36: E2.M1 read-only skill catalog HTTP projection + Bruno
2026-05-04 20:48:20 -04:00
VinPropane 8797c72e79
Merge branch 'main' into NEO-36-e2m1-read-only-skill-catalog-http-projection 2026-05-04 20:47:04 -04:00
VinPropane a0c3599525 NEO-36: address code review (manual QA header, alignment, README, Bruno) 2026-05-04 20:44:52 -04:00
VinPropane 28fcd52c8c NEO-36: add curl example to manual QA checklist 2026-05-04 20:37:20 -04:00
VinPropane ab597ff350 NEO-36: expose GET /game/world/skill-definitions with tests, Bruno, QA doc 2026-05-04 20:36:06 -04:00
VinPropane 044b4f823c NEO-36: add implementation plan for skill catalog HTTP API 2026-05-04 20:33:53 -04:00
VinPropane d6d6c81bb1
Merge pull request #66 from ViPro-Technologies/chore/pre-push-clean-working-tree
chore: require clean working tree in pre-push hook
2026-05-04 20:32:20 -04:00
VinPropane 1070cf88fd chore: require clean working tree in pre-push hook 2026-05-04 20:30:04 -04:00
VinPropane a5f7f1b4ae
Merge pull request #65 from ViPro-Technologies/chore/dotnet-workflow-all-prs-to-main
chore: Run .NET workflow on every PR to main
2026-05-04 20:27:27 -04:00
VinPropane 6b75fbdbd0 chore: grant pull-requests read for paths-filter API 2026-05-04 20:24:35 -04:00
VinPropane 0fd2dfe780 chore: skip .NET steps on PR when server paths unchanged 2026-05-04 20:21:18 -04:00
VinPropane 7b6d106240 chore: run .NET workflow on all PRs to main 2026-05-04 20:16:08 -04:00
22 changed files with 503 additions and 22 deletions

View File

@ -1,4 +1,8 @@
# Build and test the C# solution (server + tests). Targets repo root NeonSprawl.sln.
# PRs: workflow always runs (so ".NET / build" can be a required check). Path-sensitive
# work uses step-level `if` — skipped steps still leave the job successful (unlike a
# workflow skipped via `on.paths`, which leaves the required check missing/pending).
# push: path filter on `on` avoids redundant runs on unrelated commits to main.
name: .NET
on:
@ -10,10 +14,12 @@ on:
- ".github/workflows/dotnet.yml"
pull_request:
branches: [main]
paths:
- "NeonSprawl.sln"
- "server/**"
- ".github/workflows/dotnet.yml"
# dorny/paths-filter reads the PR file list from the GitHub API; the default token
# scope is too narrow without pull-requests:read ("Resource not accessible by integration").
permissions:
contents: read
pull-requests: read
jobs:
build:
@ -36,18 +42,37 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Detect .NET-relevant changes (PR only)
id: paths
if: github.event_name == 'pull_request'
uses: dorny/paths-filter@v3
with:
filters: |
server:
- "NeonSprawl.sln"
- "server/**"
- ".github/workflows/dotnet.yml"
- name: Skip .NET build (no server-side paths in this PR)
if: github.event_name == 'pull_request' && steps.paths.outputs.server != 'true'
run: echo "Skipping .NET restore, build, and tests."
- name: Setup .NET
if: github.event_name == 'push' || steps.paths.outputs.server == 'true'
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Restore
if: github.event_name == 'push' || steps.paths.outputs.server == 'true'
run: dotnet restore NeonSprawl.sln
- name: Build
if: github.event_name == 'push' || steps.paths.outputs.server == 'true'
run: dotnet build NeonSprawl.sln --no-restore --configuration Release
- name: Test
if: github.event_name == 'push' || steps.paths.outputs.server == 'true'
env:
ConnectionStrings__NeonSprawl: >-
Host=localhost;Port=5432;Database=neon_sprawl;Username=neon_sprawl;Password=neon_sprawl_dev

View File

@ -0,0 +1,47 @@
meta {
name: GET skill definitions
type: http
seq: 1
}
get {
url: {{baseUrl}}/game/world/skill-definitions
body: none
auth: none
}
tests {
test("returns 200 JSON with schema v1 and skills array", function () {
expect(res.getStatus()).to.equal(200);
expect(res.getHeader("content-type")).to.contain("application/json");
const body = res.getBody();
expect(body.schemaVersion).to.equal(1);
expect(body.skills).to.be.an("array");
expect(body.skills.length).to.equal(3);
});
test("skills are ascending by id (ordinal)", function () {
const body = res.getBody();
const ids = body.skills.map((x) => x.id);
// Match server StringComparer.Ordinal (prototype ids are ASCII snake_case).
const sorted = [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
expect(ids).to.eql(sorted);
});
test("frozen prototype trio is present", function () {
const body = res.getBody();
const ids = new Set(body.skills.map((x) => x.id));
expect(ids.has("salvage")).to.equal(true);
expect(ids.has("refine")).to.equal(true);
expect(ids.has("intrusion")).to.equal(true);
});
test("salvage row matches catalog", function () {
const body = res.getBody();
const row = body.skills.find((x) => x.id === "salvage");
expect(row).to.be.an("object");
expect(row.displayName).to.equal("Salvage");
expect(row.category).to.equal("gather");
expect(row.allowedXpSourceKinds).to.include.members(["activity", "mission_reward"]);
});
}

View File

@ -0,0 +1,3 @@
meta {
name: skill-definitions
}

View File

@ -30,6 +30,8 @@ Provide a data-driven **non-combat skill** layer: `SkillDef` registry, **skill**
- Key contracts: `XpGrantEvent`, `LevelCurve`, `LevelUpEvent`
- Dependencies: E2.M1
- Stage target: Prototype
- **Linear (Slice 2 prototype):** [Epic 2 project](https://linear.app/neon-sprawl/project/epic-2-classless-skill-and-progression-framework-5200f84bf8b6) — [NEO-37](https://linear.app/neon-sprawl/issue/NEO-37) → [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) → [NEO-39](https://linear.app/neon-sprawl/issue/NEO-39); [NEO-40](https://linear.app/neon-sprawl/issue/NEO-40) in parallel with NEO-39 after NEO-38 (label **`E2.M2`**). Detail: [E2.M2 — XpAwardAndLevelEngine](../modules/E2_M2_XpAwardAndLevelEngine.md).
- **Linear (Slice 3 integration):** [NEO-41](https://linear.app/neon-sprawl/issue/NEO-41) (gather + **E3.M1**), [NEO-42](https://linear.app/neon-sprawl/issue/NEO-42) (craft + **E3.M2**), [NEO-43](https://linear.app/neon-sprawl/issue/NEO-43) (mission rewards + **E7.M2**), [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44) (gig XP from combat, **E5.M1** only — no **`E2.M2`** label on NEO-44).
- Note: **E3.M1** and **E3.M2** (and mission/reward routers such as **E7.M2**) integrate as **callers** of **E2.M2** for **skill** XP. **E5.M1** resolves **combat** and advances **gig** progression **outside** **E2.M2**—see Slice 3. None of these are **hard** dependencies for implementing the **E2.M2** core (stub callers in Slice 2).
### E2.M3 - MasteryAndPerkUnlocks
@ -68,6 +70,7 @@ Provide a data-driven **non-combat skill** layer: `SkillDef` registry, **skill**
### Slice 3 - XP award integration at activity sites
- **Linear:** [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) (see **E2.M2** module breakdown above).
- Scope: Wire **gathering** (E3.M1) and **crafting** (E3.M2) to the **E2.M2 skill XP** award API so prototype **`SkillDef`** lines gain XP from real play. Wire **combat** (E5.M1) to **gig XP only** ([`gigs.md`](../../game-design/gigs.md), [`progression.md`](../../game-design/progression.md))—**no** hook that maps **encounter resolution** to **skill** XP or a combat `SkillDef`. **Mission / quest reward** paths may still call the **skill** XP API when the **payout** explicitly includes skill XP (**not** the same as “combat trained this skill”).
- Dependencies: E2.M2, E3.M1, E3.M2, E5.M1 (integration prerequisites; not part of the E2.M2 module dependency list). Gig XP plumbing may live beside E2.M2 or under Epic 5—**split call sites** by progression type (skill vs gig).
- Acceptance criteria:

View File

@ -9,6 +9,10 @@
| **Stage target** | Prototype |
| **Status** | Planned (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). No implementation landed yet; update this section and the [alignment](documentation_and_implementation_alignment.md) row when 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).

View File

@ -50,7 +50,8 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
| 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`**. **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) |
| E1.M4 | In Progress | **NEO-29 landed:** prototype `HotbarLoadout` v1 server contract (`GET`/`POST /game/players/{id}/hotbar-loadout`) with stable deny reason codes, per-player scope, and persistence policy matching NEO-8/NS-17 (Postgres when configured, in-memory fallback otherwise). Client boot hydration is wired via `hotbar_loadout_client.gd` + `hotbar_state.gd` from `main.gd`; manual QA checklist created. **NEO-31 landed:** prototype **`POST /game/players/{id}/ability-cast`**, digit-key cast wiring from `main.gd`, `ability_cast_client.gd`, and `hotbar_cast_slot_resolver.gd` (target id from `lockedTargetId` in cached target state); GdUnit covers resolver + HTTP client; manual QA [NEO-31](../../manual-qa/NEO-31.md). **NEO-28 landed:** cast POST validates **lock + registry + range** (`invalid_target`, `out_of_range`); **`AbilityCastClient.cast_result_received`** + **`CastFeedbackLabel`** HUD line; manual QA [NEO-28](../../manual-qa/NEO-28.md). **NEO-30 landed:** Slice 3 cast funnel telemetry **hook-site comments** in [`AbilityCastApi.cs`](../../../server/NeonSprawl.Server/Game/AbilityInput/AbilityCastApi.cs) (`ability_cast_requested` on authoritative accept, `ability_cast_denied` + non-empty `reasonCode` on each JSON deny branch); client dev hooks unchanged; manual QA [NEO-30](../../manual-qa/NEO-30.md); plan [NEO-30](../../plans/NEO-30-implementation-plan.md). **NEO-32 landed:** `GET /game/players/{id}/cooldown-snapshot` + `on_cooldown` cast deny + prototype global cooldown commit; [`cooldown_snapshot_client.gd`](../../../client/scripts/cooldown_snapshot_client.gd), [`cooldown_state.gd`](../../../client/scripts/cooldown_state.gd), **`CooldownSlotsLabel`** in [`main.gd`](../../../client/scripts/main.gd); manual QA [NEO-32](../../manual-qa/NEO-32.md); plan [NEO-32](../../plans/NEO-32-implementation-plan.md). | [NEO-29](../../plans/NEO-29-implementation-plan.md), [NEO-31](../../plans/NEO-31-implementation-plan.md), [NEO-28](../../plans/NEO-28-implementation-plan.md), [NEO-30](../../plans/NEO-30-implementation-plan.md), [NEO-32](../../plans/NEO-32-implementation-plan.md); [E1_M4_AbilityInputScaffold.md](E1_M4_AbilityInputScaffold.md); [E1M4 prototype backlog](../../plans/E1M4-prototype-backlog.md); `server/NeonSprawl.Server/Game/AbilityInput/`; [`client/scripts/hotbar_loadout_client.gd`](../../../client/scripts/hotbar_loadout_client.gd), [`client/scripts/hotbar_state.gd`](../../../client/scripts/hotbar_state.gd), [`client/scripts/ability_cast_client.gd`](../../../client/scripts/ability_cast_client.gd), [`client/scripts/hotbar_cast_slot_resolver.gd`](../../../client/scripts/hotbar_cast_slot_resolver.gd), [`client/scripts/cooldown_snapshot_client.gd`](../../../client/scripts/cooldown_snapshot_client.gd), [`client/scripts/cooldown_state.gd`](../../../client/scripts/cooldown_state.gd); [server README — Hotbar loadout](../../../server/README.md#hotbar-loadout-neo-29), [Ability cast (NEO-31)](../../../server/README.md#ability-cast-neo-31), [Cooldown snapshot (NEO-32)](../../../server/README.md#cooldown-snapshot-neo-32) |
| E2.M1 | In Progress | **NEO-33 landed:** frozen prototype trio **`salvage`** / **`refine`** / **`intrusion`** in [`content/skills/prototype_skills.json`](../../../content/skills/prototype_skills.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) enforce schema, duplicate `id`, exact trio ids, and category coverage; designer note on **`allowedXpSourceKinds`** in [E2_M1](E2_M1_SkillDefinitionRegistry.md) + [`content/README.md`](../../../content/README.md). **NEO-34 landed:** fail-fast server load of `content/skills/*.json` at startup — `server/NeonSprawl.Server/Game/Skills/` ([NEO-34](../../plans/NEO-34-implementation-plan.md)); config + discovery in [server README — Skill catalog](../../../server/README.md#skill-catalog-contentskills-neo-34). **NEO-35 landed:** `ISkillDefinitionRegistry` / `SkillDefinitionRegistry` + DI ([NEO-35](../../plans/NEO-35-implementation-plan.md)). **Follow-on:** read HTTP + Bruno ([NEO-36](https://linear.app/neon-sprawl/issue/NEO-36)). | [NEO-33](../../plans/NEO-33-implementation-plan.md), [NEO-34](../../plans/NEO-34-implementation-plan.md), [NEO-35](../../plans/NEO-35-implementation-plan.md); [E2_M1](E2_M1_SkillDefinitionRegistry.md); [module_dependency_register.md](module_dependency_register.md) **E2.M1 note**; `server/NeonSprawl.Server/Game/Skills/` |
| E2.M1 | In Progress | **NEO-33 landed:** frozen prototype trio **`salvage`** / **`refine`** / **`intrusion`** in [`content/skills/prototype_skills.json`](../../../content/skills/prototype_skills.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py) enforce schema, duplicate `id`, exact trio ids, and category coverage; designer note on **`allowedXpSourceKinds`** in [E2_M1](E2_M1_SkillDefinitionRegistry.md) + [`content/README.md`](../../../content/README.md). **NEO-34 landed:** fail-fast server load of `content/skills/*.json` at startup — `server/NeonSprawl.Server/Game/Skills/` ([NEO-34](../../plans/NEO-34-implementation-plan.md)); config + discovery in [server README — Skill catalog](../../../server/README.md#skill-catalog-contentskills-neo-34). **NEO-35 landed:** `ISkillDefinitionRegistry` / `SkillDefinitionRegistry` + DI ([NEO-35](../../plans/NEO-35-implementation-plan.md)). **NEO-36 landed:** versioned **`GET /game/world/skill-definitions`** ([NEO-36](../../plans/NEO-36-implementation-plan.md)) — `SkillDefinitionsWorldApi` + `SkillDefinitionsListDtos` in `server/NeonSprawl.Server/Game/Skills/`; Bruno `bruno/neon-sprawl-server/skill-definitions/`; manual QA [`NEO-36`](../../manual-qa/NEO-36.md). **Follow-on (E2.M2):** XP grant validation vs `allowedXpSourceKinds` per [E2_M1](E2_M1_SkillDefinitionRegistry.md). | [NEO-33](../../plans/NEO-33-implementation-plan.md), [NEO-34](../../plans/NEO-34-implementation-plan.md), [NEO-35](../../plans/NEO-35-implementation-plan.md), [NEO-36](../../plans/NEO-36-implementation-plan.md); [E2_M1](E2_M1_SkillDefinitionRegistry.md); [module_dependency_register.md](module_dependency_register.md) **E2.M1 note**; `server/NeonSprawl.Server/Game/Skills/`; [`docs/manual-qa/NEO-36.md`](../../manual-qa/NEO-36.md); [server README — Skill definitions (NEO-36)](../../../server/README.md#skill-definitions-neo-36) |
| E2.M2 | Planned | **Backlog (Linear):** Slice 2 vertical slices — [NEO-37](https://linear.app/neon-sprawl/issue/NEO-37) (player skill progression read snapshot) → [NEO-38](https://linear.app/neon-sprawl/issue/NEO-38) (apply XP grant + `allowedXpSourceKinds` validation + 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, parallel with NEO-39 after NEO-38). Slice 3 integration — [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) (multi-label with **E3**/**E7**); [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44) (gig XP from combat, **E5.M1**). See [epic_02 — E2.M2 + Slice 3](../epics/epic_02_skills_and_progression.md). | [E2_M2](E2_M2_XpAwardAndLevelEngine.md); label **`E2.M2`** on NEO-37NEO-40, NEO-41NEO-43 |
---

View File

@ -8,4 +8,4 @@
Non-implementation notes: **gigs** (combat roles), **skills** (non-combat), **[abilities](abilities.md)** (what players trigger), **[items](items.md)** (loot, craft, equip), **[gathering](gathering.md)** (resource nodes, yields), **[crafting](crafting.md)** (recipes, pipeline), **[economy](economy.md)** (faucets, sinks, trade), **[death-loss-recovery](death-loss-recovery.md)** (stakes, recovery), **[risk-security-bands](risk-security-bands.md)** (tiers, PvP eligibility), **[zones](zones.md)** (place identity), travel/mechanics stubs, and other design artifacts before they are tied to concrete systems or tickets.
Implementation-facing breakdowns stay under [`docs/decomposition/`](../decomposition/README.md) and plans under [`docs/plans/`](../plans/).
Implementation-facing breakdowns stay under [`docs/decomposition/`](../decomposition/README.md) and plans under [`docs/plans/`](../plans/) (see [`docs/plans/README.md`](../plans/README.md) for **NEO-*** vs legacy **`NEON-*`** plan filenames).

View File

@ -2,7 +2,7 @@
Vision for **what the player triggers** in play: combat **kits**, non-combat **skill interactions**, and **item**-carried actions—and how those map to **gigs**, **`SkillDef`**, and **Seams**. This is **not** a full ability list or data schema; it steers combat design, UX, and implementation boundaries.
**Framing:** [progression.md](progression.md) (**gig** vs **skill**). **Combat roles:** [gigs.md](gigs.md). **Gear, craft, consumables:** [skills.md](skills.md) **Seams** ([anchor](skills.md#seams-gigs-skills)). **Item buckets & acquisition:** [items.md](items.md). **Gather → craft economy spine:** [gathering.md](gathering.md), [crafting.md](crafting.md). **Combat pillar pointers:** [overview.md — Combat pillars](overview.md#combat-pillars-vision-stub).
**Framing:** [progression.md](progression.md) (**gig** vs **skill**). **Combat roles:** [gigs.md](gigs.md). **Gear, craft, consumables:** [skills.md](skills.md) — [Seams (gigs ↔ skills)](skills.md#seams-gigs-skills). **Item buckets & acquisition:** [items.md](items.md). **Gather → craft economy spine:** [gathering.md](gathering.md), [crafting.md](crafting.md). **Combat pillar pointers:** [overview.md — Combat pillars](overview.md#combat-pillars-vision-stub).
## Vocabulary

View File

@ -39,21 +39,14 @@ Neon Sprawls design work here starts from **concrete visions** of progression
### Planned topics (stubs)
Link new files here when they exist; remove or rewrite this subsection once the table is populated.
**Shipped vision docs** are listed in the [Artifact index](#artifact-index) above—update that table when a new first-class file lands.
- **Progression** — [progression.md](progression.md): hybrid **gig + skill** overview and vocabulary. **Gigs:** [gigs.md](gigs.md). **Skills** + seams: [skills.md](skills.md). Recruitment channel **deferred**; *professions* stay folded into **skills** + rep unless a future doc defines something separate.
- **Zones** — [zones.md](zones.md): tone, danger, faction/economic **hooks**; **graph** in [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md).
- **Travel & connections** — how places link (gates, costs, downtime, danger en route); complements zone identity.
- **Mechanics** — loops, constraints, what the server vs. client must honor.
- **Items** — [items.md](items.md): categories, rarity philosophy, crafting vs. drops; **Seams** for gig/skill gates.
- **Abilities** — [abilities.md](abilities.md): inputs, timing; combo with items; **gig** vs **skill** gates (see [progression.md](progression.md) vocabulary). **Combat context:** [Combat pillars (vision stub)](#combat-pillars-vision-stub).
- **Gathering & resources** — [gathering.md](gathering.md): nodes, competition, exhaustion, open-world contesting; **skill XP** not **gig** XP.
- **Crafting & recipes** — [crafting.md](crafting.md): depth vs breadth, failure, quality, stations; **skill XP**; [items.md](items.md) for output buckets.
- **Economy** — [economy.md](economy.md): currency, sinks, trade, **E3.M5** / **E8.M3**; **mission** payouts vs **combat** loot ([progression.md](progression.md)).
- **Factions & reputation** — who remembers you, grudges, access gates; **gig** story arcs: [gigs.md](gigs.md), rep + seams: [skills.md](skills.md).
- **Combat pillars** — [Vision stub below](#combat-pillars-vision-stub): PvE vs. PvP stance, readability, time-to-kill, fairness; **items in combat** in [items.md](items.md).
- **Death, loss & recovery** — [death-loss-recovery.md](death-loss-recovery.md): stakes, **E3.M4**/**E6.M3**, recovery UX.
- **Risk & security bands** — [risk-security-bands.md](risk-security-bands.md): **tiers**, **PvP** opt-in, **theft**/gather **contest** hooks; **place** **fiction**: [zones.md](zones.md).
The bullets below are **roadmap / not-yet-first-class-file** topics (or pointers into existing docs for cross-cutting angles):
- **Travel & connections** — how places link (gates, costs, downtime, danger en route); complements [zones.md](zones.md) and [E4.M1](../decomposition/modules/E4_M1_ZoneGraphAndTravelRules.md).
- **Mechanics** — loops, constraints, what the server vs. client must honor (no dedicated vision file yet).
- **Factions & reputation** — who remembers you, grudges, access gates; ties [gigs.md](gigs.md), [skills.md](skills.md), future faction doc.
- **Combat pillars** — [Vision stub below](#combat-pillars-vision-stub); **items in combat** in [items.md](items.md).
- **Social play** — squad scale, corps, trust, betrayal affordances, async cooperation.
- **Encounters & enemy roles** — archetypes, what a fight teaches, variety without noise. **Skill + gig split runs:** [brainstorm/hack-bodyguard-missions.md](brainstorm/hack-bodyguard-missions.md).
- **Onboarding** — first session hook, clarity vs. mystery, how much systems the new player sees.
@ -79,5 +72,5 @@ Combat is still missing a **single** pillar doc. Until then, use this table for
| Area | Location |
|------|----------|
| Module / epic breakdown | [`docs/decomposition/`](../decomposition/README.md) |
| Implementation plans | [`docs/plans/`](../plans/) |
| Implementation plans | [`docs/plans/`](../plans/) — [README (NEO vs NEON filenames)](../plans/README.md) |
| Architecture | [`docs/architecture/`](../architecture/tech_stack.md) |

View File

@ -0,0 +1,27 @@
# NEO-36 — Manual QA checklist
| Field | Value |
|-------|-------|
| Key | NEO-36 |
| Title | E2.M1: Read-only skill catalog HTTP projection + Bruno |
| Linear | https://linear.app/neon-sprawl/issue/NEO-36/e2m1-read-only-skill-catalog-http-projection-bruno |
| Plan | `docs/plans/NEO-36-implementation-plan.md` |
| Branch | `NEO-36-e2m1-read-only-skill-catalog-http-projection` |
## Preconditions
- Server built and configured with default `Content:SkillsDirectory` pointing at repo `content/skills` (local dev / `InMemoryWebApplicationFactory` tests use the same layout).
## Checklist
1. Start **`NeonSprawl.Server`** (e.g. `dotnet run` from `server/NeonSprawl.Server`).
2. **`GET /game/world/skill-definitions`** — expect **200** and **`Content-Type`** containing **`application/json`**. Example (default dev URL from `Properties/launchSettings.json` and Bruno `environments/Local.bru`; change the host/port if yours differs):
```bash
curl -sS -i "http://localhost:5253/game/world/skill-definitions"
```
3. Parse JSON — expect **`schemaVersion`** === **1**, **`skills`** array length **3**.
4. Confirm **`id`** values include **`intrusion`**, **`refine`**, **`salvage`** (frozen prototype trio).
5. Spot-check **`salvage`**: **`displayName`** “Salvage”, **`category`** **`gather`**, **`allowedXpSourceKinds`** includes **`activity`** and **`mission_reward`**.
6. Optional: run **`bruno/neon-sprawl-server/skill-definitions/Get skill definitions.bru`** against the same **`baseUrl`** (see `environments/Local.bru`).

View File

@ -0,0 +1,81 @@
# NEO-36 — Implementation plan
## Story reference
| Field | Value |
|--------|--------|
| **Key** | NEO-36 |
| **Title** | E2.M1: Read-only skill catalog HTTP projection + Bruno |
| **Linear** | https://linear.app/neon-sprawl/issue/NEO-36/e2m1-read-only-skill-catalog-http-projection-bruno |
| **Module** | [E2.M1 — SkillDefinitionRegistry](../decomposition/modules/E2_M1_SkillDefinitionRegistry.md) |
## Kickoff clarifications
No questions were put to the user during kickoff.
**Reason:** The issue specifies the response fields (`id`, `displayName`, `category`, `allowedXpSourceKinds`), excludes mutations and client polish, and points at “match server routing style.” The repo already exposes a read-only world projection at `GET /game/world/interactables` (`InteractablesWorldApi`) with a versioned JSON envelope—this story can mirror that pattern. **`ISkillDefinitionRegistry`** and the frozen prototype trio (`salvage`, `refine`, `intrusion`) are present on `main` from **NEO-33NEO-35** (`prototype_skills.json`, `SkillDefinitionRegistryTests`). Linear may still list **blockedBy NEO-35**; the implementation branch is based on **`main`** where that work is already merged.
**If you want a different public path** than the default in Technical approach (below), say so before implementation.
## Goal, scope, and out-of-scope
**Goal:** Expose a stable, read-only JSON endpoint that returns all loaded prototype skill definitions for dev tooling, Bruno, and manual QA, backed by **`ISkillDefinitionRegistry`** without duplicating catalog truth outside the server.
**In scope (from Linear):**
- One GET route consistent with existing APIs (under `/game/`, same style as other read-only projections).
- JSON rows including **`id`**, **`displayName`**, **`category`**, **`allowedXpSourceKinds`** per skill.
- Bruno request(s) under `bruno/neon-sprawl-server/`, mirroring existing collections (e.g. `interaction/Get interactables list.bru`).
- `docs/manual-qa/NEO-36.md` checklist (endpoint is user-visible for QA per repo rules).
**Out of scope (from Linear):**
- Mutations or authoring APIs.
- Client UX / debug panel (optional and not required).
## Acceptance criteria checklist
- [x] Bruno (and/or automated test) can call the endpoint against a running dev server and observe the **three frozen ids** (`salvage`, `refine`, `intrusion`).
- [x] `docs/manual-qa/NEO-36.md` exists with a short checklist for hitting the endpoint.
## Technical approach
1. **Route:** Add **`GET /game/world/skill-definitions`** (parallel to **`/game/world/interactables`**) as a parameterless read. If “world” reads odd for catalog-only data, acceptable alternative is **`GET /game/content/skill-definitions`**—pick one in implementation and keep Bruno/tests aligned; default **`/game/world/skill-definitions`** for consistency with the other static registry projection.
2. **Response shape:** Follow the interactables pattern: top-level **`schemaVersion`** (const `1` for first ship of this contract) plus an array (e.g. **`skills`**) of objects with **`id`**, **`displayName`**, **`category`**, **`allowedXpSourceKinds`**. Order skills by **`id`** ordinal to match **`ISkillDefinitionRegistry.GetDefinitionsInIdOrder()`**.
3. **Implementation:** New static **`SkillDefinitionsWorldApi`** (or **`SkillCatalogApi`**) in `Game/Skills/` that injects **`ISkillDefinitionRegistry`**, maps rows to DTOs (either anonymous projection types or small sealed DTO types in a `*Dtos.cs` file next to the API), returns **`Results.Json`**. Wire **`app.MapSkillDefinitionsWorldApi()`** (name TBD) from **`Program.cs`** next to other `Map*Api` calls.
4. **Bruno:** New folder `bruno/neon-sprawl-server/skill-definitions/` with **`Get skill definitions.bru`** (and optional **`folder.bru`** if the collection uses per-folder metadata like `ability-cast`). Reuse **`{{baseUrl}}`** from environments; tests assert 200, JSON, schema version, array length ≥ 3, and presence of the three frozen ids (mirror style of **`Get interactables list.bru`**).
5. **Automated tests:** Add **`SkillDefinitionsWorldApiTests`** (or equivalent) using **`InMemoryWebApplicationFactory`**, **`HttpClient.GetAsync`**, **`ReadFromJsonAsync`** on the response DTO—same structure as **`InteractablesWorldApiTests`**, asserting the trio and ordering.
## Files to add
| Path | Purpose |
|------|---------|
| `server/NeonSprawl.Server/Game/Skills/SkillDefinitionsWorldApi.cs` (or chosen name) | `Map*` extension registering GET route; maps registry → JSON. |
| `server/NeonSprawl.Server/Game/Skills/SkillDefinitionsListDtos.cs` (or inline records in API file if kept tiny) | Versioned response + row DTOs for JSON serialization. |
| `server/NeonSprawl.Server.Tests/Game/Skills/SkillDefinitionsWorldApiTests.cs` | HTTP integration: 200, schema, three ids, id order. |
| `bruno/neon-sprawl-server/skill-definitions/Get skill definitions.bru` | Manual / Bruno verification against dev server. |
| `bruno/neon-sprawl-server/skill-definitions/folder.bru` | Optional; match Bruno folder conventions used elsewhere. |
| `docs/manual-qa/NEO-36.md` | Checklist: start server, call GET, confirm trio (and note Bruno path). |
## Files to modify
| Path | Rationale |
|------|-----------|
| `server/NeonSprawl.Server/Program.cs` | Register the new `Map*Api()` alongside existing game APIs. |
## Tests
| Test file | What it covers |
|-----------|------------------|
| `server/NeonSprawl.Server.Tests/Game/Skills/SkillDefinitionsWorldApiTests.cs` | **Integration:** `GET` new route via **`InMemoryWebApplicationFactory`**; **`ReadFromJsonAsync`**; assert **`schemaVersion`**, ordered **`id`** list equals **`intrusion`**, **`refine`**, **`salvage`** (ordinal sort); spot-check one rows **`allowedXpSourceKinds`**. **AAA** comments per [csharp-style](../../.cursor/rules/csharp-style.md#unit-and-integration-tests-arrange-act-assert). |
Bruno scripts are manual verification; they complement but do not replace the automated test above.
## Open questions / risks
- **Linear relation:** Issue may still show **blockedBy NEO-35**; code on **`main`** already includes the registry. If **`main`** lags a fork, rebase onto **`main`** after NEO-35 merge.
- **Path naming:** If product prefers **`/content/`** over **`/world/`**, swap in one place (route + Bruno + tests) before shipping.

View File

@ -1,5 +1,7 @@
# NEON-29 Implementation Plan — Expand prototype client district for camera/nav stress QA
> **Historical — Jira archive.** This file preserved the **Atlassian / NEON-*** story record. **Active tracker:** Linear **NEO-***, team Neon Sprawl. The same work is described for implementation kickoff in [**NEO-19** — implementation plan](NEO-19-implementation-plan.md) (same title and scope). Do not open new work against **NEON-*** keys.
## Story reference
- **Key:** [NEON-29](https://neon-sprawl.atlassian.net/browse/NEON-29)

View File

@ -0,0 +1,15 @@
# Implementation plans (`docs/plans/`)
Story-level implementation plans live here, keyed by **Linear issue id** (`NEO-*`) when the work is tracked in Linear (team **NEO**, Neon Sprawl).
## Legacy `NEON-*` filenames
A small number of files keep the historical **Jira / Atlassian `NEON-*`** key in the **filename** only. They are **not** the active issue tracker for new work.
- Each such file includes a short **Historical** banner at the top pointing at the matching **`NEO-*`** plan (same scope) where kickoff and commits should align.
- Prefer opening and linking **`NEO-*`** issues and plans for new contributors.
## Conventions
- New or moved plans: `docs/plans/{LINEAR_KEY}-implementation-plan.md` (see [story kickoff](../../.cursor/rules/story-kickoff.md)).
- Cross-links from decomposition and game-design docs should use **`NEO-*`** URLs (Linear) for current engineering work.

View File

@ -0,0 +1,53 @@
# Documentation review — game design + decomposition alignment
**Date:** 2026-04-30
**Scope:** Representative pass over `docs/game-design/` (index, progression vocabulary, skills roster + seams hooks, abilities, gathering, risk/security), `docs/decomposition/README.md`, `docs/decomposition/modules/documentation_and_implementation_alignment.md`, and spot-checked epic/module paths referenced from those docs.
**Base:** Workspace as of 2026-04-30 (no branch named).
## Verdict
**Approve with suggestions** — vision docs and decomposition cross-links are coherent with the hybrid gig/skill model; no blocking factual errors found in the sampled paths. A few polish items improve discoverability and reader trust.
## Summary
Game-design entry points (`README.md`, `overview.md`, `progression.md`, `skills.md`, `abilities.md`, `gathering.md`, `risk-security-bands.md`) agree on **gig** = combat role, **skill** = non-combat, **combat encounter → gig XP** vs **gather/craft → skill XP**, and **Seams** as the gear/craft/deploy boundary. Decomposition README and the implementation alignment table mirror **E1.M3** / **E1.M4** story landings without contradicting the registers **In Progress** rows. Sampled relative links to epics, **E4.M1** / **E4.M4**, **E6.M1****M3**, **E3.M3**, and `docs/architecture/tech_stack.md` resolve. Residual risk is mostly **legacy plan naming** (**NEON-*** vs **NEO-***) and small markdown UX nits.
## Documentation checked
| Path | Assessment |
|------|------------|
| `docs/game-design/README.md` | **Matches** — index, correct links to overview, progression, decomposition, plans. |
| `docs/game-design/overview.md` | **Matches** — artifact table, hybrid links, Epic 4 slice anchor for risk, architecture link valid. |
| `docs/game-design/progression.md` | **Matches** — vocabulary table aligned with skills/gigs; Epic 2 Slice 3 note matches gathering/crafting docs. |
| `docs/game-design/skills.md` (incl. roster + `<a id="seams-gigs-skills">`) | **Matches** — explicit seam anchor; roster and Process/Make boundaries consistent with progression. |
| `docs/game-design/abilities.md` | **Partially matches** — content aligns with gigs/skills/items; link text `[anchor](skills.md#…)` is confusing (see Suggestions). |
| `docs/game-design/gathering.md` | **Matches** — skill XP vs gig XP; module links (E3.M1, E1.M3, E3.M3, E4.M2) present on disk. |
| `docs/game-design/risk-security-bands.md` | **Matches** — E4.M4 / E6.M1M2 / server authority framing matches docs-review ground truth. |
| `docs/decomposition/README.md` | **Matches** — epic table, tooling CT.* note, links to contracts and register. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E1.M3/E1.M4 narrative consistent with `module_dependency_register.md` grep for E1.M4. |
| `docs/decomposition/epics/epic_04_world_topology.md` (slice anchors) | **Matches**`#epic-4-slice-2` anchor exists for risk doc deep links. |
| `neon_sprawl_vision.plan.md` (existence check from doc links) | **Matches** — file at repo root as linked from game-design. |
| `docs/plans/NEON-29-implementation-plan.md` | **N/A** (sampled for drift) — legacy Jira **NEON-*** plan alongside Linear **NEO-*** plans; see Suggestions. |
## Blocking issues
None identified in this pass.
## Suggestions
1. ~~**`abilities.md` framing link** — Replace the link text `anchor` with something meaningful (e.g. `Seams (gigs ↔ skills)`) in the sentence that currently uses `[anchor](skills.md#seams-gigs-skills)` so readers scanning links see the destination intent.~~ **Done.** (`docs/game-design/abilities.md`)
2. ~~**Legacy `NEON-*` vs `NEO-*` plans** — `docs/plans/NEON-29-implementation-plan.md` still points at Atlassian; consider a short note in `docs/plans/` (only if you add a `README.md` there) or a header in each legacy file: “superseded by Linear NEO-* / historical” so new contributors do not treat **NEON-*** as the active tracker.~~ **Done.** — Banner on [`NEON-29-implementation-plan.md`](../plans/NEON-29-implementation-plan.md) → [NEO-19](../plans/NEO-19-implementation-plan.md); [`docs/plans/README.md`](../plans/README.md).
3. ~~**`overview.md` “Planned topics (stubs)”** — Large overlap with the artifact index above it; optional consolidation would reduce maintenance when adding a new vision file (update one table instead of two).~~ **Done.** — Stub list trimmed to roadmap-only bullets; index owns shipped files.
## Nits
- **`documentation_and_implementation_alignment.md`** — The E1.M3 table cell is very long; fine for a living log, but harder to diff. Optional: split per-ticket bullets under the module page only, keep one line + “see module doc” in the table.
- **TODO block in `overview.md` (rating / compliance)** — Appropriate as vision debt; no change required unless stakeholders want it moved to a single “compliance” stub file.
## Verification
After any edits:
1. ~~Re-open `docs/game-design/abilities.md` and confirm the Seams link reads naturally.~~ **Checked** (2026-05-05 follow-up).
2. ~~`rg 'NEON-' docs/plans` — decide whether each hit is archival or should gain a one-line deprecation pointer.~~ **Checked** — only [`NEON-29-implementation-plan.md`](../plans/NEON-29-implementation-plan.md); archival banner + [`docs/plans/README.md`](../plans/README.md).
3. Spot-check any new `docs/game-design/*.md` against `progression.md` vocabulary and `skills.md` Seams once before filing implementation plans.

View File

@ -0,0 +1,47 @@
# Code review: NEO-36 (read-only skill catalog HTTP)
**Date:** 2026-05-04
**Scope:** Branch `NEO-36-e2m1-read-only-skill-catalog-http-projection` vs merge-base `a5f7f1b4aedfb7928440ac2e1fef7035895544a6` (`origin/main` at review time): `GET /game/world/skill-definitions`, `SkillDefinitionsWorldApi`, `SkillDefinitionsListDtos`, `SkillDefinitionsWorldApiTests`, Bruno `skill-definitions/`, `docs/plans/NEO-36-implementation-plan.md`, `docs/manual-qa/NEO-36.md`.
**Base:** `origin/main` (merge-base above)
## Verdict
**Approve with nits**
## Summary
The branch adds a versioned read-only JSON projection backed by `ISkillDefinitionRegistry.GetDefinitionsInIdOrder()`, wired next to existing world projections in `Program.cs`, with integration tests mirroring `InteractablesWorldApiTests` (HTTP 200, `ReadFromJsonAsync`, schema version, frozen trio order, and spot-checks on `allowedXpSourceKinds`). Bruno requests assert the same contract. Behavior matches the NEO-36 plan and E2.M1 catalog intent: no mutations, no duplicate source of truth beyond the registry.
## Documentation checked
| Document | Result |
|----------|--------|
| `docs/plans/NEO-36-implementation-plan.md` | **Matches** — route `/game/world/skill-definitions`, `schemaVersion` + `skills` rows with the four fields, registry-backed mapping, tests + Bruno + manual QA as listed. |
| `docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md` | **Matches** — read-only exposure of stable ids, display names, categories, and `allowedXpSourceKinds`; aligns with “dev tooling / QA” style use; no XP grant enforcement here (E2.M2). |
| `docs/decomposition/modules/module_dependency_register.md` | **Matches** — E2.M1 in progress; HTTP projection is consistent with module scope. |
| `docs/decomposition/modules/documentation_and_implementation_alignment.md` | **Matches** — E2.M1 row updated for NEO-36 landed (route, paths, README + manual QA pointers). |
| `docs/decomposition/modules/contracts.md` | **N/A** (not opened end-to-end) — response uses explicit `JsonPropertyName` and a `schemaVersion` const; same pattern as interactables list. |
## Blocking issues
(none)
## Suggestions
1. ~~**`docs/manual-qa/NEO-36.md` header** — [planning-implementation-docs](../../.cursor/rules/planning-implementation-docs.md) asks for a small table with **Key**, **Title**, **Linear** link, **Plan** link, and **Branch** on ticketed manual QA files. Add that table so the checklist matches repo convention (same pattern as other `docs/manual-qa/NEO-*.md` files that follow the rule).~~ **Done.**
2. ~~**`documentation_and_implementation_alignment.md`** — Extend the E2.M1 row to note NEO-36: `GET /game/world/skill-definitions`, `Game/Skills/SkillDefinitionsWorldApi.cs` / DTOs, and pointer to `docs/plans/NEO-36-implementation-plan.md` (same PR or immediate follow-up per [documentation_and_implementation_alignment.md](../decomposition/modules/documentation_and_implementation_alignment.md)).~~ **Done.**
3. ~~**`server/README.md` (optional)** — Skill catalog section documents load policy (NEO-34) but not the read projection. A one-line anchor to the GET route would match how NEO-25 interactables are surfaced for operators; only if you want parity with other HTTP surfaces.~~ **Done.**
## Nits
1. ~~**Nit:** Bruno test sorts ids with `localeCompare`; server order is ordinal (`GetDefinitionsInIdOrder`). For current ASCII `snake_case` ids they agree; if ids ever used non-ASCII, consider documenting or aligning sort semantics in Bruno.~~ **Done.** (ordinal sort + comment in Bruno.)
## Verification
- `dotnet test NeonSprawl.sln --filter "FullyQualifiedName~SkillDefinitionsWorldApiTests"` — passed at review time.
- Optional: full `dotnet test NeonSprawl.sln` before merge.
- Manual: follow `docs/manual-qa/NEO-36.md` (after adding the header table if you take suggestion 1) and/or run the Bruno request against `Local` environment.

View File

@ -0,0 +1,57 @@
# Documentation review — game design, decomposition, and plans
**Date:** 2026-05-04
**Scope:** Follow-up pass on `docs/game-design/` (abilities, progression, overview, zones, death-loss-recovery, economy, gigs, skills seam anchor), `docs/decomposition/README.md` and `docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md`, plus recent plans `NEO-35-implementation-plan.md` and `NEO-36-implementation-plan.md`. Link spot-checks for epic anchors and schema paths referenced from those docs.
**Base:** Workspace as of 2026-05-04.
## Verdict
**Approve with suggestions** — hybrid gig/skill vocabulary, Seams, zone graph vs security overlay, and death/loss module wiring remain aligned with the docs-review ground truth. No blocking contradictions found. A few link and polish items improve trust; one item repeats an open suggestion from [2026-04-30-documentation-review.md](2026-04-30-documentation-review.md).
## Summary
Sampled vision docs still agree that **gig** = combat role, **skill** = non-combat, **combat encounters → gig XP** vs **gather/craft/mission-scripted → skill XP** where defined, and **economy** does not re-gate **craft** on **gig** (`docs/game-design/economy.md` **Gig vs skill**). `docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md` matches **NEO-35** / **NEO-36** plans on registry surface, HTTP projection scope, and `allowedXpSourceKinds` vs combat. Epic deep links checked from this pass resolve (`#epic-4-slice-1`, `#epic-3-slice-4`, `#seams-gigs-skills`). Residual work is mostly **markdown UX** and **one broken rule link** in an older plan.
## Documentation checked
| Path | Assessment |
|------|------------|
| `docs/game-design/abilities.md` | **Partially matches** — vocabulary and module links align with gigs/skills/items; first-line Seams link still uses placeholder link text `anchor` in the markdown link label (see Suggestions). |
| `docs/game-design/progression.md` | **Matches** — vocabulary and Epic 2 Slice 3 note consistent with E2.M1 and hybrid model. |
| `docs/game-design/overview.md` | **Matches** — artifact table, combat pillar stub, decomposition links coherent. |
| `docs/game-design/skills.md` (`#seams-gigs-skills`) | **Matches** — stable HTML id present; Seams remain authoritative boundary reference. |
| `docs/game-design/gigs.md` (roster) | **Matches** — nine-gig table consistent with overview “9-gig roster.” |
| `docs/game-design/economy.md` | **Matches** — gig vs skill economic identity aligned with progression and Seams. |
| `docs/game-design/zones.md` | **Matches** — E4.M1 fiction on same graph as tiers; links to E4.M2M4 and epic slice anchors valid. |
| `docs/game-design/death-loss-recovery.md` | **Matches** — E5.M1 / E3.M4 / E6.M3 / E9.M4 ordering and Seams pointer for chrome vs gear. |
| `docs/game-design/README.md` | **Matches** — index paths to overview, progression, decomposition, plans. |
| `docs/decomposition/README.md` | **Matches** — epic table, Linear alignment, links to contracts and register. |
| `docs/decomposition/modules/E2_M1_SkillDefinitionRegistry.md` | **Matches** — NEO-33→36 ordering, schema paths, combat vs skill XP rule matches game-design. |
| `docs/plans/NEO-35-implementation-plan.md` | **Matches** — acceptance criteria and E2.M2 forward notes align with E2.M1 module contract. |
| `docs/plans/NEO-36-implementation-plan.md` | **Matches** — route/DTO/Bruno/test story aligned with module and interactables pattern. |
| `content/schemas/skill-def.schema.json` | **Matches** — exists; referenced consistently from E2.M1 and NEO-35 plan. |
| `docs/plans/NEO-22-implementation-plan.md` (spot) | **Partially matches** — content fine; one link targets a non-existent rules file (see Suggestions). |
| `docs/plans/NEON-29-implementation-plan.md` (existence) | **N/A** — archival Jira-keyed plan; same caveat as 2026-04-30 review. |
## Blocking issues
None identified in this pass.
## Suggestions
1. **`abilities.md` Seams link text** — Replace `[anchor](skills.md#seams-gigs-skills)` with readable text (e.g. `Seams (gigs ↔ skills)`) on the framing line so scanners see intent. Same item as 2026-04-30 review; still open.
2. **`NEO-22-implementation-plan.md` broken rules link** — The bullet referencing **NEON-*** keys links to `../../.cursor/rules/jira-git-naming.md`, which does not exist. Point at [`linear-git-naming.md`](../../.cursor/rules/linear-git-naming.md) (or whichever rule is canonical) and adjust visible label if “Jira” is no longer accurate.
3. **Legacy `NEON-29` plan** — Still Atlassian-keyed; optional one-line header or `docs/plans/README.md` note so new contributors do not treat it as active Linear scope (same theme as 2026-04-30 review).
4. **`E2_M1_SkillDefinitionRegistry.md` plans note** — The sentence that plans are added at kickoff is satisfied for NEO-3336; optional one-line refresh (“plans exist under `docs/plans/NEO-33``NEO-36`”) to reduce staleness.
## Nits
- **`overview.md`**: Overlap between **Artifact index** and **Planned topics (stubs)** remains large; consolidation is optional maintenance savings (same as prior review).
## Verification
After edits:
1. Open `docs/game-design/abilities.md` and confirm the Seams link reads naturally in the first paragraph.
2. `rg 'jira-git-naming' docs` — should return no hits after fixing NEO-22.
3. `rg 'NEON-' docs/plans` — confirm each legacy file is either annotated or intentionally unchanged.

View File

@ -4,6 +4,13 @@ set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
echo "pre-push: checking for a clean working tree"
if [[ -n "$(git status --porcelain)" ]]; then
echo "pre-push: refuse to push — working tree is not clean. Commit, stash, or remove changes first:" >&2
git status --short >&2
exit 1
fi
gdlint_bin=""
gdformat_bin=""

View File

@ -0,0 +1,39 @@
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using NeonSprawl.Server.Game.Skills;
using Xunit;
namespace NeonSprawl.Server.Tests.Game.Skills;
public class SkillDefinitionsWorldApiTests
{
[Fact]
public async Task GetSkillDefinitions_ShouldReturnSchemaV1_WithFrozenTrioInIdOrder()
{
// Arrange
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
// Act
var response = await client.GetAsync("/game/world/skill-definitions");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<SkillDefinitionsListResponse>();
Assert.NotNull(body);
Assert.Equal(SkillDefinitionsListResponse.CurrentSchemaVersion, body!.SchemaVersion);
Assert.NotNull(body.Skills);
var ids = body.Skills.Select(static s => s.Id).ToList();
Assert.Equal(new[] { "intrusion", "refine", "salvage" }, ids);
var salvage = body.Skills.Single(s => s.Id == "salvage");
Assert.Equal("Salvage", salvage.DisplayName);
Assert.Equal("gather", salvage.Category);
Assert.Equal(new[] { "activity", "mission_reward" }, salvage.AllowedXpSourceKinds);
var refine = body.Skills.Single(s => s.Id == "refine");
Assert.Contains("trainer", refine.AllowedXpSourceKinds);
var intrusion = body.Skills.Single(s => s.Id == "intrusion");
Assert.Contains("book_or_item", intrusion.AllowedXpSourceKinds);
}
}

View File

@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
namespace NeonSprawl.Server.Game.Skills;
/// <summary>JSON body for <c>GET /game/world/skill-definitions</c> (NEO-36).</summary>
public sealed class SkillDefinitionsListResponse
{
public const int CurrentSchemaVersion = 1;
[JsonPropertyName("schemaVersion")]
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
/// <summary>Loaded skills ordered by stable <c>id</c> (ordinal), matching <see cref="ISkillDefinitionRegistry.GetDefinitionsInIdOrder"/>.</summary>
[JsonPropertyName("skills")]
public required IReadOnlyList<SkillDefinitionJson> Skills { get; init; }
}
/// <summary>One row in the read-only skill definition projection.</summary>
public sealed class SkillDefinitionJson
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("displayName")]
public required string DisplayName { get; init; }
[JsonPropertyName("category")]
public required string Category { get; init; }
[JsonPropertyName("allowedXpSourceKinds")]
public required IReadOnlyList<string> AllowedXpSourceKinds { get; init; }
}

View File

@ -0,0 +1,36 @@
namespace NeonSprawl.Server.Game.Skills;
/// <summary>Maps <c>GET /game/world/skill-definitions</c> (NEO-36).</summary>
public static class SkillDefinitionsWorldApi
{
public static WebApplication MapSkillDefinitionsWorldApi(this WebApplication app)
{
app.MapGet(
"/game/world/skill-definitions",
(ISkillDefinitionRegistry registry) =>
{
var defs = registry.GetDefinitionsInIdOrder();
var skills = new List<SkillDefinitionJson>(defs.Count);
foreach (var d in defs)
{
skills.Add(
new SkillDefinitionJson
{
Id = d.Id,
DisplayName = d.DisplayName,
Category = d.Category,
AllowedXpSourceKinds = d.AllowedXpSourceKinds,
});
}
return Results.Json(
new SkillDefinitionsListResponse
{
SchemaVersion = SkillDefinitionsListResponse.CurrentSchemaVersion,
Skills = skills,
});
});
return app;
}
}

View File

@ -27,6 +27,7 @@ app.MapGet("/health", () => Results.Json(new
app.MapPositionStateApi();
app.MapInteractionApi();
app.MapInteractablesWorldApi();
app.MapSkillDefinitionsWorldApi();
app.MapTargetingApi();
app.MapHotbarLoadoutApi();
app.MapCooldownSnapshotApi();

View File

@ -37,6 +37,14 @@ On startup the host loads every **`*.json`** under the skills directory, validat
On success, **Information** logs include the resolved skills directory path and distinct skill count.
## Skill definitions (NEO-36)
**`GET /game/world/skill-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`skills`**) backed by **`ISkillDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Plan: [NEO-36 implementation plan](../../docs/plans/NEO-36-implementation-plan.md); manual QA: [`docs/manual-qa/NEO-36.md`](../../docs/manual-qa/NEO-36.md); Bruno: `bruno/neon-sprawl-server/skill-definitions/`.
```bash
curl -sS -i "http://localhost:5253/game/world/skill-definitions"
```
## Position persistence (NEO-8)
When **`ConnectionStrings:NeonSprawl`** is set to a valid PostgreSQL connection string, the server uses the **`player_position`** table (relational columns `pos_x``sequence`, not JSONB). DDL lives in [`server/db/migrations/V001__player_position.sql`](../db/migrations/V001__player_position.sql); the app applies that script on startup and on first store use (idempotent `CREATE TABLE IF NOT EXISTS`). The configured dev player (`Game:DevPlayerId` / `Game:DefaultPosition`) is seeded once at host startup, matching the in-memory stores constructor seed (not on every HTTP request).