NEO-63: wire interact to gather engine and expand registry.
Four resource_node anchors; gather denies map to InteractionResponse; integration tests, Bruno gather/inventory and depletion flows.pull/97/head
parent
f58ab4a2eb
commit
e681857778
|
|
@ -0,0 +1,62 @@
|
|||
meta {
|
||||
name: POST interact depleted node deny
|
||||
type: http
|
||||
seq: 13
|
||||
}
|
||||
|
||||
docs {
|
||||
NEO-63: pre-request exhausts prototype_resource_node_alpha capacity (10 successful gathers), then this interact should deny with node_depleted.
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const axios = require("axios");
|
||||
const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl");
|
||||
const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId");
|
||||
await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/move`,
|
||||
{
|
||||
schemaVersion: 1,
|
||||
target: { x: 10, y: 0.9, z: -6 },
|
||||
},
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const res = await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/interact`,
|
||||
{
|
||||
schemaVersion: 1,
|
||||
interactableId: "prototype_resource_node_alpha",
|
||||
},
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
if (!res.data.allowed) {
|
||||
throw new Error(`expected gather ${i + 1}/10 to succeed, got ${res.data.reasonCode}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/game/players/{{playerId}}/interact
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
content-type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"interactableId": "prototype_resource_node_alpha"
|
||||
}
|
||||
}
|
||||
|
||||
tests {
|
||||
test("depleted node interact denied with node_depleted", function () {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
const body = res.getBody();
|
||||
expect(body.allowed).to.equal(false);
|
||||
expect(body.reasonCode).to.equal("node_depleted");
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
meta {
|
||||
name: POST interact gather then GET inventory
|
||||
type: http
|
||||
seq: 12
|
||||
}
|
||||
|
||||
docs {
|
||||
NEO-63 happy path: move near prototype_resource_node_alpha, interact once, then GET inventory and assert scrap_metal_bulk quantity >= 1.
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const axios = require("axios");
|
||||
const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl");
|
||||
const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId");
|
||||
await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/move`,
|
||||
{
|
||||
schemaVersion: 1,
|
||||
target: { x: 10, y: 0.9, z: -6 },
|
||||
},
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/interact`,
|
||||
{
|
||||
schemaVersion: 1,
|
||||
interactableId: "prototype_resource_node_alpha",
|
||||
},
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/game/players/{{playerId}}/inventory
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
tests {
|
||||
test("inventory includes scrap_metal_bulk after gather interact", function () {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
const body = res.getBody();
|
||||
const total = body.bagSlots
|
||||
.filter((s) => s.itemId === "scrap_metal_bulk")
|
||||
.reduce((sum, s) => sum + s.quantity, 0);
|
||||
expect(total).to.be.at.least(1);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
meta {
|
||||
name: POST interact - prototype_resource_node_alpha (NEO-41 gather XP)
|
||||
name: POST interact - prototype_resource_node_alpha (NEO-63 gather)
|
||||
type: http
|
||||
seq: 11
|
||||
}
|
||||
|
||||
docs {
|
||||
NEO-41: successful interact on `resource_node` kind grants `salvage` XP (`sourceKind: activity`, 10 XP) via same server path as POST skill-progression. Pre-request moves the dev player in horizontal range of the node so this request passes when run alone or when global `seq` order would otherwise leave the player far from the anchor. Optional: GET skill-progression to read `salvage.xp`.
|
||||
Server tests include a stub-registry case where `activity` is disallowed for `salvage` (interact still allowed; XP unchanged).
|
||||
NEO-63: successful interact on `resource_node` kind runs GatherOperations — inventory grant (scrap_metal_bulk), salvage XP (activity, 10), and depletion. Pre-request moves in range of alpha anchor (12, -6). Optional: GET inventory / skill-progression after this request.
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public class InteractablesWorldApiTests
|
|||
Assert.NotNull(body);
|
||||
Assert.Equal(InteractablesListResponse.CurrentSchemaVersion, body!.SchemaVersion);
|
||||
Assert.NotNull(body.Interactables);
|
||||
Assert.True(body.Interactables.Count >= 2);
|
||||
Assert.Equal(5, body.Interactables.Count);
|
||||
|
||||
var ids = body.Interactables.Select(static x => x.InteractableId).ToList();
|
||||
Assert.Equal(ids.OrderBy(static x => x, StringComparer.Ordinal).ToList(), ids);
|
||||
|
|
@ -37,6 +37,21 @@ public class InteractablesWorldApiTests
|
|||
Assert.Equal(-6, alpha.Anchor.Z);
|
||||
Assert.Equal(3.0, alpha.InteractionRadius);
|
||||
|
||||
var beta = body.Interactables.Single(x => x.InteractableId == PrototypeInteractableRegistry.PrototypeSubsurfaceVeinBetaId);
|
||||
Assert.Equal("resource_node", beta.Kind);
|
||||
Assert.Equal(18, beta.Anchor.X);
|
||||
Assert.Equal(-6, beta.Anchor.Z);
|
||||
|
||||
var gamma = body.Interactables.Single(x => x.InteractableId == PrototypeInteractableRegistry.PrototypeBioMatGammaId);
|
||||
Assert.Equal("resource_node", gamma.Kind);
|
||||
Assert.Equal(12, gamma.Anchor.X);
|
||||
Assert.Equal(0, gamma.Anchor.Z);
|
||||
|
||||
var delta = body.Interactables.Single(x => x.InteractableId == PrototypeInteractableRegistry.PrototypeUrbanBulkDeltaId);
|
||||
Assert.Equal("resource_node", delta.Kind);
|
||||
Assert.Equal(18, delta.Anchor.X);
|
||||
Assert.Equal(0, delta.Anchor.Z);
|
||||
|
||||
var term = body.Interactables.Single(x => x.InteractableId == PrototypeInteractableRegistry.PrototypeTerminalId);
|
||||
Assert.Equal("terminal", term.Kind);
|
||||
Assert.Equal(0, term.Anchor.X);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using NeonSprawl.Server.Game.Gathering;
|
||||
using NeonSprawl.Server.Game.Interaction;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
using NeonSprawl.Server.Game.World;
|
||||
using Xunit;
|
||||
|
||||
namespace NeonSprawl.Server.Tests.Game.Interaction;
|
||||
|
||||
/// <summary>NEO-63: <c>resource_node</c> interact delegates to <see cref="GatherOperations"/>.</summary>
|
||||
public sealed class InteractionResourceNodeGatherIntegrationTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task PostInteract_ResourceNode_WhenInRange_ShouldGrantInventoryAndSalvageXp()
|
||||
{
|
||||
// Arrange — node at (12, -6), radius 3; stand at (10, -6) → horizontal distance 2
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
await MoveNearAlphaAsync(client);
|
||||
|
||||
// Act
|
||||
var interact = await client.PostAsJsonAsync(
|
||||
"/game/players/dev-local-1/interact",
|
||||
new InteractionRequest
|
||||
{
|
||||
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
|
||||
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
|
||||
});
|
||||
var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
|
||||
var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
|
||||
var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
|
||||
Assert.NotNull(envelope);
|
||||
Assert.True(envelope!.Allowed);
|
||||
Assert.Equal(PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, envelope.InteractableId);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, inventory.StatusCode);
|
||||
var bag = await inventory.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>();
|
||||
Assert.NotNull(bag);
|
||||
Assert.Equal(1, CountItem(bag!, "scrap_metal_bulk"));
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, progression.StatusCode);
|
||||
var skills = await progression.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
|
||||
Assert.NotNull(skills);
|
||||
var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
|
||||
Assert.Equal(GatherSkillXpConstants.ActivityXpPerResourceNodeGather, salvage.Xp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostInteract_ResourceNode_WhenDepleted_ShouldDenyWithNodeDepletedWithoutGrants()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
await MoveNearAlphaAsync(client);
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
var gather = await client.PostAsJsonAsync(
|
||||
"/game/players/dev-local-1/interact",
|
||||
new InteractionRequest
|
||||
{
|
||||
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
|
||||
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.OK, gather.StatusCode);
|
||||
var body = await gather.Content.ReadFromJsonAsync<InteractionResponse>();
|
||||
Assert.NotNull(body);
|
||||
Assert.True(body!.Allowed);
|
||||
}
|
||||
|
||||
var inventoryBefore = await client.GetAsync("/game/players/dev-local-1/inventory");
|
||||
var progressionBefore = await client.GetAsync("/game/players/dev-local-1/skill-progression");
|
||||
var bagBefore = await inventoryBefore.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>();
|
||||
var skillsBefore = await progressionBefore.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
|
||||
var scrapBefore = CountItem(bagBefore!, "scrap_metal_bulk");
|
||||
var salvageBefore = skillsBefore!.Skills!.Single(static s => s.Id == "salvage").Xp;
|
||||
|
||||
// Act
|
||||
var interact = await client.PostAsJsonAsync(
|
||||
"/game/players/dev-local-1/interact",
|
||||
new InteractionRequest
|
||||
{
|
||||
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
|
||||
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
|
||||
});
|
||||
var inventoryAfter = await client.GetAsync("/game/players/dev-local-1/inventory");
|
||||
var progressionAfter = await client.GetAsync("/game/players/dev-local-1/skill-progression");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
|
||||
var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
|
||||
Assert.NotNull(envelope);
|
||||
Assert.False(envelope!.Allowed);
|
||||
Assert.Equal(GatherReasonCodes.NodeDepleted, envelope.ReasonCode);
|
||||
|
||||
var bagAfter = await inventoryAfter.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>();
|
||||
var skillsAfter = await progressionAfter.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
|
||||
Assert.Equal(scrapBefore, CountItem(bagAfter!, "scrap_metal_bulk"));
|
||||
Assert.Equal(
|
||||
salvageBefore,
|
||||
skillsAfter!.Skills!.Single(static s => s.Id == "salvage").Xp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostInteract_PrototypeTerminal_ShouldNotGrantInventoryOrSalvageXp()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
var move = new MoveCommandRequest
|
||||
{
|
||||
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
||||
Target = new PositionVector { X = 0.5, Y = 0.9, Z = 0.5 },
|
||||
};
|
||||
await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);
|
||||
|
||||
// Act
|
||||
var interact = await client.PostAsJsonAsync(
|
||||
"/game/players/dev-local-1/interact",
|
||||
new InteractionRequest
|
||||
{
|
||||
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
|
||||
InteractableId = PrototypeInteractableRegistry.PrototypeTerminalId,
|
||||
});
|
||||
var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
|
||||
var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
|
||||
var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
|
||||
Assert.NotNull(envelope);
|
||||
Assert.True(envelope!.Allowed);
|
||||
|
||||
var bag = await inventory.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>();
|
||||
Assert.NotNull(bag);
|
||||
Assert.Equal(0, CountItem(bag!, "scrap_metal_bulk"));
|
||||
|
||||
var skills = await progression.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
|
||||
Assert.NotNull(skills);
|
||||
var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
|
||||
Assert.Equal(0, salvage.Xp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostInteract_ResourceNode_WhenSalvageDisallowsActivity_ShouldDenyWithoutGrants()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new SalvageActivityDeniedRegistryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
await MoveNearAlphaAsync(client);
|
||||
|
||||
// Act
|
||||
var interact = await client.PostAsJsonAsync(
|
||||
"/game/players/dev-local-1/interact",
|
||||
new InteractionRequest
|
||||
{
|
||||
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
|
||||
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
|
||||
});
|
||||
var inventory = await client.GetAsync("/game/players/dev-local-1/inventory");
|
||||
var progression = await client.GetAsync("/game/players/dev-local-1/skill-progression");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
|
||||
var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
|
||||
Assert.NotNull(envelope);
|
||||
Assert.False(envelope!.Allowed);
|
||||
Assert.False(string.IsNullOrEmpty(envelope.ReasonCode));
|
||||
|
||||
var bag = await inventory.Content.ReadFromJsonAsync<PlayerInventorySnapshotResponse>();
|
||||
Assert.NotNull(bag);
|
||||
Assert.Equal(0, CountItem(bag!, "scrap_metal_bulk"));
|
||||
|
||||
var skills = await progression.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
|
||||
Assert.NotNull(skills);
|
||||
var salvage = skills!.Skills!.Single(static s => s.Id == "salvage");
|
||||
Assert.Equal(0, salvage.Xp);
|
||||
}
|
||||
|
||||
private static async Task MoveNearAlphaAsync(HttpClient client)
|
||||
{
|
||||
var move = new MoveCommandRequest
|
||||
{
|
||||
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
||||
Target = new PositionVector { X = 10, Y = 0.9, Z = -6 },
|
||||
};
|
||||
await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);
|
||||
}
|
||||
|
||||
private static int CountItem(PlayerInventorySnapshotResponse snapshot, string itemId)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var slot in snapshot.BagSlots!)
|
||||
{
|
||||
if (string.Equals(slot.ItemId, itemId, StringComparison.Ordinal))
|
||||
{
|
||||
total += slot.Quantity;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ using Xunit;
|
|||
|
||||
namespace NeonSprawl.Server.Tests.Game.Interaction;
|
||||
|
||||
/// <summary>NEO-41: successful <c>resource_node</c> interact grants <c>salvage</c> XP via shared NEO-38 grant path.</summary>
|
||||
/// <summary>NEO-41 legacy coverage retained for terminal control; gather side effects covered by NEO-63 integration tests.</summary>
|
||||
public sealed class InteractionResourceNodeGatherXpTests
|
||||
{
|
||||
[Fact]
|
||||
|
|
@ -79,40 +79,4 @@ public sealed class InteractionResourceNodeGatherXpTests
|
|||
var salvage = body!.Skills!.Single(static s => s.Id == "salvage");
|
||||
Assert.Equal(GatherSkillXpConstants.ActivityXpPerResourceNodeGather, salvage.Xp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostInteract_ResourceNode_WhenSalvageDisallowsActivity_ShouldStillAllowInteract_AndNotApplyXp()
|
||||
{
|
||||
// Arrange — stub registry: salvage exists but activity is not in allowedXpSourceKinds (defensive / corrupt-catalog stand-in).
|
||||
await using var factory = new SalvageActivityDeniedRegistryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
var move = new MoveCommandRequest
|
||||
{
|
||||
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
||||
Target = new PositionVector { X = 10, Y = 0.9, Z = -6 },
|
||||
};
|
||||
await client.PostAsJsonAsync("/game/players/dev-local-1/move", move);
|
||||
|
||||
// Act
|
||||
var interact = await client.PostAsJsonAsync(
|
||||
"/game/players/dev-local-1/interact",
|
||||
new InteractionRequest
|
||||
{
|
||||
SchemaVersion = InteractionRequest.CurrentSchemaVersion,
|
||||
InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId,
|
||||
});
|
||||
var snapshot = await client.GetAsync("/game/players/dev-local-1/skill-progression");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, interact.StatusCode);
|
||||
var envelope = await interact.Content.ReadFromJsonAsync<InteractionResponse>();
|
||||
Assert.NotNull(envelope);
|
||||
Assert.True(envelope!.Allowed);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, snapshot.StatusCode);
|
||||
var body = await snapshot.Content.ReadFromJsonAsync<SkillProgressionSnapshotResponse>();
|
||||
Assert.NotNull(body);
|
||||
var salvage = body!.Skills!.Single(static s => s.Id == "salvage");
|
||||
Assert.Equal(0, salvage.Xp);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NeonSprawl.Server.Game.AbilityInput;
|
||||
using NeonSprawl.Server.Game.Gathering;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
|
@ -15,7 +17,7 @@ namespace NeonSprawl.Server.Tests.Game.Interaction;
|
|||
|
||||
/// <summary>
|
||||
/// Same in-memory overrides as <see cref="InMemoryWebApplicationFactory"/>, plus a stub <see cref="ISkillDefinitionRegistry"/>
|
||||
/// where <c>salvage</c> does not allow <c>activity</c> — for NEO-41 defensive tests without mutating disk catalog.
|
||||
/// where <c>salvage</c> does not allow <c>activity</c> — for gather deny tests without mutating disk catalog.
|
||||
/// </summary>
|
||||
public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
|
|
@ -29,8 +31,17 @@ public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebAppl
|
|||
var masteryDir = MasteryCatalogPathResolution.TryDiscoverMasteryDirectory(AppContext.BaseDirectory)
|
||||
?? throw new InvalidOperationException(
|
||||
"Could not discover repo content/mastery from AppContext.BaseDirectory; run tests from the neon-sprawl clone.");
|
||||
var itemsDir = ItemCatalogPathResolution.TryDiscoverItemsDirectory(AppContext.BaseDirectory)
|
||||
?? throw new InvalidOperationException(
|
||||
"Could not discover repo content/items from AppContext.BaseDirectory; run tests from the neon-sprawl clone.");
|
||||
var resourceNodesDir = ResourceNodeCatalogPathResolution.TryDiscoverResourceNodesDirectory(AppContext.BaseDirectory)
|
||||
?? throw new InvalidOperationException(
|
||||
"Could not discover repo content/resource-nodes from AppContext.BaseDirectory; run tests from the neon-sprawl clone.");
|
||||
builder.UseSetting("Content:SkillsDirectory", skillsDir);
|
||||
builder.UseSetting("Content:MasteryDirectory", masteryDir);
|
||||
builder.UseSetting("Content:ItemsDirectory", itemsDir);
|
||||
builder.UseSetting("Content:ResourceNodesDirectory", resourceNodesDir);
|
||||
builder.UseSetting("Game:EnableMasteryFixtureApi", "true");
|
||||
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
|
|
@ -40,6 +51,8 @@ public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebAppl
|
|||
if (d.ServiceType == typeof(IPositionStateStore) ||
|
||||
d.ServiceType == typeof(IPlayerHotbarLoadoutStore) ||
|
||||
d.ServiceType == typeof(IPlayerSkillProgressionStore) ||
|
||||
d.ServiceType == typeof(IPlayerInventoryStore) ||
|
||||
d.ServiceType == typeof(IResourceNodeInstanceStore) ||
|
||||
d.ServiceType == typeof(IPlayerPerkStateStore) ||
|
||||
d.ServiceType == typeof(TimeProvider) ||
|
||||
d.ServiceType == typeof(IPlayerAbilityCooldownStore) ||
|
||||
|
|
@ -58,6 +71,8 @@ public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebAppl
|
|||
services.AddSingleton<IPositionStateStore, InMemoryPositionStateStore>();
|
||||
services.AddSingleton<IPlayerHotbarLoadoutStore, InMemoryPlayerHotbarLoadoutStore>();
|
||||
services.AddSingleton<IPlayerSkillProgressionStore, InMemoryPlayerSkillProgressionStore>();
|
||||
services.AddSingleton<IPlayerInventoryStore, InMemoryPlayerInventoryStore>();
|
||||
services.AddSingleton<IResourceNodeInstanceStore, InMemoryResourceNodeInstanceStore>();
|
||||
services.AddSingleton<IPlayerPerkStateStore, InMemoryPlayerPerkStateStore>();
|
||||
services.AddSingleton<IPlayerAbilityCooldownStore, InMemoryPlayerAbilityCooldownStore>();
|
||||
services.AddSingleton<ISkillDefinitionRegistry, SalvageActivityDeniedSkillRegistry>();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
using NeonSprawl.Server.Game.Gathering;
|
||||
using NeonSprawl.Server.Game.Items;
|
||||
using NeonSprawl.Server.Game.Mastery;
|
||||
using NeonSprawl.Server.Game.PositionState;
|
||||
using NeonSprawl.Server.Game.Skills;
|
||||
|
|
@ -16,8 +18,10 @@ public static class InteractionApi
|
|||
app.MapPost(
|
||||
"/game/players/{id}/interact",
|
||||
(string id, InteractionRequest? body, IPositionStateStore store,
|
||||
ISkillDefinitionRegistry registry, IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve,
|
||||
PerkUnlockEngine perkUnlockEngine) =>
|
||||
IResourceNodeDefinitionRegistry nodeRegistry, IItemDefinitionRegistry itemRegistry,
|
||||
IPlayerInventoryStore inventoryStore, ISkillDefinitionRegistry skillRegistry,
|
||||
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine,
|
||||
IResourceNodeInstanceStore nodeInstanceStore) =>
|
||||
{
|
||||
if (body is null || body.SchemaVersion != InteractionRequest.CurrentSchemaVersion)
|
||||
{
|
||||
|
|
@ -72,17 +76,37 @@ public static class InteractionApi
|
|||
|
||||
if (string.Equals(entry.Kind, PrototypeInteractableRegistry.ResourceNodeKind, StringComparison.Ordinal))
|
||||
{
|
||||
// NEO-41: prototype gather completion — same grant rules/persistence as POST …/skill-progression.
|
||||
// Interact response stays Allowed=true; grant outcome is intentionally ignored (silent no-op on deny / store miss per plan).
|
||||
_ = SkillProgressionGrantOperations.TryApplyGrant(
|
||||
var gather = GatherOperations.TryGather(
|
||||
playerKey,
|
||||
GatherSkillXpConstants.SalvageSkillId,
|
||||
GatherSkillXpConstants.ActivityXpPerResourceNodeGather,
|
||||
GatherSkillXpConstants.ActivitySourceKind,
|
||||
registry,
|
||||
lookupKey,
|
||||
nodeRegistry,
|
||||
itemRegistry,
|
||||
inventoryStore,
|
||||
skillRegistry,
|
||||
xpStore,
|
||||
levelCurve,
|
||||
perkUnlockEngine);
|
||||
perkUnlockEngine,
|
||||
nodeInstanceStore);
|
||||
|
||||
if (!gather.Success)
|
||||
{
|
||||
return Results.Json(
|
||||
new InteractionResponse
|
||||
{
|
||||
SchemaVersion = InteractionResponse.CurrentSchemaVersion,
|
||||
Allowed = false,
|
||||
ReasonCode = gather.ReasonCode,
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Json(
|
||||
new InteractionResponse
|
||||
{
|
||||
SchemaVersion = InteractionResponse.CurrentSchemaVersion,
|
||||
Allowed = true,
|
||||
InteractableId = lookupKey,
|
||||
Payload = new InteractionPlaceholderPayload(),
|
||||
});
|
||||
}
|
||||
|
||||
return Results.Json(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,15 @@ public static class PrototypeInteractableRegistry
|
|||
/// <summary>Second stub row for multi-entity projection (NEO-25).</summary>
|
||||
public const string PrototypeResourceNodeAlphaId = "prototype_resource_node_alpha";
|
||||
|
||||
/// <summary>Subsurface lens gather anchor (NEO-63).</summary>
|
||||
public const string PrototypeSubsurfaceVeinBetaId = "prototype_subsurface_vein_beta";
|
||||
|
||||
/// <summary>Bio lens gather anchor (NEO-63).</summary>
|
||||
public const string PrototypeBioMatGammaId = "prototype_bio_mat_gamma";
|
||||
|
||||
/// <summary>Urban bulk lens gather anchor (NEO-63).</summary>
|
||||
public const string PrototypeUrbanBulkDeltaId = "prototype_urban_bulk_delta";
|
||||
|
||||
/// <summary><see cref="PrototypeInteractableEntry.Kind"/> for gather-anchor nodes (NEO-41).</summary>
|
||||
public const string ResourceNodeKind = "resource_node";
|
||||
|
||||
|
|
@ -21,10 +30,22 @@ public static class PrototypeInteractableRegistry
|
|||
/// <summary>+X / −Z open floor (avoids overlap with scene <c>Obstacle</c> at ~(6,1,5)); same radius as terminal.</summary>
|
||||
public static readonly PrototypeInteractableEntry PrototypeResourceNodeAlpha = new(12, 0.5, -6, 3.0, ResourceNodeKind);
|
||||
|
||||
/// <summary>6 m east of alpha on −Z band (NEO-63).</summary>
|
||||
public static readonly PrototypeInteractableEntry PrototypeSubsurfaceVeinBeta = new(18, 0.5, -6, 3.0, ResourceNodeKind);
|
||||
|
||||
/// <summary>6 m north (+Z) of alpha (NEO-63).</summary>
|
||||
public static readonly PrototypeInteractableEntry PrototypeBioMatGamma = new(12, 0.5, 0, 3.0, ResourceNodeKind);
|
||||
|
||||
/// <summary>Northeast open quadrant (NEO-63).</summary>
|
||||
public static readonly PrototypeInteractableEntry PrototypeUrbanBulkDelta = new(18, 0.5, 0, 3.0, ResourceNodeKind);
|
||||
|
||||
private static readonly Dictionary<string, PrototypeInteractableEntry> ById = new(StringComparer.Ordinal)
|
||||
{
|
||||
[PrototypeTerminalId] = PrototypeTerminal,
|
||||
[PrototypeResourceNodeAlphaId] = PrototypeResourceNodeAlpha,
|
||||
[PrototypeSubsurfaceVeinBetaId] = PrototypeSubsurfaceVeinBeta,
|
||||
[PrototypeBioMatGammaId] = PrototypeBioMatGamma,
|
||||
[PrototypeUrbanBulkDeltaId] = PrototypeUrbanBulkDelta,
|
||||
};
|
||||
|
||||
public static bool TryGet(string interactableIdLowercase, out PrototypeInteractableEntry entry) =>
|
||||
|
|
|
|||
Loading…
Reference in New Issue