feat: vente items + stats combat avec équipement + forge visible
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s
Inventaire: bouton Vendre sur items non équipés (40% du prix d'achat). Stats forge visibles: "+5 ATK (3+2)" montre base + bonus forge. Dashboard combat: attaque/défense calculés avec arme+armure+forge équipées. 10 side quests Égouts seedées (level 5-7).
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { characterApi } from '../api/endpoints';
|
import { characterApi, itemApi } from '../api/endpoints';
|
||||||
import { Bar } from '../components/Bar';
|
import { Bar } from '../components/Bar';
|
||||||
import { Zap, Heart, Star, Coins, Sword, Shield, BedDouble } from 'lucide-react';
|
import { Zap, Heart, Star, Coins, Sword, Shield, BedDouble } from 'lucide-react';
|
||||||
|
|
||||||
@@ -134,6 +134,42 @@ function StatDistributor({ char }: { char: any }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CombatStatsPanel({ char }: { char: any }) {
|
||||||
|
const { data: inventory } = useQuery({ queryKey: ['inventory'], queryFn: itemApi.inventory });
|
||||||
|
|
||||||
|
const weapon = inventory?.find((ci: any) => ci.equipped && ci.item.type === 'weapon');
|
||||||
|
const armor = inventory?.find((ci: any) => ci.equipped && ci.item.type === 'armor');
|
||||||
|
|
||||||
|
const weaponATK = weapon ? weapon.item.attackBonus + weapon.forgeLevel * 2 : 0;
|
||||||
|
const armorDEF = armor ? armor.item.defenseBonus + armor.forgeLevel * 2 : 0;
|
||||||
|
const baseDmg = 3 + weaponATK + Math.floor(char.force * 1.5);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card" style={{ gridColumn: '1 / -1' }}>
|
||||||
|
<p style={{ margin: '0 0 0.75rem', fontSize: 13, fontWeight: 700, color: '#9ca3af' }}>Combat actuel</p>
|
||||||
|
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Sword size={14} color="#f4c94e" />
|
||||||
|
<span style={{ fontSize: 13, color: '#6b7a99' }}>Attaque : </span>
|
||||||
|
<span style={{ fontSize: 14, fontWeight: 700 }}>{baseDmg}</span>
|
||||||
|
{weapon && <span style={{ fontSize: 10, color: '#6b7a99' }}>({weapon.item.name} {weapon.forgeLevel > 0 ? `+${weapon.forgeLevel}` : ''})</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Shield size={14} color="#5ba4f5" />
|
||||||
|
<span style={{ fontSize: 13, color: '#6b7a99' }}>Défense : </span>
|
||||||
|
<span style={{ fontSize: 14, fontWeight: 700 }}>{armorDEF}</span>
|
||||||
|
{armor && <span style={{ fontSize: 10, color: '#6b7a99' }}>({armor.item.name} {armor.forgeLevel > 0 ? `+${armor.forgeLevel}` : ''})</span>}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<Zap size={14} color="#3ddc84" />
|
||||||
|
<span style={{ fontSize: 13, color: '#6b7a99' }}>Critique : </span>
|
||||||
|
<span style={{ fontSize: 14, fontWeight: 700 }}>{(5 + char.chance * 0.2).toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const { data: char, isLoading, isError } = useQuery({
|
const { data: char, isLoading, isError } = useQuery({
|
||||||
@@ -266,26 +302,7 @@ export function DashboardPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Équipement résumé */}
|
{/* Équipement résumé */}
|
||||||
<div className="card" style={{ gridColumn: '1 / -1' }}>
|
<CombatStatsPanel char={char} />
|
||||||
<p style={{ margin: '0 0 0.75rem', fontSize: 13, fontWeight: 700, color: '#9ca3af' }}>Combat actuel</p>
|
|
||||||
<div style={{ display: 'flex', gap: 24 }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<Sword size={14} color="#f4c94e" />
|
|
||||||
<span style={{ fontSize: 13, color: '#6b7a99' }}>Attaque : </span>
|
|
||||||
<span style={{ fontSize: 14, fontWeight: 700 }}>{Math.floor(char.force * 1.5)}</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<Shield size={14} color="#5ba4f5" />
|
|
||||||
<span style={{ fontSize: 13, color: '#6b7a99' }}>Critique : </span>
|
|
||||||
<span style={{ fontSize: 14, fontWeight: 700 }}>{(5 + char.chance * 0.2).toFixed(1)}%</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<Zap size={14} color="#3ddc84" />
|
|
||||||
<span style={{ fontSize: 13, color: '#6b7a99' }}>Esquive : </span>
|
|
||||||
<span style={{ fontSize: 14, fontWeight: 700 }}>{(5 + char.chance * 0.1).toFixed(1)}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { itemApi, materialApi } from '../api/endpoints';
|
import { itemApi, materialApi } from '../api/endpoints';
|
||||||
|
import { api } from '../api/client';
|
||||||
import type { CharacterItem } from '../api/types';
|
import type { CharacterItem } from '../api/types';
|
||||||
import { Package, Sword, Shield } from 'lucide-react';
|
import { Package, Sword, Shield, Coins } from 'lucide-react';
|
||||||
|
|
||||||
const RARITY_LABEL: Record<string, string> = {
|
const RARITY_LABEL: Record<string, string> = {
|
||||||
common: 'Commun', rare: 'Rare', epic: 'Épique', legendary: 'Légendaire',
|
common: 'Commun', rare: 'Rare', epic: 'Épique', legendary: 'Légendaire',
|
||||||
};
|
};
|
||||||
|
|
||||||
function ItemCard({ ci, onEquip, onUnequip }: { ci: CharacterItem; onEquip: () => void; onUnequip: () => void }) {
|
function ItemCard({ ci, onEquip, onUnequip, onSell, selling }: {
|
||||||
|
ci: CharacterItem; onEquip: () => void; onUnequip: () => void; onSell: () => void; selling: boolean;
|
||||||
|
}) {
|
||||||
const { item } = ci;
|
const { item } = ci;
|
||||||
|
const forgeBonusATK = item.type === 'weapon' ? ci.forgeLevel * 2 : 0;
|
||||||
|
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 bonuses = [
|
const bonuses = [
|
||||||
item.attackBonus && `+${item.attackBonus} ATK`,
|
totalATK > 0 && `+${totalATK} ATK${forgeBonusATK > 0 ? ` (${item.attackBonus}+${forgeBonusATK})` : ''}`,
|
||||||
item.defenseBonus && `+${item.defenseBonus} DEF`,
|
totalDEF > 0 && `+${totalDEF} DEF${forgeBonusDEF > 0 ? ` (${item.defenseBonus}+${forgeBonusDEF})` : ''}`,
|
||||||
item.forceBonus && `+${item.forceBonus} FOR`,
|
item.forceBonus && `+${item.forceBonus} FOR`,
|
||||||
item.agiliteBonus && `+${item.agiliteBonus} AGI`,
|
item.agiliteBonus && `+${item.agiliteBonus} AGI`,
|
||||||
item.intelligenceBonus && `+${item.intelligenceBonus} INT`,
|
item.intelligenceBonus && `+${item.intelligenceBonus} INT`,
|
||||||
@@ -37,11 +46,17 @@ function ItemCard({ ci, onEquip, onUnequip }: { ci: CharacterItem; onEquip: () =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{bonuses && <div style={{ fontSize: 11, color: '#3ddc84', marginBottom: 8 }}>{bonuses}</div>}
|
{bonuses && <div style={{ fontSize: 11, color: '#3ddc84', marginBottom: 8 }}>{bonuses}</div>}
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||||
{!ci.equipped
|
{!ci.equipped
|
||||||
? <button className="btn btn-ghost" style={{ fontSize: 11, padding: '0.2rem 0.6rem' }} onClick={onEquip}>Équiper</button>
|
? <button className="btn btn-ghost" style={{ fontSize: 11, padding: '0.2rem 0.6rem' }} onClick={onEquip}>Équiper</button>
|
||||||
: <button className="btn btn-ghost" style={{ fontSize: 11, padding: '0.2rem 0.6rem' }} onClick={onUnequip}>Déséquiper</button>
|
: <button className="btn btn-ghost" style={{ fontSize: 11, padding: '0.2rem 0.6rem' }} onClick={onUnequip}>Déséquiper</button>
|
||||||
}
|
}
|
||||||
|
{!ci.equipped && sellPrice > 0 && (
|
||||||
|
<button className="btn btn-ghost" style={{ fontSize: 10, padding: '0.15rem 0.5rem', color: '#6b7a99' }}
|
||||||
|
disabled={selling} onClick={onSell}>
|
||||||
|
<Coins size={10} style={{ display: 'inline', marginRight: 3 }} />{selling ? '...' : `Vendre (${sellPrice}💰)`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -70,6 +85,14 @@ export function InventoryPage() {
|
|||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['inventory'] }),
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['inventory'] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sellMut = useMutation({
|
||||||
|
mutationFn: (charItemId: string) => api.post<any>(`/shop/sell/${charItemId}`),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['inventory'] });
|
||||||
|
qc.invalidateQueries({ queryKey: ['character'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (loadInv || loadMat) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
if (loadInv || loadMat) return <div style={{ padding: '2rem', color: '#6b7a99' }}>Chargement…</div>;
|
||||||
|
|
||||||
const weapons = inventory?.filter(ci => ci.item.type === 'weapon') ?? [];
|
const weapons = inventory?.filter(ci => ci.item.type === 'weapon') ?? [];
|
||||||
@@ -99,6 +122,8 @@ export function InventoryPage() {
|
|||||||
key={ci.id} ci={ci}
|
key={ci.id} ci={ci}
|
||||||
onEquip={() => equipMut.mutate(ci.id)}
|
onEquip={() => equipMut.mutate(ci.id)}
|
||||||
onUnequip={() => unequipMut.mutate('weapon')}
|
onUnequip={() => unequipMut.mutate('weapon')}
|
||||||
|
onSell={() => sellMut.mutate(ci.id)}
|
||||||
|
selling={sellMut.isPending}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -117,6 +142,8 @@ export function InventoryPage() {
|
|||||||
key={ci.id} ci={ci}
|
key={ci.id} ci={ci}
|
||||||
onEquip={() => equipMut.mutate(ci.id)}
|
onEquip={() => equipMut.mutate(ci.id)}
|
||||||
onUnequip={() => unequipMut.mutate('armor')}
|
onUnequip={() => unequipMut.mutate('armor')}
|
||||||
|
onSell={() => sellMut.mutate(ci.id)}
|
||||||
|
selling={sellMut.isPending}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user