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.
pull/147/head
VinPropane 2026-05-31 18:43:50 -04:00
parent 25f3714554
commit a5182a4c14
10 changed files with 125 additions and 4 deletions

View File

@ -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",
},

View File

@ -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" } };

View File

@ -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 {

View File

@ -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<IThreatStateStore>();
var runtimeStore = factory.Services.GetRequiredService<INpcRuntimeStateStore>();
var playerHealthStore = factory.Services.GetRequiredService<IPlayerCombatHealthStore>();
var progressStore = factory.Services.GetRequiredService<IEncounterProgressStore>();
var completionStore = factory.Services.GetRequiredService<IEncounterCompletionStore>();
var eventStore = factory.Services.GetRequiredService<IEncounterCompleteEventStore>();
_ = 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]

View File

@ -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<GamePositionOptions> 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
{

View File

@ -0,0 +1,32 @@
namespace NeonSprawl.Server.Game.Encounters;
/// <summary>Clears per-player encounter stores for dev Bruno/QA resets (NEO-108).</summary>
internal static class EncounterDevFixtureOperations
{
/// <summary>Removes progress, completion, and event rows for <paramref name="playerId"/> across catalog encounters.</summary>
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);
}
}
}
}

View File

@ -48,4 +48,21 @@ public sealed class InMemoryEncounterCompleteEventStore : IEncounterCompleteEven
return eventsByKey.TryGetValue(key, out completeEvent);
}
}
/// <summary>Dev fixture only — removes one player+encounter event row (NEO-108 Bruno reset).</summary>
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;
}
}
}

View File

@ -60,4 +60,21 @@ public sealed class InMemoryEncounterCompletionStore : IEncounterCompletionStore
return completedAtByKey.TryGetValue(key, out completedAt);
}
}
/// <summary>Dev fixture only — removes one player+encounter completion row (NEO-108 Bruno reset).</summary>
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;
}
}
}

View File

@ -85,4 +85,21 @@ public sealed class InMemoryEncounterProgressStore : IEncounterProgressStore
return row.DefeatedNpcInstanceIds.Add(npcKey);
}
}
/// <summary>Dev fixture only — removes one player+encounter progress row (NEO-108 Bruno reset).</summary>
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;
}
}
}

View File

@ -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)