refacto: découpage composants — 5 extractions
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 34s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 34s
- MonsterCard, CombatViews (Log+Multi+History), CreateCharacter - RarityBadge + RarityDot partagés (Guide, Drawer, pages) - CombatPage 341→215 lignes (−37%) - DashboardPage 368→307 lignes (−17%) - 9 composants dans components/
This commit is contained in:
101
frontend/src/components/CombatViews.tsx
Normal file
101
frontend/src/components/CombatViews.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import type { CombatResult, MultiCombatResult, CombatLog } from '../api/types';
|
||||||
|
import { Trophy, Skull } from 'lucide-react';
|
||||||
|
|
||||||
|
export function CombatLogView({ result }: { result: CombatResult }) {
|
||||||
|
const won = result.winner === 'player';
|
||||||
|
return (
|
||||||
|
<div className="card" style={{ marginTop: '1rem' }}>
|
||||||
|
<div style={{ textAlign: 'center', padding: '0.75rem 0', marginBottom: '0.75rem', borderBottom: '1px solid #2a3448' }}>
|
||||||
|
{won
|
||||||
|
? <div style={{ color: '#3ddc84', fontWeight: 800, fontSize: 18 }}>
|
||||||
|
<Trophy size={20} style={{ display: 'inline', marginRight: 8 }} />
|
||||||
|
Victoire ! +{result.rewards.xp} XP +{result.rewards.gold} or
|
||||||
|
</div>
|
||||||
|
: <div style={{ color: '#e84040', fontWeight: 800, fontSize: 18 }}>
|
||||||
|
<Skull size={20} style={{ display: 'inline', marginRight: 8 }} />
|
||||||
|
Défaite… Retour à l'auberge
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{result.rewards.loot && (
|
||||||
|
<div style={{ fontSize: 13, color: '#f4c94e', marginTop: 4 }}>
|
||||||
|
🎁 Loot : {result.rewards.loot.name} ×{result.rewards.loot.quantity}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result.rewards.levelUp && (
|
||||||
|
<div style={{ fontSize: 13, color: '#a78bfa', marginTop: 4 }}>
|
||||||
|
🎉 LEVEL UP ! Niveau {result.rewards.newLevel} — +{result.rewards.statPointsGained} points de stats
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style={{ margin: '0 0 6px', fontSize: 12, fontWeight: 700, color: '#6b7a99' }}>
|
||||||
|
Log — {result.rounds.length} tour{result.rounds.length > 1 ? 's' : ''}
|
||||||
|
</p>
|
||||||
|
<div className="combat-log">
|
||||||
|
{result.rounds.flatMap(r =>
|
||||||
|
r.log.map((line, i) => {
|
||||||
|
const cls = line.includes('CRITIQUE') ? 'log-crit'
|
||||||
|
: line.includes('esquive') ? 'log-crit'
|
||||||
|
: line.includes('HP') ? 'log-system'
|
||||||
|
: i === 0 ? 'log-player'
|
||||||
|
: 'log-monster';
|
||||||
|
return <div key={`${r.round}-${i}`} className={cls}>[T{r.round}] {line}</div>;
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
{won
|
||||||
|
? <div className="log-system">══ Victoire ══</div>
|
||||||
|
: <div className="log-monster">══ Défaite ══</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MultiCombatView({ result }: { result: MultiCombatResult }) {
|
||||||
|
const t = result.totals;
|
||||||
|
return (
|
||||||
|
<div className="card" style={{ marginTop: '1rem' }}>
|
||||||
|
<div style={{ textAlign: 'center', padding: '0.75rem 0', marginBottom: '0.75rem', borderBottom: '1px solid #2a3448' }}>
|
||||||
|
<div style={{ fontWeight: 800, fontSize: 18, color: t.losses > 0 ? '#e84040' : '#3ddc84' }}>
|
||||||
|
{t.losses > 0 ? <Skull size={20} style={{ display: 'inline', marginRight: 8 }} /> : <Trophy size={20} style={{ display: 'inline', marginRight: 8 }} />}
|
||||||
|
{result.count} combat{result.count > 1 ? 's' : ''} — {t.wins}V / {t.losses}D
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 14, color: '#dce4f0', marginTop: 6 }}>
|
||||||
|
+{t.xp} XP +{t.gold} Or
|
||||||
|
{t.goldLost > 0 && <span style={{ color: '#e84040' }}> −{t.goldLost} Or</span>}
|
||||||
|
</div>
|
||||||
|
{t.levelsGained > 0 && (
|
||||||
|
<div style={{ fontSize: 13, color: '#a78bfa', marginTop: 4 }}>
|
||||||
|
🎉 {t.levelsGained} level up{t.levelsGained > 1 ? 's' : ''} !
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{t.loot.length > 0 && (
|
||||||
|
<div style={{ fontSize: 13, color: '#f4c94e', marginTop: 4 }}>
|
||||||
|
🎁 Loot : {t.loot.reduce((sum, l) => sum + l.quantity, 0)} matériaux
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{t.losses > 0 && (
|
||||||
|
<div style={{ fontSize: 11, color: '#6b7a99', marginTop: 4 }}>
|
||||||
|
Série interrompue par une défaite
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryEntry({ h }: { h: CombatLog }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, padding: '3px 0', borderBottom: '1px solid #1e2535' }}>
|
||||||
|
<span style={{ color: h.winner === 'player' ? '#3ddc84' : '#e84040' }}>
|
||||||
|
{h.winner === 'player' ? '✓' : '✗'} {h.monster.name}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: '#6b7a99' }}>
|
||||||
|
{h.winner === 'player'
|
||||||
|
? `+${h.xpEarned}xp +${h.goldEarned}or${h.lootQuantity > 0 ? ` 🎁×${h.lootQuantity}` : ''}`
|
||||||
|
: `${h.totalRounds} tours`
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
frontend/src/components/CreateCharacter.tsx
Normal file
70
frontend/src/components/CreateCharacter.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { characterApi } from '../api/endpoints';
|
||||||
|
import { STAT_LABELS } from '../constants';
|
||||||
|
|
||||||
|
const STATS = ['force', 'agilite', 'intelligence', 'chance', 'vitalite'] as const;
|
||||||
|
|
||||||
|
export function CreateCharacter() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [pts, setPts] = useState<Record<string, number>>({ force:1, agilite:1, intelligence:1, chance:1, vitalite:1 });
|
||||||
|
const used = Object.values(pts).reduce((a, b) => a + b, 0) - 5;
|
||||||
|
const remaining = 5 - used;
|
||||||
|
|
||||||
|
const mut = useMutation({
|
||||||
|
mutationFn: () => characterApi.create(name, pts),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['character'] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const adjust = (stat: string, delta: number) => {
|
||||||
|
const next = (pts[stat] ?? 1) + delta;
|
||||||
|
if (next < 1 || next > 10) return;
|
||||||
|
if (delta > 0 && remaining <= 0) return;
|
||||||
|
setPts(p => ({ ...p, [stat]: next }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 420, margin: '4rem auto' }}>
|
||||||
|
<div className="card card-gold" style={{ padding: '1.5rem' }}>
|
||||||
|
<h2 style={{ margin: '0 0 4px', color: '#f4c94e', fontSize: 20 }}>Créer ton personnage</h2>
|
||||||
|
<p style={{ margin: '0 0 1.25rem', color: '#6b7a99', fontSize: 13 }}>
|
||||||
|
{remaining > 0 ? `${remaining} point${remaining > 1 ? 's' : ''} à répartir` : 'Tous les points répartis'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="input-rpg"
|
||||||
|
placeholder="Nom du personnage"
|
||||||
|
value={name}
|
||||||
|
onChange={e => setName(e.target.value)}
|
||||||
|
style={{ marginBottom: '1rem' }}
|
||||||
|
maxLength={30}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: '1.25rem' }}>
|
||||||
|
{STATS.map(s => (
|
||||||
|
<div key={s} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ fontSize: 13, width: 110, color: '#dce4f0' }}>{STAT_LABELS[s]}</span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<button className="btn btn-ghost" style={{ padding: '0.15rem 0.5rem', fontSize: 14 }} onClick={() => adjust(s, -1)}>−</button>
|
||||||
|
<span style={{ width: 20, textAlign: 'center', fontWeight: 700, color: '#f4c94e' }}>{pts[s]}</span>
|
||||||
|
<button className="btn btn-ghost" style={{ padding: '0.15rem 0.5rem', fontSize: 14 }} onClick={() => adjust(s, +1)}>+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-gold"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
disabled={!name.trim() || remaining !== 0 || mut.isPending}
|
||||||
|
onClick={() => mut.mutate()}
|
||||||
|
>
|
||||||
|
{mut.isPending ? 'Création…' : 'Commencer l\'aventure ⚔️'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{mut.isError && <p style={{ color: '#e84040', fontSize: 12, marginTop: 8 }}>{(mut.error as Error).message}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Search, X } from 'lucide-react';
|
import { Search, X } from 'lucide-react';
|
||||||
import { useGuideData } from '../hooks/useGuideData';
|
import { useGuideData } from '../hooks/useGuideData';
|
||||||
|
import { RARITY_COLORS } from '../constants';
|
||||||
const RARITY_COLORS: Record<string, string> = {
|
import { RarityDot } from './RarityBadge';
|
||||||
common: '#9ca3af', rare: '#5ba4f5', epic: '#a78bfa', legendary: '#f4c94e',
|
|
||||||
};
|
|
||||||
|
|
||||||
function RarityDot({ rarity }: { rarity: string }) {
|
|
||||||
return <span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: RARITY_COLORS[rarity] ?? '#6b7a99', marginRight: 4 }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GuideDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
export function GuideDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
|||||||
30
frontend/src/components/MonsterCard.tsx
Normal file
30
frontend/src/components/MonsterCard.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { Monster } from '../api/types';
|
||||||
|
|
||||||
|
export function MonsterCard({ m, selected, onSelect, playerLevel }: {
|
||||||
|
m: Monster; selected: boolean; onSelect: () => void; playerLevel: number;
|
||||||
|
}) {
|
||||||
|
const tooHard = m.minLevel > playerLevel + 2;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`card card-hover ${selected ? 'card-gold' : ''}`}
|
||||||
|
onClick={onSelect}
|
||||||
|
style={{ cursor: 'pointer', transition: 'all 0.15s', opacity: tooHard ? 0.4 : 1 }}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 6 }}>
|
||||||
|
<span style={{ fontWeight: 700, fontSize: 14, color: selected ? '#f4c94e' : '#dce4f0' }}>{m.name}</span>
|
||||||
|
<span className={tooHard ? 'badge badge-red' : 'badge badge-green'} style={{ fontSize: 10 }}>
|
||||||
|
Niv. {m.minLevel}–{m.maxLevel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 12, fontSize: 12, color: '#6b7a99' }}>
|
||||||
|
<span>❤️ {m.hp}</span>
|
||||||
|
<span>⚔️ {m.attack}</span>
|
||||||
|
<span>🛡️ {m.defense}</span>
|
||||||
|
<span>⭐ {m.xpReward} XP</span>
|
||||||
|
<span>💰 {m.goldMin}–{m.goldMax}</span>
|
||||||
|
</div>
|
||||||
|
{tooHard && <div style={{ fontSize: 10, color: '#e84040', marginTop: 4 }}>Niveau trop élevé</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
frontend/src/components/RarityBadge.tsx
Normal file
22
frontend/src/components/RarityBadge.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { RARITY_COLORS, RARITY_LABELS } from '../constants';
|
||||||
|
|
||||||
|
export function RarityBadge({ rarity }: { rarity: string }) {
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
fontSize: 10, fontWeight: 700, padding: '2px 6px', borderRadius: 4,
|
||||||
|
background: (RARITY_COLORS[rarity] ?? '#6b7a99') + '22',
|
||||||
|
color: RARITY_COLORS[rarity] ?? '#6b7a99',
|
||||||
|
}}>
|
||||||
|
{RARITY_LABELS[rarity] ?? rarity}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RarityDot({ rarity }: { rarity: string }) {
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block', width: 6, height: 6, borderRadius: '50%',
|
||||||
|
background: RARITY_COLORS[rarity] ?? '#6b7a99', marginRight: 4,
|
||||||
|
}} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,137 +2,11 @@ import { useState, useCallback } from 'react';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { combatApi, characterApi } from '../api/endpoints';
|
import { combatApi, characterApi } from '../api/endpoints';
|
||||||
import type { Monster, CombatResult, MultiCombatResult, CombatLog } from '../api/types';
|
import type { Monster, CombatResult, MultiCombatResult } from '../api/types';
|
||||||
import { Swords, Trophy, Skull, Clock, Zap, Heart, Lock } from 'lucide-react';
|
import { Swords, Clock, Zap, Heart, Lock } from 'lucide-react';
|
||||||
|
|
||||||
import { COMBAT_COST, REST_COST, ATTACK_TYPES, ZONE_INFO } from '../constants';
|
import { COMBAT_COST, REST_COST, ATTACK_TYPES, ZONE_INFO } from '../constants';
|
||||||
|
import { MonsterCard } from '../components/MonsterCard';
|
||||||
function MonsterCard({ m, selected, onSelect, playerLevel }: { m: Monster; selected: boolean; onSelect: () => void; playerLevel: number }) {
|
import { CombatLogView, MultiCombatView, HistoryEntry } from '../components/CombatViews';
|
||||||
const tooHard = m.minLevel > playerLevel + 2;
|
|
||||||
const tooEasy = m.maxLevel < playerLevel - 3;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`card card-hover ${selected ? 'card-gold' : ''}`}
|
|
||||||
onClick={onSelect}
|
|
||||||
style={{ cursor: 'pointer', transition: 'all 0.15s', opacity: tooHard ? 0.4 : 1 }}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 6 }}>
|
|
||||||
<span style={{ fontWeight: 700, fontSize: 14, color: selected ? '#f4c94e' : '#dce4f0' }}>{m.name}</span>
|
|
||||||
<span className={tooHard ? 'badge badge-red' : tooEasy ? 'badge' : 'badge badge-green'} style={{ fontSize: 10 }}>
|
|
||||||
Niv. {m.minLevel}–{m.maxLevel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: 12, fontSize: 12, color: '#6b7a99' }}>
|
|
||||||
<span>❤️ {m.hp}</span>
|
|
||||||
<span>⚔️ {m.attack}</span>
|
|
||||||
<span>🛡️ {m.defense}</span>
|
|
||||||
<span>⭐ {m.xpReward} XP</span>
|
|
||||||
<span>💰 {m.goldMin}–{m.goldMax}</span>
|
|
||||||
</div>
|
|
||||||
{tooHard && <div style={{ fontSize: 10, color: '#e84040', marginTop: 4 }}>Niveau trop élevé</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CombatLogView({ result }: { result: CombatResult }) {
|
|
||||||
const won = result.winner === 'player';
|
|
||||||
return (
|
|
||||||
<div className="card" style={{ marginTop: '1rem' }}>
|
|
||||||
<div style={{ textAlign: 'center', padding: '0.75rem 0', marginBottom: '0.75rem', borderBottom: '1px solid #2a3448' }}>
|
|
||||||
{won
|
|
||||||
? <div style={{ color: '#3ddc84', fontWeight: 800, fontSize: 18 }}>
|
|
||||||
<Trophy size={20} style={{ display: 'inline', marginRight: 8 }} />
|
|
||||||
Victoire ! +{result.rewards.xp} XP +{result.rewards.gold} or
|
|
||||||
</div>
|
|
||||||
: <div style={{ color: '#e84040', fontWeight: 800, fontSize: 18 }}>
|
|
||||||
<Skull size={20} style={{ display: 'inline', marginRight: 8 }} />
|
|
||||||
Défaite… Retour à l'auberge
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{result.rewards.loot && (
|
|
||||||
<div style={{ fontSize: 13, color: '#f4c94e', marginTop: 4 }}>
|
|
||||||
🎁 Loot : {result.rewards.loot.name} ×{result.rewards.loot.quantity}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{result.rewards.levelUp && (
|
|
||||||
<div style={{ fontSize: 13, color: '#a78bfa', marginTop: 4 }}>
|
|
||||||
🎉 LEVEL UP ! Niveau {result.rewards.newLevel} — +{result.rewards.statPointsGained} points de stats
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p style={{ margin: '0 0 6px', fontSize: 12, fontWeight: 700, color: '#6b7a99' }}>
|
|
||||||
Log — {result.rounds.length} tour{result.rounds.length > 1 ? 's' : ''}
|
|
||||||
</p>
|
|
||||||
<div className="combat-log">
|
|
||||||
{result.rounds.flatMap(r =>
|
|
||||||
r.log.map((line, i) => {
|
|
||||||
const cls = line.includes('CRITIQUE') ? 'log-crit'
|
|
||||||
: line.includes('esquive') ? 'log-crit'
|
|
||||||
: line.includes('HP') ? 'log-system'
|
|
||||||
: i === 0 ? 'log-player'
|
|
||||||
: 'log-monster';
|
|
||||||
return <div key={`${r.round}-${i}`} className={cls}>[T{r.round}] {line}</div>;
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
{won
|
|
||||||
? <div className="log-system">══ Victoire ══</div>
|
|
||||||
: <div className="log-monster">══ Défaite ══</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MultiCombatView({ result }: { result: MultiCombatResult }) {
|
|
||||||
const t = result.totals;
|
|
||||||
return (
|
|
||||||
<div className="card" style={{ marginTop: '1rem' }}>
|
|
||||||
<div style={{ textAlign: 'center', padding: '0.75rem 0', marginBottom: '0.75rem', borderBottom: '1px solid #2a3448' }}>
|
|
||||||
<div style={{ fontWeight: 800, fontSize: 18, color: t.losses > 0 ? '#e84040' : '#3ddc84' }}>
|
|
||||||
{t.losses > 0 ? <Skull size={20} style={{ display: 'inline', marginRight: 8 }} /> : <Trophy size={20} style={{ display: 'inline', marginRight: 8 }} />}
|
|
||||||
{result.count} combat{result.count > 1 ? 's' : ''} — {t.wins}V / {t.losses}D
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 14, color: '#dce4f0', marginTop: 6 }}>
|
|
||||||
+{t.xp} XP +{t.gold} Or
|
|
||||||
{t.goldLost > 0 && <span style={{ color: '#e84040' }}> −{t.goldLost} Or</span>}
|
|
||||||
</div>
|
|
||||||
{t.levelsGained > 0 && (
|
|
||||||
<div style={{ fontSize: 13, color: '#a78bfa', marginTop: 4 }}>
|
|
||||||
🎉 {t.levelsGained} level up{t.levelsGained > 1 ? 's' : ''} !
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{t.loot.length > 0 && (
|
|
||||||
<div style={{ fontSize: 13, color: '#f4c94e', marginTop: 4 }}>
|
|
||||||
🎁 Loot : {t.loot.reduce((sum, l) => sum + l.quantity, 0)} matériaux
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{t.losses > 0 && (
|
|
||||||
<div style={{ fontSize: 11, color: '#6b7a99', marginTop: 4 }}>
|
|
||||||
Série interrompue par une défaite
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function HistoryEntry({ h }: { h: CombatLog }) {
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, padding: '3px 0', borderBottom: '1px solid #1e2535' }}>
|
|
||||||
<span style={{ color: h.winner === 'player' ? '#3ddc84' : '#e84040' }}>
|
|
||||||
{h.winner === 'player' ? '✓' : '✗'} {h.monster.name}
|
|
||||||
</span>
|
|
||||||
<span style={{ color: '#6b7a99' }}>
|
|
||||||
{h.winner === 'player'
|
|
||||||
? `+${h.xpEarned}xp +${h.goldEarned}or${h.lootQuantity > 0 ? ` 🎁×${h.lootQuantity}` : ''}`
|
|
||||||
: `${h.totalRounds} tours`
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CombatPage() {
|
export function CombatPage() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
|||||||
@@ -4,76 +4,13 @@ import { characterApi, itemApi } from '../api/endpoints';
|
|||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import { Bar } from '../components/Bar';
|
import { Bar } from '../components/Bar';
|
||||||
import { Onboarding } from '../components/Onboarding';
|
import { Onboarding } from '../components/Onboarding';
|
||||||
|
import { CreateCharacter } from '../components/CreateCharacter';
|
||||||
import { STAT_LABELS as STAT_LABELS_MAP } from '../constants';
|
import { STAT_LABELS as STAT_LABELS_MAP } from '../constants';
|
||||||
import { Zap, Heart, Star, Coins, Sword, Shield, BedDouble } from 'lucide-react';
|
import { Zap, Heart, Star, Coins, Sword, Shield, BedDouble } from 'lucide-react';
|
||||||
|
|
||||||
const STATS = ['force', 'agilite', 'intelligence', 'chance', 'vitalite'] as const;
|
const STATS = ['force', 'agilite', 'intelligence', 'chance', 'vitalite'] as const;
|
||||||
const STAT_LABELS = STAT_LABELS_MAP;
|
const STAT_LABELS = STAT_LABELS_MAP;
|
||||||
|
|
||||||
function CreateCharacter() {
|
|
||||||
const qc = useQueryClient();
|
|
||||||
const [name, setName] = useState('');
|
|
||||||
const [pts, setPts] = useState<Record<string, number>>({ force:1, agilite:1, intelligence:1, chance:1, vitalite:1 });
|
|
||||||
const used = Object.values(pts).reduce((a, b) => a + b, 0) - 5;
|
|
||||||
const remaining = 5 - used;
|
|
||||||
|
|
||||||
const mut = useMutation({
|
|
||||||
mutationFn: () => characterApi.create(name, pts),
|
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['character'] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const adjust = (stat: string, delta: number) => {
|
|
||||||
const next = (pts[stat] ?? 1) + delta;
|
|
||||||
if (next < 1 || next > 10) return;
|
|
||||||
if (delta > 0 && remaining <= 0) return;
|
|
||||||
setPts(p => ({ ...p, [stat]: next }));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ maxWidth: 420, margin: '4rem auto' }}>
|
|
||||||
<div className="card card-gold" style={{ padding: '1.5rem' }}>
|
|
||||||
<h2 style={{ margin: '0 0 4px', color: '#f4c94e', fontSize: 20 }}>Créer ton personnage</h2>
|
|
||||||
<p style={{ margin: '0 0 1.25rem', color: '#6b7a99', fontSize: 13 }}>
|
|
||||||
{remaining > 0 ? `${remaining} point${remaining > 1 ? 's' : ''} à répartir` : 'Tous les points répartis'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="input-rpg"
|
|
||||||
placeholder="Nom du personnage"
|
|
||||||
value={name}
|
|
||||||
onChange={e => setName(e.target.value)}
|
|
||||||
style={{ marginBottom: '1rem' }}
|
|
||||||
maxLength={30}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: '1.25rem' }}>
|
|
||||||
{STATS.map(s => (
|
|
||||||
<div key={s} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
||||||
<span style={{ fontSize: 13, width: 110, color: '#dce4f0' }}>{STAT_LABELS[s]}</span>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<button className="btn btn-ghost" style={{ padding: '0.15rem 0.5rem', fontSize: 14 }} onClick={() => adjust(s, -1)}>−</button>
|
|
||||||
<span style={{ width: 20, textAlign: 'center', fontWeight: 700, color: '#f4c94e' }}>{pts[s]}</span>
|
|
||||||
<button className="btn btn-ghost" style={{ padding: '0.15rem 0.5rem', fontSize: 14 }} onClick={() => adjust(s, +1)}>+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn btn-gold"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
disabled={!name.trim() || remaining !== 0 || mut.isPending}
|
|
||||||
onClick={() => mut.mutate()}
|
|
||||||
>
|
|
||||||
{mut.isPending ? 'Création…' : 'Commencer l\'aventure ⚔️'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{mut.isError && <p style={{ color: '#e84040', fontSize: 12, marginTop: 8 }}>{(mut.error as Error).message}</p>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatDistributor({ char }: { char: any }) {
|
function StatDistributor({ char }: { char: any }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [pts, setPts] = useState<Record<string, number>>({ force: 0, agilite: 0, intelligence: 0, chance: 0, vitalite: 0 });
|
const [pts, setPts] = useState<Record<string, number>>({ force: 0, agilite: 0, intelligence: 0, chance: 0, vitalite: 0 });
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import type { Monster, Item, Recipe } from '../api/types';
|
import type { Monster, Item, Recipe } from '../api/types';
|
||||||
import { Swords, Shield, Map as MapIcon, Hammer, ShoppingBag, BookOpen, Sparkles, Search, Gamepad2 } from 'lucide-react';
|
import { Swords, Shield, Map as MapIcon, Hammer, ShoppingBag, BookOpen, Sparkles, Search, Gamepad2 } from 'lucide-react';
|
||||||
import { useGuideData } from '../hooks/useGuideData';
|
import { useGuideData } from '../hooks/useGuideData';
|
||||||
import { RARITY_COLORS, RARITY_LABELS, FORGE_TABLE, ZONE_INFO } from '../constants';
|
import { RARITY_COLORS, FORGE_TABLE, ZONE_INFO } from '../constants';
|
||||||
|
import { RarityBadge } from '../components/RarityBadge';
|
||||||
|
|
||||||
const ZONES = [
|
const ZONES = [
|
||||||
{ id: 'marais', ...ZONE_INFO.marais, desc: 'Zone de départ. Monstres niv. 1-9. Terre de boue et de brume.' },
|
{ id: 'marais', ...ZONE_INFO.marais, desc: 'Zone de départ. Monstres niv. 1-9. Terre de boue et de brume.' },
|
||||||
@@ -23,17 +24,6 @@ const TABS = [
|
|||||||
|
|
||||||
// ── Components ──
|
// ── Components ──
|
||||||
|
|
||||||
function RarityBadge({ rarity }: { rarity: string }) {
|
|
||||||
return (
|
|
||||||
<span style={{
|
|
||||||
fontSize: 10, fontWeight: 700, padding: '2px 6px', borderRadius: 4,
|
|
||||||
background: RARITY_COLORS[rarity] + '22', color: RARITY_COLORS[rarity],
|
|
||||||
}}>
|
|
||||||
{RARITY_LABELS[rarity] ?? rarity}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatBar({ label, value, max, color }: { label: string; value: number; max: number; color: string }) {
|
function StatBar({ label, value, max, color }: { label: string; value: number; max: number; color: string }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user