feat: page Guide publique — wiki joueur dynamique
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
- /guide accessible sans authentification - 7 onglets : Démarrer, Zones, Bestiaire, Équipement, Artisanat, Forge, Boutique - Données dynamiques (API publiques) — toujours à jour - Endpoint /monsters/bestiary public (bestiaire complet toutes zones) - Fix Item.type → inclut 'consumable'
This commit is contained in:
@@ -12,6 +12,7 @@ import { ForgePage } from './pages/ForgePage';
|
||||
import { QuestPage } from './pages/QuestPage';
|
||||
import { AchievementsPage } from './pages/AchievementsPage';
|
||||
import { ShopPage } from './pages/ShopPage';
|
||||
import { GuidePage } from './pages/GuidePage';
|
||||
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: 1, staleTime: 30_000 } } });
|
||||
|
||||
@@ -31,6 +32,7 @@ function AppRoutes() {
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route path="/guide" element={<GuidePage />} />
|
||||
<Route path="/dashboard" element={<ProtectedLayout><DashboardPage /></ProtectedLayout>} />
|
||||
<Route path="/quests" element={<ProtectedLayout><QuestPage /></ProtectedLayout>} />
|
||||
<Route path="/combat" element={<ProtectedLayout><CombatPage /></ProtectedLayout>} />
|
||||
|
||||
@@ -119,7 +119,7 @@ export interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: 'weapon' | 'armor';
|
||||
type: 'weapon' | 'armor' | 'consumable';
|
||||
rarity: Rarity;
|
||||
attackBonus: number;
|
||||
defenseBonus: number;
|
||||
|
||||
479
frontend/src/pages/GuidePage.tsx
Normal file
479
frontend/src/pages/GuidePage.tsx
Normal file
@@ -0,0 +1,479 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../api/client';
|
||||
import type { Monster, Item, Recipe } from '../api/types';
|
||||
import { Swords, Shield, Map as MapIcon, Hammer, ShoppingBag, BookOpen, Sparkles } from 'lucide-react';
|
||||
|
||||
// ── Data fetching (public endpoints, no auth) ──
|
||||
|
||||
const guideApi = {
|
||||
monsters: () => api.get<(Monster & { zone: string })[]>('/monsters/bestiary'),
|
||||
items: () => api.get<Item[]>('/items'),
|
||||
materials: () => api.get<any[]>('/materials'),
|
||||
recipes: () => api.get<Recipe[]>('/craft/recipes'),
|
||||
};
|
||||
|
||||
// ── Constants ──
|
||||
|
||||
const ZONES = [
|
||||
{ id: 'marais', name: 'Les Marais', emoji: '🌿', desc: 'Zone de départ. Monstres niv. 1-9. Terre de boue et de brume.', color: '#3ddc84' },
|
||||
{ id: 'egouts', name: 'Les Égouts', emoji: '🕳️', desc: 'Sous-terrain infesté. Monstres niv. 4-10. Rats, slimes et croco.', color: '#5ba4f5' },
|
||||
{ id: 'desert', name: 'Le Désert', emoji: '🏜️', desc: 'Sable brûlant. Monstres niv. 8-15. Scorpions, momies et le Sphinx.', color: '#f4c94e' },
|
||||
];
|
||||
|
||||
const RARITY_COLORS: Record<string, string> = {
|
||||
common: '#9ca3af', rare: '#5ba4f5', epic: '#a78bfa', legendary: '#f4c94e',
|
||||
};
|
||||
|
||||
const RARITY_LABELS: Record<string, string> = {
|
||||
common: 'Commun', rare: 'Rare', epic: 'Épique', legendary: 'Légendaire',
|
||||
};
|
||||
|
||||
const FORGE_TABLE = [
|
||||
{ level: 1, gold: 50, endurance: 10, risk: '0%', bonus: '+2' },
|
||||
{ level: 2, gold: 100, endurance: 10, risk: '0%', bonus: '+4' },
|
||||
{ level: 3, gold: 250, endurance: 10, risk: '20%', bonus: '+6' },
|
||||
{ level: 4, gold: 500, endurance: 10, risk: '30%', bonus: '+8' },
|
||||
{ level: 5, gold: 1000, endurance: 10, risk: '40%', bonus: '+10' },
|
||||
];
|
||||
|
||||
const TABS = [
|
||||
{ id: 'start', label: 'Démarrer', icon: BookOpen },
|
||||
{ id: 'zones', label: 'Zones', icon: MapIcon },
|
||||
{ id: 'bestiary', label: 'Bestiaire', icon: Swords },
|
||||
{ id: 'items', label: 'Équipement', icon: Shield },
|
||||
{ id: 'craft', label: 'Artisanat', icon: Hammer },
|
||||
{ id: 'forge', label: 'Forge', icon: Sparkles },
|
||||
{ id: 'shop', label: 'Boutique', icon: ShoppingBag },
|
||||
];
|
||||
|
||||
// ── 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 }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11 }}>
|
||||
<span style={{ width: 24, color: '#6b7a99' }}>{label}</span>
|
||||
<div style={{ flex: 1, height: 6, background: '#1e2535', borderRadius: 3 }}>
|
||||
<div style={{ width: `${Math.min(100, (value / max) * 100)}%`, height: '100%', background: color, borderRadius: 3 }} />
|
||||
</div>
|
||||
<span style={{ width: 28, textAlign: 'right', color: '#dce4f0' }}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Démarrer ──
|
||||
|
||||
function StartTab() {
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Bienvenue dans TetaRdPG</h3>
|
||||
<div className="card" style={{ marginBottom: '1rem' }}>
|
||||
<p style={{ color: '#dce4f0', fontSize: 13, lineHeight: 1.6, margin: 0 }}>
|
||||
TetaRdPG est un RPG textuel idle où chaque action coûte de l'endurance.
|
||||
Combattez des monstres, récoltez des matériaux, craftez de l'équipement et forgez-le pour devenir plus fort.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h4 style={{ color: '#dce4f0', margin: '1rem 0 0.5rem', fontSize: 14 }}>Comment progresser ?</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
|
||||
{[
|
||||
{ step: '1', title: 'Combattre', desc: 'Affrontez des monstres pour gagner XP, Or et matériaux.' },
|
||||
{ step: '2', title: 'Récolter', desc: 'Chaque monstre peut dropper un matériau propre à sa zone.' },
|
||||
{ step: '3', title: 'Crafter', desc: 'Utilisez vos matériaux pour fabriquer des armes et armures.' },
|
||||
{ step: '4', title: 'Forger', desc: 'Améliorez votre équipement (+1 à +5) pour des bonus de stats.' },
|
||||
].map(s => (
|
||||
<div key={s.step} className="card" style={{ padding: '0.75rem' }}>
|
||||
<div style={{ fontSize: 20, color: '#f4c94e', fontWeight: 800 }}>{s.step}</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#dce4f0', marginBottom: 4 }}>{s.title}</div>
|
||||
<div style={{ fontSize: 11, color: '#6b7a99' }}>{s.desc}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h4 style={{ color: '#dce4f0', margin: '1rem 0 0.5rem', fontSize: 14 }}>Mécaniques clés</h4>
|
||||
<div className="card" style={{ fontSize: 12, lineHeight: 1.8, color: '#9ca3af' }}>
|
||||
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#dce4f0' }}>Endurance</strong> — Chaque combat coûte 5. Recharge : 1 point / 6 min. Max : 100+.</p>
|
||||
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#dce4f0' }}>Types d'attaque</strong> — Mêlée (Force ×1.5), Distance (Agilité ×1.5), Magie (Intelligence ×1.5).</p>
|
||||
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#dce4f0' }}>Drop rate</strong> — Varie selon la difficulté du monstre : 25% (facile) → 80% (boss). Les boss droppent 2-3 matériaux.</p>
|
||||
<p style={{ margin: 0 }}><strong style={{ color: '#dce4f0' }}>Défaite</strong> — Perte d'endurance (−25), PV réduits à 20%, perte de 5% de l'or.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Zones ──
|
||||
|
||||
function ZonesTab() {
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Les Zones</h3>
|
||||
<p style={{ color: '#6b7a99', fontSize: 12, marginBottom: '1rem' }}>
|
||||
Progressez de zone en zone en complétant les arcs de quêtes. Chaque zone a ses monstres, matériaux et équipements.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{ZONES.map((z, i) => (
|
||||
<div key={z.id} className="card" style={{ borderLeft: `3px solid ${z.color}` }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
||||
<span style={{ fontSize: 24 }}>{z.emoji}</span>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, color: z.color, fontSize: 15 }}>{z.name}</div>
|
||||
<div style={{ fontSize: 11, color: '#6b7a99' }}>{z.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#9ca3af' }}>
|
||||
{i === 0 ? '🔓 Toujours accessible' : `🔒 Déblocage : compléter l'arc de quêtes ${ZONES[i - 1].name}`}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Bestiaire ──
|
||||
|
||||
function BestiaryTab({ monsters, materials }: { monsters: (Monster & { zone: string })[]; materials: any[] }) {
|
||||
const matMap = new Map<string, any>(materials.map(m => [m.id, m]));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Bestiaire</h3>
|
||||
{ZONES.map(zone => {
|
||||
const zoneMonsters = monsters.filter(m => m.zone === zone.id).sort((a, b) => a.minLevel - b.minLevel);
|
||||
if (!zoneMonsters.length) return null;
|
||||
return (
|
||||
<div key={zone.id} style={{ marginBottom: '1.5rem' }}>
|
||||
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: zone.color }}>
|
||||
{zone.emoji} {zone.name}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{zoneMonsters.map(m => {
|
||||
const dropMat = m.dropMaterialId ? matMap.get(m.dropMaterialId) : null;
|
||||
return (
|
||||
<div key={m.id} className="card" style={{ padding: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<span style={{ fontWeight: 700, color: '#dce4f0', fontSize: 13 }}>{m.name}</span>
|
||||
<span className="badge badge-green" style={{ fontSize: 10 }}>Niv. {m.minLevel}–{m.maxLevel}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4, marginBottom: 6 }}>
|
||||
<StatBar label="PV" value={m.hp} max={300} color="#e84040" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 16, fontSize: 11, color: '#6b7a99' }}>
|
||||
<span>⚔️ {m.attack}</span>
|
||||
<span>🛡️ {m.defense}</span>
|
||||
<span>⭐ {m.xpReward} XP</span>
|
||||
<span>💰 {m.goldMin}–{m.goldMax}</span>
|
||||
<span style={{ textTransform: 'capitalize' }}>🎯 {m.attackType}</span>
|
||||
</div>
|
||||
{dropMat && (
|
||||
<div style={{ marginTop: 6, fontSize: 11, color: '#f4c94e' }}>
|
||||
🎁 Drop : {dropMat.name} <RarityBadge rarity={dropMat.rarity} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Équipement ──
|
||||
|
||||
function ItemsTab({ items }: { items: Item[] }) {
|
||||
const equipment = items.filter(i => i.type !== 'consumable');
|
||||
const consumables = items.filter(i => i.type === 'consumable');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Équipement</h3>
|
||||
|
||||
{ZONES.map(zone => {
|
||||
const zoneItems = equipment.filter(i => (i as any).zone === zone.id).sort((a, b) => a.attackBonus + a.defenseBonus - b.attackBonus - b.defenseBonus);
|
||||
if (!zoneItems.length) return null;
|
||||
return (
|
||||
<div key={zone.id} style={{ marginBottom: '1.5rem' }}>
|
||||
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: zone.color }}>
|
||||
{zone.emoji} {zone.name}
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||||
{zoneItems.map(item => (
|
||||
<div key={item.id} className="card" style={{ padding: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ fontWeight: 700, color: RARITY_COLORS[item.rarity], fontSize: 12 }}>
|
||||
{item.type === 'weapon' ? '⚔️' : '🛡️'} {item.name}
|
||||
</span>
|
||||
<RarityBadge rarity={item.rarity} />
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6b7a99', marginBottom: 4 }}>{item.description}</div>
|
||||
<div style={{ fontSize: 11, color: '#dce4f0' }}>
|
||||
{item.attackBonus > 0 && <span>ATK +{item.attackBonus} </span>}
|
||||
{item.defenseBonus > 0 && <span>DEF +{item.defenseBonus} </span>}
|
||||
{item.agiliteBonus > 0 && <span>AGI +{item.agiliteBonus} </span>}
|
||||
{item.intelligenceBonus > 0 && <span>INT +{item.intelligenceBonus} </span>}
|
||||
{(item as any).buyPrice > 0 && <span style={{ color: '#f4c94e' }}>💰 {(item as any).buyPrice}</span>}
|
||||
{(item as any).buyPrice === 0 && <span style={{ color: '#a78bfa' }}>🔨 Craft only</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{consumables.length > 0 && (
|
||||
<div style={{ marginBottom: '1.5rem' }}>
|
||||
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: '#e84040' }}>🧪 Consommables</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||||
{consumables.map(item => (
|
||||
<div key={item.id} className="card" style={{ padding: '0.75rem' }}>
|
||||
<div style={{ fontWeight: 700, color: '#dce4f0', fontSize: 12, marginBottom: 4 }}>
|
||||
🧪 {item.name}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6b7a99' }}>{item.description}</div>
|
||||
{(item as any).buyPrice > 0 && (
|
||||
<div style={{ fontSize: 11, color: '#f4c94e', marginTop: 4 }}>💰 {(item as any).buyPrice}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Artisanat ──
|
||||
|
||||
function CraftTab({ recipes, materials }: { recipes: Recipe[]; materials: any[] }) {
|
||||
const matMap = new Map<string, any>(materials.map(m => [m.id, m]));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Artisanat</h3>
|
||||
<p style={{ color: '#6b7a99', fontSize: 12, marginBottom: '1rem' }}>
|
||||
Combinez des matériaux droppés par les monstres pour crafter de l'équipement unique — souvent meilleur que la boutique.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{recipes.sort((a, b) => a.enduranceCost - b.enduranceCost).map(recipe => (
|
||||
<div key={recipe.id} className="card" style={{ padding: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<span style={{ fontWeight: 700, color: RARITY_COLORS[recipe.resultItem?.rarity] ?? '#dce4f0', fontSize: 13 }}>
|
||||
{recipe.resultItem?.type === 'weapon' ? '⚔️' : recipe.resultItem?.type === 'armor' ? '🛡️' : '🧪'} {recipe.resultItem?.name ?? recipe.name}
|
||||
</span>
|
||||
{recipe.resultItem && <RarityBadge rarity={recipe.resultItem.rarity} />}
|
||||
</div>
|
||||
{recipe.resultItem && (
|
||||
<div style={{ fontSize: 11, color: '#dce4f0', marginBottom: 6 }}>
|
||||
{recipe.resultItem.attackBonus > 0 && <span>ATK +{recipe.resultItem.attackBonus} </span>}
|
||||
{recipe.resultItem.defenseBonus > 0 && <span>DEF +{recipe.resultItem.defenseBonus} </span>}
|
||||
{recipe.resultItem.agiliteBonus > 0 && <span>AGI +{recipe.resultItem.agiliteBonus} </span>}
|
||||
{recipe.resultItem.intelligenceBonus > 0 && <span>INT +{recipe.resultItem.intelligenceBonus} </span>}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 11, color: '#9ca3af', marginBottom: 6 }}>
|
||||
<span>⏱️ {recipe.craftDurationSeconds}s</span>
|
||||
<span style={{ marginLeft: 12 }}>⚡ {recipe.enduranceCost} endurance</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6b7a99' }}>
|
||||
Ingrédients : {recipe.ingredients.map((ing, i) => {
|
||||
const mat = matMap.get(ing.materialId);
|
||||
return (
|
||||
<span key={i}>
|
||||
{i > 0 && ' + '}
|
||||
<span style={{ color: RARITY_COLORS[mat?.rarity] ?? '#dce4f0' }}>
|
||||
{ing.quantity}× {mat?.name ?? '???'}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Forge ──
|
||||
|
||||
function ForgeTab() {
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Forge</h3>
|
||||
<p style={{ color: '#6b7a99', fontSize: 12, marginBottom: '1rem' }}>
|
||||
Améliorez vos armes et armures de +1 à +5. Les niveaux 1-2 sont garantis. A partir du +3, il y a un risque d'échec — l'or et l'endurance sont perdus même en cas d'échec.
|
||||
</p>
|
||||
<div className="card">
|
||||
<table style={{ width: '100%', fontSize: 12, borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '1px solid #2a3448', color: '#6b7a99', fontSize: 11 }}>
|
||||
<th style={{ padding: '8px 4px', textAlign: 'left' }}>Niveau</th>
|
||||
<th style={{ padding: '8px 4px', textAlign: 'right' }}>Or</th>
|
||||
<th style={{ padding: '8px 4px', textAlign: 'right' }}>Endurance</th>
|
||||
<th style={{ padding: '8px 4px', textAlign: 'right' }}>Risque</th>
|
||||
<th style={{ padding: '8px 4px', textAlign: 'right' }}>Bonus total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{FORGE_TABLE.map(row => (
|
||||
<tr key={row.level} style={{ borderBottom: '1px solid #1e2535' }}>
|
||||
<td style={{ padding: '6px 4px', color: '#f4c94e', fontWeight: 700 }}>+{row.level}</td>
|
||||
<td style={{ padding: '6px 4px', textAlign: 'right', color: '#dce4f0' }}>{row.gold} 💰</td>
|
||||
<td style={{ padding: '6px 4px', textAlign: 'right', color: '#5ba4f5' }}>{row.endurance} ⚡</td>
|
||||
<td style={{ padding: '6px 4px', textAlign: 'right', color: row.risk === '0%' ? '#3ddc84' : '#e84040' }}>{row.risk}</td>
|
||||
<td style={{ padding: '6px 4px', textAlign: 'right', color: '#a78bfa', fontWeight: 700 }}>{row.bonus}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="card" style={{ marginTop: '0.75rem', fontSize: 12, color: '#9ca3af', lineHeight: 1.6 }}>
|
||||
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#3ddc84' }}>Niv. 1-2</strong> — Succès garanti. Aucun risque.</p>
|
||||
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#e84040' }}>Niv. 3-5</strong> — Risque croissant. En cas d'échec : or et endurance perdus, équipement intact.</p>
|
||||
<p style={{ margin: 0 }}><strong style={{ color: '#f4c94e' }}>Bonus</strong> — +2 ATK (armes) ou +2 DEF (armures) par niveau de forge.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tab: Boutique ──
|
||||
|
||||
function ShopTab({ items }: { items: Item[] }) {
|
||||
const shopItems = items.filter(i => (i as any).buyPrice > 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Boutique</h3>
|
||||
<p style={{ color: '#6b7a99', fontSize: 12, marginBottom: '1rem' }}>
|
||||
Achetez de l'équipement avec votre or. Les items de la boutique sont accessibles dès que vous débloquez la zone correspondante.
|
||||
Revente : 40% du prix + 50% de l'investissement de forge.
|
||||
</p>
|
||||
{ZONES.map(zone => {
|
||||
const zoneShop = shopItems.filter(i => (i as any).zone === zone.id).sort((a, b) => (a as any).buyPrice - (b as any).buyPrice);
|
||||
if (!zoneShop.length) return null;
|
||||
return (
|
||||
<div key={zone.id} style={{ marginBottom: '1.5rem' }}>
|
||||
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: zone.color }}>
|
||||
{zone.emoji} {zone.name}
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||||
{zoneShop.map(item => (
|
||||
<div key={item.id} className="card" style={{ padding: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
|
||||
<span style={{ fontWeight: 700, color: '#dce4f0', fontSize: 12 }}>
|
||||
{item.type === 'weapon' ? '⚔️' : item.type === 'armor' ? '🛡️' : '🧪'} {item.name}
|
||||
</span>
|
||||
<span style={{ color: '#f4c94e', fontSize: 12, fontWeight: 700 }}>{(item as any).buyPrice} 💰</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#6b7a99' }}>
|
||||
{item.attackBonus > 0 && <span>ATK +{item.attackBonus} </span>}
|
||||
{item.defenseBonus > 0 && <span>DEF +{item.defenseBonus} </span>}
|
||||
<span> · Niv. {(item as any).minLevel}+</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Potions */}
|
||||
{(() => {
|
||||
const potions = shopItems.filter(i => i.type === 'consumable');
|
||||
if (!potions.length) return null;
|
||||
return (
|
||||
<div>
|
||||
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: '#e84040' }}>🧪 Potions</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
|
||||
{potions.map(item => (
|
||||
<div key={item.id} className="card" style={{ padding: '0.75rem' }}>
|
||||
<div style={{ fontWeight: 700, color: '#dce4f0', fontSize: 12, marginBottom: 4 }}>{item.name}</div>
|
||||
<div style={{ fontSize: 11, color: '#6b7a99' }}>{item.description}</div>
|
||||
<div style={{ fontSize: 11, color: '#f4c94e', marginTop: 4 }}>{(item as any).buyPrice} 💰</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Page ──
|
||||
|
||||
export function GuidePage() {
|
||||
const [tab, setTab] = useState('start');
|
||||
|
||||
const { data: monsters = [] } = useQuery({ queryKey: ['guide-monsters'], queryFn: guideApi.monsters });
|
||||
const { data: items = [] } = useQuery({ queryKey: ['guide-items'], queryFn: guideApi.items });
|
||||
const { data: materials = [] } = useQuery({ queryKey: ['guide-materials'], queryFn: guideApi.materials });
|
||||
const { data: recipes = [] } = useQuery({ queryKey: ['guide-recipes'], queryFn: guideApi.recipes });
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 900, margin: '0 auto', padding: '2rem 1rem' }}>
|
||||
{/* Header */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
|
||||
<h1 style={{ color: '#f4c94e', fontSize: 28, margin: '0 0 0.25rem', fontWeight: 800 }}>
|
||||
📖 Guide du Têtard
|
||||
</h1>
|
||||
<p style={{ color: '#6b7a99', fontSize: 13, margin: 0 }}>
|
||||
Tout ce qu'il faut savoir pour survivre dans le monde de TetaRdPG
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab navigation */}
|
||||
<div style={{
|
||||
display: 'flex', gap: 4, marginBottom: '1.5rem', overflowX: 'auto',
|
||||
borderBottom: '1px solid #2a3448', paddingBottom: 8,
|
||||
}}>
|
||||
{TABS.map(t => {
|
||||
const Icon = t.icon;
|
||||
const active = tab === t.id;
|
||||
return (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setTab(t.id)}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 14px', border: 'none', borderRadius: 6,
|
||||
background: active ? '#f4c94e22' : 'transparent',
|
||||
color: active ? '#f4c94e' : '#6b7a99',
|
||||
fontWeight: active ? 700 : 500, fontSize: 12,
|
||||
cursor: 'pointer', whiteSpace: 'nowrap',
|
||||
transition: 'all 0.15s',
|
||||
}}
|
||||
>
|
||||
<Icon size={14} />
|
||||
{t.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
{tab === 'start' && <StartTab />}
|
||||
{tab === 'zones' && <ZonesTab />}
|
||||
{tab === 'bestiary' && <BestiaryTab monsters={monsters} materials={materials} />}
|
||||
{tab === 'items' && <ItemsTab items={items} />}
|
||||
{tab === 'craft' && <CraftTab recipes={recipes} materials={materials} />}
|
||||
{tab === 'forge' && <ForgeTab />}
|
||||
{tab === 'shop' && <ShopTab items={items} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { PlayerQuestArc } from '../quest/player-quest-arc.entity';
|
||||
import { getUnlockedZones } from '../common/zone-access';
|
||||
|
||||
@Controller('monsters')
|
||||
@UseGuards(AuthGuard)
|
||||
export class MonsterController {
|
||||
constructor(
|
||||
private readonly monsterService: MonsterService,
|
||||
@@ -22,7 +21,14 @@ export class MonsterController {
|
||||
private readonly playerArcRepo: Repository<PlayerQuestArc>,
|
||||
) {}
|
||||
|
||||
@Get('bestiary')
|
||||
async getBestiary() {
|
||||
const allMonsters = await this.monsterService.findAll();
|
||||
return allMonsters;
|
||||
}
|
||||
|
||||
@Get('zones')
|
||||
@UseGuards(AuthGuard)
|
||||
async getZones(@Req() req: Request) {
|
||||
const user = (req as any).user;
|
||||
const char = await this.characterRepo.findOne({ where: { userId: user.id } });
|
||||
@@ -43,6 +49,7 @@ export class MonsterController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard)
|
||||
async findAll(@Req() req: Request) {
|
||||
const user = (req as any).user;
|
||||
const char = await this.characterRepo.findOne({ where: { userId: user.id } });
|
||||
|
||||
Reference in New Issue
Block a user