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:
|
env:
|
||||||
ConnectionStrings__NeonSprawl: >-
|
ConnectionStrings__NeonSprawl: >-
|
||||||
Host=localhost;Port=5432;Database=neon_sprawl;Username=neon_sprawl;Password=neon_sprawl_dev
|
Host=localhost;Port=5432;Database=neon_sprawl;Username=neon_sprawl;Password=neon_sprawl_dev
|
||||||
|
Game__EnableCombatTargetFixtureApi: "true"
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
dotnet run --project server/NeonSprawl.Server/NeonSprawl.Server.csproj \
|
dotnet run --project server/NeonSprawl.Server/NeonSprawl.Server.csproj \
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ docs {
|
||||||
|
|
||||||
script:pre-request {
|
script:pre-request {
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
const { resetPrototypeCombatTargets } = require("./scripts/combat-targets-reset-helper.js");
|
||||||
const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl");
|
const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl");
|
||||||
const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId");
|
const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId");
|
||||||
const jsonHeaders = { headers: { "Content-Type": "application/json" } };
|
const jsonHeaders = { headers: { "Content-Type": "application/json" } };
|
||||||
|
|
@ -18,6 +19,8 @@ script:pre-request {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await resetPrototypeCombatTargets(bru);
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${baseUrl}/game/players/${playerId}/move`,
|
`${baseUrl}/game/players/${playerId}/move`,
|
||||||
{ schemaVersion: 1, target: { x: -5, y: 0.9, z: -5 } },
|
{ schemaVersion: 1, target: { x: -5, y: 0.9, z: -5 } },
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,13 @@ docs {
|
||||||
|
|
||||||
script:pre-request {
|
script:pre-request {
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
const { resetPrototypeCombatTargets } = require("./scripts/combat-targets-reset-helper.js");
|
||||||
const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl");
|
const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl");
|
||||||
const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId");
|
const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId");
|
||||||
const jsonHeaders = { headers: { "Content-Type": "application/json" } };
|
const jsonHeaders = { headers: { "Content-Type": "application/json" } };
|
||||||
|
|
||||||
|
await resetPrototypeCombatTargets(bru);
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${baseUrl}/game/players/${playerId}/move`,
|
`${baseUrl}/game/players/${playerId}/move`,
|
||||||
{ schemaVersion: 1, target: { x: -5, y: 0.9, z: -5 } },
|
{ schemaVersion: 1, target: { x: -5, y: 0.9, z: -5 } },
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ meta {
|
||||||
seq: 1
|
seq: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
script:pre-request {
|
||||||
|
const { resetPrototypeCombatTargets } = require("./scripts/combat-targets-reset-helper.js");
|
||||||
|
await resetPrototypeCombatTargets(bru);
|
||||||
|
}
|
||||||
|
|
||||||
get {
|
get {
|
||||||
url: {{baseUrl}}/game/world/combat-targets
|
url: {{baseUrl}}/game/world/combat-targets
|
||||||
body: none
|
body: none
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ meta {
|
||||||
}
|
}
|
||||||
|
|
||||||
docs {
|
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.
|
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>
|
/// <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; }
|
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>
|
/// <summary>World position for the dev player at process start.</summary>
|
||||||
public DefaultPositionOptions DefaultPosition { get; set; } = new();
|
public DefaultPositionOptions DefaultPosition { get; set; } = new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,13 @@ if (app.Environment.IsDevelopment() ||
|
||||||
app.MapMasteryFixtureApi();
|
app.MapMasteryFixtureApi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (app.Environment.IsDevelopment() ||
|
||||||
|
app.Environment.IsEnvironment("Testing") ||
|
||||||
|
app.Configuration.GetValue<bool>("Game:EnableCombatTargetFixtureApi"))
|
||||||
|
{
|
||||||
|
app.MapCombatTargetFixtureApi();
|
||||||
|
}
|
||||||
|
|
||||||
app.MapTargetingApi();
|
app.MapTargetingApi();
|
||||||
app.MapHotbarLoadoutApi();
|
app.MapHotbarLoadoutApi();
|
||||||
app.MapCooldownSnapshotApi();
|
app.MapCooldownSnapshotApi();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Game": {
|
"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"
|
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)
|
## 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.
|
**`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