NEO-83: Fix Bruno CI combat-targets HP state pollution.
Add dev combat-targets fixture reset endpoint, Bruno pre-request helper, and enable fixture in CI Bruno step so combat-targets tests run after ability-cast defeat spine.pull/117/head
parent
24eb355a26
commit
ea13b05196
|
|
@ -92,6 +92,7 @@ jobs:
|
|||
env:
|
||||
ConnectionStrings__NeonSprawl: >-
|
||||
Host=localhost;Port=5432;Database=neon_sprawl;Username=neon_sprawl;Password=neon_sprawl_dev
|
||||
Game__EnableCombatTargetFixtureApi: "true"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
dotnet run --project server/NeonSprawl.Server/NeonSprawl.Server.csproj \
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ docs {
|
|||
|
||||
script:pre-request {
|
||||
const axios = require("axios");
|
||||
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" } };
|
||||
|
|
@ -18,6 +19,8 @@ script:pre-request {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
await resetPrototypeCombatTargets(bru);
|
||||
|
||||
await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/move`,
|
||||
{ schemaVersion: 1, target: { x: -5, y: 0.9, z: -5 } },
|
||||
|
|
|
|||
|
|
@ -10,10 +10,13 @@ docs {
|
|||
|
||||
script:pre-request {
|
||||
const axios = require("axios");
|
||||
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" } };
|
||||
|
||||
await resetPrototypeCombatTargets(bru);
|
||||
|
||||
await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/move`,
|
||||
{ schemaVersion: 1, target: { x: -5, y: 0.9, z: -5 } },
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ meta {
|
|||
seq: 1
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
const { resetPrototypeCombatTargets } = require("./scripts/combat-targets-reset-helper.js");
|
||||
await resetPrototypeCombatTargets(bru);
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/game/world/combat-targets
|
||||
body: none
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ meta {
|
|||
}
|
||||
|
||||
docs {
|
||||
NEO-83: fresh GET on seq 1; one-pulse cast→GET on seq 2 (~0s); four-pulse defeat spine on seq 3 (~13s pre-request).
|
||||
NEO-83: fresh GET on seq 1 (~reset fixture); one-pulse cast→GET on seq 2 (~0s); four-pulse defeat spine on seq 3 (~13s pre-request).
|
||||
Pre-request scripts call POST /game/__dev/combat-targets-fixture so CI survives ability-cast folder pollution.
|
||||
Cross-link: bruno/neon-sprawl-server/ability-cast/ for cast-only verification.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
const axios = require("axios");
|
||||
|
||||
function resolveConfig(bru) {
|
||||
return {
|
||||
baseUrl: bru.getEnvVar("baseUrl") || bru.getVar("baseUrl"),
|
||||
jsonHeaders: { headers: { "Content-Type": "application/json" } },
|
||||
};
|
||||
}
|
||||
|
||||
async function resetPrototypeCombatTargets(bru) {
|
||||
const { baseUrl, jsonHeaders } = resolveConfig(bru);
|
||||
const response = await axios.post(
|
||||
`${baseUrl}/game/__dev/combat-targets-fixture`,
|
||||
{ schemaVersion: 1 },
|
||||
jsonHeaders,
|
||||
);
|
||||
if (response.status !== 200 || response.data?.applied !== true) {
|
||||
throw new Error(
|
||||
`combat-targets fixture reset failed: ${response.status} ${JSON.stringify(response.data)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resetPrototypeCombatTargets,
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NeonSprawl.Server.Game.Combat;
|
||||
using NeonSprawl.Server.Game.Targeting;
|
||||
using Xunit;
|
||||
|
||||
namespace NeonSprawl.Server.Tests.Game.Combat;
|
||||
|
||||
public sealed class CombatTargetFixtureApiTests
|
||||
{
|
||||
private const string FixturePath = "/game/__dev/combat-targets-fixture";
|
||||
|
||||
[Fact]
|
||||
public async Task PostCombatTargetFixture_ShouldResetPrototypeTargets_WhenEnabled()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
var store = factory.Services.GetRequiredService<ICombatEntityHealthStore>();
|
||||
_ = store.TryApplyDamage(PrototypeTargetRegistry.PrototypeTargetAlphaId, 100, out _);
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsJsonAsync(
|
||||
FixturePath,
|
||||
new CombatTargetFixtureRequest
|
||||
{
|
||||
SchemaVersion = CombatTargetFixtureRequest.CurrentSchemaVersion,
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<CombatTargetFixtureResponse>();
|
||||
Assert.NotNull(body);
|
||||
Assert.True(body!.Applied);
|
||||
store.TryGet(PrototypeTargetRegistry.PrototypeTargetAlphaId, out var alpha);
|
||||
Assert.Equal(PrototypeCombatConstants.MaxPrototypeTargetHp, alpha.CurrentHp);
|
||||
Assert.False(alpha.Defeated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostCombatTargetFixture_ShouldReturnNotFound_WhenRouteNotRegistered()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var client = factory.WithWebHostBuilder(b =>
|
||||
{
|
||||
b.UseEnvironment("Production");
|
||||
b.UseSetting("Game:EnableCombatTargetFixtureApi", "false");
|
||||
}).CreateClient();
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsJsonAsync(
|
||||
FixturePath,
|
||||
new CombatTargetFixtureRequest
|
||||
{
|
||||
SchemaVersion = CombatTargetFixtureRequest.CurrentSchemaVersion,
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
using NeonSprawl.Server.Game.Targeting;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Combat;
|
||||
|
||||
/// <summary>Dev-only route to reset prototype combat dummy HP for Bruno and manual QA (NEO-83).</summary>
|
||||
public static class CombatTargetFixtureApi
|
||||
{
|
||||
public static WebApplication MapCombatTargetFixtureApi(this WebApplication app)
|
||||
{
|
||||
app.MapPost(
|
||||
"/game/__dev/combat-targets-fixture",
|
||||
(CombatTargetFixtureRequest? body, ICombatEntityHealthStore healthStore) =>
|
||||
{
|
||||
if (body is null || body.SchemaVersion != CombatTargetFixtureRequest.CurrentSchemaVersion)
|
||||
{
|
||||
return Results.BadRequest();
|
||||
}
|
||||
|
||||
foreach (var id in PrototypeTargetRegistry.GetPrototypeTargetIdsInOrder())
|
||||
{
|
||||
if (!healthStore.TryResetToFull(id, out _))
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Json(
|
||||
new CombatTargetFixtureResponse
|
||||
{
|
||||
Applied = true,
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NeonSprawl.Server.Game.Combat;
|
||||
|
||||
/// <summary>JSON body for <c>POST /game/__dev/combat-targets-fixture</c> (NEO-83 Bruno/QA).</summary>
|
||||
public sealed class CombatTargetFixtureRequest
|
||||
{
|
||||
public const int CurrentSchemaVersion = 1;
|
||||
|
||||
[JsonPropertyName("schemaVersion")]
|
||||
public int SchemaVersion { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>JSON response when the dev combat-target fixture applies.</summary>
|
||||
public sealed class CombatTargetFixtureResponse
|
||||
{
|
||||
[JsonPropertyName("applied")]
|
||||
public required bool Applied { get; init; }
|
||||
}
|
||||
|
|
@ -13,6 +13,9 @@ public sealed class GamePositionOptions
|
|||
/// <summary>When true, maps <c>POST …/__dev/mastery-fixture</c> for Bruno/manual QA state setup (also enabled in Development).</summary>
|
||||
public bool EnableMasteryFixtureApi { get; set; }
|
||||
|
||||
/// <summary>When true, maps <c>POST /game/__dev/combat-targets-fixture</c> to reset prototype dummy HP (also enabled in Development/Testing).</summary>
|
||||
public bool EnableCombatTargetFixtureApi { get; set; }
|
||||
|
||||
/// <summary>World position for the dev player at process start.</summary>
|
||||
public DefaultPositionOptions DefaultPosition { get; set; } = new();
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,13 @@ if (app.Environment.IsDevelopment() ||
|
|||
app.MapMasteryFixtureApi();
|
||||
}
|
||||
|
||||
if (app.Environment.IsDevelopment() ||
|
||||
app.Environment.IsEnvironment("Testing") ||
|
||||
app.Configuration.GetValue<bool>("Game:EnableCombatTargetFixtureApi"))
|
||||
{
|
||||
app.MapCombatTargetFixtureApi();
|
||||
}
|
||||
|
||||
app.MapTargetingApi();
|
||||
app.MapHotbarLoadoutApi();
|
||||
app.MapCooldownSnapshotApi();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
}
|
||||
},
|
||||
"Game": {
|
||||
"EnableMasteryFixtureApi": true
|
||||
"EnableMasteryFixtureApi": true,
|
||||
"EnableCombatTargetFixtureApi": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ 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 **`PrototypeTargetRegistry`** dummies to full HP via **`TryResetToFull`**. **404** when disabled. Bruno: `scripts/combat-targets-reset-helper.js`.
|
||||
|
||||
## Combat engine (NEO-81)
|
||||
|
||||
**`CombatOperations.TryResolve`** in **`Game/Combat/`** resolves server-internal **`CombatResult`**: ability lookup via **`IAbilityDefinitionRegistry`**, target HP read via **`ICombatEntityHealthStore`**, defeated pre-check (no damage on re-hit), then catalog **`baseDamage`** application. Zero-damage abilities (**`prototype_guard`**, **`prototype_dash`**) succeed without mutating HP. **`AbilityCastApi`** (NEO-82) invokes **`TryResolve`** after E1.M4 gates and returns nested wire **`combatResolution`** on accept.
|
||||
|
|
|
|||
Loading…
Reference in New Issue