NEO-82: Fix cast definition lookup order and Bruno target isolation.
Load ability definition before TryResolve so cooldown lookup cannot deny after HP mutation (Bugbot). Happy Bruno cast uses beta; defeat spine keeps alpha; bump spine seq and cooldown sleep margin for CI.pull/116/head
parent
322798b2ef
commit
52bf9fc4bf
|
|
@ -8,13 +8,27 @@ script:pre-request {
|
|||
const axios = require("axios");
|
||||
const baseUrl = bru.getEnvVar("baseUrl") || bru.getVar("baseUrl");
|
||||
const playerId = bru.getEnvVar("playerId") || bru.getVar("playerId");
|
||||
const jsonHeaders = { headers: { "Content-Type": "application/json" } };
|
||||
|
||||
await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/move`,
|
||||
{ schemaVersion: 1, target: { x: 5, y: 0.9, z: 5 } },
|
||||
jsonHeaders,
|
||||
);
|
||||
|
||||
await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/hotbar-loadout`,
|
||||
{
|
||||
schemaVersion: 1,
|
||||
target: { x: -5, y: 0.9, z: -5 },
|
||||
slots: [{ slotIndex: 0, abilityId: "prototype_pulse" }],
|
||||
},
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
jsonHeaders,
|
||||
);
|
||||
|
||||
await axios.post(
|
||||
`${baseUrl}/game/players/${playerId}/target/select`,
|
||||
{ schemaVersion: 1, targetId: "prototype_target_beta" },
|
||||
jsonHeaders,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +43,7 @@ body:json {
|
|||
"schemaVersion": 1,
|
||||
"slotIndex": 0,
|
||||
"abilityId": "prototype_pulse",
|
||||
"targetId": "prototype_target_alpha"
|
||||
"targetId": "prototype_target_beta"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +60,7 @@ tests {
|
|||
expect(body.accepted).to.equal(true);
|
||||
expect(body.combatResolution).to.be.an("object");
|
||||
expect(body.combatResolution.abilityId).to.equal("prototype_pulse");
|
||||
expect(body.combatResolution.targetId).to.equal("prototype_target_alpha");
|
||||
expect(body.combatResolution.targetId).to.equal("prototype_target_beta");
|
||||
expect(body.combatResolution.damageDealt).to.equal(25);
|
||||
expect(body.combatResolution.targetRemainingHp).to.equal(75);
|
||||
expect(body.combatResolution.targetDefeated).to.equal(false);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
meta {
|
||||
name: POST cast lock pulse defeat spine (NEO-82)
|
||||
type: http
|
||||
seq: 23
|
||||
seq: 30
|
||||
}
|
||||
|
||||
docs {
|
||||
|
|
@ -55,12 +55,12 @@ script:pre-request {
|
|||
);
|
||||
castResults.push(response.data);
|
||||
if (i < 3) {
|
||||
await sleep(3100);
|
||||
await sleep(3200);
|
||||
}
|
||||
}
|
||||
|
||||
bru.setVar("neo82CastResults", JSON.stringify(castResults));
|
||||
await sleep(3100);
|
||||
await sleep(3200);
|
||||
}
|
||||
|
||||
post {
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ meta {
|
|||
}
|
||||
|
||||
docs {
|
||||
NEO-82: happy cast asserts combatResolution; defeat spine (seq 23) requires ~10s pre-request for pulse cooldowns between four lethal casts.
|
||||
NEO-82: happy cast uses beta at spawn (5,5) so alpha stays fresh for defeat spine (seq 30).
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ None.
|
|||
|
||||
- ~~Nit: `PostAbilityCast_ShouldDefeatTargetAfterFourPulses_ThenDenyTargetDefeated` runs pulses 1–3 inside **Act**; move them to **Arrange** for stricter AAA (same note as NEO-81 four-pulse pattern).~~ **Done.** — Pulses 1–3 in Arrange; Act is 4th + 5th cast; Assert includes HP stepping.
|
||||
|
||||
- Nit: After successful `TryResolve`, `TryGetDefinition` runs again solely for `CooldownSeconds` — `CombatOperations` already loaded the definition internally. Acceptable for thin handler clarity; optional future refactor if catalog lookups become hot-path. **Deferred** — no hot-path concern in prototype.
|
||||
- ~~Nit: After successful `TryResolve`, `TryGetDefinition` runs again solely for `CooldownSeconds` — `CombatOperations` already loaded the definition internally. Acceptable for thin handler clarity; optional future refactor if catalog lookups become hot-path. **Deferred** — no hot-path concern in prototype.~~ **Done.** — `TryGetDefinition` moved before `TryResolve` so cooldown lookup cannot deny after HP mutation (Bugbot fix).
|
||||
|
||||
- Nit: `AbilityPrototypeCooldown.GlobalDuration` is `[Obsolete]` but retained — matches plan; no remaining runtime references in tests or accept path. **No action** — intentional per plan.
|
||||
|
||||
|
|
|
|||
|
|
@ -444,6 +444,32 @@ public sealed class AbilityCastApiTests
|
|||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostAbilityCast_ShouldStartCatalogCooldown_WhenResolveSucceedsAfterDefinitionGate()
|
||||
{
|
||||
// Arrange
|
||||
await using var factory = new InMemoryWebApplicationFactory();
|
||||
var client = factory.CreateClient();
|
||||
await BindSlot0PulseAsync(client);
|
||||
await LockPrototypeTargetAlphaAsync(client);
|
||||
var cast = PulseCastRequest();
|
||||
|
||||
// Act
|
||||
var castResponse = await client.PostAsJsonAsync("/game/players/dev-local-1/ability-cast", cast);
|
||||
var snapshotResponse = await client.GetAsync("/game/players/dev-local-1/cooldown-snapshot");
|
||||
|
||||
// Assert
|
||||
var body = await castResponse.Content.ReadFromJsonAsync<AbilityCastResponse>();
|
||||
Assert.Equal(HttpStatusCode.OK, castResponse.StatusCode);
|
||||
Assert.NotNull(body);
|
||||
Assert.True(body!.Accepted);
|
||||
Assert.NotNull(body.CombatResolution);
|
||||
var snapshot = await snapshotResponse.Content.ReadFromJsonAsync<CooldownSnapshotResponse>();
|
||||
Assert.NotNull(snapshot);
|
||||
var slot0 = snapshot!.Slots.Single(s => s.SlotIndex == 0);
|
||||
Assert.NotNull(slot0.CooldownEndsAtUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostAbilityCast_ShouldDenyOnCooldown_WhenRepeatedWhileCooling()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -184,6 +184,20 @@ public static class AbilityCastApi
|
|||
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonOnCooldown });
|
||||
}
|
||||
|
||||
if (!abilities.TryGetDefinition(normalizedRequest, out var definition))
|
||||
{
|
||||
// NEO-30 telemetry hook site: `ability_cast_denied` + reasonCode (TODO(E9.M1): catalog emit).
|
||||
return JsonAbilityCast(
|
||||
id,
|
||||
body,
|
||||
new AbilityCastResponse
|
||||
{
|
||||
Accepted = false,
|
||||
ReasonCode = ReasonUnknownAbility,
|
||||
},
|
||||
normalizedRequest);
|
||||
}
|
||||
|
||||
var combatResult = CombatOperations.TryResolve(
|
||||
normalizedRequest,
|
||||
lookupKey,
|
||||
|
|
@ -204,19 +218,6 @@ public static class AbilityCastApi
|
|||
normalizedRequest);
|
||||
}
|
||||
|
||||
if (!abilities.TryGetDefinition(normalizedRequest, out var definition))
|
||||
{
|
||||
return JsonAbilityCast(
|
||||
id,
|
||||
body,
|
||||
new AbilityCastResponse
|
||||
{
|
||||
Accepted = false,
|
||||
ReasonCode = ReasonUnknownAbility,
|
||||
},
|
||||
normalizedRequest);
|
||||
}
|
||||
|
||||
cooldowns.StartCooldown(
|
||||
id,
|
||||
body.SlotIndex,
|
||||
|
|
|
|||
Loading…
Reference in New Issue