From 93b34b1f7b942de575a972f779545eae03b0b30f Mon Sep 17 00:00:00 2001 From: Tetardtek Date: Tue, 24 Mar 2026 16:09:55 +0100 Subject: [PATCH] feat: stat distribution UI + rest button + xpToNextLevel from backend Dashboard: stat distributor with +/- buttons when statPoints > 0, rest button (+50% HP, -20 endurance) when HP < max, XP bar uses xpToNextLevel from backend instead of local formula. API: distributeStats + rest endpoints added to client. --- frontend/src/api/endpoints.ts | 3 + frontend/src/pages/DashboardPage.tsx | 96 ++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index a72e42c..3b8aefd 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -17,6 +17,9 @@ export const characterApi = { create: (name: string, stats: Record) => api.post('/characters', { name, ...stats }), me: () => api.get('/characters/me'), + distributeStats: (stats: Record) => + api.post('/characters/stats', stats), + rest: () => api.post<{ hpBefore: number; hpAfter: number; hpMax: number; healed: number }>('/characters/rest'), }; // Combat diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index b5134c9..b8ec6cb 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { characterApi } from '../api/endpoints'; import { Bar } from '../components/Bar'; -import { Zap, Heart, Star, Coins, Sword, Shield } from 'lucide-react'; +import { Zap, Heart, Star, Coins, Sword, Shield, BedDouble } from 'lucide-react'; const STATS = ['force', 'agilite', 'intelligence', 'chance', 'vitalite'] as const; const STAT_LABELS: Record = { @@ -73,17 +73,86 @@ function CreateCharacter() { ); } +function StatDistributor({ char }: { char: any }) { + const qc = useQueryClient(); + const [pts, setPts] = useState>({ force: 0, agilite: 0, intelligence: 0, chance: 0, vitalite: 0 }); + const used = Object.values(pts).reduce((a, b) => a + b, 0); + const remaining = (char.statPoints ?? 0) - used; + + const mut = useMutation({ + mutationFn: () => characterApi.distributeStats(pts), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['character'] }); + setPts({ force: 0, agilite: 0, intelligence: 0, chance: 0, vitalite: 0 }); + }, + }); + + const adjust = (stat: string, delta: number) => { + const next = (pts[stat] ?? 0) + delta; + if (next < 0) return; + if (delta > 0 && remaining <= 0) return; + setPts(p => ({ ...p, [stat]: next })); + }; + + return ( +
+

+ Répartir {char.statPoints} point{char.statPoints > 1 ? 's' : ''} de stats +

+

+ {remaining > 0 ? `${remaining} restant${remaining > 1 ? 's' : ''}` : 'Prêt à valider'} +

+ +
+ {STATS.map(s => ( +
+ + {STAT_LABELS[s]} ({char[s]}) + +
+ + 0 ? '#3ddc84' : '#6b7a99', fontSize: 13 }}> + {pts[s] > 0 ? `+${pts[s]}` : '0'} + + +
+
+ ))} +
+ + + + {mut.isError &&

{(mut.error as Error).message}

} +
+ ); +} + export function DashboardPage() { + const qc = useQueryClient(); const { data: char, isLoading, isError } = useQuery({ queryKey: ['character'], queryFn: characterApi.me, retry: 1, }); + const restMut = useMutation({ + mutationFn: () => characterApi.rest(), + onSuccess: () => qc.invalidateQueries({ queryKey: ['character'] }), + }); + if (isLoading) return
Chargement…
; if (isError || !char) return ; - const xpNext = Math.round(100 * Math.pow(char.level, 1.5)); + const xpNext = (char as any).xpToNextLevel ?? Math.round(100 * Math.pow(char.level, 1.5)); + const statPoints = (char as any).statPoints ?? 0; + const needsHeal = char.hpCurrent < char.hpMax; return (
@@ -102,8 +171,8 @@ export function DashboardPage() { {char.xp} / {xpNext} XP - {(char as any).statPoints > 0 && ( - +{(char as any).statPoints} pts à répartir + {statPoints > 0 && ( + +{statPoints} pts à répartir )}
@@ -141,6 +210,18 @@ export function DashboardPage() { + {needsHeal && ( + + )} + {restMut.isError &&

{(restMut.error as Error).message}

} @@ -161,6 +242,13 @@ export function DashboardPage() { + {/* Distributeur de stats */} + {statPoints > 0 && ( +
+ +
+ )} + {/* Équipement résumé */}

Combat actuel