From 1576e6b71769ded8d67e27645f0a2f460d392100 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 7 Mar 2026 13:36:37 +0000 Subject: [PATCH] feat: add optional positionHint to spawnNPC Co-Authored-By: Claude Opus 4.6 --- server/src/game/__tests__/spawner.test.ts | 48 +++++++++++++++++++++++ server/src/game/spawner.ts | 6 ++- 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 server/src/game/__tests__/spawner.test.ts diff --git a/server/src/game/__tests__/spawner.test.ts b/server/src/game/__tests__/spawner.test.ts new file mode 100644 index 0000000..9c7b092 --- /dev/null +++ b/server/src/game/__tests__/spawner.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { World } from '../../ecs/World.js'; +import { GameMap } from '../../map/GameMap.js'; +import { spawnNPC } from '../spawner.js'; +import type { Position } from '@dflike/shared'; + +describe('spawnNPC', () => { + it('spawns at random position when no hint given', () => { + const world = new World(); + const map = new GameMap(); + const entity = spawnNPC(world, map); + const pos = world.getComponent(entity, 'position')!; + expect(pos).toBeDefined(); + expect(pos.x).toBeGreaterThanOrEqual(0); + expect(pos.y).toBeGreaterThanOrEqual(0); + }); + + it('spawns at hint position when walkable', () => { + const world = new World(); + const map = new GameMap(); + const entity = spawnNPC(world, map, { x: 20, y: 20 }); + const pos = world.getComponent(entity, 'position')!; + expect(pos).toEqual({ x: 20, y: 20 }); + }); + + it('spawns at random position when hint is not walkable', () => { + const world = new World(); + const map = new GameMap(); + map.setObstacle(20, 20); + const entity = spawnNPC(world, map, { x: 20, y: 20 }); + const pos = world.getComponent(entity, 'position')!; + expect(pos).toBeDefined(); + expect(pos.x !== 20 || pos.y !== 20).toBe(true); + }); + + it('creates all required components', () => { + const world = new World(); + const map = new GameMap(); + const entity = spawnNPC(world, map); + expect(world.getComponent(entity, 'position')).toBeDefined(); + expect(world.getComponent(entity, 'needs')).toBeDefined(); + expect(world.getComponent(entity, 'movement')).toBeDefined(); + expect(world.getComponent(entity, 'npcBrain')).toBeDefined(); + expect(world.getComponent(entity, 'appearance')).toBeDefined(); + expect(world.getComponent(entity, 'name')).toBeDefined(); + expect(world.getComponent(entity, 'socialState')).toBeDefined(); + }); +}); diff --git a/server/src/game/spawner.ts b/server/src/game/spawner.ts index 06cbb6a..ef0350f 100644 --- a/server/src/game/spawner.ts +++ b/server/src/game/spawner.ts @@ -4,9 +4,11 @@ import { generateRandomAppearance } from '../spawner/appearanceGenerator.js'; import { generateName } from '../spawner/nameGenerator.js'; import type { EntityId, Position, Needs, Movement, NPCBrain, Appearance, SocialState } from '@dflike/shared'; -export function spawnNPC(world: World, map: GameMap): EntityId { +export function spawnNPC(world: World, map: GameMap, positionHint?: Position): EntityId { const entity = world.createEntity(); - const pos = map.getRandomWalkable(); + const pos = positionHint && map.isWalkable(positionHint.x, positionHint.y) + ? positionHint + : map.getRandomWalkable(); world.addComponent(entity, 'position', pos); world.addComponent(entity, 'needs', {