feat(npcs): derive quest markers for the local view
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ export class NpcController {
|
|||||||
getNpcsAtLocation(
|
getNpcsAtLocation(
|
||||||
@Param('locationId') locationId: string,
|
@Param('locationId') locationId: string,
|
||||||
): Promise<NpcSummaryDto[]> {
|
): Promise<NpcSummaryDto[]> {
|
||||||
return this.npcService.getNpcsAtLocation(locationId);
|
return this.npcService.getNpcsAtLocation(DEMO_CHARACTER_ID, locationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import { NpcShop } from '../shops/entities/npc-shop.entity';
|
|||||||
import { CharacterNpcState } from './entities/character-npc-state.entity';
|
import { CharacterNpcState } from './entities/character-npc-state.entity';
|
||||||
import { DialogueNode } from './entities/dialogue-node.entity';
|
import { DialogueNode } from './entities/dialogue-node.entity';
|
||||||
import { NpcDefinition } from './entities/npc-definition.entity';
|
import { NpcDefinition } from './entities/npc-definition.entity';
|
||||||
|
import {
|
||||||
|
NpcQuestState,
|
||||||
|
QuestProgressService,
|
||||||
|
} from '../quests/quest-progress.service';
|
||||||
|
import { NpcQuestRole, QuestObjectiveType } from '../quests/quest.types';
|
||||||
import { NpcService } from './npc.service';
|
import { NpcService } from './npc.service';
|
||||||
import { NpcCapability } from './npc.types';
|
import { NpcCapability } from './npc.types';
|
||||||
|
|
||||||
@@ -24,6 +29,12 @@ interface Fixture {
|
|||||||
hasExchangeProfile?: boolean;
|
hasExchangeProfile?: boolean;
|
||||||
exchangeRuleCount?: number;
|
exchangeRuleCount?: number;
|
||||||
existingState?: Record<string, unknown> | null;
|
existingState?: Record<string, unknown> | null;
|
||||||
|
questSituation?:
|
||||||
|
| 'offers'
|
||||||
|
| 'offers-and-receives'
|
||||||
|
| 'step-is-here'
|
||||||
|
| 'step-is-elsewhere'
|
||||||
|
| 'completed';
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWorld(fixture: Fixture = {}) {
|
function createWorld(fixture: Fixture = {}) {
|
||||||
@@ -124,7 +135,79 @@ function createWorld(fixture: Fixture = {}) {
|
|||||||
}),
|
}),
|
||||||
} as unknown as GameConditionService;
|
} as unknown as GameConditionService;
|
||||||
|
|
||||||
return { service: new NpcService(dataSource, conditions), savedStates, npc };
|
const questProgress = {
|
||||||
|
getNpcQuestStates: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(questStatesFor(fixture)),
|
||||||
|
} as unknown as QuestProgressService;
|
||||||
|
|
||||||
|
return {
|
||||||
|
service: new NpcService(dataSource, conditions, questProgress),
|
||||||
|
savedStates,
|
||||||
|
npc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The quest situations `QuestProgressService` can report for one NPC.
|
||||||
|
*
|
||||||
|
* Kept as a fixture switch rather than a hand-built state per test: what
|
||||||
|
* `NpcService` does with a quest state is a small precedence decision, and the
|
||||||
|
* derivation itself is already covered by `quest-progress.service.spec.ts`.
|
||||||
|
*/
|
||||||
|
function questStatesFor(fixture: Fixture): NpcQuestState[] {
|
||||||
|
const quest = { id: 'quest-1', key: 'trouble-beyond-the-gate' };
|
||||||
|
|
||||||
|
const build = (
|
||||||
|
status: 'AVAILABLE' | 'ACTIVE' | 'COMPLETED',
|
||||||
|
currentIndex: number | null,
|
||||||
|
roles: NpcQuestRole[],
|
||||||
|
): NpcQuestState =>
|
||||||
|
({
|
||||||
|
state: {
|
||||||
|
quest,
|
||||||
|
status,
|
||||||
|
currentIndex,
|
||||||
|
row: null,
|
||||||
|
objectives: [
|
||||||
|
{
|
||||||
|
objective: {
|
||||||
|
key: 'collect',
|
||||||
|
type: QuestObjectiveType.COLLECT_ITEM,
|
||||||
|
targetKey: 'ash-pelt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objective: {
|
||||||
|
key: 'talk',
|
||||||
|
type: QuestObjectiveType.TALK_TO_NPC,
|
||||||
|
targetKey: 'borin-quartermaster',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
roles,
|
||||||
|
}) as unknown as NpcQuestState;
|
||||||
|
|
||||||
|
switch (fixture.questSituation) {
|
||||||
|
case 'offers':
|
||||||
|
return [build('AVAILABLE', null, [NpcQuestRole.OFFER])];
|
||||||
|
case 'offers-and-receives':
|
||||||
|
// One person with something to start *and* a step waiting: exactly what
|
||||||
|
// the South Gate Warden looks like mid-chain (NPC spec §14).
|
||||||
|
return [
|
||||||
|
build('AVAILABLE', null, [NpcQuestRole.OFFER]),
|
||||||
|
build('ACTIVE', 1, [NpcQuestRole.TURN_IN]),
|
||||||
|
];
|
||||||
|
case 'step-is-here':
|
||||||
|
return [build('ACTIVE', 1, [NpcQuestRole.TURN_IN])];
|
||||||
|
case 'step-is-elsewhere':
|
||||||
|
return [build('ACTIVE', 0, [NpcQuestRole.PROGRESS])];
|
||||||
|
case 'completed':
|
||||||
|
return [build('COMPLETED', null, [NpcQuestRole.OFFER])];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function node(overrides: Partial<DialogueNode>): Partial<DialogueNode> {
|
function node(overrides: Partial<DialogueNode>): Partial<DialogueNode> {
|
||||||
@@ -146,7 +229,10 @@ describe('NpcService', () => {
|
|||||||
it('lists the people at a location with their markers', async () => {
|
it('lists the people at a location with their markers', async () => {
|
||||||
const world = createWorld();
|
const world = createWorld();
|
||||||
|
|
||||||
const npcs = await world.service.getNpcsAtLocation(LOCATION_ID);
|
const npcs = await world.service.getNpcsAtLocation(
|
||||||
|
CHARACTER_ID,
|
||||||
|
LOCATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
expect(npcs).toHaveLength(1);
|
expect(npcs).toHaveLength(1);
|
||||||
expect(npcs[0]).toMatchObject({
|
expect(npcs[0]).toMatchObject({
|
||||||
@@ -156,6 +242,109 @@ describe('NpcService', () => {
|
|||||||
expect(npcs[0].markers).toEqual(['MERCHANT', 'EXCHANGE']);
|
expect(npcs[0].markers).toEqual(['MERCHANT', 'EXCHANGE']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks an NPC that has a quest to give', async () => {
|
||||||
|
const world = createWorld({ questSituation: 'offers' });
|
||||||
|
|
||||||
|
const [borin] = await world.service.getNpcsAtLocation(
|
||||||
|
CHARACTER_ID,
|
||||||
|
LOCATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(borin.markers).toContain('QUEST_AVAILABLE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks the NPC the current step points at', async () => {
|
||||||
|
const world = createWorld({ questSituation: 'step-is-here' });
|
||||||
|
|
||||||
|
const [borin] = await world.service.getNpcsAtLocation(
|
||||||
|
CHARACTER_ID,
|
||||||
|
LOCATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(borin.markers).toContain('QUEST_TURN_IN');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks an assigned NPC whose step is elsewhere as in progress', async () => {
|
||||||
|
const world = createWorld({ questSituation: 'step-is-elsewhere' });
|
||||||
|
|
||||||
|
const [borin] = await world.service.getNpcsAtLocation(
|
||||||
|
CHARACTER_ID,
|
||||||
|
LOCATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(borin.markers).toContain('QUEST_IN_PROGRESS');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers the waiting step over a quest that is merely available', async () => {
|
||||||
|
// Otherwise the warden mid-chain shows two badges and neither tells the
|
||||||
|
// player where to go.
|
||||||
|
const world = createWorld({ questSituation: 'offers-and-receives' });
|
||||||
|
|
||||||
|
const [borin] = await world.service.getNpcsAtLocation(
|
||||||
|
CHARACTER_ID,
|
||||||
|
LOCATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(borin.markers).toContain('QUEST_TURN_IN');
|
||||||
|
expect(borin.markers).not.toContain('QUEST_AVAILABLE');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits at most one quest marker', async () => {
|
||||||
|
const world = createWorld({ questSituation: 'offers-and-receives' });
|
||||||
|
|
||||||
|
const [borin] = await world.service.getNpcsAtLocation(
|
||||||
|
CHARACTER_ID,
|
||||||
|
LOCATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
borin.markers.filter((marker) => marker.startsWith('QUEST_')),
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits no quest marker for an NPC with no assignment', async () => {
|
||||||
|
const world = createWorld();
|
||||||
|
|
||||||
|
const [borin] = await world.service.getNpcsAtLocation(
|
||||||
|
CHARACTER_ID,
|
||||||
|
LOCATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
borin.markers.some((marker) => marker.startsWith('QUEST_')),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits no quest marker once the quest is finished', async () => {
|
||||||
|
const world = createWorld({ questSituation: 'completed' });
|
||||||
|
|
||||||
|
const [borin] = await world.service.getNpcsAtLocation(
|
||||||
|
CHARACTER_ID,
|
||||||
|
LOCATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
borin.markers.some((marker) => marker.startsWith('QUEST_')),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('offers a quests action exactly when a quest marker applies', async () => {
|
||||||
|
const withQuest = await createWorld({
|
||||||
|
questSituation: 'offers',
|
||||||
|
}).service.getInteraction(CHARACTER_ID, 'borin-quartermaster');
|
||||||
|
const withoutQuest = await createWorld().service.getInteraction(
|
||||||
|
CHARACTER_ID,
|
||||||
|
'borin-quartermaster',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(withQuest.availableActions.map((action) => action.type)).toContain(
|
||||||
|
'VIEW_QUESTS',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
withoutQuest.availableActions.map((action) => action.type),
|
||||||
|
).not.toContain('VIEW_QUESTS');
|
||||||
|
});
|
||||||
|
|
||||||
it('refuses an NPC the character has not travelled to', async () => {
|
it('refuses an NPC the character has not travelled to', async () => {
|
||||||
// Reachability comes from the character's own location, never the request.
|
// Reachability comes from the character's own location, never the request.
|
||||||
const world = createWorld({ characterLocationId: 'somewhere-else' });
|
const world = createWorld({ characterLocationId: 'somewhere-else' });
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { Character } from '../characters/entities/character.entity';
|
|||||||
import { GameConditionService } from '../conditions/game-condition.service';
|
import { GameConditionService } from '../conditions/game-condition.service';
|
||||||
import { ExchangeRule } from '../exchanges/entities/exchange-rule.entity';
|
import { ExchangeRule } from '../exchanges/entities/exchange-rule.entity';
|
||||||
import { NpcExchangeProfile } from '../exchanges/entities/npc-exchange-profile.entity';
|
import { NpcExchangeProfile } from '../exchanges/entities/npc-exchange-profile.entity';
|
||||||
|
import { QuestProgressService } from '../quests/quest-progress.service';
|
||||||
|
import { NpcQuestRole, QuestObjectiveType } from '../quests/quest.types';
|
||||||
import { NpcShop } from '../shops/entities/npc-shop.entity';
|
import { NpcShop } from '../shops/entities/npc-shop.entity';
|
||||||
import { CharacterNpcState } from './entities/character-npc-state.entity';
|
import { CharacterNpcState } from './entities/character-npc-state.entity';
|
||||||
import { DialogueNode } from './entities/dialogue-node.entity';
|
import { DialogueNode } from './entities/dialogue-node.entity';
|
||||||
@@ -30,10 +32,19 @@ export class NpcService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
private readonly conditions: GameConditionService,
|
private readonly conditions: GameConditionService,
|
||||||
|
private readonly questProgress: QuestProgressService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Every enabled NPC at a location, for the local view (spec §22, §24). */
|
/**
|
||||||
async getNpcsAtLocation(locationId: string): Promise<NpcSummaryDto[]> {
|
* Every enabled NPC at a location, for the local view (spec §22, §24).
|
||||||
|
*
|
||||||
|
* Takes the character because markers are per-player: whether the warden has
|
||||||
|
* something to ask depends on what this character has already done.
|
||||||
|
*/
|
||||||
|
async getNpcsAtLocation(
|
||||||
|
characterId: string,
|
||||||
|
locationId: string,
|
||||||
|
): Promise<NpcSummaryDto[]> {
|
||||||
const npcs = await this.dataSource.getRepository(NpcDefinition).find({
|
const npcs = await this.dataSource.getRepository(NpcDefinition).find({
|
||||||
where: { locationId, enabled: true },
|
where: { locationId, enabled: true },
|
||||||
order: { key: 'ASC' },
|
order: { key: 'ASC' },
|
||||||
@@ -47,7 +58,7 @@ export class NpcService {
|
|||||||
name: npc.name,
|
name: npc.name,
|
||||||
title: npc.title,
|
title: npc.title,
|
||||||
portraitPath: npc.portraitPath,
|
portraitPath: npc.portraitPath,
|
||||||
markers: await this.resolveMarkers(npc),
|
markers: await this.resolveMarkers(characterId, npc),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return summaries;
|
return summaries;
|
||||||
@@ -84,7 +95,7 @@ export class NpcService {
|
|||||||
capabilities: npc.capabilities ?? [],
|
capabilities: npc.capabilities ?? [],
|
||||||
},
|
},
|
||||||
dialogue,
|
dialogue,
|
||||||
availableActions: await this.resolveActions(npc),
|
availableActions: await this.resolveActions(characterId, npc),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +176,10 @@ export class NpcService {
|
|||||||
* declared capability list -- an NPC that claims MERCHANT but has no
|
* declared capability list -- an NPC that claims MERCHANT but has no
|
||||||
* enabled shop offers no shop button (spec §5).
|
* enabled shop offers no shop button (spec §5).
|
||||||
*/
|
*/
|
||||||
private async resolveActions(npc: NpcDefinition): Promise<NpcActionDto[]> {
|
private async resolveActions(
|
||||||
|
characterId: string,
|
||||||
|
npc: NpcDefinition,
|
||||||
|
): Promise<NpcActionDto[]> {
|
||||||
const actions: NpcActionDto[] = [
|
const actions: NpcActionDto[] = [
|
||||||
{ type: 'TALK', label: 'Talk', key: null },
|
{ type: 'TALK', label: 'Talk', key: null },
|
||||||
];
|
];
|
||||||
@@ -186,11 +200,21 @@ export class NpcService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Offered on the same rule as the marker: if this person has nothing to
|
||||||
|
// say about a quest, the screen shows one fewer button rather than an
|
||||||
|
// empty panel.
|
||||||
|
if ((await this.resolveQuestMarker(characterId, npc)) !== null) {
|
||||||
|
actions.push({ type: 'VIEW_QUESTS', label: 'Quests', key: null });
|
||||||
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Markers for the local view. Only backed interactions get one (spec §24). */
|
/** Markers for the local view. Only backed interactions get one (spec §24). */
|
||||||
private async resolveMarkers(npc: NpcDefinition): Promise<NpcMarker[]> {
|
private async resolveMarkers(
|
||||||
|
characterId: string,
|
||||||
|
npc: NpcDefinition,
|
||||||
|
): Promise<NpcMarker[]> {
|
||||||
const markers: NpcMarker[] = [];
|
const markers: NpcMarker[] = [];
|
||||||
|
|
||||||
const shop = await this.dataSource
|
const shop = await this.dataSource
|
||||||
@@ -204,9 +228,74 @@ export class NpcService {
|
|||||||
markers.push('EXCHANGE');
|
markers.push('EXCHANGE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const quest = await this.resolveQuestMarker(characterId, npc);
|
||||||
|
if (quest) {
|
||||||
|
markers.push(quest);
|
||||||
|
}
|
||||||
|
|
||||||
return markers;
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one quest marker this NPC earns right now, or null (Slice 0.9 §12).
|
||||||
|
*
|
||||||
|
* Precedence is load-bearing rather than cosmetic: the South Gate Warden both
|
||||||
|
* offers this quest and receives it, so without an order they would show two
|
||||||
|
* badges at once and the player would learn nothing from either. "Your next
|
||||||
|
* step is here" beats "something starts here" beats "you are on a quest this
|
||||||
|
* person is part of".
|
||||||
|
*
|
||||||
|
* Read through `QuestProgressService` rather than by querying the quest
|
||||||
|
* tables directly, so "which step am I on" is answered in exactly one place
|
||||||
|
* -- including the capacity-blocked case, which is the whole reason the
|
||||||
|
* warden lights up while the player is stuck at 1 / 5.
|
||||||
|
*/
|
||||||
|
private async resolveQuestMarker(
|
||||||
|
characterId: string,
|
||||||
|
npc: NpcDefinition,
|
||||||
|
): Promise<NpcMarker | null> {
|
||||||
|
const entries = await this.questProgress.getNpcQuestStates(
|
||||||
|
characterId,
|
||||||
|
npc.id,
|
||||||
|
);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let available = false;
|
||||||
|
let inProgress = false;
|
||||||
|
|
||||||
|
for (const { state, roles } of entries) {
|
||||||
|
if (state.status === 'ACTIVE') {
|
||||||
|
const currentIndex = state.currentIndex;
|
||||||
|
const step =
|
||||||
|
currentIndex === null
|
||||||
|
? undefined
|
||||||
|
: state.objectives[currentIndex]?.objective;
|
||||||
|
|
||||||
|
// Matched on the NPC's business key, because that is what a talk step
|
||||||
|
// names -- content should not have to know generated ids.
|
||||||
|
if (
|
||||||
|
step?.type === QuestObjectiveType.TALK_TO_NPC &&
|
||||||
|
step.targetKey === npc.key
|
||||||
|
) {
|
||||||
|
return 'QUEST_TURN_IN';
|
||||||
|
}
|
||||||
|
inProgress = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.status === 'AVAILABLE' && roles.includes(NpcQuestRole.OFFER)) {
|
||||||
|
available = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (available) {
|
||||||
|
return 'QUEST_AVAILABLE';
|
||||||
|
}
|
||||||
|
return inProgress ? 'QUEST_IN_PROGRESS' : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An enabled exchange profile that actually has an enabled rule.
|
* An enabled exchange profile that actually has an enabled rule.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -27,10 +27,13 @@ export enum NpcCapability {
|
|||||||
/**
|
/**
|
||||||
* Actions a dialogue node may trigger (spec §13).
|
* Actions a dialogue node may trigger (spec §13).
|
||||||
*
|
*
|
||||||
* START_QUEST and COMPLETE_QUEST are part of the V1 vocabulary but have no
|
* START_QUEST and COMPLETE_QUEST stay inert even now that quests exist
|
||||||
* quest system behind them yet (Slice 0.9). They are listed so content and
|
* (Slice 0.9 decision D4). Carrying them out would need a dialogue-response
|
||||||
* the stored enum do not need rewriting later; `NpcService` refuses to offer
|
* endpoint and an action executor, and Slice 0.9 §10 rules out building a
|
||||||
* an action it cannot actually carry out.
|
* branching narrative engine for one tutorial chain. Quest steps run through
|
||||||
|
* `POST /api/npcs/:npcKey/quests/:questKey/{accept,advance}` instead, and the
|
||||||
|
* line for a step is content on the objective. They stay listed so the stored
|
||||||
|
* enum does not need rewriting if that changes.
|
||||||
*/
|
*/
|
||||||
export enum DialogueActionType {
|
export enum DialogueActionType {
|
||||||
OPEN_SHOP = 'OPEN_SHOP',
|
OPEN_SHOP = 'OPEN_SHOP',
|
||||||
@@ -76,13 +79,23 @@ export interface NpcSummaryDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presentation markers (spec §24).
|
* Presentation markers (spec §24, Slice 0.9 §12).
|
||||||
*
|
*
|
||||||
* Only markers backed by a real, currently available interaction are emitted.
|
* Only markers backed by a real, currently available interaction are emitted.
|
||||||
* The view does not turn every capability into a permanent symbol.
|
* The view does not turn every capability into a permanent symbol.
|
||||||
|
*
|
||||||
|
* The three quest markers answer three different questions, in this order of
|
||||||
|
* usefulness: `QUEST_TURN_IN` means "your next step is here", `QUEST_AVAILABLE`
|
||||||
|
* means "something starts here", and `QUEST_IN_PROGRESS` means "this person is
|
||||||
|
* part of a quest you are on, but not right now". At most one is emitted per
|
||||||
|
* NPC -- a row of badges on one portrait tells the player nothing.
|
||||||
*/
|
*/
|
||||||
export type NpcMarker =
|
export type NpcMarker =
|
||||||
'MERCHANT' | 'EXCHANGE' | 'QUEST_AVAILABLE' | 'QUEST_TURN_IN';
|
| 'MERCHANT'
|
||||||
|
| 'EXCHANGE'
|
||||||
|
| 'QUEST_AVAILABLE'
|
||||||
|
| 'QUEST_IN_PROGRESS'
|
||||||
|
| 'QUEST_TURN_IN';
|
||||||
|
|
||||||
export interface DialogueResponseDto {
|
export interface DialogueResponseDto {
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Character } from '../characters/entities/character.entity';
|
|||||||
import { ConditionsModule } from '../conditions/conditions.module';
|
import { ConditionsModule } from '../conditions/conditions.module';
|
||||||
import { ExchangeRule } from '../exchanges/entities/exchange-rule.entity';
|
import { ExchangeRule } from '../exchanges/entities/exchange-rule.entity';
|
||||||
import { NpcExchangeProfile } from '../exchanges/entities/npc-exchange-profile.entity';
|
import { NpcExchangeProfile } from '../exchanges/entities/npc-exchange-profile.entity';
|
||||||
|
import { QuestProgressModule } from '../quests/quest-progress.module';
|
||||||
import { NpcShop } from '../shops/entities/npc-shop.entity';
|
import { NpcShop } from '../shops/entities/npc-shop.entity';
|
||||||
import { CharacterNpcState } from './entities/character-npc-state.entity';
|
import { CharacterNpcState } from './entities/character-npc-state.entity';
|
||||||
import { DialogueNode } from './entities/dialogue-node.entity';
|
import { DialogueNode } from './entities/dialogue-node.entity';
|
||||||
@@ -31,6 +32,9 @@ import { NpcService } from './npc.service';
|
|||||||
ExchangeRule,
|
ExchangeRule,
|
||||||
]),
|
]),
|
||||||
ConditionsModule,
|
ConditionsModule,
|
||||||
|
// The read-only half of the quest system, which exists as its own module
|
||||||
|
// precisely so this import does not close a cycle with `QuestsModule`.
|
||||||
|
QuestProgressModule,
|
||||||
],
|
],
|
||||||
controllers: [NpcController],
|
controllers: [NpcController],
|
||||||
providers: [NpcService],
|
providers: [NpcService],
|
||||||
|
|||||||
Reference in New Issue
Block a user