Files
whale-town-end-v2/src/business/world_npc/world_npc.service.spec.ts

83 lines
3.5 KiB
TypeScript

import { WorldNpcPlanner, fallbackResearcherPlan, townDate } from './world_npc.planner';
import { WorldNpcService } from './world_npc.service';
import { WorldNpcDailyPlan } from './world_npc.types';
import { findWorldRoute } from './world_npc.world';
describe('WorldNpcService', () => {
const previousPersistence = process.env.WORLD_NPC_PERSISTENCE;
let service: WorldNpcService;
beforeEach(() => {
process.env.WORLD_NPC_PERSISTENCE = 'off';
const planner = {
createDailyPlan: async (_definition: unknown, _context: unknown, now: number) => fallbackResearcherPlan(now),
} as unknown as WorldNpcPlanner;
service = new WorldNpcService(planner);
});
afterAll(() => {
process.env.WORLD_NPC_PERSISTENCE = previousPersistence;
});
it('returns the versioned NPC snapshot only on its current map', () => {
const snapshot = service.getMapSnapshot('whale_port');
expect(snapshot.npcs[0]).toEqual(expect.objectContaining({
npcId: 'npc_whale_researcher', name: '鲸小研', dailyGoal: expect.any(String), planSource: 'fallback',
}));
expect(service.getMapSnapshot('work_zone').npcs).toEqual([]);
});
it('builds a semantic cross-map route instead of raw coordinate patrol', () => {
const route = findWorldRoute('square_dock_research', 'cafe_research_table');
expect(route[0]).toBe('square_dock_research');
expect(route[route.length - 1]).toBe('cafe_research_table');
expect(route).toEqual(expect.arrayContaining([
'square_work_gate', 'work_square_gate', 'work_cafe_gate', 'cafe_entrance',
]));
});
it('executes todays activity route continuously without teleporting to the initial point', async () => {
const now = Date.now();
const plan: WorldNpcDailyPlan = {
date: townDate(now), goal: '去咖啡馆收集研究问题', source: 'agent',
activities: [{
id: 'cafe_visit', title: '咖啡馆访谈', intention: '前往咖啡馆访谈',
locationId: 'cafe_research_table', startMinute: 0, endMinute: 1440,
activityKind: 'socialize', dialogue: '你最近在研究什么?',
}],
};
service.replacePlanForTesting(plan);
let clock = now;
const observedLocations = ['square_dock_research'];
let sawTransition = false;
for (let index = 0; index < 24; index += 1) {
const result = await service.tick(clock);
const active = service.getRuntimeForTesting().activeAction;
expect(active).toBeDefined();
if (active?.kind === 'transition') sawTransition = true;
clock = active!.completesAt + 1;
await service.tick(clock);
observedLocations.push(service.getRuntimeForTesting().locationId);
if (service.getRuntimeForTesting().locationId === 'cafe_research_table') break;
}
expect(sawTransition).toBe(true);
expect(observedLocations).toContain('work_square_gate');
expect(observedLocations[observedLocations.length - 1]).toBe('cafe_research_table');
expect(observedLocations.slice(1)).not.toContain('square_dock_research');
expect(service.getMapSnapshot('whale_cafe', clock).npcs[0]).toEqual(expect.objectContaining({
state: 'talking', publicIntention: '前往咖啡馆访谈',
}));
});
it('uses deterministic schedules that cover the full town day', () => {
const plan = fallbackResearcherPlan(Date.now());
expect(plan.activities[0].startMinute).toBe(0);
expect(plan.activities[plan.activities.length - 1].endMinute).toBe(1440);
plan.activities.slice(1).forEach((item, index) => {
expect(plan.activities[index].endMinute).toBe(item.startMinute);
});
});
});