using NeonSprawl.Server.Game.Gathering; using NeonSprawl.Server.Game.Interaction; using NeonSprawl.Server.Game.Items; using NeonSprawl.Server.Game.PositionState; 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); var request = new InteractionRequest { SchemaVersion = InteractionRequest.CurrentSchemaVersion, InteractableId = PrototypeInteractableRegistry.PrototypeResourceNodeAlphaId, }; // Act — exhaust capacity, then one more interact var successfulGathers = new InteractionResponse[10]; for (var i = 0; i < successfulGathers.Length; i++) { var gather = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", request); successfulGathers[i] = (await gather.Content.ReadFromJsonAsync())!; } var depletedInteract = await client.PostAsJsonAsync("/game/players/dev-local-1/interact", request); 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.All(successfulGathers, static r => Assert.True(r.Allowed)); Assert.Equal(HttpStatusCode.OK, depletedInteract.StatusCode); var envelope = await depletedInteract.Content.ReadFromJsonAsync(); Assert.NotNull(envelope); Assert.False(envelope!.Allowed); Assert.Equal(GatherReasonCodes.NodeDepleted, envelope.ReasonCode); Assert.Equal(10, CountItem( (await inventoryAfter.Content.ReadFromJsonAsync())!, "scrap_metal_bulk")); Assert.Equal( 10 * GatherSkillXpConstants.ActivityXpPerResourceNodeGather, (await progressionAfter.Content.ReadFromJsonAsync())! .Skills!.Single(static s => s.Id == "salvage").Xp); } [Fact] public async Task PostInteract_ResourceNode_WhenBagFull_ShouldDenyWithInventoryFullWithoutDepletingNode() { // Arrange await using var factory = new InMemoryWebApplicationFactory(); var client = factory.CreateClient(); for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++) { await client.PostAsJsonAsync( "/game/players/dev-local-1/inventory", new PlayerInventoryMutationRequest { SchemaVersion = PlayerInventoryMutationRequest.CurrentSchemaVersion, MutationKind = "add", ItemId = "survey_drone_kit", Quantity = 1, }); } await MoveNearAlphaAsync(client); var inventoryBefore = await client.GetAsync("/game/players/dev-local-1/inventory"); var progressionBefore = await client.GetAsync("/game/players/dev-local-1/skill-progression"); var scrapBefore = CountItem( (await inventoryBefore.Content.ReadFromJsonAsync())!, "scrap_metal_bulk"); var salvageBefore = (await progressionBefore.Content.ReadFromJsonAsync())! .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, }); // Assert Assert.Equal(HttpStatusCode.OK, interact.StatusCode); var envelope = await interact.Content.ReadFromJsonAsync(); Assert.NotNull(envelope); Assert.False(envelope!.Allowed); Assert.Equal(GatherReasonCodes.InventoryFull, envelope.ReasonCode); Assert.Equal( scrapBefore, CountItem( (await (await client.GetAsync("/game/players/dev-local-1/inventory")).Content .ReadFromJsonAsync())!, "scrap_metal_bulk")); Assert.Equal( salvageBefore, (await (await client.GetAsync("/game/players/dev-local-1/skill-progression")).Content .ReadFromJsonAsync())! .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.Equal(SkillProgressionSnapshotApi.ReasonSourceKindNotAllowed, 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; } }