import { requestGameApiJson } from './gameRepository' export type PvpContentType = 'dungeon' | 'raid' | 'stadium' export type CpuDifficulty = 1 | 2 | 3 | 4 | 5 export type PvpMatchSide = 'a' | 'b' export type PvpMatchStatus = 'playing' | 'upgrade-choice' | 'shop' | 'won' | 'lost' export type PvpPlayerInfo = { side: PvpMatchSide accountId: number characterId: number characterName: string className: string } export type PvpUpgradeChoicePayload = { encounterIndex: number buffId: string debuffId: string purchases?: string[] shopReady?: boolean } export type PvpMatchSnapshot = { id: string contentType: PvpContentType startStage: number createdAt: number players: Record states: Partial> statuses: Partial> progress: Partial> upgradeChoices: Partial>> rematchRequests?: Partial> updatedAt: number } export type PvpQueueResponse = { ticketId: string status: 'waiting' | 'matched' match?: PvpMatchSnapshot side?: PvpMatchSide } export type PvpRematchResponse = { status: 'waiting' | 'matched' match?: PvpMatchSnapshot side?: PvpMatchSide } export type CpuPvpLeaderboardEntry = { characterName: string className: string contentType: PvpContentType encountersCleared: number cpuDifficulty: CpuDifficulty result: 'victory' | 'defeat' completedAt: string } const cpuLeaderboardKey = 'chronicle.pvpCpuLeaderboard.v1' const checkpointKey = 'chronicle.pvpRoguelikeCheckpoint.v1' export function randomCpuDifficulty(): CpuDifficulty { return (Math.floor(Math.random() * 5) + 1) as CpuDifficulty } export function loadCpuPvpLeaderboard(contentType?: PvpContentType): CpuPvpLeaderboardEntry[] { try { const raw = JSON.parse(localStorage.getItem(cpuLeaderboardKey) ?? '[]') as CpuPvpLeaderboardEntry[] return raw .filter((entry) => !contentType || entry.contentType === contentType) .sort((left, right) => right.encountersCleared - left.encountersCleared || right.cpuDifficulty - left.cpuDifficulty || right.completedAt.localeCompare(left.completedAt), ) .slice(0, 12) } catch { return [] } } export function recordCpuPvpLeaderboard(entry: CpuPvpLeaderboardEntry) { const current = loadCpuPvpLeaderboard() const next = [...current, entry] .sort((left, right) => right.encountersCleared - left.encountersCleared || right.cpuDifficulty - left.cpuDifficulty || right.completedAt.localeCompare(left.completedAt), ) .slice(0, 30) localStorage.setItem(cpuLeaderboardKey, JSON.stringify(next)) } function checkpointStorageKey(characterId: number, contentType: PvpContentType) { return `${checkpointKey}:${characterId}:${contentType}` } export function loadPvpRoguelikeCheckpoint(characterId: number, contentType: PvpContentType) { const value = Number(localStorage.getItem(checkpointStorageKey(characterId, contentType)) ?? 1) return Number.isInteger(value) && value >= 5 ? value : 1 } export function recordPvpRoguelikeCheckpoint( characterId: number, contentType: PvpContentType, stage: number, ) { if (stage < 5 || stage % 5 !== 0) return loadPvpRoguelikeCheckpoint(characterId, contentType) const current = loadPvpRoguelikeCheckpoint(characterId, contentType) const next = Math.max(current, stage) localStorage.setItem(checkpointStorageKey(characterId, contentType), String(next)) return next } export function joinPvpQueue( contentType: PvpContentType, startStage: number, ): Promise> { return requestGameApiJson('/api/pvp/queue', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contentType, startStage }), }) } export function checkPvpQueue(ticketId: string): Promise> { return requestGameApiJson(`/api/pvp/queue/${encodeURIComponent(ticketId)}`) } export function cancelPvpQueue(ticketId: string): Promise<{ ok: true }> { return requestGameApiJson(`/api/pvp/queue/${encodeURIComponent(ticketId)}`, { method: 'DELETE', }) } export function publishPvpMatchState( matchId: string, payload: { state: TSideState status: PvpMatchStatus stage: number encounterIndex: number encountersCleared: number enemyHealth: number alive: boolean elapsedTicks: number }, ): Promise> { return requestGameApiJson(`/api/pvp/matches/${encodeURIComponent(matchId)}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) } export function loadPvpMatch(matchId: string): Promise> { return requestGameApiJson(`/api/pvp/matches/${encodeURIComponent(matchId)}`) } export function submitPvpUpgradeChoice( matchId: string, payload: PvpUpgradeChoicePayload, ): Promise { return requestGameApiJson(`/api/pvp/matches/${encodeURIComponent(matchId)}/upgrade-choice`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) } export function requestPvpRematch(matchId: string): Promise> { return requestGameApiJson(`/api/pvp/matches/${encodeURIComponent(matchId)}/rematch`, { method: 'POST', }) }