From a5182a4c1486a8ece0512a9e08dc27148fe06d00 Mon Sep 17 00:00:00 2001 From: VinPropane Date: Sun, 31 May 2026 18:43:50 -0400 Subject: [PATCH] NEO-108: Fix Bruno encounter-progress and combat-health smokes. Use collection-relative ./scripts require path; reset encounter stores in combat-targets dev fixture for inactive GET; combat-health uses slot 3. --- .../Get combat health after elite attack.bru | 6 ++-- ... encounter progress after defeat spine.bru | 2 +- .../Get encounter progress inactive.bru | 6 ++++ .../Combat/CombatTargetFixtureApiTests.cs | 18 +++++++++++ .../Game/Combat/CombatTargetFixtureApi.cs | 12 +++++++ .../EncounterDevFixtureOperations.cs | 32 +++++++++++++++++++ .../InMemoryEncounterCompleteEventStore.cs | 17 ++++++++++ .../InMemoryEncounterCompletionStore.cs | 17 ++++++++++ .../InMemoryEncounterProgressStore.cs | 17 ++++++++++ server/README.md | 2 +- 10 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 server/NeonSprawl.Server/Game/Encounters/EncounterDevFixtureOperations.cs diff --git a/bruno/neon-sprawl-server/combat-health/Get combat health after elite attack.bru b/bruno/neon-sprawl-server/combat-health/Get combat health after elite attack.bru index 3e2667f..faf175a 100644 --- a/bruno/neon-sprawl-server/combat-health/Get combat health after elite attack.bru +++ b/bruno/neon-sprawl-server/combat-health/Get combat health after elite attack.bru @@ -29,11 +29,13 @@ script:pre-request { jsonHeaders, ); + const slotIndex = 3; + await axios.post( `${baseUrl}/game/players/${playerId}/hotbar-loadout`, { schemaVersion: 1, - slots: [{ slotIndex: 0, abilityId: "prototype_pulse" }], + slots: [{ slotIndex, abilityId: "prototype_pulse" }], }, jsonHeaders, ); @@ -48,7 +50,7 @@ script:pre-request { `${baseUrl}/game/players/${playerId}/ability-cast`, { schemaVersion: 1, - slotIndex: 0, + slotIndex, abilityId: "prototype_pulse", targetId: "prototype_npc_elite", }, diff --git a/bruno/neon-sprawl-server/encounter-progress/Get encounter progress after defeat spine.bru b/bruno/neon-sprawl-server/encounter-progress/Get encounter progress after defeat spine.bru index 3eb4898..5a5194f 100644 --- a/bruno/neon-sprawl-server/encounter-progress/Get encounter progress after defeat spine.bru +++ b/bruno/neon-sprawl-server/encounter-progress/Get encounter progress after defeat spine.bru @@ -10,7 +10,7 @@ docs { script:pre-request { const axios = require("axios"); - const { resetPrototypeCombatTargets } = require("../scripts/combat-targets-reset-helper.js"); + const { resetPrototypeCombatTargets } = require("./scripts/combat-targets-reset-helper.js"); const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl"); const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId"); const jsonHeaders = { headers: { "Content-Type": "application/json" } }; diff --git a/bruno/neon-sprawl-server/encounter-progress/Get encounter progress inactive.bru b/bruno/neon-sprawl-server/encounter-progress/Get encounter progress inactive.bru index b7a4357..ccd29e6 100644 --- a/bruno/neon-sprawl-server/encounter-progress/Get encounter progress inactive.bru +++ b/bruno/neon-sprawl-server/encounter-progress/Get encounter progress inactive.bru @@ -6,6 +6,12 @@ meta { docs { NEO-108: fresh player row before engagement — inactive with empty defeatedTargetIds. + Pre-request uses combat-targets dev fixture (also clears encounter progress for dev-local-1). +} + +script:pre-request { + const { resetPrototypeCombatTargets } = require("./scripts/combat-targets-reset-helper.js"); + await resetPrototypeCombatTargets(bru); } get { diff --git a/server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetFixtureApiTests.cs b/server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetFixtureApiTests.cs index 584fbe9..0cf8c35 100644 --- a/server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetFixtureApiTests.cs +++ b/server/NeonSprawl.Server.Tests/Game/Combat/CombatTargetFixtureApiTests.cs @@ -3,6 +3,7 @@ using System.Net.Http.Json; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using NeonSprawl.Server.Game.Combat; +using NeonSprawl.Server.Game.Encounters; using NeonSprawl.Server.Game.Npc; using NeonSprawl.Server.Tests; using Xunit; @@ -23,6 +24,20 @@ public sealed class CombatTargetFixtureApiTests var threatStore = factory.Services.GetRequiredService(); var runtimeStore = factory.Services.GetRequiredService(); var playerHealthStore = factory.Services.GetRequiredService(); + var progressStore = factory.Services.GetRequiredService(); + var completionStore = factory.Services.GetRequiredService(); + var eventStore = factory.Services.GetRequiredService(); + _ = progressStore.TryActivate("dev-local-1", "prototype_combat_pocket"); + _ = progressStore.TryAddDefeatedTarget("dev-local-1", "prototype_combat_pocket", "prototype_npc_melee"); + _ = completionStore.TryMarkCompleted("dev-local-1", "prototype_combat_pocket", DateTimeOffset.UtcNow); + _ = eventStore.TryRecord( + new EncounterCompleteEvent( + "dev-local-1", + "prototype_combat_pocket", + "prototype_combat_pocket_clear", + [new EncounterGrantApplied("scrap_metal_bulk", 10)], + DateTimeOffset.UtcNow, + "dev-local-1:prototype_combat_pocket")); _ = store.TryApplyDamage(PrototypeNpcRegistry.PrototypeNpcMeleeId, 100, out _); _ = playerHealthStore.TryApplyDamage("dev-local-1", 40, out _); _ = threatStore.TrySetHolder(PrototypeNpcRegistry.PrototypeNpcMeleeId, "dev-local-1"); @@ -59,6 +74,9 @@ public sealed class CombatTargetFixtureApiTests playerHealthStore.TryGet("dev-local-1", out var playerHealth); Assert.Equal(PlayerCombatHealthDefaults.MaxHp, playerHealth.CurrentHp); Assert.False(playerHealth.Defeated); + Assert.False(progressStore.TryGetProgress("dev-local-1", "prototype_combat_pocket", out _)); + Assert.False(completionStore.IsCompleted("dev-local-1", "prototype_combat_pocket")); + Assert.False(eventStore.TryGet("dev-local-1", "prototype_combat_pocket", out _)); } [Fact] diff --git a/server/NeonSprawl.Server/Game/Combat/CombatTargetFixtureApi.cs b/server/NeonSprawl.Server/Game/Combat/CombatTargetFixtureApi.cs index 7befa5b..246e6c8 100644 --- a/server/NeonSprawl.Server/Game/Combat/CombatTargetFixtureApi.cs +++ b/server/NeonSprawl.Server/Game/Combat/CombatTargetFixtureApi.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Options; +using NeonSprawl.Server.Game.Encounters; using NeonSprawl.Server.Game.Npc; using NeonSprawl.Server.Game.PositionState; @@ -17,6 +18,10 @@ public static class CombatTargetFixtureApi IThreatStateStore threatStore, INpcRuntimeStateStore runtimeStore, IPlayerCombatHealthStore playerHealthStore, + IEncounterDefinitionRegistry encounterRegistry, + IEncounterProgressStore encounterProgressStore, + IEncounterCompletionStore encounterCompletionStore, + IEncounterCompleteEventStore encounterCompleteEventStore, IOptions gameOptions) => { if (body is null || body.SchemaVersion != CombatTargetFixtureRequest.CurrentSchemaVersion) @@ -41,6 +46,13 @@ public static class CombatTargetFixtureApi return Results.NotFound(); } + EncounterDevFixtureOperations.ClearPlayerEncounterState( + devPlayerId, + encounterRegistry, + encounterProgressStore, + encounterCompletionStore, + encounterCompleteEventStore); + return Results.Json( new CombatTargetFixtureResponse { diff --git a/server/NeonSprawl.Server/Game/Encounters/EncounterDevFixtureOperations.cs b/server/NeonSprawl.Server/Game/Encounters/EncounterDevFixtureOperations.cs new file mode 100644 index 0000000..658f612 --- /dev/null +++ b/server/NeonSprawl.Server/Game/Encounters/EncounterDevFixtureOperations.cs @@ -0,0 +1,32 @@ +namespace NeonSprawl.Server.Game.Encounters; + +/// Clears per-player encounter stores for dev Bruno/QA resets (NEO-108). +internal static class EncounterDevFixtureOperations +{ + /// Removes progress, completion, and event rows for across catalog encounters. + public static void ClearPlayerEncounterState( + string playerId, + IEncounterDefinitionRegistry encounterRegistry, + IEncounterProgressStore progressStore, + IEncounterCompletionStore completionStore, + IEncounterCompleteEventStore eventStore) + { + foreach (var def in encounterRegistry.GetDefinitionsInIdOrder()) + { + if (progressStore is InMemoryEncounterProgressStore inMemoryProgress) + { + inMemoryProgress.TryClear(playerId, def.Id); + } + + if (completionStore is InMemoryEncounterCompletionStore inMemoryCompletion) + { + inMemoryCompletion.TryClear(playerId, def.Id); + } + + if (eventStore is InMemoryEncounterCompleteEventStore inMemoryEvent) + { + inMemoryEvent.TryClear(playerId, def.Id); + } + } + } +} diff --git a/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompleteEventStore.cs b/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompleteEventStore.cs index bd7236d..539b549 100644 --- a/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompleteEventStore.cs +++ b/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompleteEventStore.cs @@ -48,4 +48,21 @@ public sealed class InMemoryEncounterCompleteEventStore : IEncounterCompleteEven return eventsByKey.TryGetValue(key, out completeEvent); } } + + /// Dev fixture only — removes one player+encounter event row (NEO-108 Bruno reset). + public bool TryClear(string playerId, string encounterId) + { + var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId); + if (key.Length == 0) + { + return false; + } + + lock (keyLocks.GetOrAdd(key, _ => new object())) + { + var removed = eventsByKey.TryRemove(key, out _); + keyLocks.TryRemove(key, out _); + return removed; + } + } } diff --git a/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompletionStore.cs b/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompletionStore.cs index 656f674..dfafb72 100644 --- a/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompletionStore.cs +++ b/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterCompletionStore.cs @@ -60,4 +60,21 @@ public sealed class InMemoryEncounterCompletionStore : IEncounterCompletionStore return completedAtByKey.TryGetValue(key, out completedAt); } } + + /// Dev fixture only — removes one player+encounter completion row (NEO-108 Bruno reset). + public bool TryClear(string playerId, string encounterId) + { + var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId); + if (key.Length == 0) + { + return false; + } + + lock (keyLocks.GetOrAdd(key, _ => new object())) + { + var removed = completedAtByKey.TryRemove(key, out _); + keyLocks.TryRemove(key, out _); + return removed; + } + } } diff --git a/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterProgressStore.cs b/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterProgressStore.cs index 3adf4c4..01e5ef5 100644 --- a/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterProgressStore.cs +++ b/server/NeonSprawl.Server/Game/Encounters/InMemoryEncounterProgressStore.cs @@ -85,4 +85,21 @@ public sealed class InMemoryEncounterProgressStore : IEncounterProgressStore return row.DefeatedNpcInstanceIds.Add(npcKey); } } + + /// Dev fixture only — removes one player+encounter progress row (NEO-108 Bruno reset). + public bool TryClear(string playerId, string encounterId) + { + var key = EncounterProgressIds.MakeProgressKey(playerId, encounterId); + if (key.Length == 0) + { + return false; + } + + lock (keyLocks.GetOrAdd(key, _ => new object())) + { + var removed = byKey.TryRemove(key, out _); + keyLocks.TryRemove(key, out _); + return removed; + } + } } diff --git a/server/README.md b/server/README.md index 3510340..06d6bb5 100644 --- a/server/README.md +++ b/server/README.md @@ -264,7 +264,7 @@ Plan: [NEO-80 implementation plan](../../docs/plans/NEO-80-implementation-plan.m curl -sS -i "http://localhost:5253/game/world/combat-targets" ``` -**Dev combat-target fixture (Bruno/manual QA):** When `Game:EnableCombatTargetFixtureApi` is **true** (default in **Development** via `appsettings.Development.json`, **Testing**, or CI Bruno step) or the host environment is **Development** / **Testing**, **`POST /game/__dev/combat-targets-fixture`** with `schemaVersion` **1** resets all **`PrototypeNpcRegistry`** instances to catalog full HP via **`TryResetToFull`**, clears all prototype aggro holders (**NEO-92**), resets NPC runtime behavior rows to **`idle`** (**NEO-93**), and restores dev player combat HP to full (**NEO-95**). **404** when disabled. Bruno: `scripts/combat-targets-reset-helper.js`. +**Dev combat-target fixture (Bruno/manual QA):** When `Game:EnableCombatTargetFixtureApi` is **true** (default in **Development** via `appsettings.Development.json`, **Testing**, or CI Bruno step) or the host environment is **Development** / **Testing**, **`POST /game/__dev/combat-targets-fixture`** with `schemaVersion` **1** resets all **`PrototypeNpcRegistry`** instances to catalog full HP via **`TryResetToFull`**, clears all prototype aggro holders (**NEO-92**), resets NPC runtime behavior rows to **`idle`** (**NEO-93**), restores dev player combat HP to full (**NEO-95**), and clears in-memory encounter progress/completion/event rows for the dev player across catalog encounters (**NEO-108** Bruno). **404** when disabled. Bruno: `scripts/combat-targets-reset-helper.js` (request scripts use `./scripts/…` relative to the collection). ## Threat / aggro state (NEO-92)