NEO-53: GET world item-definitions, Bruno, docs, and tests
Registry-backed read model at /game/world/item-definitions; Bruno collection, manual QA checklist, README and module alignment updates.pull/88/head
parent
9e2e4ab3a7
commit
08428c4da4
|
|
@ -0,0 +1,50 @@
|
|||
meta {
|
||||
name: GET item definitions
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/game/world/item-definitions
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("returns 200 JSON with schema v1 and items 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.items).to.be.an("array");
|
||||
expect(body.items.length).to.equal(6);
|
||||
});
|
||||
|
||||
test("items are ascending by id (ordinal)", function () {
|
||||
const body = res.getBody();
|
||||
const ids = body.items.map((x) => x.id);
|
||||
const sorted = [...ids].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
expect(ids).to.eql(sorted);
|
||||
});
|
||||
|
||||
test("frozen prototype six is present", function () {
|
||||
const body = res.getBody();
|
||||
const ids = new Set(body.items.map((x) => x.id));
|
||||
expect(ids.has("scrap_metal_bulk")).to.equal(true);
|
||||
expect(ids.has("refined_plate_stock")).to.equal(true);
|
||||
expect(ids.has("field_stim_mk0")).to.equal(true);
|
||||
expect(ids.has("survey_drone_kit")).to.equal(true);
|
||||
expect(ids.has("contract_handoff_token")).to.equal(true);
|
||||
expect(ids.has("prototype_armor_shell")).to.equal(true);
|
||||
});
|
||||
|
||||
test("scrap_metal_bulk row matches catalog", function () {
|
||||
const body = res.getBody();
|
||||
const row = body.items.find((x) => x.id === "scrap_metal_bulk");
|
||||
expect(row).to.be.an("object");
|
||||
expect(row.displayName).to.equal("Scrap Metal (Bulk)");
|
||||
expect(row.prototypeRole).to.equal("material");
|
||||
expect(row.stackMax).to.equal(999);
|
||||
expect(row.inventorySlotKind).to.equal("bag");
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
meta {
|
||||
name: item-definitions
|
||||
}
|
||||
|
|
@ -64,7 +64,9 @@ Epic 3 **Slice 1** — MVP inventory; `item_created`, transfer failures.
|
|||
|
||||
**Server load (NEO-51):** On host startup, `server/NeonSprawl.Server/Game/Items/` loads `content/items/*_items.json` with the same validation gates as CI (`scripts/validate_content.py`) and **refuses to listen** when the catalog is invalid. Config and discovery: [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). Plan: [NEO-51 implementation plan](../../plans/NEO-51-implementation-plan.md).
|
||||
|
||||
**Item definition registry (NEO-52):** **`IItemDefinitionRegistry`** in `server/NeonSprawl.Server/Game/Items/` wraps the startup-loaded catalog for read-only lookup by stable `itemId` and ordered enumeration. **NEO-53+** (HTTP read model), **NEO-54+** (inventory engine), and future craft/gather callers should inject the interface rather than `ItemDefinitionCatalog`. Plan: [NEO-52 implementation plan](../../plans/NEO-52-implementation-plan.md).
|
||||
**Item definition registry (NEO-52):** **`IItemDefinitionRegistry`** in `server/NeonSprawl.Server/Game/Items/` wraps the startup-loaded catalog for read-only lookup by stable `itemId` and ordered enumeration. **NEO-54+** (inventory engine) and future craft/gather callers should inject the interface rather than `ItemDefinitionCatalog`. Plan: [NEO-52 implementation plan](../../plans/NEO-52-implementation-plan.md).
|
||||
|
||||
**Item definitions HTTP (NEO-53):** **`GET /game/world/item-definitions`** — versioned read-only projection (`schemaVersion` **1**, **`items`**) backed by **`IItemDefinitionRegistry`**; Bruno `bruno/neon-sprawl-server/item-definitions/`. Plan: [NEO-53 implementation plan](../../plans/NEO-53-implementation-plan.md).
|
||||
|
||||
**Linear backlog (decomposed):** [E3M3-prototype-backlog.md](../../plans/E3M3-prototype-backlog.md) — **E3M3-01** [NEO-50](https://linear.app/neon-sprawl/issue/NEO-50) (content + CI) through **E3M3-07** [NEO-56](https://linear.app/neon-sprawl/issue/NEO-56) (telemetry hooks).
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ Rows appear when work starts; default for unlisted modules is **Planned** / not
|
|||
| E2.M3 | In Progress | **NEO-45 landed:** prototype **`salvage`** mastery catalog + CI gates (see [NEO-45 plan](../../plans/NEO-45-implementation-plan.md)). **NEO-46 landed:** fail-fast server load under `server/NeonSprawl.Server/Game/Mastery/` — `MasteryCatalogLoader`, `IMasteryCatalogRegistry`, cross-check vs `ISkillDefinitionRegistry`, Slice 4 + **`tierIndex`** 1..N gate; see [NEO-46 plan](../../plans/NEO-46-implementation-plan.md). **NEO-47 landed:** `PerkUnlockEngine`, `IPlayerPerkStateStore` + **`V004`**, level-up hook in skill XP grants; see [NEO-47 plan](../../plans/NEO-47-implementation-plan.md). **NEO-49 landed:** comment-only **`perk_unlock`** telemetry hook site in [`PerkUnlockEngine.TryUnlockPerks`](../../../server/NeonSprawl.Server/Game/Mastery/PerkUnlockEngine.cs) ([NEO-49 plan](../../plans/NEO-49-implementation-plan.md), [`NEO-49` manual QA](../../manual-qa/NEO-49.md)); [server README — Perk unlock telemetry (NEO-49)](../../../server/README.md#perk-unlock-engine-and-telemetry-hooks-neo-47-neo-49). **NEO-48 landed:** **`GET`/`POST /game/players/{id}/perk-state`** — `PerkStateApi` + DTOs in `Game/Mastery/` ([NEO-48](../../plans/NEO-48-implementation-plan.md), [`NEO-48` manual QA](../../manual-qa/NEO-48.md)); [server README — Perk state (NEO-48)](../../../server/README.md#perk-state-neo-48); Bruno `bruno/neon-sprawl-server/perk-state/`. | [NEO-45](../../plans/NEO-45-implementation-plan.md), [NEO-46](../../plans/NEO-46-implementation-plan.md), [NEO-47](../../plans/NEO-47-implementation-plan.md), [NEO-48](../../plans/NEO-48-implementation-plan.md), [NEO-49](../../plans/NEO-49-implementation-plan.md), [E2M3-pre-production-backlog](../../plans/E2M3-pre-production-backlog.md), [E2_M3](E2_M3_MasteryAndPerkUnlocks.md) |
|
||||
| E2.M2 | In Progress | **NEO-37 landed:** versioned **`GET /game/players/{id}/skill-progression`** ([NEO-37](../../plans/NEO-37-implementation-plan.md)) — read model for every registered skill; known-player gate via `IPositionStateStore`; [server README — Skill progression snapshot (NEO-37)](../../../server/README.md#skill-progression-snapshot-neo-37); manual QA [`NEO-37`](../../manual-qa/NEO-37.md). **NEO-38 landed:** **`POST`** same path — grant apply, persistence (`IPlayerSkillProgressionStore`, `V003` migration), structured denies + **`levelUps`** ([NEO-38](../../plans/NEO-38-implementation-plan.md)); manual QA [`NEO-38`](../../manual-qa/NEO-38.md); Bruno `bruno/neon-sprawl-server/skill-progression/`; [server README — Skill progression grant (NEO-38)](../../../server/README.md#skill-progression-grant-neo-38). **NEO-39 landed:** data-driven `LevelCurve` content + schema + CI validation (`*_level_curve.json`) with fail-fast startup schema checks; progression GET/POST level resolution now uses `ISkillLevelCurve` content-backed thresholds ([NEO-39](../../plans/NEO-39-implementation-plan.md)); manual QA [`NEO-39`](../../manual-qa/NEO-39.md). **NEO-40 landed:** comment-only telemetry hook sites in [`SkillProgressionSnapshotApi.cs`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionSnapshotApi.cs) on **`POST …/skill-progression`** for future **`xp_grant`** / **`level_up`** ([NEO-40](../../plans/NEO-40-implementation-plan.md), [`NEO-40` manual QA](../../manual-qa/NEO-40.md)). **NEO-41 landed:** gather prototype — **`POST …/interact`** with **`kind: resource_node`** grants **`salvage`** + **`activity`** (10 XP) via [`SkillProgressionGrantOperations`](../../../server/NeonSprawl.Server/Game/Skills/SkillProgressionGrantOperations.cs) ([NEO-41](../../plans/NEO-41-implementation-plan.md), [`NEO-41` manual QA](../../manual-qa/NEO-41.md)); [server README — Interaction](../../../server/README.md#interaction-neo-9). **NEO-42 landed (prep):** **`RefineActivitySkillXpGrant`** / **`RefineSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack; **E3.M2** must invoke on craft/refine success ([NEO-42](../../plans/NEO-42-implementation-plan.md), [`NEO-42` manual QA](../../manual-qa/NEO-42.md)); [server README — Craft / refine hook (NEO-42)](../../../server/README.md#craft--refine-hook--skill-xp-neo-42). **NEO-43 landed (prep):** **`MissionRewardSkillXpGrant`** / **`MissionRewardSkillXpConstants`** + deny-registry factory + tests — same NEO-38 grant stack with fixed **`sourceKind: mission_reward`**; **E7.M2** must invoke from quest hand-in ([NEO-43](../../plans/NEO-43-implementation-plan.md), [`NEO-43` manual QA](../../manual-qa/NEO-43.md)); [server README — Mission / quest reward (NEO-43)](../../../server/README.md#mission--quest-reward--skill-xp-neo-43). **Slice 3 still open:** [NEO-44](https://linear.app/neon-sprawl/issue/NEO-44). See [epic_02 — E2.M2 + Slice 3](../epics/epic_02_skills_and_progression.md). | [NEO-37](../../plans/NEO-37-implementation-plan.md), [NEO-38](../../plans/NEO-38-implementation-plan.md), [NEO-39](../../plans/NEO-39-implementation-plan.md), [NEO-40](../../plans/NEO-40-implementation-plan.md), [NEO-41](../../plans/NEO-41-implementation-plan.md), [NEO-42](../../plans/NEO-42-implementation-plan.md), [NEO-43](../../plans/NEO-43-implementation-plan.md), [E2_M2](E2_M2_XpAwardAndLevelEngine.md); label **`E2.M2`** on NEO-37–NEO-41, NEO-42–NEO-44 |
|
||||
| E3.M1 | In Progress | **NEO-41 landed (prototype):** `POST …/interact` success on **`resource_node`** (`prototype_resource_node_alpha`) applies **`salvage`** skill XP (**`sourceKind: activity`**, 10 XP) via shared NEO-38 grant operations. **Still planned:** `GatherResult`, yields, inventory per [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md). | [NEO-41](../../plans/NEO-41-implementation-plan.md), [E3_M1](E3_M1_ResourceNodeAndGatherLoop.md); `server/NeonSprawl.Server/Game/Interaction/`, `Game/Skills/` |
|
||||
| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **Still planned:** inventory store, HTTP. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) |
|
||||
| E3.M3 | In Progress | **NEO-50 landed:** frozen prototype six-item catalog in [`content/items/prototype_items.json`](../../../content/items/prototype_items.json); [`item-def.schema.json`](../../../content/schemas/item-def.schema.json); PR gate + [`validate_content.py`](../../../scripts/validate_content.py). **NEO-51 landed:** fail-fast server load of `content/items/*_items.json` at startup — `server/NeonSprawl.Server/Game/Items/` ([NEO-51](../../plans/NEO-51-implementation-plan.md)); [server README — Item catalog](../../../server/README.md#item-catalog-contentitems-neo-51). **NEO-52 landed:** injectable **`IItemDefinitionRegistry`** + lookup tests ([NEO-52](../../plans/NEO-52-implementation-plan.md)). **NEO-53 landed:** **`GET /game/world/item-definitions`** — `ItemDefinitionsWorldApi` + DTOs in `Game/Items/` ([NEO-53](../../plans/NEO-53-implementation-plan.md), [`NEO-53` manual QA](../../manual-qa/NEO-53.md)); [server README — Item definitions (NEO-53)](../../../server/README.md#item-definitions-neo-53); Bruno `bruno/neon-sprawl-server/item-definitions/`. **Still planned:** inventory store, per-player HTTP. | [NEO-50](../../plans/NEO-50-implementation-plan.md), [NEO-51](../../plans/NEO-51-implementation-plan.md), [NEO-52](../../plans/NEO-52-implementation-plan.md), [NEO-53](../../plans/NEO-53-implementation-plan.md), [E3M3-prototype-backlog](../../plans/E3M3-prototype-backlog.md), [E3_M3](E3_M3_ItemizationAndInventorySchema.md) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# NEO-53 — Manual QA checklist
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Key | NEO-53 |
|
||||
| Title | E3.M3: GET world item-definitions + Bruno |
|
||||
| Linear | https://linear.app/neon-sprawl/issue/NEO-53/e3m3-get-world-item-definitions-bruno |
|
||||
| Plan | `docs/plans/NEO-53-implementation-plan.md` |
|
||||
| Branch | `NEO-53-get-world-item-definitions-bruno` |
|
||||
|
||||
## Preconditions
|
||||
|
||||
- Server built and configured with default `Content:ItemsDirectory` pointing at repo `content/items` (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/item-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/item-definitions"
|
||||
```
|
||||
|
||||
3. Parse JSON — expect **`schemaVersion`** === **1**, **`items`** array length **6**.
|
||||
4. Confirm **`id`** values include the frozen prototype six: **`contract_handoff_token`**, **`field_stim_mk0`**, **`prototype_armor_shell`**, **`refined_plate_stock`**, **`scrap_metal_bulk`**, **`survey_drone_kit`** (ordinal order).
|
||||
5. Spot-check **`scrap_metal_bulk`**: **`displayName`** “Scrap Metal (Bulk)”, **`prototypeRole`** **`material`**, **`stackMax`** **999**, **`inventorySlotKind`** **`bag`**.
|
||||
6. Spot-check **`prototype_armor_shell`**: **`inventorySlotKind`** **`equipment`**, **`prototypeRole`** **`equip_stub`**.
|
||||
7. Optional: run **`bruno/neon-sprawl-server/item-definitions/Get item definitions.bru`** against the same **`baseUrl`** (see `environments/Local.bru`).
|
||||
|
|
@ -42,9 +42,9 @@
|
|||
|
||||
## Acceptance criteria checklist
|
||||
|
||||
- [ ] GET returns all **six** prototype defs with **`schemaVersion`** **1**.
|
||||
- [ ] Bruno happy path documented and runnable against dev server.
|
||||
- [ ] Automated API tests (AAA) assert six ids, id order, and spot-check metadata.
|
||||
- [x] GET returns all **six** prototype defs with **`schemaVersion`** **1**.
|
||||
- [x] Bruno happy path documented and runnable against dev server.
|
||||
- [x] Automated API tests (AAA) assert six ids, id order, and spot-check metadata.
|
||||
|
||||
## Technical approach
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace NeonSprawl.Server.Tests.Game.Items;
|
||||
|
||||
public class ItemDefinitionsWorldApiTests
|
||||
{
|
||||
private static readonly string[] FrozenSixInIdOrder =
|
||||
[
|
||||
"contract_handoff_token",
|
||||
"field_stim_mk0",
|
||||
"prototype_armor_shell",
|
||||
"refined_plate_stock",
|
||||
"scrap_metal_bulk",
|
||||
"survey_drone_kit",
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public async Task GetItemDefinitions_ShouldReturnSchemaV1_WithFrozenSixInIdOrder()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
// Act
|
||||
var response = await client.GetAsync("/game/world/item-definitions");
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<ItemDefinitionsListResponse>();
|
||||
Assert.NotNull(body);
|
||||
Assert.Equal(ItemDefinitionsListResponse.CurrentSchemaVersion, body!.SchemaVersion);
|
||||
Assert.NotNull(body.Items);
|
||||
var ids = body.Items.Select(static i => i.Id).ToList();
|
||||
Assert.Equal(FrozenSixInIdOrder, ids);
|
||||
|
||||
var scrap = body.Items.Single(i => i.Id == "scrap_metal_bulk");
|
||||
Assert.Equal("Scrap Metal (Bulk)", scrap.DisplayName);
|
||||
Assert.Equal("material", scrap.PrototypeRole);
|
||||
Assert.Equal(999, scrap.StackMax);
|
||||
Assert.Equal("bag", scrap.InventorySlotKind);
|
||||
|
||||
var armor = body.Items.Single(i => i.Id == "prototype_armor_shell");
|
||||
Assert.Equal("equip_stub", armor.PrototypeRole);
|
||||
Assert.Equal(1, armor.StackMax);
|
||||
Assert.Equal("equipment", armor.InventorySlotKind);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Items;
|
||||
|
||||
/// <summary>JSON body for <c>GET /game/world/item-definitions</c> (NEO-53).</summary>
|
||||
public sealed class ItemDefinitionsListResponse
|
||||
{
|
||||
public const int CurrentSchemaVersion = 1;
|
||||
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
|
||||
|
||||
/// <summary>Loaded items ordered by stable <c>id</c> (ordinal), matching <see cref="IItemDefinitionRegistry.GetDefinitionsInIdOrder"/>.</summary>
|
||||
[JsonPropertyName("items")]
|
||||
public required IReadOnlyList<ItemDefinitionJson> Items { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>One row in the read-only item definition projection.</summary>
|
||||
public sealed class ItemDefinitionJson
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public required string Id { get; init; }
|
||||
|
||||
[JsonPropertyName("displayName")]
|
||||
public required string DisplayName { get; init; }
|
||||
|
||||
[JsonPropertyName("prototypeRole")]
|
||||
public required string PrototypeRole { get; init; }
|
||||
|
||||
[JsonPropertyName("stackMax")]
|
||||
public required int StackMax { get; init; }
|
||||
|
||||
[JsonPropertyName("inventorySlotKind")]
|
||||
public required string InventorySlotKind { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
namespace NeonSprawl.Server.Game.Items;
|
||||
|
||||
/// <summary>Maps <c>GET /game/world/item-definitions</c> (NEO-53).</summary>
|
||||
public static class ItemDefinitionsWorldApi
|
||||
{
|
||||
public static WebApplication MapItemDefinitionsWorldApi(this WebApplication app)
|
||||
{
|
||||
app.MapGet(
|
||||
"/game/world/item-definitions",
|
||||
(IItemDefinitionRegistry registry) =>
|
||||
{
|
||||
var defs = registry.GetDefinitionsInIdOrder();
|
||||
var items = new List<ItemDefinitionJson>(defs.Count);
|
||||
foreach (var d in defs)
|
||||
{
|
||||
items.Add(
|
||||
new ItemDefinitionJson
|
||||
{
|
||||
Id = d.Id,
|
||||
DisplayName = d.DisplayName,
|
||||
PrototypeRole = d.PrototypeRole,
|
||||
StackMax = d.StackMax,
|
||||
InventorySlotKind = d.InventorySlotKind,
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Json(
|
||||
new ItemDefinitionsListResponse
|
||||
{
|
||||
SchemaVersion = ItemDefinitionsListResponse.CurrentSchemaVersion,
|
||||
Items = items,
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ app.MapPositionStateApi();
|
|||
app.MapInteractionApi();
|
||||
app.MapInteractablesWorldApi();
|
||||
app.MapSkillDefinitionsWorldApi();
|
||||
app.MapItemDefinitionsWorldApi();
|
||||
app.MapSkillProgressionSnapshotApi();
|
||||
app.MapPerkStateApi();
|
||||
if (app.Environment.IsDevelopment() ||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ On startup the host loads every **`*_items.json`** under the items directory, va
|
|||
|
||||
On success, **Information** logs include the resolved items directory path, distinct item count, and catalog file count. Game code should use **`IItemDefinitionRegistry`** for lookups (NEO-52).
|
||||
|
||||
## Item definitions (NEO-53)
|
||||
|
||||
**`GET /game/world/item-definitions`** returns a versioned JSON body (`schemaVersion` **1**, **`items`**) backed by **`IItemDefinitionRegistry`** — the same prototype rows loaded at startup (no second source of truth). Plan: [NEO-53 implementation plan](../../docs/plans/NEO-53-implementation-plan.md); manual QA: [`docs/manual-qa/NEO-53.md`](../../docs/manual-qa/NEO-53.md); Bruno: `bruno/neon-sprawl-server/item-definitions/`.
|
||||
|
||||
```bash
|
||||
curl -sS -i "http://localhost:5253/game/world/item-definitions"
|
||||
```
|
||||
|
||||
## Mastery catalog (`content/mastery`, NEO-46)
|
||||
|
||||
After the skill catalog loads, the host loads every **`*_mastery.json`** under the mastery directory, validates each file against **`content/schemas/mastery-catalog.schema.json`**, cross-checks track **`skillId`** values against **`ISkillDefinitionRegistry`**, and enforces the same post-schema gates as **`scripts/validate_content.py`** (unknown perk references, duplicate perk ids, branch-set equality tier-to-tier, unreferenced perks, prototype **Slice 4** single **`salvage`** track). The server also requires **`tierIndex`** values per track to be **unique and sequential 1..N** (stricter than CI today). Invalid data **exits during startup**—no silent fallback.
|
||||
|
|
|
|||
Loading…
Reference in New Issue