diff --git a/frontend/src/pages/QuestPage.tsx b/frontend/src/pages/QuestPage.tsx index 1b53bce..8acab72 100644 --- a/frontend/src/pages/QuestPage.tsx +++ b/frontend/src/pages/QuestPage.tsx @@ -1,6 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { questApi } from '../api/endpoints'; -import { Scroll, CheckCircle, Circle, Trophy, ChevronDown, ChevronRight, Star, Coins, Swords } from 'lucide-react'; +import { Scroll, CheckCircle, Circle, Trophy, ChevronDown, ChevronRight, Star, Coins, Swords, Lock } from 'lucide-react'; import { useState } from 'react'; const OBJ_LABELS: Record = { @@ -116,44 +116,90 @@ function QuestCard({ pq, mode }: { pq: any; mode: 'active' | 'available' | 'comp ); } +function ArcQuestRow({ q }: { q: any }) { + const qc = useQueryClient(); + + const acceptMut = useMutation({ + mutationFn: () => questApi.accept(q.id), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['questArcs'] }); + qc.invalidateQueries({ queryKey: ['questsActive'] }); + }, + }); + + const claimMut = useMutation({ + mutationFn: () => questApi.claim(q.playerQuestId), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['questArcs'] }); + qc.invalidateQueries({ queryKey: ['questsActive'] }); + qc.invalidateQueries({ queryKey: ['character'] }); + }, + }); + + return ( +
+ {q.playerStatus === 'claimed' + ? + : q.playerStatus === 'completed' + ? + : q.playerStatus === 'active' + ? + : + } +
+ {q.name} + {q.playerStatus === 'active' && ( + {q.progress}/{q.objectiveCount} + )} +
+ {q.rewardXp} XP + {q.minLevel > 1 && !q.levelOk && Niv.{q.minLevel}} + + {/* Actions */} + {q.canAccept && ( + + )} + {q.playerStatus === 'completed' && ( + + )} + {acceptMut.isError && {(acceptMut.error as Error).message}} +
+ ); +} + function ArcSection({ arc }: { arc: any }) { const [open, setOpen] = useState(true); const { completed, total } = arc.progress; + const locked = !arc.zoneUnlocked; return ( -
+
setOpen(!open)} > - {open ? : } - - {arc.name} + {locked ? : open ? : } + + + {arc.name} + {completed}/{total} {arc.completed && } + {locked && 🔒 ComplĂ©tez l'arc prĂ©cĂ©dent}
- {open && ( + {open && !locked && ( <>

{arc.description}

-
- {arc.quests.map((q: any) => ( -
- {q.playerStatus === 'claimed' - ? - : q.playerStatus === 'completed' - ? - : q.playerStatus === 'active' - ? - : - } - {q.name} - {q.rewardXp} XP - {q.minLevel > 1 && Niv.{q.minLevel}+} -
- ))} +
+ {arc.quests.map((q: any) => )}
)} diff --git a/src/quest/quest.service.ts b/src/quest/quest.service.ts index 7277130..513571e 100644 --- a/src/quest/quest.service.ts +++ b/src/quest/quest.service.ts @@ -50,9 +50,8 @@ export class QuestService { return quests.filter((q) => { if (q.minLevel > character.level) return false; - - // Zone filter: if quest belongs to an arc with a zone, check zone is unlocked - if (q.arc?.zone && !unlockedZones.includes(q.arc.zone)) return false; + // Arc quests managed from arc panel — not in available + if (q.arcId) return false; const status = questStatusMap.get(q.id); if (status === 'active' || status === 'completed') return false; @@ -287,6 +286,11 @@ export class QuestService { // --- Arcs --- async getArcs(characterId: string) { + const character = await this.characterRepo.findOne({ where: { id: characterId } }); + const playerLevel = character?.level ?? 1; + + const unlockedZones = await getUnlockedZones(characterId, this.arcRepo, this.playerArcRepo); + const arcs = await this.arcRepo.find({ relations: ['quests'], order: { sortOrder: 'ASC' }, @@ -297,24 +301,35 @@ export class QuestService { const playerQuests = await this.playerQuestRepo.find({ where: { characterId }, - select: ['questId', 'status'], + select: ['questId', 'status', 'id', 'progress'], }); - const questStatusMap = new Map(playerQuests.map((pq) => [pq.questId, pq.status])); + const questDataMap = new Map(playerQuests.map((pq) => [pq.questId, pq])); - return arcs.map((arc) => ({ - ...arc, - completed: arcMap.get(arc.id)?.completed ?? false, - completedAt: arcMap.get(arc.id)?.completedAt ?? null, - quests: arc.quests - .sort((a, b) => a.arcOrder - b.arcOrder) - .map((q) => ({ - ...q, - playerStatus: questStatusMap.get(q.id) ?? 'available', - })), - progress: { - completed: arc.quests.filter((q) => questStatusMap.get(q.id) === 'claimed').length, - total: arc.quests.length, - }, - })); + return arcs.map((arc) => { + const zoneUnlocked = !arc.zone || unlockedZones.includes(arc.zone); + return { + ...arc, + zoneUnlocked, + completed: arcMap.get(arc.id)?.completed ?? false, + completedAt: arcMap.get(arc.id)?.completedAt ?? null, + quests: arc.quests + .sort((a, b) => a.arcOrder - b.arcOrder) + .map((q) => { + const pq = questDataMap.get(q.id); + return { + ...q, + playerStatus: pq?.status ?? 'available', + playerQuestId: pq?.id ?? null, + progress: pq?.progress ?? 0, + canAccept: zoneUnlocked && !pq && q.minLevel <= playerLevel, + levelOk: q.minLevel <= playerLevel, + }; + }), + progress: { + completed: arc.quests.filter((q) => questDataMap.get(q.id)?.status === 'claimed').length, + total: arc.quests.length, + }, + }; + }); } }