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 axios = require("axios");
|
||||||
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" } };
|
||||||
|
|
||||||
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 } },
|
||||||
|
jsonHeaders,
|
||||||
|
);
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
`${baseUrl}/game/players/${playerId}/hotbar-loadout`,
|
||||||
{
|
{
|
||||||
schemaVersion: 1,
|
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,
|
"schemaVersion": 1,
|
||||||
"slotIndex": 0,
|
"slotIndex": 0,
|
||||||
"abilityId": "prototype_pulse",
|
"abilityId": "prototype_pulse",
|
||||||
"targetId": "prototype_target_alpha"
|
"targetId": "prototype_target_beta"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +60,7 @@ tests {
|
||||||
expect(body.accepted).to.equal(true);
|
expect(body.accepted).to.equal(true);
|
||||||
expect(body.combatResolution).to.be.an("object");
|
expect(body.combatResolution).to.be.an("object");
|
||||||
expect(body.combatResolution.abilityId).to.equal("prototype_pulse");
|
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.damageDealt).to.equal(25);
|
||||||
expect(body.combatResolution.targetRemainingHp).to.equal(75);
|
expect(body.combatResolution.targetRemainingHp).to.equal(75);
|
||||||
expect(body.combatResolution.targetDefeated).to.equal(false);
|
expect(body.combatResolution.targetDefeated).to.equal(false);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
meta {
|
meta {
|
||||||
name: POST cast lock pulse defeat spine (NEO-82)
|
name: POST cast lock pulse defeat spine (NEO-82)
|
||||||
type: http
|
type: http
|
||||||
seq: 23
|
seq: 30
|
||||||
}
|
}
|
||||||
|
|
||||||
docs {
|
docs {
|
||||||
|
|
@ -55,12 +55,12 @@ script:pre-request {
|
||||||
);
|
);
|
||||||
castResults.push(response.data);
|
castResults.push(response.data);
|
||||||
if (i < 3) {
|
if (i < 3) {
|
||||||
await sleep(3100);
|
await sleep(3200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bru.setVar("neo82CastResults", JSON.stringify(castResults));
|
bru.setVar("neo82CastResults", JSON.stringify(castResults));
|
||||||
await sleep(3100);
|
await sleep(3200);
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,5 @@ meta {
|
||||||
}
|
}
|
||||||
|
|
||||||
docs {
|
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: `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.
|
- 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);
|
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]
|
[Fact]
|
||||||
public async Task PostAbilityCast_ShouldDenyOnCooldown_WhenRepeatedWhileCooling()
|
public async Task PostAbilityCast_ShouldDenyOnCooldown_WhenRepeatedWhileCooling()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -184,6 +184,20 @@ public static class AbilityCastApi
|
||||||
new AbilityCastResponse { Accepted = false, ReasonCode = ReasonOnCooldown });
|
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(
|
var combatResult = CombatOperations.TryResolve(
|
||||||
normalizedRequest,
|
normalizedRequest,
|
||||||
lookupKey,
|
lookupKey,
|
||||||
|
|
@ -204,19 +218,6 @@ public static class AbilityCastApi
|
||||||
normalizedRequest);
|
normalizedRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!abilities.TryGetDefinition(normalizedRequest, out var definition))
|
|
||||||
{
|
|
||||||
return JsonAbilityCast(
|
|
||||||
id,
|
|
||||||
body,
|
|
||||||
new AbilityCastResponse
|
|
||||||
{
|
|
||||||
Accepted = false,
|
|
||||||
ReasonCode = ReasonUnknownAbility,
|
|
||||||
},
|
|
||||||
normalizedRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
cooldowns.StartCooldown(
|
cooldowns.StartCooldown(
|
||||||
id,
|
id,
|
||||||
body.SlotIndex,
|
body.SlotIndex,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue