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; /// NEO-63: resource_node interact delegates to . 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(); 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(); Assert.NotNull(bag); Assert.Equal(1, CountItem(bag!, "scrap_metal_bulk")); Assert.Equal(HttpStatusCode.OK, progression.StatusCode); var skills = await progression.Content.ReadFromJsonAsync(); 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(); 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(); var skillsBefore = await progressionBefore.Content.ReadFromJsonAsync(); 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(); Assert.NotNull(envelope); Assert.False(envelope!.Allowed); Assert.Equal(GatherReasonCodes.NodeDepleted, envelope.ReasonCode); var bagAfter = await inventoryAfter.Content.ReadFromJsonAsync(); var skillsAfter = await progressionAfter.Content.ReadFromJsonAsync(); 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(); Assert.NotNull(envelope); Assert.True(envelope!.Allowed); var bag = await inventory.Content.ReadFromJsonAsync(); Assert.NotNull(bag); Assert.Equal(0, CountItem(bag!, "scrap_metal_bulk")); var skills = await progression.Content.ReadFromJsonAsync(); 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(); Assert.NotNull(envelope); Assert.False(envelope!.Allowed); Assert.False(string.IsNullOrEmpty(envelope.ReasonCode)); var bag = await inventory.Content.ReadFromJsonAsync(); Assert.NotNull(bag); Assert.Equal(0, CountItem(bag!, "scrap_metal_bulk")); var skills = await progression.Content.ReadFromJsonAsync(); 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; } }