feat: titres sélectionnables + prix revente forge inclus
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 31s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 31s
Dashboard: titre actif affiché "« Champion »" + sélecteur avec tous les titres débloqués (achievements claimed avec rewardTitle). Header: titre visible à côté du level. Revente: prix inclut l'investissement forge (50% des coûts cumulés). Épée +5 (investissement 1900 or) → revente base + 950 au lieu de base seul. API client: ajout méthode PUT.
This commit is contained in:
@@ -55,5 +55,6 @@ async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
export const api = {
|
||||
get: <T>(path: string) => request<T>(path),
|
||||
post: <T>(path: string, body?: unknown) => request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }),
|
||||
put: <T>(path: string, body?: unknown) => request<T>(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
|
||||
del: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ export const characterApi = {
|
||||
distributeStats: (stats: Record<string, number>) =>
|
||||
api.post<Character>('/characters/stats', stats),
|
||||
rest: () => api.post<{ hpBefore: number; hpAfter: number; hpMax: number; healed: number }>('/characters/rest'),
|
||||
setTitle: (title: string | null) => api.put<any>('/profile/title', { title }),
|
||||
};
|
||||
|
||||
// Combat
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { characterApi, itemApi } from '../api/endpoints';
|
||||
import { api } from '../api/client';
|
||||
import { Bar } from '../components/Bar';
|
||||
import { Zap, Heart, Star, Coins, Sword, Shield, BedDouble } from 'lucide-react';
|
||||
|
||||
@@ -134,6 +135,58 @@ function StatDistributor({ char }: { char: any }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TitleSelector({ char }: { char: any }) {
|
||||
const qc = useQueryClient();
|
||||
const { data: achievements } = useQuery({
|
||||
queryKey: ['achievements'],
|
||||
queryFn: () => api.get<any[]>('/achievements/me'),
|
||||
});
|
||||
|
||||
const titleMut = useMutation({
|
||||
mutationFn: (title: string | null) => characterApi.setTitle(title),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['character'] }),
|
||||
});
|
||||
|
||||
// Collect unlocked titles from claimed achievements
|
||||
const unlockedTitles: string[] = [];
|
||||
if (achievements) {
|
||||
for (const a of achievements) {
|
||||
if (a.claimed && a.rewardTitle) {
|
||||
unlockedTitles.push(a.rewardTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unlockedTitles.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="card" style={{ gridColumn: '1 / -1' }}>
|
||||
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: '#9ca3af' }}>🏅 Titre actif</p>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className={`btn ${!char.activeTitle ? 'btn-gold' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 11, padding: '0.2rem 0.6rem' }}
|
||||
disabled={titleMut.isPending}
|
||||
onClick={() => titleMut.mutate(null)}
|
||||
>
|
||||
Aucun
|
||||
</button>
|
||||
{unlockedTitles.map(t => (
|
||||
<button
|
||||
key={t}
|
||||
className={`btn ${char.activeTitle === t ? 'btn-gold' : 'btn-ghost'}`}
|
||||
style={{ fontSize: 11, padding: '0.2rem 0.6rem' }}
|
||||
disabled={titleMut.isPending}
|
||||
onClick={() => titleMut.mutate(t)}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CombatStatsPanel({ char }: { char: any }) {
|
||||
const { data: inventory } = useQuery({ queryKey: ['inventory'], queryFn: itemApi.inventory });
|
||||
|
||||
@@ -204,6 +257,9 @@ export function DashboardPage() {
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
|
||||
<h2 style={{ margin: 0, fontSize: 22, color: '#f4c94e' }}>{char.name}</h2>
|
||||
<span style={{ fontSize: 13, color: '#6b7a99' }}>Niveau {char.level}</span>
|
||||
{char.activeTitle && (
|
||||
<span style={{ fontSize: 11, color: '#a78bfa', fontStyle: 'italic' }}>« {char.activeTitle} »</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16, marginTop: 6, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 12, color: '#6b7a99', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
@@ -301,6 +357,9 @@ export function DashboardPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Titres */}
|
||||
<TitleSelector char={char} />
|
||||
|
||||
{/* Équipement résumé */}
|
||||
<CombatStatsPanel char={char} />
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,10 @@ function ItemCard({ ci, onEquip, onUnequip, onSell, selling }: {
|
||||
const forgeBonusDEF = item.type === 'armor' ? ci.forgeLevel * 2 : 0;
|
||||
const totalATK = item.attackBonus + forgeBonusATK;
|
||||
const totalDEF = item.defenseBonus + forgeBonusDEF;
|
||||
const sellPrice = Math.floor((item as any).buyPrice * 0.4) || 0;
|
||||
const FORGE_COSTS: Record<number, number> = { 1: 50, 2: 100, 3: 250, 4: 500, 5: 1000 };
|
||||
let forgeInvestment = 0;
|
||||
for (let i = 1; i <= ci.forgeLevel; i++) forgeInvestment += FORGE_COSTS[i] ?? 0;
|
||||
const sellPrice = Math.floor(((item as any).buyPrice || 0) * 0.4 + forgeInvestment * 0.5);
|
||||
|
||||
const bonuses = [
|
||||
totalATK > 0 && `+${totalATK} ATK${forgeBonusATK > 0 ? ` (${item.attackBonus}+${forgeBonusATK})` : ''}`,
|
||||
|
||||
Reference in New Issue
Block a user