feat(web): add quest API contracts
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -346,6 +346,7 @@ export type NpcMarker =
|
||||
| 'MERCHANT'
|
||||
| 'EXCHANGE'
|
||||
| 'QUEST_AVAILABLE'
|
||||
| 'QUEST_IN_PROGRESS'
|
||||
| 'QUEST_TURN_IN';
|
||||
|
||||
export interface NpcSummary {
|
||||
@@ -468,3 +469,55 @@ export interface ShopPurchaseResult {
|
||||
silverSpent: number;
|
||||
silverBalance: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quest transport types (Playable Slice 0.9).
|
||||
*
|
||||
* Mirrors the API's quest DTOs field for field. Nothing here is computed
|
||||
* client-side: which step is current, how far along it is and whether it is
|
||||
* blocked are all decided by the server (AGENTS.md §22).
|
||||
*/
|
||||
export type QuestStatus = 'AVAILABLE' | 'ACTIVE' | 'COMPLETED';
|
||||
export type QuestObjectiveType = 'COLLECT_ITEM' | 'TALK_TO_NPC';
|
||||
|
||||
export interface QuestObjectiveView {
|
||||
key: string;
|
||||
description: string;
|
||||
type: QuestObjectiveType;
|
||||
targetKey: string;
|
||||
required: number;
|
||||
current: number;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export interface QuestView {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: QuestStatus;
|
||||
objectives: QuestObjectiveView[];
|
||||
currentObjectiveKey: string | null;
|
||||
/** Why the current step cannot progress, when it cannot (slice §4, §12). */
|
||||
hint: string | null;
|
||||
}
|
||||
|
||||
export interface GrantedLootBag {
|
||||
key: string;
|
||||
name: string;
|
||||
lootCategory: string;
|
||||
capacity: number;
|
||||
}
|
||||
|
||||
export interface QuestInteractionResult {
|
||||
quest: QuestView;
|
||||
/** What the NPC says for the step just performed (slice §5, §6, §8). */
|
||||
npcLine: string | null;
|
||||
/** Non-null only on the step that hands a bag over (slice §12 Bag UI). */
|
||||
grantedBag: GrantedLootBag | null;
|
||||
consumedItems: Array<{ itemKey: string; quantity: number }>;
|
||||
rewards: {
|
||||
factionKey: string | null;
|
||||
reputation: number;
|
||||
silver: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -156,4 +156,47 @@ describe('GameApiService', () => {
|
||||
});
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('reads the quest log', () => {
|
||||
service.getQuests().subscribe();
|
||||
|
||||
const req = http.expectOne('/api/quests');
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush([]);
|
||||
});
|
||||
|
||||
it('accepts a quest without sending a body', () => {
|
||||
service
|
||||
.acceptQuest('south-gate-warden', 'trouble-beyond-the-gate')
|
||||
.subscribe();
|
||||
|
||||
const req = http.expectOne(
|
||||
'/api/npcs/south-gate-warden/quests/trouble-beyond-the-gate/accept',
|
||||
);
|
||||
expect(req.request.method).toBe('POST');
|
||||
// The server decides where the quest starts; the client only names it.
|
||||
expect(req.request.body).toEqual({});
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('advances a quest step without naming the step', () => {
|
||||
service
|
||||
.advanceQuest('borin-quartermaster', 'trouble-beyond-the-gate')
|
||||
.subscribe();
|
||||
|
||||
const req = http.expectOne(
|
||||
'/api/npcs/borin-quartermaster/quests/trouble-beyond-the-gate/advance',
|
||||
);
|
||||
expect(req.request.method).toBe('POST');
|
||||
expect(req.request.body).toEqual({});
|
||||
req.flush({});
|
||||
});
|
||||
|
||||
it('encodes npc and quest keys into the path', () => {
|
||||
service.advanceQuest('odd/key', 'quest key').subscribe();
|
||||
|
||||
const req = http.expectOne('/api/npcs/odd%2Fkey/quests/quest%20key/advance');
|
||||
expect(req.request.method).toBe('POST');
|
||||
req.flush({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
ExchangeView,
|
||||
NpcInteraction,
|
||||
NpcSummary,
|
||||
QuestInteractionResult,
|
||||
QuestView,
|
||||
ReputationEntry,
|
||||
ShopPurchaseResult,
|
||||
ShopView,
|
||||
@@ -128,6 +130,40 @@ export class GameApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getQuests(): Observable<QuestView[]> {
|
||||
return this.http.get<QuestView[]>('/api/quests');
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a quest on. Only the two keys travel: the server decides whether this
|
||||
* person offers it and where the quest starts (slice §10).
|
||||
*/
|
||||
acceptQuest(
|
||||
npcKey: string,
|
||||
questKey: string,
|
||||
): Observable<QuestInteractionResult> {
|
||||
return this.http.post<QuestInteractionResult>(
|
||||
`/api/npcs/${encodeURIComponent(npcKey)}/quests/${encodeURIComponent(questKey)}/accept`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs whatever step this NPC is currently owed.
|
||||
*
|
||||
* The client never names the step. Which one is current, what it grants and
|
||||
* what it consumes are the server's to decide (AGENTS.md §5).
|
||||
*/
|
||||
advanceQuest(
|
||||
npcKey: string,
|
||||
questKey: string,
|
||||
): Observable<QuestInteractionResult> {
|
||||
return this.http.post<QuestInteractionResult>(
|
||||
`/api/npcs/${encodeURIComponent(npcKey)}/quests/${encodeURIComponent(questKey)}/advance`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
getShop(merchantKey: string): Observable<ShopView> {
|
||||
return this.http.get<ShopView>(
|
||||
`/api/merchants/${encodeURIComponent(merchantKey)}/shop`,
|
||||
|
||||
Reference in New Issue
Block a user