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:
@@ -53,12 +53,18 @@ class FakeRepository<T extends { id: string }> {
|
||||
relations?: Record<string, unknown>;
|
||||
lock?: { mode: string };
|
||||
}): Promise<T | null> {
|
||||
const row = this.rows().find((candidate) => this.matches(candidate, options.where)) ?? null;
|
||||
return Promise.resolve(row ? this.withRelations(row, options.relations) : null);
|
||||
const row =
|
||||
this.rows().find((candidate) => this.matches(candidate, options.where)) ??
|
||||
null;
|
||||
return Promise.resolve(
|
||||
row ? this.withRelations(row, options.relations) : null,
|
||||
);
|
||||
}
|
||||
|
||||
findOneBy(where: Partial<T>): Promise<T | null> {
|
||||
return Promise.resolve(this.rows().find((row) => this.matches(row, where)) ?? null);
|
||||
return Promise.resolve(
|
||||
this.rows().find((row) => this.matches(row, where)) ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
find(options: {
|
||||
@@ -66,12 +72,18 @@ class FakeRepository<T extends { id: string }> {
|
||||
relations?: Record<string, unknown>;
|
||||
order?: Partial<Record<keyof T, 'ASC' | 'DESC'>>;
|
||||
}): Promise<T[]> {
|
||||
const matched = this.rows().filter((row) => this.matches(row, options.where));
|
||||
return Promise.resolve(matched.map((row) => this.withRelations(row, options.relations)));
|
||||
const matched = this.rows().filter((row) =>
|
||||
this.matches(row, options.where),
|
||||
);
|
||||
return Promise.resolve(
|
||||
matched.map((row) => this.withRelations(row, options.relations)),
|
||||
);
|
||||
}
|
||||
|
||||
count(options: { where: Partial<T> }): Promise<number> {
|
||||
return Promise.resolve(this.rows().filter((row) => this.matches(row, options.where)).length);
|
||||
return Promise.resolve(
|
||||
this.rows().filter((row) => this.matches(row, options.where)).length,
|
||||
);
|
||||
}
|
||||
|
||||
create(values: Partial<T>): T {
|
||||
@@ -98,12 +110,18 @@ class FakeRepository<T extends { id: string }> {
|
||||
}
|
||||
const copy = { ...row } as T & Record<string, unknown>;
|
||||
if (this.target === CharacterItem && relations['itemDefinition']) {
|
||||
const itemDefinitionId = (row as unknown as CharacterItem).itemDefinitionId;
|
||||
copy['itemDefinition'] = this.state.itemDefinitions.find((d) => d.id === itemDefinitionId);
|
||||
const itemDefinitionId = (row as unknown as CharacterItem)
|
||||
.itemDefinitionId;
|
||||
copy['itemDefinition'] = this.state.itemDefinitions.find(
|
||||
(d) => d.id === itemDefinitionId,
|
||||
);
|
||||
}
|
||||
if (this.target === CharacterEquipment && relations['characterItem']) {
|
||||
const characterItemId = (row as unknown as CharacterEquipment).characterItemId;
|
||||
const characterItem = this.state.characterItems.find((ci) => ci.id === characterItemId);
|
||||
const characterItemId = (row as unknown as CharacterEquipment)
|
||||
.characterItemId;
|
||||
const characterItem = this.state.characterItems.find(
|
||||
(ci) => ci.id === characterItemId,
|
||||
);
|
||||
copy['characterItem'] = characterItem
|
||||
? {
|
||||
...characterItem,
|
||||
@@ -113,7 +131,7 @@ class FakeRepository<T extends { id: string }> {
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
return copy as T;
|
||||
return copy;
|
||||
}
|
||||
|
||||
private rows(): T[] {
|
||||
@@ -123,18 +141,24 @@ class FakeRepository<T extends { id: string }> {
|
||||
if (this.target === MonsterDefinition) return this.state.monsters as T[];
|
||||
if (this.target === Combat) return this.state.combats as T[];
|
||||
if (this.target === CombatEvent) return this.state.combatEvents as T[];
|
||||
if (this.target === ItemDefinition) return this.state.itemDefinitions as T[];
|
||||
if (this.target === ItemDefinition)
|
||||
return this.state.itemDefinitions as T[];
|
||||
if (this.target === CharacterItem) return this.state.characterItems as T[];
|
||||
if (this.target === CharacterEquipment) return this.state.characterEquipment as T[];
|
||||
if (this.target === CharacterEquipment)
|
||||
return this.state.characterEquipment as T[];
|
||||
throw new Error(`Unsupported repository ${this.targetName()}`);
|
||||
}
|
||||
|
||||
private matches(row: T, where: Partial<T>): boolean {
|
||||
return Object.entries(where).every(([key, value]) => row[key as keyof T] === value);
|
||||
return Object.entries(where).every(
|
||||
([key, value]) => row[key as keyof T] === value,
|
||||
);
|
||||
}
|
||||
|
||||
private targetName(): string {
|
||||
return typeof this.target === 'function' ? this.target.name : 'EntitySchema';
|
||||
return typeof this.target === 'function'
|
||||
? this.target.name
|
||||
: 'EntitySchema';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +170,9 @@ class FakeDataSource {
|
||||
return new FakeRepository(this.state, target, this);
|
||||
}
|
||||
|
||||
async transaction<T>(work: (manager: EntityManager) => Promise<T>): Promise<T> {
|
||||
async transaction<T>(
|
||||
work: (manager: EntityManager) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return work({
|
||||
getRepository: <U extends { id: string }>(target: EntityTarget<U>) =>
|
||||
this.getRepository(target),
|
||||
@@ -177,7 +203,9 @@ function character(overrides: Partial<Character> = {}): Character {
|
||||
} as Character;
|
||||
}
|
||||
|
||||
function monster(overrides: Partial<MonsterDefinition> = {}): MonsterDefinition {
|
||||
function monster(
|
||||
overrides: Partial<MonsterDefinition> = {},
|
||||
): MonsterDefinition {
|
||||
return {
|
||||
id: MONSTER_ID,
|
||||
key: 'road-bandit',
|
||||
@@ -186,7 +214,6 @@ function monster(overrides: Partial<MonsterDefinition> = {}): MonsterDefinition
|
||||
maxHp: 75,
|
||||
attack: 9,
|
||||
armor: 5,
|
||||
experienceReward: 16,
|
||||
silverMin: 9,
|
||||
silverMax: 15,
|
||||
artworkPath: '/images/monsters/road-bandit.png',
|
||||
@@ -207,7 +234,10 @@ function hunt(overrides: Partial<Hunt> = {}): Hunt {
|
||||
} as Hunt;
|
||||
}
|
||||
|
||||
function encounter(id: string, overrides: Partial<HuntEncounter> = {}): HuntEncounter {
|
||||
function encounter(
|
||||
id: string,
|
||||
overrides: Partial<HuntEncounter> = {},
|
||||
): HuntEncounter {
|
||||
return {
|
||||
id,
|
||||
huntId: HUNT_ID,
|
||||
@@ -219,7 +249,9 @@ function encounter(id: string, overrides: Partial<HuntEncounter> = {}): HuntEnco
|
||||
} as HuntEncounter;
|
||||
}
|
||||
|
||||
function itemDefinition(overrides: Partial<ItemDefinition> = {}): ItemDefinition {
|
||||
function itemDefinition(
|
||||
overrides: Partial<ItemDefinition> = {},
|
||||
): ItemDefinition {
|
||||
return {
|
||||
id: WORN_SWORD_DEFINITION_ID,
|
||||
key: 'worn-short-sword',
|
||||
@@ -243,12 +275,16 @@ function itemDefinition(overrides: Partial<ItemDefinition> = {}): ItemDefinition
|
||||
}
|
||||
|
||||
function fakeTravelService(): TravelService {
|
||||
return { completeTravelIfDue: jest.fn().mockResolvedValue({ status: 'IDLE' }) } as unknown as TravelService;
|
||||
return {
|
||||
completeTravelIfDue: jest.fn().mockResolvedValue({ status: 'IDLE' }),
|
||||
} as unknown as TravelService;
|
||||
}
|
||||
|
||||
function fakeRewardService(): CombatRewardService {
|
||||
return {
|
||||
grantVictoryRewards: jest.fn().mockResolvedValue({ experience: 0, silver: 0, items: [] }),
|
||||
grantVictoryRewards: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ experience: 0, silver: 0, items: [] }),
|
||||
loadRewards: jest.fn().mockResolvedValue(null),
|
||||
} as unknown as CombatRewardService;
|
||||
}
|
||||
@@ -302,8 +338,13 @@ function createHarness() {
|
||||
],
|
||||
};
|
||||
const dataSource = new FakeDataSource(state);
|
||||
const characterStats = new CharacterStatsService(dataSource as unknown as DataSource);
|
||||
const equipmentService = new EquipmentService(dataSource as unknown as DataSource, characterStats);
|
||||
const characterStats = new CharacterStatsService(
|
||||
dataSource as unknown as DataSource,
|
||||
);
|
||||
const equipmentService = new EquipmentService(
|
||||
dataSource as unknown as DataSource,
|
||||
characterStats,
|
||||
);
|
||||
const combatService = new CombatService(
|
||||
dataSource as unknown as DataSource,
|
||||
fakeTravelService(),
|
||||
@@ -322,8 +363,16 @@ describe('equipping Räuberklinge increases combat damage (spec §45, §60)', ()
|
||||
const before = await combatService.startCombat(CHARACTER_ID, 'encounter-1');
|
||||
let beforeDamage = 0;
|
||||
let beforeResult = before;
|
||||
for (let round = 0; round < 10 && beforeResult.status === 'ACTIVE'; round += 1) {
|
||||
beforeResult = await combatService.performAction(CHARACTER_ID, before.id, CombatAction.ATTACK);
|
||||
for (
|
||||
let round = 0;
|
||||
round < 10 && beforeResult.status === 'ACTIVE';
|
||||
round += 1
|
||||
) {
|
||||
beforeResult = await combatService.performAction(
|
||||
CHARACTER_ID,
|
||||
before.id,
|
||||
CombatAction.ATTACK,
|
||||
);
|
||||
if (round === 0) {
|
||||
beforeDamage = before.monster.maxHp - beforeResult.monster.currentHp;
|
||||
}
|
||||
@@ -336,7 +385,11 @@ describe('equipping Räuberklinge increases combat damage (spec §45, §60)', ()
|
||||
|
||||
state.huntEncounters.push(encounter('encounter-2'));
|
||||
const after = await combatService.startCombat(CHARACTER_ID, 'encounter-2');
|
||||
const afterResult = await combatService.performAction(CHARACTER_ID, after.id, CombatAction.ATTACK);
|
||||
const afterResult = await combatService.performAction(
|
||||
CHARACTER_ID,
|
||||
after.id,
|
||||
CombatAction.ATTACK,
|
||||
);
|
||||
const afterDamage = after.monster.maxHp - afterResult.monster.currentHp;
|
||||
|
||||
// (6+8) vs 5 armor -> round(14 * 60/65) = 13
|
||||
@@ -346,7 +399,7 @@ describe('equipping Räuberklinge increases combat damage (spec §45, §60)', ()
|
||||
expect(afterDamage).toBeGreaterThan(beforeDamage);
|
||||
});
|
||||
|
||||
it('rejects equipping during an active combat, and never retroactively rewrites a finished combat\'s snapshot', async () => {
|
||||
it("rejects equipping during an active combat, and never retroactively rewrites a finished combat's snapshot", async () => {
|
||||
const { state, combatService, equipmentService } = createHarness();
|
||||
state.huntEncounters.push(encounter('encounter-1'));
|
||||
|
||||
@@ -361,7 +414,11 @@ describe('equipping Räuberklinge increases combat damage (spec §45, §60)', ()
|
||||
// (status/round) must stay exactly what it was when the fight ended.
|
||||
let result = combat;
|
||||
for (let round = 0; round < 10 && result.status === 'ACTIVE'; round += 1) {
|
||||
result = await combatService.performAction(CHARACTER_ID, combat.id, CombatAction.ATTACK);
|
||||
result = await combatService.performAction(
|
||||
CHARACTER_ID,
|
||||
combat.id,
|
||||
CombatAction.ATTACK,
|
||||
);
|
||||
}
|
||||
expect(result.status).not.toBe('ACTIVE');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user