feat: seed Aschenratte and Straßenräuber for Verbrannte Straße
Adds the two starter monster definitions and their location-monster mapping (weights 70/30) to the vertical-slice seed, following the same findOneBy/update-or-insert pattern used for locations, plus an upsert on (locationId, monsterId) for the mapping. Copies the source artwork into the served images/monsters directory and extends the seed spec harness to cover both idempotent inserts. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,4 @@
|
|||||||
export const SOUTH_GATE_ID = '20000000-0000-4000-8000-000000000001';
|
export const SOUTH_GATE_ID = '20000000-0000-4000-8000-000000000001';
|
||||||
export const BURNED_ROAD_ID = '20000000-0000-4000-8000-000000000002';
|
export const BURNED_ROAD_ID = '20000000-0000-4000-8000-000000000002';
|
||||||
|
export const ASH_RAT_MONSTER_ID = '30000000-0000-4000-8000-000000000001';
|
||||||
|
export const ROAD_BANDIT_MONSTER_ID = '30000000-0000-4000-8000-000000000002';
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { Character } from '../../characters/entities/character.entity';
|
import { Character } from '../../characters/entities/character.entity';
|
||||||
|
import { LocationMonster } from '../../monsters/entities/location-monster.entity';
|
||||||
|
import { MonsterDefinition } from '../../monsters/entities/monster-definition.entity';
|
||||||
import { LocationConnection } from '../../world/entities/location-connection.entity';
|
import { LocationConnection } from '../../world/entities/location-connection.entity';
|
||||||
import { LocationDefinition } from '../../world/entities/location-definition.entity';
|
import { LocationDefinition } from '../../world/entities/location-definition.entity';
|
||||||
import { seedVisibleVerticalSlice } from './vertical-slice.seed';
|
import { seedVisibleVerticalSlice } from './vertical-slice.seed';
|
||||||
@@ -9,6 +11,8 @@ type Row = Record<string, unknown>;
|
|||||||
const DEMO_CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
|
const DEMO_CHARACTER_ID = '10000000-0000-4000-8000-000000000001';
|
||||||
const SOUTH_GATE_ID = '20000000-0000-4000-8000-000000000001';
|
const SOUTH_GATE_ID = '20000000-0000-4000-8000-000000000001';
|
||||||
const BURNED_ROAD_ID = '20000000-0000-4000-8000-000000000002';
|
const BURNED_ROAD_ID = '20000000-0000-4000-8000-000000000002';
|
||||||
|
const ASH_RAT_MONSTER_ID = '30000000-0000-4000-8000-000000000001';
|
||||||
|
const ROAD_BANDIT_MONSTER_ID = '30000000-0000-4000-8000-000000000002';
|
||||||
|
|
||||||
class InMemoryRepository {
|
class InMemoryRepository {
|
||||||
readonly rows: Row[] = [];
|
readonly rows: Row[] = [];
|
||||||
@@ -61,6 +65,8 @@ function createDataSource(
|
|||||||
locationRepository: InMemoryRepository,
|
locationRepository: InMemoryRepository,
|
||||||
connectionRepository: InMemoryRepository,
|
connectionRepository: InMemoryRepository,
|
||||||
characterRepository: InMemoryRepository,
|
characterRepository: InMemoryRepository,
|
||||||
|
monsterRepository: InMemoryRepository,
|
||||||
|
locationMonsterRepository: InMemoryRepository,
|
||||||
): DataSource {
|
): DataSource {
|
||||||
return {
|
return {
|
||||||
getRepository: jest.fn((entity: unknown) => {
|
getRepository: jest.fn((entity: unknown) => {
|
||||||
@@ -73,6 +79,12 @@ function createDataSource(
|
|||||||
if (entity === Character) {
|
if (entity === Character) {
|
||||||
return characterRepository;
|
return characterRepository;
|
||||||
}
|
}
|
||||||
|
if (entity === MonsterDefinition) {
|
||||||
|
return monsterRepository;
|
||||||
|
}
|
||||||
|
if (entity === LocationMonster) {
|
||||||
|
return locationMonsterRepository;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error('Unexpected repository');
|
throw new Error('Unexpected repository');
|
||||||
}),
|
}),
|
||||||
@@ -84,10 +96,14 @@ describe('seedVisibleVerticalSlice', () => {
|
|||||||
const locationRepository = new InMemoryRepository();
|
const locationRepository = new InMemoryRepository();
|
||||||
const connectionRepository = new InMemoryRepository();
|
const connectionRepository = new InMemoryRepository();
|
||||||
const characterRepository = new InMemoryRepository();
|
const characterRepository = new InMemoryRepository();
|
||||||
|
const monsterRepository = new InMemoryRepository();
|
||||||
|
const locationMonsterRepository = new InMemoryRepository();
|
||||||
const dataSource = createDataSource(
|
const dataSource = createDataSource(
|
||||||
locationRepository,
|
locationRepository,
|
||||||
connectionRepository,
|
connectionRepository,
|
||||||
characterRepository,
|
characterRepository,
|
||||||
|
monsterRepository,
|
||||||
|
locationMonsterRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
await seedVisibleVerticalSlice(dataSource);
|
await seedVisibleVerticalSlice(dataSource);
|
||||||
@@ -136,13 +152,63 @@ describe('seedVisibleVerticalSlice', () => {
|
|||||||
experience: 39,
|
experience: 39,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(monsterRepository.insert).toHaveBeenCalledTimes(2);
|
||||||
|
expect(monsterRepository.rows).toHaveLength(2);
|
||||||
|
expect(monsterRepository.rows).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'ash-rat',
|
||||||
|
name: 'Aschenratte',
|
||||||
|
level: 1,
|
||||||
|
maxHp: 45,
|
||||||
|
attack: 5,
|
||||||
|
armor: 0,
|
||||||
|
experienceReward: 8,
|
||||||
|
silverMin: 4,
|
||||||
|
silverMax: 7,
|
||||||
|
artworkPath: '/images/monsters/ash-rat.png',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
key: 'road-bandit',
|
||||||
|
name: 'Straßenräuber',
|
||||||
|
level: 2,
|
||||||
|
maxHp: 75,
|
||||||
|
attack: 9,
|
||||||
|
armor: 5,
|
||||||
|
experienceReward: 16,
|
||||||
|
silverMin: 9,
|
||||||
|
silverMax: 15,
|
||||||
|
artworkPath: '/images/monsters/road-bandit.png',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(locationMonsterRepository.upsert).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
locationId: BURNED_ROAD_ID,
|
||||||
|
monsterId: ASH_RAT_MONSTER_ID,
|
||||||
|
weight: 70,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
locationId: BURNED_ROAD_ID,
|
||||||
|
monsterId: ROAD_BANDIT_MONSTER_ID,
|
||||||
|
weight: 30,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
['locationId', 'monsterId'],
|
||||||
|
);
|
||||||
|
expect(locationMonsterRepository.rows).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves existing location IDs and uses them for the directed connections', async () => {
|
it('preserves existing location IDs and uses them for the directed connections', async () => {
|
||||||
const locationRepository = new InMemoryRepository();
|
const locationRepository = new InMemoryRepository();
|
||||||
const connectionRepository = new InMemoryRepository();
|
const connectionRepository = new InMemoryRepository();
|
||||||
const characterRepository = new InMemoryRepository();
|
const characterRepository = new InMemoryRepository();
|
||||||
const persistedSouthGateId = '30000000-0000-4000-8000-000000000001';
|
const monsterRepository = new InMemoryRepository();
|
||||||
|
const locationMonsterRepository = new InMemoryRepository();
|
||||||
|
const persistedSouthGateId = '40000000-0000-4000-8000-000000000001';
|
||||||
locationRepository.rows.push({
|
locationRepository.rows.push({
|
||||||
id: persistedSouthGateId,
|
id: persistedSouthGateId,
|
||||||
key: 'south-gate',
|
key: 'south-gate',
|
||||||
@@ -152,6 +218,8 @@ describe('seedVisibleVerticalSlice', () => {
|
|||||||
locationRepository,
|
locationRepository,
|
||||||
connectionRepository,
|
connectionRepository,
|
||||||
characterRepository,
|
characterRepository,
|
||||||
|
monsterRepository,
|
||||||
|
locationMonsterRepository,
|
||||||
);
|
);
|
||||||
|
|
||||||
await seedVisibleVerticalSlice(dataSource);
|
await seedVisibleVerticalSlice(dataSource);
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { DEMO_CHARACTER_ID } from '../../demo/demo-character.constants';
|
import { DEMO_CHARACTER_ID } from '../../demo/demo-character.constants';
|
||||||
import { Character } from '../../characters/entities/character.entity';
|
import { Character } from '../../characters/entities/character.entity';
|
||||||
|
import { EncounterType } from '../../monsters/entities/encounter-type.enum';
|
||||||
|
import { LocationMonster } from '../../monsters/entities/location-monster.entity';
|
||||||
|
import { MonsterDefinition } from '../../monsters/entities/monster-definition.entity';
|
||||||
import { LocationConnection } from '../../world/entities/location-connection.entity';
|
import { LocationConnection } from '../../world/entities/location-connection.entity';
|
||||||
import { LocationDefinition } from '../../world/entities/location-definition.entity';
|
import { LocationDefinition } from '../../world/entities/location-definition.entity';
|
||||||
import { BURNED_ROAD_ID, SOUTH_GATE_ID } from './vertical-slice.constants';
|
import {
|
||||||
|
ASH_RAT_MONSTER_ID,
|
||||||
|
BURNED_ROAD_ID,
|
||||||
|
ROAD_BANDIT_MONSTER_ID,
|
||||||
|
SOUTH_GATE_ID,
|
||||||
|
} from './vertical-slice.constants';
|
||||||
|
|
||||||
export async function seedVisibleVerticalSlice(
|
export async function seedVisibleVerticalSlice(
|
||||||
dataSource: DataSource,
|
dataSource: DataSource,
|
||||||
@@ -11,6 +19,9 @@ export async function seedVisibleVerticalSlice(
|
|||||||
const locationRepository = dataSource.getRepository(LocationDefinition);
|
const locationRepository = dataSource.getRepository(LocationDefinition);
|
||||||
const connectionRepository = dataSource.getRepository(LocationConnection);
|
const connectionRepository = dataSource.getRepository(LocationConnection);
|
||||||
const characterRepository = dataSource.getRepository(Character);
|
const characterRepository = dataSource.getRepository(Character);
|
||||||
|
const monsterRepository = dataSource.getRepository(MonsterDefinition);
|
||||||
|
const locationMonsterRepository =
|
||||||
|
dataSource.getRepository(LocationMonster);
|
||||||
|
|
||||||
const locations = [
|
const locations = [
|
||||||
{
|
{
|
||||||
@@ -85,6 +96,77 @@ export async function seedVisibleVerticalSlice(
|
|||||||
['fromLocationId', 'toLocationId'],
|
['fromLocationId', 'toLocationId'],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const monsters = [
|
||||||
|
{
|
||||||
|
id: ASH_RAT_MONSTER_ID,
|
||||||
|
key: 'ash-rat',
|
||||||
|
name: 'Aschenratte',
|
||||||
|
level: 1,
|
||||||
|
maxHp: 45,
|
||||||
|
attack: 5,
|
||||||
|
armor: 0,
|
||||||
|
experienceReward: 8,
|
||||||
|
silverMin: 4,
|
||||||
|
silverMax: 7,
|
||||||
|
artworkPath: '/images/monsters/ash-rat.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: ROAD_BANDIT_MONSTER_ID,
|
||||||
|
key: 'road-bandit',
|
||||||
|
name: 'Straßenräuber',
|
||||||
|
level: 2,
|
||||||
|
maxHp: 75,
|
||||||
|
attack: 9,
|
||||||
|
armor: 5,
|
||||||
|
experienceReward: 16,
|
||||||
|
silverMin: 9,
|
||||||
|
silverMax: 15,
|
||||||
|
artworkPath: '/images/monsters/road-bandit.png',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let ashRatId = ASH_RAT_MONSTER_ID;
|
||||||
|
let roadBanditId = ROAD_BANDIT_MONSTER_ID;
|
||||||
|
|
||||||
|
for (const monster of monsters) {
|
||||||
|
const existingMonster = await monsterRepository.findOneBy({
|
||||||
|
key: monster.key,
|
||||||
|
});
|
||||||
|
const { id, key, ...definition } = monster;
|
||||||
|
const persistedId = existingMonster?.id ?? id;
|
||||||
|
|
||||||
|
if (existingMonster) {
|
||||||
|
await monsterRepository.update(existingMonster.id, definition);
|
||||||
|
} else {
|
||||||
|
await monsterRepository.insert(monster);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'ash-rat') {
|
||||||
|
ashRatId = persistedId;
|
||||||
|
} else {
|
||||||
|
roadBanditId = persistedId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await locationMonsterRepository.upsert(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
locationId: burnedRoadId,
|
||||||
|
monsterId: ashRatId,
|
||||||
|
weight: 70,
|
||||||
|
encounterType: EncounterType.NORMAL,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
locationId: burnedRoadId,
|
||||||
|
monsterId: roadBanditId,
|
||||||
|
weight: 30,
|
||||||
|
encounterType: EncounterType.NORMAL,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['locationId', 'monsterId'],
|
||||||
|
);
|
||||||
|
|
||||||
const existing = await characterRepository.findOneBy({
|
const existing = await characterRepository.findOneBy({
|
||||||
id: DEMO_CHARACTER_ID,
|
id: DEMO_CHARACTER_ID,
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
apps/web/public/images/monsters/ash-rat.png
Normal file
BIN
apps/web/public/images/monsters/ash-rat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
BIN
apps/web/public/images/monsters/road-bandit.png
Normal file
BIN
apps/web/public/images/monsters/road-bandit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Reference in New Issue
Block a user