feat: endurance tickets — coûts visibles partout + budget dashboard
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
Combat: coût 5⚡ affiché, compteur "X combats possibles", bouton disabled Forge: coût 10⚡ + or affiché (baissé de 15 à 10), bouton disabled Dashboard: indicateur budget "X combats · Y forges · Z repos" Repos: coût 10⚡ affiché, disabled si insuffisant
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { combatApi } from '../api/endpoints';
|
||||
import { combatApi, characterApi } from '../api/endpoints';
|
||||
import type { Monster, CombatResult } from '../api/types';
|
||||
import { Swords, Trophy, Skull, Clock } from 'lucide-react';
|
||||
import { Swords, Trophy, Skull, Clock, Zap } from 'lucide-react';
|
||||
|
||||
const COMBAT_COST = 5;
|
||||
|
||||
const ATTACK_TYPES = [
|
||||
{ id: 'melee', label: 'Mêlée', emoji: '⚔️', stat: 'Force × 1.5' },
|
||||
@@ -84,6 +86,10 @@ export function CombatPage() {
|
||||
const [attackType, setAttackType] = useState('melee');
|
||||
const [lastResult, setLastResult] = useState<CombatResult | null>(null);
|
||||
|
||||
const { data: char } = useQuery({ queryKey: ['character'], queryFn: characterApi.me });
|
||||
const endurance = (char as any)?.enduranceCurrent ?? (char as any)?.endurance ?? 0;
|
||||
const canFight = endurance >= COMBAT_COST;
|
||||
|
||||
const { data: monsters, isLoading } = useQuery({
|
||||
queryKey: ['monsters'],
|
||||
queryFn: combatApi.monsters,
|
||||
@@ -150,17 +156,23 @@ export function CombatPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Coût endurance */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, marginBottom: 6, fontSize: 12, color: canFight ? '#5ba4f5' : '#e84040' }}>
|
||||
<Zap size={12} /> Coût : {COMBAT_COST} endurance — Disponible : {endurance}
|
||||
{canFight && <span style={{ color: '#6b7a99' }}>({Math.floor(endurance / COMBAT_COST)} combats possibles)</span>}
|
||||
</div>
|
||||
|
||||
{/* Bouton combattre */}
|
||||
<button
|
||||
className="btn btn-red"
|
||||
style={{ width: '100%', fontSize: 15, padding: '0.75rem' }}
|
||||
disabled={!selectedMonster || fight.isPending}
|
||||
style={{ width: '100%', fontSize: 15, padding: '0.75rem', opacity: canFight ? 1 : 0.5 }}
|
||||
disabled={!selectedMonster || fight.isPending || !canFight}
|
||||
onClick={() => fight.mutate()}
|
||||
>
|
||||
{fight.isPending ? (
|
||||
<span><Swords size={14} style={{ display: 'inline', marginRight: 6 }} />Combat…</span>
|
||||
) : (
|
||||
<span>⚔️ Combattre {selectedMonster ? `— ${selectedMonster.name}` : ''}</span>
|
||||
<span>⚔️ Combattre {selectedMonster ? `— ${selectedMonster.name}` : ''} ({COMBAT_COST}⚡)</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
|
||||
@@ -153,6 +153,11 @@ export function DashboardPage() {
|
||||
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;
|
||||
const endurance = (char as any).enduranceCurrent ?? (char as any).endurance ?? 0;
|
||||
const REST_COST = 10;
|
||||
const COMBAT_COST = 5;
|
||||
const FORGE_COST = 10;
|
||||
const canRest = endurance >= REST_COST && needsHeal;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -210,17 +215,28 @@ export function DashboardPage() {
|
||||
</div>
|
||||
<Bar value={char.xp} max={xpNext} type="xp" showValues={false} />
|
||||
</div>
|
||||
{/* Budget endurance */}
|
||||
<div style={{ marginTop: 8, padding: '6px 8px', background: '#111620', borderRadius: 6, fontSize: 11, color: '#6b7a99' }}>
|
||||
<span style={{ fontWeight: 700, color: '#5ba4f5' }}>⚡ Budget :</span>
|
||||
{' '}{Math.floor(endurance / COMBAT_COST)} combats
|
||||
{' · '}{Math.floor(endurance / FORGE_COST)} forges
|
||||
{' · '}{Math.floor(endurance / REST_COST)} repos
|
||||
</div>
|
||||
|
||||
{needsHeal && (
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
style={{ marginTop: 4, fontSize: 12, display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center' }}
|
||||
disabled={restMut.isPending}
|
||||
style={{ marginTop: 4, fontSize: 12, display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center', opacity: canRest ? 1 : 0.5 }}
|
||||
disabled={restMut.isPending || !canRest}
|
||||
onClick={() => restMut.mutate()}
|
||||
>
|
||||
<BedDouble size={13} />
|
||||
{restMut.isPending ? 'Repos…' : 'Se reposer (+50% PV, -20 endurance)'}
|
||||
{restMut.isPending ? 'Repos…' : `Se reposer (+50% PV, ${REST_COST}⚡)`}
|
||||
</button>
|
||||
)}
|
||||
{needsHeal && !canRest && endurance < REST_COST && (
|
||||
<p style={{ fontSize: 10, color: '#e84040', textAlign: 'center', margin: '2px 0 0' }}>Endurance insuffisante pour se reposer</p>
|
||||
)}
|
||||
{restMut.isError && <p style={{ color: '#e84040', fontSize: 11, marginTop: 2 }}>{(restMut.error as Error).message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,71 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { itemApi, forgeApi } from '../api/endpoints';
|
||||
import { itemApi, forgeApi, characterApi } from '../api/endpoints';
|
||||
import type { CharacterItem } from '../api/types';
|
||||
import { Shield, CheckCircle, XCircle, AlertTriangle } from 'lucide-react';
|
||||
import { Shield, CheckCircle, XCircle, AlertTriangle, Zap, Coins } from 'lucide-react';
|
||||
|
||||
const FORGE_RISK = [0, 0, 0, 20, 30, 40];
|
||||
const FORGE_LABEL = ['—', '—', 'Garanti', '20% échec', '30% échec', '40% échec'];
|
||||
const FORGE_ENDURANCE_COST = 10;
|
||||
const FORGE_GOLD_COST: Record<number, number> = { 1: 50, 2: 100, 3: 250, 4: 500, 5: 1000 };
|
||||
|
||||
function ForgePanel({ nextLevel, risk, endurance, gold, isPending, onForge }: {
|
||||
nextLevel: number; risk: number; endurance: number; gold: number; isPending: boolean; onForge: () => void;
|
||||
}) {
|
||||
const goldCost = FORGE_GOLD_COST[nextLevel] ?? 0;
|
||||
const canForge = endurance >= FORGE_ENDURANCE_COST && gold >= goldCost;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card" style={{ marginBottom: '0.75rem', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 12, color: '#6b7a99', marginBottom: 4 }}>Prochain niveau : +{nextLevel}</div>
|
||||
<div style={{
|
||||
fontSize: 14, fontWeight: 700,
|
||||
color: risk === 0 ? '#3ddc84' : risk <= 20 ? '#f4c94e' : '#e84040',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4
|
||||
}}>
|
||||
{risk === 0
|
||||
? <><CheckCircle size={14} /> Succès garanti</>
|
||||
: <><AlertTriangle size={14} /> {FORGE_LABEL[nextLevel]}</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 16, marginBottom: 8, fontSize: 12 }}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4, color: endurance >= FORGE_ENDURANCE_COST ? '#5ba4f5' : '#e84040' }}>
|
||||
<Zap size={11} /> {FORGE_ENDURANCE_COST} endurance
|
||||
</span>
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 4, color: gold >= goldCost ? '#f4c94e' : '#e84040' }}>
|
||||
<Coins size={11} /> {goldCost} or
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-gold"
|
||||
style={{ width: '100%', fontSize: 14, padding: '0.75rem', opacity: canForge ? 1 : 0.5 }}
|
||||
disabled={isPending || !canForge}
|
||||
onClick={onForge}
|
||||
>
|
||||
{isPending ? 'Forge en cours…' : `🔨 Forger → +${nextLevel} (${FORGE_ENDURANCE_COST}⚡ ${goldCost}💰)`}
|
||||
</button>
|
||||
{!canForge && (
|
||||
<p style={{ textAlign: 'center', fontSize: 11, color: '#e84040', marginTop: 4 }}>
|
||||
{endurance < FORGE_ENDURANCE_COST ? 'Endurance insuffisante' : 'Or insuffisant'}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ForgePage() {
|
||||
const qc = useQueryClient();
|
||||
const [selected, setSelected] = useState<CharacterItem | null>(null);
|
||||
const [lastResult, setLastResult] = useState<{ success: boolean; newLevel: number } | null>(null);
|
||||
|
||||
const { data: char } = useQuery({ queryKey: ['character'], queryFn: characterApi.me });
|
||||
const endurance = (char as any)?.enduranceCurrent ?? (char as any)?.endurance ?? 0;
|
||||
const gold = char?.gold ?? 0;
|
||||
|
||||
const { data: inventory, isLoading } = useQuery({
|
||||
queryKey: ['inventory'],
|
||||
queryFn: itemApi.inventory,
|
||||
@@ -22,7 +76,7 @@ export function ForgePage() {
|
||||
onSuccess: (res) => {
|
||||
setLastResult({ success: res.success, newLevel: res.newForgeLevel });
|
||||
qc.invalidateQueries({ queryKey: ['inventory'] });
|
||||
// Refresh selected item from updated inventory
|
||||
qc.invalidateQueries({ queryKey: ['character'] });
|
||||
setSelected(res.item);
|
||||
},
|
||||
});
|
||||
@@ -88,30 +142,10 @@ export function ForgePage() {
|
||||
</div>
|
||||
|
||||
{!atMax ? (
|
||||
<>
|
||||
<div className="card" style={{ marginBottom: '1rem', textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 12, color: '#6b7a99', marginBottom: 4 }}>Prochain niveau : +{nextLevel}</div>
|
||||
<div style={{
|
||||
fontSize: 14, fontWeight: 700,
|
||||
color: risk === 0 ? '#3ddc84' : risk <= 20 ? '#f4c94e' : '#e84040',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4
|
||||
}}>
|
||||
{risk === 0
|
||||
? <><CheckCircle size={14} /> Succès garanti</>
|
||||
: <><AlertTriangle size={14} /> {FORGE_LABEL[nextLevel]}</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-gold"
|
||||
style={{ width: '100%', fontSize: 14, padding: '0.75rem' }}
|
||||
disabled={forgeMut.isPending}
|
||||
onClick={() => forgeMut.mutate()}
|
||||
>
|
||||
{forgeMut.isPending ? 'Forge en cours…' : `🔨 Forger → +${nextLevel}`}
|
||||
</button>
|
||||
</>
|
||||
<ForgePanel
|
||||
nextLevel={nextLevel} risk={risk} endurance={endurance} gold={gold}
|
||||
isPending={forgeMut.isPending} onForge={() => forgeMut.mutate()}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', color: '#f4c94e', fontSize: 13, padding: '0.5rem' }}>
|
||||
✨ Niveau maximum atteint (+5)
|
||||
|
||||
Reference in New Issue
Block a user