You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
698 lines
20 KiB
TypeScript
698 lines
20 KiB
TypeScript
import { API_BASE } from '../shared/constants';
|
|
import { getTelegramInitData } from '../shared/telegram';
|
|
import type { ServerActiveBuffRow, ServerActiveDebuffRow } from './buffMap';
|
|
|
|
/**
|
|
* REST API client for /api/v1/* endpoints.
|
|
*
|
|
* All requests include Telegram initData for authentication.
|
|
* Responses are typed via generics.
|
|
*/
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
public status: number,
|
|
public statusText: string,
|
|
public body: string,
|
|
) {
|
|
super(`API Error ${status}: ${statusText}`);
|
|
this.name = 'ApiError';
|
|
}
|
|
}
|
|
|
|
/** Build headers with Telegram auth */
|
|
function buildHeaders(extra?: Record<string, string>): Record<string, string> {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...extra,
|
|
};
|
|
|
|
const initData = getTelegramInitData();
|
|
if (initData) {
|
|
headers['X-Telegram-Init-Data'] = initData;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
/** Parse response or throw ApiError */
|
|
async function handleResponse<T>(res: Response): Promise<T> {
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '');
|
|
throw new ApiError(res.status, res.statusText, body);
|
|
}
|
|
|
|
// 204 No Content
|
|
if (res.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
return res.json() as Promise<T>;
|
|
}
|
|
|
|
/** GET request */
|
|
export async function apiGet<T>(path: string): Promise<T> {
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method: 'GET',
|
|
headers: buildHeaders(),
|
|
});
|
|
return handleResponse<T>(res);
|
|
}
|
|
|
|
/** POST request */
|
|
export async function apiPost<T>(path: string, body?: unknown): Promise<T> {
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method: 'POST',
|
|
headers: buildHeaders(),
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
});
|
|
return handleResponse<T>(res);
|
|
}
|
|
|
|
/** PUT request */
|
|
export async function apiPut<T>(path: string, body?: unknown): Promise<T> {
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method: 'PUT',
|
|
headers: buildHeaders(),
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
});
|
|
return handleResponse<T>(res);
|
|
}
|
|
|
|
/** DELETE request */
|
|
export async function apiDelete<T>(path: string): Promise<T> {
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
method: 'DELETE',
|
|
headers: buildHeaders(),
|
|
});
|
|
return handleResponse<T>(res);
|
|
}
|
|
|
|
// ---- Typed API Methods ----
|
|
|
|
export interface HeroResponse {
|
|
id: number;
|
|
telegramId: number;
|
|
name: string;
|
|
hp: number;
|
|
maxHp: number;
|
|
attack: number;
|
|
defense: number;
|
|
speed: number;
|
|
attackSpeed?: number;
|
|
attackPower?: number;
|
|
defensePower?: number;
|
|
strength: number;
|
|
constitution: number;
|
|
agility: number;
|
|
luck: number;
|
|
state: string;
|
|
restKind?: string;
|
|
excursionPhase?: string;
|
|
weaponId: number;
|
|
armorId: number;
|
|
weapon: WeaponResponse | null;
|
|
armor: ArmorResponse | null;
|
|
gold: number;
|
|
xp: number;
|
|
xpToNext?: number;
|
|
level: number;
|
|
reviveCount?: number;
|
|
subscriptionActive?: boolean;
|
|
buffFreeChargesRemaining?: number;
|
|
buffQuotaPeriodEnd?: string;
|
|
buffCharges?: Record<string, { remaining: number; periodEnd: string | null }>;
|
|
potions?: number;
|
|
positionX?: number;
|
|
positionY?: number;
|
|
moveSpeed?: number;
|
|
buffs?: ServerActiveBuffRow[];
|
|
debuffs?: ServerActiveDebuffRow[];
|
|
/** Extended equipment slots (§6.3) keyed by slot name */
|
|
equipment?: Record<string, {
|
|
id: number;
|
|
slot: string;
|
|
formId?: string;
|
|
name: string;
|
|
rarity: string;
|
|
ilvl?: number;
|
|
primaryStat?: number;
|
|
statType?: string;
|
|
}>;
|
|
/** Same slot data as `equipment`; WebSocket `hero_state` from Go uses `gear` */
|
|
gear?: HeroResponse['equipment'];
|
|
/** Unequipped gear (backpack), max 40 — mirrors Go `Hero.Inventory` */
|
|
inventory?: Array<{
|
|
id: number;
|
|
slot: string;
|
|
formId?: string;
|
|
name: string;
|
|
rarity: string;
|
|
ilvl?: number;
|
|
primaryStat?: number;
|
|
statType?: string;
|
|
}>;
|
|
}
|
|
|
|
export interface AuthResponse {
|
|
token: string;
|
|
/** 0 if the player has not created a hero yet. */
|
|
heroId: number;
|
|
}
|
|
|
|
/** Authenticate via Telegram initData, returns session info */
|
|
export async function authenticate(): Promise<AuthResponse> {
|
|
return apiPost<AuthResponse>('/auth/telegram');
|
|
}
|
|
|
|
/** Get current hero state */
|
|
export async function getHero(telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiGet<HeroResponse>(`/hero${query}`);
|
|
}
|
|
|
|
export interface OfflineReport {
|
|
offlineSeconds: number;
|
|
monstersKilled: number;
|
|
xpGained: number;
|
|
goldGained: number;
|
|
levelsGained: number;
|
|
message: string;
|
|
}
|
|
|
|
export interface InitHeroResponse {
|
|
/** Null until the player submits a valid name (no DB row until then). */
|
|
hero: HeroResponse | null;
|
|
offlineReport: OfflineReport | null;
|
|
mapRef: MapRefResponse;
|
|
needsName?: boolean;
|
|
}
|
|
|
|
/** Initialize or retrieve a hero from the backend (creates on first call) */
|
|
export async function initHero(telegramId?: number): Promise<InitHeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiGet<InitHeroResponse>(`/hero/init${query}`);
|
|
}
|
|
|
|
/** Set the hero's display name (first time only). Returns updated hero on success. */
|
|
export async function setHeroName(name: string, telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/hero/name${query}`, { name });
|
|
}
|
|
|
|
export interface MapRefResponse {
|
|
mapId: string;
|
|
mapVersion: string;
|
|
etag: string;
|
|
biome: string;
|
|
recommendedLevelMin: number;
|
|
recommendedLevelMax: number;
|
|
}
|
|
|
|
export interface MapTileResponse {
|
|
x: number;
|
|
y: number;
|
|
terrain: string;
|
|
}
|
|
|
|
export interface MapObjectResponse {
|
|
id: string;
|
|
type: string;
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export interface SpawnPointResponse {
|
|
id: string;
|
|
kind: string;
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export interface ServerMapResponse {
|
|
mapId: string;
|
|
mapVersion: string;
|
|
biome: string;
|
|
recommendedLevelMin: number;
|
|
recommendedLevelMax: number;
|
|
width: number;
|
|
height: number;
|
|
tiles: MapTileResponse[];
|
|
objects: MapObjectResponse[];
|
|
spawnPoints: SpawnPointResponse[];
|
|
}
|
|
|
|
export interface GetMapResult {
|
|
map: ServerMapResponse | null;
|
|
etag: string;
|
|
notModified: boolean;
|
|
}
|
|
|
|
/** Get map payload by map ID with optional ETag conditional fetch. */
|
|
export async function getMap(mapId: string, etag?: string): Promise<GetMapResult> {
|
|
const headers = buildHeaders();
|
|
if (etag) {
|
|
headers['If-None-Match'] = etag;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}/maps/${encodeURIComponent(mapId)}`, {
|
|
method: 'GET',
|
|
headers,
|
|
});
|
|
|
|
const responseEtag = res.headers.get('ETag') ?? etag ?? '';
|
|
if (res.status === 304) {
|
|
return {
|
|
map: null,
|
|
etag: responseEtag,
|
|
notModified: true,
|
|
};
|
|
}
|
|
if (!res.ok) {
|
|
const body = await res.text().catch(() => '');
|
|
throw new ApiError(res.status, res.statusText, body);
|
|
}
|
|
|
|
return {
|
|
map: await res.json() as ServerMapResponse,
|
|
etag: responseEtag,
|
|
notModified: false,
|
|
};
|
|
}
|
|
|
|
export interface ActivateBuffResponse {
|
|
buff: ServerActiveBuffRow;
|
|
heroBuffs: ServerActiveBuffRow[];
|
|
hero?: HeroResponse;
|
|
}
|
|
|
|
/** Activate a buff by type */
|
|
export async function activateBuff(
|
|
buffType: string,
|
|
telegramId?: number,
|
|
): Promise<ActivateBuffResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<ActivateBuffResponse>(`/hero/buff/${buffType}${query}`);
|
|
}
|
|
|
|
/** Request revive after death — returns authoritative hero snapshot */
|
|
export async function requestRevive(telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/hero/revive${query}`);
|
|
}
|
|
|
|
export interface VictoryDropResponse {
|
|
itemType: string;
|
|
itemId?: number;
|
|
itemName?: string;
|
|
rarity: string;
|
|
goldAmount: number;
|
|
}
|
|
|
|
export interface ReportVictoryResponse {
|
|
hero: HeroResponse;
|
|
drops: VictoryDropResponse[];
|
|
}
|
|
|
|
/** Apply server-side kill rewards (XP, gold, loot rolls, HP rules) after client-resolved combat */
|
|
export async function reportVictory(
|
|
body: { enemyType: string; heroHp: number; positionX?: number; positionY?: number },
|
|
telegramId?: number,
|
|
): Promise<ReportVictoryResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<ReportVictoryResponse>(`/hero/victory${query}`, body);
|
|
}
|
|
|
|
// ---- Loot & Equipment ----
|
|
|
|
export interface LootResponse {
|
|
itemType: 'weapon' | 'armor' | 'gold';
|
|
itemName?: string;
|
|
rarity: string;
|
|
goldAmount: number;
|
|
}
|
|
|
|
export interface WeaponResponse {
|
|
id: number;
|
|
name: string;
|
|
type: string;
|
|
rarity: string;
|
|
damage: number;
|
|
speedMultiplier: number;
|
|
ilvl?: number;
|
|
}
|
|
|
|
export interface ArmorResponse {
|
|
id: number;
|
|
name: string;
|
|
type: string;
|
|
rarity: string;
|
|
defense: number;
|
|
speedMultiplier: number;
|
|
ilvl?: number;
|
|
}
|
|
|
|
export interface EncounterEnemyResponse {
|
|
id: number;
|
|
name: string;
|
|
hp: number;
|
|
maxHp: number;
|
|
attack: number;
|
|
defense: number;
|
|
speed: number;
|
|
enemyType: string;
|
|
}
|
|
|
|
/** Fetch extended equipment slots for the hero */
|
|
export async function getHeroEquipment(telegramId?: number): Promise<Record<string, EquipmentItemResponse>> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiGet<Record<string, EquipmentItemResponse>>(`/hero/equipment${query}`);
|
|
}
|
|
|
|
export interface EquipmentItemResponse {
|
|
id: number;
|
|
slot: string;
|
|
formId: string;
|
|
name: string;
|
|
rarity: string;
|
|
ilvl: number;
|
|
basePrimary: number;
|
|
primaryStat: number;
|
|
statType: string;
|
|
}
|
|
|
|
/** Ask backend to generate the next encounter enemy for this hero. */
|
|
export async function requestEncounter(telegramId?: number, posX?: number, posY?: number): Promise<EncounterEnemyResponse> {
|
|
const params = new URLSearchParams();
|
|
if (telegramId != null) params.set('telegramId', String(telegramId));
|
|
if (posX != null) params.set('posX', posX.toFixed(2));
|
|
if (posY != null) params.set('posY', posY.toFixed(2));
|
|
const query = params.toString() ? `?${params.toString()}` : '';
|
|
return apiPost<EncounterEnemyResponse>(`/hero/encounter${query}`);
|
|
}
|
|
|
|
/** Get recent loot drops for the hero */
|
|
export async function getLoot(): Promise<LootResponse[]> {
|
|
return apiGet<LootResponse[]>('/hero/loot');
|
|
}
|
|
|
|
/** Get available weapons catalog */
|
|
export async function getWeapons(): Promise<WeaponResponse[]> {
|
|
return apiGet<WeaponResponse[]>('/weapons');
|
|
}
|
|
|
|
/** Get available armor catalog */
|
|
export async function getArmor(): Promise<ArmorResponse[]> {
|
|
return apiGet<ArmorResponse[]>('/armor');
|
|
}
|
|
|
|
// ---- Adventure Log ----
|
|
|
|
export interface LogEntry {
|
|
id: number;
|
|
message: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
/** Fetch recent adventure log entries (offline events, etc.) */
|
|
export async function getAdventureLog(telegramId?: number, limit?: number): Promise<LogEntry[]> {
|
|
const params = new URLSearchParams();
|
|
if (telegramId != null) params.set('telegramId', String(telegramId));
|
|
if (limit != null) params.set('limit', String(limit));
|
|
const query = params.toString() ? `?${params.toString()}` : '';
|
|
const raw = await apiGet<{ log?: LogEntry[] } | LogEntry[]>(`/hero/log${query}`);
|
|
if (Array.isArray(raw)) {
|
|
return raw;
|
|
}
|
|
return raw.log ?? [];
|
|
}
|
|
|
|
// ---- Potions ----
|
|
|
|
/** Use a healing potion; returns updated hero state */
|
|
export async function usePotion(telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/hero/use-potion${query}`);
|
|
}
|
|
|
|
// ---- Towns ----
|
|
|
|
import type { Town, HeroQuest, NPC, Quest, BuildingData } from '../game/types';
|
|
|
|
/** Fetch all towns */
|
|
export async function getTowns(): Promise<Town[]> {
|
|
return apiGet<Town[]>('/towns');
|
|
}
|
|
|
|
/** Fetch NPCs for a town */
|
|
export async function getTownNPCs(townId: number): Promise<NPC[]> {
|
|
return apiGet<NPC[]>(`/towns/${townId}/npcs`);
|
|
}
|
|
|
|
/** Fetch buildings for a town */
|
|
export async function getTownBuildings(townId: number): Promise<BuildingData[]> {
|
|
return apiGet<BuildingData[]>(`/towns/${townId}/buildings`);
|
|
}
|
|
|
|
/** Fetch available quests from an NPC */
|
|
export async function getNPCQuests(npcId: number, telegramId?: number): Promise<Quest[]> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiGet<Quest[]>(`/npcs/${npcId}/quests${query}`);
|
|
}
|
|
|
|
// ---- Hero Quests ----
|
|
|
|
/** Raw shape from backend — quest details are nested under "quest" */
|
|
interface HeroQuestRaw {
|
|
id: number;
|
|
heroId: number;
|
|
questId: number;
|
|
status: string;
|
|
progress: number;
|
|
quest?: {
|
|
id: number;
|
|
npcId: number;
|
|
title: string;
|
|
description: string;
|
|
type: string;
|
|
targetCount: number;
|
|
targetEnemyType?: string;
|
|
targetTownId?: number;
|
|
dropChance: number;
|
|
minLevel: number;
|
|
maxLevel: number;
|
|
rewardXp: number;
|
|
rewardGold: number;
|
|
rewardPotions: number;
|
|
};
|
|
// Also accept flat fields in case backend changes later
|
|
title?: string;
|
|
description?: string;
|
|
type?: string;
|
|
targetCount?: number;
|
|
rewardXp?: number;
|
|
rewardGold?: number;
|
|
rewardPotions?: number;
|
|
npcName?: string;
|
|
townName?: string;
|
|
}
|
|
|
|
function flattenHeroQuest(raw: HeroQuestRaw): HeroQuest {
|
|
const q = raw.quest;
|
|
return {
|
|
id: raw.id,
|
|
questId: raw.questId,
|
|
title: raw.title ?? q?.title ?? '',
|
|
description: raw.description ?? q?.description ?? '',
|
|
type: raw.type ?? q?.type ?? '',
|
|
targetCount: raw.targetCount ?? q?.targetCount ?? 0,
|
|
progress: raw.progress,
|
|
status: (raw.status as HeroQuest['status']) ?? 'accepted',
|
|
rewardXp: raw.rewardXp ?? q?.rewardXp ?? 0,
|
|
rewardGold: raw.rewardGold ?? q?.rewardGold ?? 0,
|
|
rewardPotions: raw.rewardPotions ?? q?.rewardPotions ?? 0,
|
|
npcName: raw.npcName ?? '',
|
|
townName: raw.townName ?? '',
|
|
};
|
|
}
|
|
|
|
/** Fetch hero's active/completed quests */
|
|
export async function getHeroQuests(telegramId?: number): Promise<HeroQuest[]> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
const raw = await apiGet<HeroQuestRaw[]>(`/hero/quests${query}`);
|
|
return raw.map(flattenHeroQuest);
|
|
}
|
|
|
|
/** Accept a quest */
|
|
export async function acceptQuest(questId: number, telegramId?: number): Promise<HeroQuest> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroQuest>(`/hero/quests/${questId}/accept${query}`);
|
|
}
|
|
|
|
/** Claim a completed quest's rewards */
|
|
export async function claimQuest(heroQuestId: number, telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/hero/quests/${heroQuestId}/claim${query}`);
|
|
}
|
|
|
|
/** Abandon a quest */
|
|
export async function abandonQuest(heroQuestId: number, telegramId?: number): Promise<void> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiDelete<void>(`/hero/quests/${heroQuestId}${query}`);
|
|
}
|
|
|
|
// ---- NPC Services ----
|
|
|
|
/** Buy a potion from a merchant NPC (matches backend POST /api/v1/hero/npc-buy-potion) */
|
|
export async function buyPotion(telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/hero/npc-buy-potion${query}`);
|
|
}
|
|
|
|
/** Heal to full at a healer NPC (matches backend POST /api/v1/hero/npc-heal) */
|
|
export async function healAtNPC(telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/hero/npc-heal${query}`);
|
|
}
|
|
|
|
// ---- NPC Proximity & Interaction ----
|
|
|
|
export interface NearbyNPCResponse {
|
|
id: number;
|
|
name: string;
|
|
type: string;
|
|
worldX: number;
|
|
worldY: number;
|
|
}
|
|
|
|
export interface NPCInteractResponse {
|
|
npcName: string;
|
|
npcType: string;
|
|
actions: Array<{ type: string; label: string; cost?: number }>;
|
|
quests?: Quest[];
|
|
}
|
|
|
|
/** Fetch NPCs near the hero's current position */
|
|
export async function getNearbyNPCs(telegramId?: number, posX?: number, posY?: number): Promise<NearbyNPCResponse[]> {
|
|
const params = new URLSearchParams();
|
|
if (telegramId != null) params.set('telegramId', String(telegramId));
|
|
if (posX != null) params.set('posX', String(posX));
|
|
if (posY != null) params.set('posY', String(posY));
|
|
const query = params.toString() ? `?${params.toString()}` : '';
|
|
return apiGet<NearbyNPCResponse[]>(`/hero/nearby-npcs${query}`);
|
|
}
|
|
|
|
/** Interact with an NPC to get available actions */
|
|
export async function interactWithNPC(npcId: number, telegramId?: number): Promise<NPCInteractResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<NPCInteractResponse>(`/npcs/${npcId}/interact${query}`);
|
|
}
|
|
|
|
/** Buy an item from a merchant NPC */
|
|
export async function buyFromMerchant(npcId: number, item: string, telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/npcs/${npcId}/buy${query}`, { item });
|
|
}
|
|
|
|
/** Heal at a healer NPC */
|
|
export async function healAtHealerNPC(npcId: number, telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/npcs/${npcId}/heal${query}`);
|
|
}
|
|
|
|
// ---- Wandering NPC alms ----
|
|
|
|
/** Matches backend model.AlmsResponse (POST /hero/npc-alms) */
|
|
export interface NPCAlmsResponse {
|
|
accepted: boolean;
|
|
message: string;
|
|
goldSpent?: number;
|
|
itemDrop?: {
|
|
itemType: string;
|
|
itemId?: number;
|
|
itemName?: string;
|
|
rarity: string;
|
|
goldAmount?: number;
|
|
};
|
|
hero?: HeroResponse;
|
|
}
|
|
|
|
/** Give gold to a wandering NPC for a mysterious item */
|
|
export async function giveNPCAlms(telegramId?: number): Promise<NPCAlmsResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<NPCAlmsResponse>(`/hero/npc-alms${query}`, { accept: true });
|
|
}
|
|
|
|
/** Purchase a buff charge refill (placeholder for Telegram Payments integration) */
|
|
export async function purchaseBuffRefill(
|
|
buffType: string,
|
|
telegramId?: number,
|
|
): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/hero/purchase-buff-refill${query}`, { buffType });
|
|
}
|
|
|
|
// ---- Achievements (spec 10.3) ----
|
|
|
|
export interface Achievement {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
conditionType: string;
|
|
conditionValue: number;
|
|
rewardType: string;
|
|
rewardAmount: number;
|
|
unlocked: boolean;
|
|
unlockedAt?: string;
|
|
}
|
|
|
|
/** Fetch all achievements with unlock status */
|
|
export async function getAchievements(telegramId?: number): Promise<Achievement[]> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiGet<Achievement[]>(`/hero/achievements${query}`);
|
|
}
|
|
|
|
// ---- Nearby Heroes (spec 2.3 shared world) ----
|
|
|
|
export interface NearbyHero {
|
|
id: number;
|
|
name: string;
|
|
level: number;
|
|
positionX: number;
|
|
positionY: number;
|
|
}
|
|
|
|
/** Fetch heroes near the player's current position */
|
|
export async function getNearbyHeroes(telegramId?: number): Promise<NearbyHero[]> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiGet<NearbyHero[]>(`/hero/nearby${query}`);
|
|
}
|
|
|
|
// ---- Daily Tasks (backend-driven, spec 10.1) ----
|
|
|
|
export interface DailyTaskResponse {
|
|
taskId: string;
|
|
title: string;
|
|
description: string;
|
|
objectiveType: string;
|
|
objectiveCount: number;
|
|
progress: number;
|
|
completed: boolean;
|
|
claimed: boolean;
|
|
rewardType: string;
|
|
rewardAmount: number;
|
|
period: string;
|
|
}
|
|
|
|
/** Fetch daily tasks with progress */
|
|
export async function getDailyTasks(telegramId?: number): Promise<DailyTaskResponse[]> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiGet<DailyTaskResponse[]>(`/hero/daily-tasks${query}`);
|
|
}
|
|
|
|
/** Claim a completed daily task reward */
|
|
export async function claimDailyTask(taskId: string, telegramId?: number): Promise<HeroResponse> {
|
|
const query = telegramId != null ? `?telegramId=${telegramId}` : '';
|
|
return apiPost<HeroResponse>(`/hero/daily-tasks/${encodeURIComponent(taskId)}/claim${query}`);
|
|
}
|