fix(monsters): finish deleting experienceReward, column included
Design ruling R7 abolishes XP as a concept and says the column goes with it, but no task in the plan actually dropped it -- the plan only dropped characters.experience and combat_rewards.experience_granted. Task 9 removed experienceReward from the seed literals, leaving monster_definitions.experience_reward as a NOT NULL column with no default that nothing supplies. The first monster insert against a real database would have failed on a constraint violation. No suite here could have caught it: none of them connect to Postgres. Drops the column in the slice migration (which has never been run, so amending it in place is correct rather than stacking a second one), removes the entity field, and clears the three test fixtures that still set it. silver_min/silver_max deliberately stay -- spec 15 keeps a direct currency drop available as a lore-valid exception, and XP has no such carve-out. Also retargets the seed idempotency test off renown: 1, which is the seed's own default and so could not distinguish "preserved" from "reset to default". NOTE ON SCOPE: this commit also absorbs a Prettier reformatting pass that was already sitting uncommitted in the working tree, which is why it touches ~59 files. That churn is purely cosmetic line-rewrapping -- verified by inspection, and the suite is green at 267/267 with the build at exactly the 3 expected errors owned by Tasks 10 and 11. The repo is not Prettier-clean at baseline (119 files still flagged), so this was a partial run by an earlier step, not a deliberate repo-wide format. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
import { CombatAction } from './combat-action.enum';
|
||||
import { CombatEngineService, UnsupportedCombatActionError } from './combat-engine.service';
|
||||
import {
|
||||
CombatEngineService,
|
||||
UnsupportedCombatActionError,
|
||||
} from './combat-engine.service';
|
||||
import { CombatEngineState } from './combat-engine.types';
|
||||
import { CombatEventType } from './combat-event-type.enum';
|
||||
import { CombatStatus } from './combat-status.enum';
|
||||
import { Combatant } from './combatant.enum';
|
||||
|
||||
function baseState(overrides: Partial<CombatEngineState> = {}): CombatEngineState {
|
||||
function baseState(
|
||||
overrides: Partial<CombatEngineState> = {},
|
||||
): CombatEngineState {
|
||||
return {
|
||||
status: CombatStatus.ACTIVE,
|
||||
round: 1,
|
||||
@@ -31,7 +36,9 @@ describe('CombatEngineService', () => {
|
||||
});
|
||||
|
||||
it('reduces monster HP by the calculated damage and emits a DAMAGE event', () => {
|
||||
const result = engine.resolveAction(baseState(), { action: CombatAction.ATTACK });
|
||||
const result = engine.resolveAction(baseState(), {
|
||||
action: CombatAction.ATTACK,
|
||||
});
|
||||
|
||||
// raw = 6 + 8 = 14; armor 0 -> 14 mitigated
|
||||
expect(result.state.monster.currentHp).toBe(45 - 14);
|
||||
@@ -44,7 +51,9 @@ describe('CombatEngineService', () => {
|
||||
});
|
||||
|
||||
it('lets the monster retaliate when it survives the player attack, and advances the round', () => {
|
||||
const result = engine.resolveAction(baseState(), { action: CombatAction.ATTACK });
|
||||
const result = engine.resolveAction(baseState(), {
|
||||
action: CombatAction.ATTACK,
|
||||
});
|
||||
|
||||
// raw = 5; armor 6 -> 5*60/66 = 4.545 -> rounds to 5
|
||||
expect(result.state.player.currentHp).toBe(100 - 5);
|
||||
@@ -69,14 +78,27 @@ describe('CombatEngineService', () => {
|
||||
expect(result.state.status).toBe(CombatStatus.WON);
|
||||
expect(result.state.round).toBe(1);
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.DAMAGE, amount: 14 },
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.COMBAT_WON },
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 14,
|
||||
},
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.COMBAT_WON,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('ends the combat as LOST when the monster attack reduces the player to 0 HP', () => {
|
||||
const state = baseState({
|
||||
player: { currentHp: 3, maxHp: 100, stats: { attack: 6, weaponDamage: 8, armor: 6 } },
|
||||
player: {
|
||||
currentHp: 3,
|
||||
maxHp: 100,
|
||||
stats: { attack: 6, weaponDamage: 8, armor: 6 },
|
||||
},
|
||||
});
|
||||
|
||||
const result = engine.resolveAction(state, { action: CombatAction.ATTACK });
|
||||
@@ -84,9 +106,23 @@ describe('CombatEngineService', () => {
|
||||
expect(result.state.player.currentHp).toBe(0);
|
||||
expect(result.state.status).toBe(CombatStatus.LOST);
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.DAMAGE, amount: 14 },
|
||||
{ source: Combatant.MONSTER, target: Combatant.PLAYER, type: CombatEventType.DAMAGE, amount: 5 },
|
||||
{ source: Combatant.MONSTER, target: Combatant.PLAYER, type: CombatEventType.COMBAT_LOST },
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 14,
|
||||
},
|
||||
{
|
||||
source: Combatant.MONSTER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 5,
|
||||
},
|
||||
{
|
||||
source: Combatant.MONSTER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.COMBAT_LOST,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -106,7 +142,9 @@ describe('CombatEngineService', () => {
|
||||
});
|
||||
|
||||
it('HEAVY_STRIKE deals 160% damage to the monster', () => {
|
||||
const result = engine.resolveAction(baseState(), { action: CombatAction.HEAVY_STRIKE });
|
||||
const result = engine.resolveAction(baseState(), {
|
||||
action: CombatAction.HEAVY_STRIKE,
|
||||
});
|
||||
|
||||
// raw = 14; 14 * 1.6 = 22.4 -> rounds to 22
|
||||
expect(result.events[0]).toEqual({
|
||||
@@ -119,7 +157,9 @@ describe('CombatEngineService', () => {
|
||||
});
|
||||
|
||||
it('SHIELD_BASH deals 70% damage and does not emit INTERRUPT when nothing is pending', () => {
|
||||
const result = engine.resolveAction(baseState(), { action: CombatAction.SHIELD_BASH });
|
||||
const result = engine.resolveAction(baseState(), {
|
||||
action: CombatAction.SHIELD_BASH,
|
||||
});
|
||||
|
||||
// raw = 14; 14 * 0.7 = 9.8 -> rounds to 10
|
||||
expect(result.events[0]).toEqual({
|
||||
@@ -128,7 +168,9 @@ describe('CombatEngineService', () => {
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 10,
|
||||
});
|
||||
expect(result.events.some((event) => event.type === CombatEventType.INTERRUPT)).toBe(false);
|
||||
expect(
|
||||
result.events.some((event) => event.type === CombatEventType.INTERRUPT),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('SHIELD_BASH interrupts a pending Heavy Attack and the monster does not act this round', () => {
|
||||
@@ -140,11 +182,22 @@ describe('CombatEngineService', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const result = engine.resolveAction(state, { action: CombatAction.SHIELD_BASH });
|
||||
const result = engine.resolveAction(state, {
|
||||
action: CombatAction.SHIELD_BASH,
|
||||
});
|
||||
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.DAMAGE, amount: 10 },
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.INTERRUPT },
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 10,
|
||||
},
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.INTERRUPT,
|
||||
},
|
||||
]);
|
||||
expect(result.state.monster.stats.pendingAction).toBeUndefined();
|
||||
expect(result.state.player.currentHp).toBe(100);
|
||||
@@ -152,13 +205,24 @@ describe('CombatEngineService', () => {
|
||||
});
|
||||
|
||||
it('DEFEND deals no damage and halves the monster normal attack this round', () => {
|
||||
const result = engine.resolveAction(baseState(), { action: CombatAction.DEFEND });
|
||||
const result = engine.resolveAction(baseState(), {
|
||||
action: CombatAction.DEFEND,
|
||||
});
|
||||
|
||||
expect(result.state.monster.currentHp).toBe(45);
|
||||
// raw = 5; mitigated = 4.545...; * 0.5 = 2.27 -> rounds to 2
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.PLAYER, type: CombatEventType.DEFEND },
|
||||
{ source: Combatant.MONSTER, target: Combatant.PLAYER, type: CombatEventType.DAMAGE, amount: 2 },
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.DEFEND,
|
||||
},
|
||||
{
|
||||
source: Combatant.MONSTER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 2,
|
||||
},
|
||||
]);
|
||||
expect(result.state.player.currentHp).toBe(98);
|
||||
});
|
||||
@@ -215,10 +279,14 @@ describe('CombatEngineService', () => {
|
||||
it('telegraphs a Heavy Attack instead of attacking on the third round, and resolves it the round after', () => {
|
||||
const round3State = baseState({ round: 3 });
|
||||
|
||||
const telegraphResult = engine.resolveAction(round3State, { action: CombatAction.ATTACK });
|
||||
const telegraphResult = engine.resolveAction(round3State, {
|
||||
action: CombatAction.ATTACK,
|
||||
});
|
||||
|
||||
expect(telegraphResult.state.player.currentHp).toBe(100);
|
||||
expect(telegraphResult.state.monster.stats.pendingAction).toBe('HEAVY_ATTACK');
|
||||
expect(telegraphResult.state.monster.stats.pendingAction).toBe(
|
||||
'HEAVY_ATTACK',
|
||||
);
|
||||
expect(telegraphResult.events[1]).toEqual({
|
||||
source: Combatant.MONSTER,
|
||||
target: Combatant.PLAYER,
|
||||
@@ -226,7 +294,9 @@ describe('CombatEngineService', () => {
|
||||
});
|
||||
expect(telegraphResult.state.round).toBe(4);
|
||||
|
||||
const resolveResult = engine.resolveAction(telegraphResult.state, { action: CombatAction.ATTACK });
|
||||
const resolveResult = engine.resolveAction(telegraphResult.state, {
|
||||
action: CombatAction.ATTACK,
|
||||
});
|
||||
|
||||
// raw = 5; mitigated = 4.545...; heavy * 1.6 = 7.27 -> rounds to 7
|
||||
expect(resolveResult.events[1]).toEqual({
|
||||
@@ -254,16 +324,29 @@ describe('CombatEngineService', () => {
|
||||
expect(result.state.player.currentHp).toBe(100);
|
||||
expect(result.state.monster.stats.pendingAction).toBeUndefined();
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.DAMAGE, amount: 14 },
|
||||
{ source: Combatant.PLAYER, target: Combatant.MONSTER, type: CombatEventType.COMBAT_WON },
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 14,
|
||||
},
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.MONSTER,
|
||||
type: CombatEventType.COMBAT_WON,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('is deterministic for HEAVY_STRIKE as well', () => {
|
||||
const state = baseState();
|
||||
|
||||
const first = engine.resolveAction(state, { action: CombatAction.HEAVY_STRIKE });
|
||||
const second = engine.resolveAction(state, { action: CombatAction.HEAVY_STRIKE });
|
||||
const first = engine.resolveAction(state, {
|
||||
action: CombatAction.HEAVY_STRIKE,
|
||||
});
|
||||
const second = engine.resolveAction(state, {
|
||||
action: CombatAction.HEAVY_STRIKE,
|
||||
});
|
||||
|
||||
expect(first).toEqual(second);
|
||||
});
|
||||
@@ -281,8 +364,17 @@ describe('CombatEngineService', () => {
|
||||
|
||||
// raw = 5; mitigated = 4.545...; heavy * 1.6 = 7.27; * defend 0.5 = 3.636 -> rounds to 4
|
||||
expect(result.events).toEqual([
|
||||
{ source: Combatant.PLAYER, target: Combatant.PLAYER, type: CombatEventType.DEFEND },
|
||||
{ source: Combatant.MONSTER, target: Combatant.PLAYER, type: CombatEventType.DAMAGE, amount: 4 },
|
||||
{
|
||||
source: Combatant.PLAYER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.DEFEND,
|
||||
},
|
||||
{
|
||||
source: Combatant.MONSTER,
|
||||
target: Combatant.PLAYER,
|
||||
type: CombatEventType.DAMAGE,
|
||||
amount: 4,
|
||||
},
|
||||
]);
|
||||
expect(result.state.monster.stats.pendingAction).toBeUndefined();
|
||||
expect(result.state.player.currentHp).toBe(100 - 4);
|
||||
|
||||
Reference in New Issue
Block a user