fix: quest progression (events after tx), abandon quest, endurance display
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 34s

- Events (achievement/community/quest) émis APRÈS la transaction combat
  au lieu de dedans — corrige les quêtes qui ne progressaient pas
- POST /api/quests/abandon/:id — abandonner une quête active
- Frontend: bouton "Abandonner" sur les quêtes actives non complétées
- Fix endurance display (enduranceCurrent field mapping)
- Types Character mis à jour (xpToNextLevel, activeTitle, enduranceCurrent)
This commit is contained in:
2026-03-24 16:52:48 +01:00
parent 8038ca5d0a
commit af247a1c6b
7 changed files with 99 additions and 68 deletions

View File

@@ -57,6 +57,7 @@ export const questApi = {
completed: () => api.get<any[]>('/quests/completed'),
accept: (questId: string) => api.post<any>(`/quests/accept/${questId}`),
claim: (playerQuestId: string) => api.post<any>(`/quests/claim/${playerQuestId}`),
abandon: (playerQuestId: string) => api.post<any>(`/quests/abandon/${playerQuestId}`),
arcs: () => api.get<any[]>('/quests/arcs'),
};

View File

@@ -18,9 +18,13 @@ export interface Character {
vitalite: number;
hpCurrent: number;
hpMax: number;
endurance: number; // calculé à la lecture
endurance: number;
enduranceCurrent: number; // calculé à la lecture (backend field)
enduranceMax: number;
statPoints: number;
xpToNextLevel: number;
activeTitle: string | null;
totalGoldEarned: number;
createdAt: string;
}

View File

@@ -197,9 +197,9 @@ export function DashboardPage() {
<span style={{ fontSize: 12, color: '#5ba4f5', display: 'flex', alignItems: 'center', gap: 4 }}>
<Zap size={11} /> Endurance
</span>
<span style={{ fontSize: 11, color: '#6b7a99' }}>{char.endurance} / {char.enduranceMax}</span>
<span style={{ fontSize: 11, color: '#6b7a99' }}>{char.enduranceCurrent ?? char.endurance} / {char.enduranceMax}</span>
</div>
<Bar value={char.endurance} max={char.enduranceMax} type="end" showValues={false} />
<Bar value={char.enduranceCurrent ?? char.endurance} max={char.enduranceMax} type="end" showValues={false} />
</div>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>

View File

@@ -18,25 +18,28 @@ function QuestCard({ pq, mode }: { pq: any; mode: 'active' | 'available' | 'comp
const status = mode === 'active' ? pq.status : 'available';
const pct = Math.min(100, Math.floor((progress / quest.objectiveCount) * 100));
const invalidateAll = () => {
qc.invalidateQueries({ queryKey: ['quests'] });
qc.invalidateQueries({ queryKey: ['questsActive'] });
qc.invalidateQueries({ queryKey: ['questsAvailable'] });
qc.invalidateQueries({ queryKey: ['questsCompleted'] });
qc.invalidateQueries({ queryKey: ['questArcs'] });
qc.invalidateQueries({ queryKey: ['character'] });
};
const acceptMut = useMutation({
mutationFn: () => questApi.accept(quest.id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['quests'] });
qc.invalidateQueries({ queryKey: ['questsActive'] });
qc.invalidateQueries({ queryKey: ['questsAvailable'] });
},
onSuccess: invalidateAll,
});
const claimMut = useMutation({
mutationFn: () => questApi.claim(pq.id),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['quests'] });
qc.invalidateQueries({ queryKey: ['questsActive'] });
qc.invalidateQueries({ queryKey: ['questsAvailable'] });
qc.invalidateQueries({ queryKey: ['questsCompleted'] });
qc.invalidateQueries({ queryKey: ['questArcs'] });
qc.invalidateQueries({ queryKey: ['character'] });
},
onSuccess: invalidateAll,
});
const abandonMut = useMutation({
mutationFn: () => questApi.abandon(pq.id),
onSuccess: invalidateAll,
});
const isCompleted = status === 'completed';
@@ -96,8 +99,19 @@ function QuestCard({ pq, mode }: { pq: any; mode: 'active' | 'available' | 'comp
{claimMut.isPending ? 'Réclamation…' : '🎁 Réclamer la récompense'}
</button>
)}
{mode === 'active' && !isCompleted && (
<button
className="btn btn-ghost"
style={{ fontSize: 10, padding: '0.2rem 0.5rem', color: '#6b7a99' }}
disabled={abandonMut.isPending}
onClick={() => abandonMut.mutate()}
>
{abandonMut.isPending ? '…' : '✕ Abandonner'}
</button>
)}
{acceptMut.isError && <p style={{ color: '#e84040', fontSize: 11, marginTop: 4 }}>{(acceptMut.error as Error).message}</p>}
{claimMut.isError && <p style={{ color: '#e84040', fontSize: 11, marginTop: 4 }}>{(claimMut.error as Error).message}</p>}
{abandonMut.isError && <p style={{ color: '#e84040', fontSize: 11, marginTop: 4 }}>{(abandonMut.error as Error).message}</p>}
</div>
);
}