Compare commits
16 Commits
efad0e51b2
...
f89efdce9b
| Author | SHA1 | Date |
|---|---|---|
|
|
f89efdce9b | |
|
|
e2e413f42c | |
|
|
f67bae79d8 | |
|
|
4236d1ec00 | |
|
|
de43ec1e4a | |
|
|
8797c72e79 | |
|
|
a0c3599525 | |
|
|
28fcd52c8c | |
|
|
ab597ff350 | |
|
|
044b4f823c | |
|
|
d6d6c81bb1 | |
|
|
1070cf88fd | |
|
|
a5f7f1b4ae | |
|
|
6b75fbdbd0 | |
|
|
0fd2dfe780 | |
|
|
7b6d106240 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
meta {
|
||||
name: skill-definitions
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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-37–NEO-40, NEO-41–NEO-43 |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -39,21 +39,14 @@ Neon Sprawl’s 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) |
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
|
|
@ -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-33–NEO-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 row’s **`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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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 register’s **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.M1–M2 / 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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.M2–M4 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-33–36; 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.
|
||||
|
|
@ -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=""
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ app.MapGet("/health", () => Results.Json(new
|
|||
app.MapPositionStateApi();
|
||||
app.MapInteractionApi();
|
||||
app.MapInteractablesWorldApi();
|
||||
app.MapSkillDefinitionsWorldApi();
|
||||
app.MapTargetingApi();
|
||||
app.MapHotbarLoadoutApi();
|
||||
app.MapCooldownSnapshotApi();
|
||||
|
|
|
|||
|
|
@ -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 store’s constructor seed (not on every HTTP request).
|
||||
|
|
|
|||
Loading…
Reference in New Issue