diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7269022..3788622 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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() { } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 5b39fdd..bed7933 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -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; diff --git a/frontend/src/pages/GuidePage.tsx b/frontend/src/pages/GuidePage.tsx new file mode 100644 index 0000000..d4f53ed --- /dev/null +++ b/frontend/src/pages/GuidePage.tsx @@ -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('/items'), + materials: () => api.get('/materials'), + recipes: () => api.get('/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 = { + common: '#9ca3af', rare: '#5ba4f5', epic: '#a78bfa', legendary: '#f4c94e', +}; + +const RARITY_LABELS: Record = { + 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 ( + + {RARITY_LABELS[rarity] ?? rarity} + + ); +} + +function StatBar({ label, value, max, color }: { label: string; value: number; max: number; color: string }) { + return ( +
+ {label} +
+
+
+ {value} +
+ ); +} + +// ── Tab: Démarrer ── + +function StartTab() { + return ( +
+

Bienvenue dans TetaRdPG

+
+

+ 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. +

+
+ +

Comment progresser ?

+
+ {[ + { 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 => ( +
+
{s.step}
+
{s.title}
+
{s.desc}
+
+ ))} +
+ +

Mécaniques clés

+
+

Endurance — Chaque combat coûte 5. Recharge : 1 point / 6 min. Max : 100+.

+

Types d'attaque — Mêlée (Force ×1.5), Distance (Agilité ×1.5), Magie (Intelligence ×1.5).

+

Drop rate — Varie selon la difficulté du monstre : 25% (facile) → 80% (boss). Les boss droppent 2-3 matériaux.

+

Défaite — Perte d'endurance (−25), PV réduits à 20%, perte de 5% de l'or.

+
+
+ ); +} + +// ── Tab: Zones ── + +function ZonesTab() { + return ( +
+

Les Zones

+

+ Progressez de zone en zone en complétant les arcs de quêtes. Chaque zone a ses monstres, matériaux et équipements. +

+
+ {ZONES.map((z, i) => ( +
+
+ {z.emoji} +
+
{z.name}
+
{z.desc}
+
+
+
+ {i === 0 ? '🔓 Toujours accessible' : `🔒 Déblocage : compléter l'arc de quêtes ${ZONES[i - 1].name}`} +
+
+ ))} +
+
+ ); +} + +// ── Tab: Bestiaire ── + +function BestiaryTab({ monsters, materials }: { monsters: (Monster & { zone: string })[]; materials: any[] }) { + const matMap = new Map(materials.map(m => [m.id, m])); + + return ( +
+

Bestiaire

+ {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 ( +
+

+ {zone.emoji} {zone.name} +

+
+ {zoneMonsters.map(m => { + const dropMat = m.dropMaterialId ? matMap.get(m.dropMaterialId) : null; + return ( +
+
+ {m.name} + Niv. {m.minLevel}–{m.maxLevel} +
+
+ +
+
+ ⚔️ {m.attack} + 🛡️ {m.defense} + ⭐ {m.xpReward} XP + 💰 {m.goldMin}–{m.goldMax} + 🎯 {m.attackType} +
+ {dropMat && ( +
+ 🎁 Drop : {dropMat.name} +
+ )} +
+ ); + })} +
+
+ ); + })} +
+ ); +} + +// ── Tab: Équipement ── + +function ItemsTab({ items }: { items: Item[] }) { + const equipment = items.filter(i => i.type !== 'consumable'); + const consumables = items.filter(i => i.type === 'consumable'); + + return ( +
+

Équipement

+ + {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 ( +
+

+ {zone.emoji} {zone.name} +

+
+ {zoneItems.map(item => ( +
+
+ + {item.type === 'weapon' ? '⚔️' : '🛡️'} {item.name} + + +
+
{item.description}
+
+ {item.attackBonus > 0 && ATK +{item.attackBonus} } + {item.defenseBonus > 0 && DEF +{item.defenseBonus} } + {item.agiliteBonus > 0 && AGI +{item.agiliteBonus} } + {item.intelligenceBonus > 0 && INT +{item.intelligenceBonus} } + {(item as any).buyPrice > 0 && 💰 {(item as any).buyPrice}} + {(item as any).buyPrice === 0 && 🔨 Craft only} +
+
+ ))} +
+
+ ); + })} + + {consumables.length > 0 && ( +
+

🧪 Consommables

+
+ {consumables.map(item => ( +
+
+ 🧪 {item.name} +
+
{item.description}
+ {(item as any).buyPrice > 0 && ( +
💰 {(item as any).buyPrice}
+ )} +
+ ))} +
+
+ )} +
+ ); +} + +// ── Tab: Artisanat ── + +function CraftTab({ recipes, materials }: { recipes: Recipe[]; materials: any[] }) { + const matMap = new Map(materials.map(m => [m.id, m])); + + return ( +
+

Artisanat

+

+ Combinez des matériaux droppés par les monstres pour crafter de l'équipement unique — souvent meilleur que la boutique. +

+
+ {recipes.sort((a, b) => a.enduranceCost - b.enduranceCost).map(recipe => ( +
+
+ + {recipe.resultItem?.type === 'weapon' ? '⚔️' : recipe.resultItem?.type === 'armor' ? '🛡️' : '🧪'} {recipe.resultItem?.name ?? recipe.name} + + {recipe.resultItem && } +
+ {recipe.resultItem && ( +
+ {recipe.resultItem.attackBonus > 0 && ATK +{recipe.resultItem.attackBonus} } + {recipe.resultItem.defenseBonus > 0 && DEF +{recipe.resultItem.defenseBonus} } + {recipe.resultItem.agiliteBonus > 0 && AGI +{recipe.resultItem.agiliteBonus} } + {recipe.resultItem.intelligenceBonus > 0 && INT +{recipe.resultItem.intelligenceBonus} } +
+ )} +
+ ⏱️ {recipe.craftDurationSeconds}s + ⚡ {recipe.enduranceCost} endurance +
+
+ Ingrédients : {recipe.ingredients.map((ing, i) => { + const mat = matMap.get(ing.materialId); + return ( + + {i > 0 && ' + '} + + {ing.quantity}× {mat?.name ?? '???'} + + + ); + })} +
+
+ ))} +
+
+ ); +} + +// ── Tab: Forge ── + +function ForgeTab() { + return ( +
+

Forge

+

+ 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. +

+
+ + + + + + + + + + + + {FORGE_TABLE.map(row => ( + + + + + + + + ))} + +
NiveauOrEnduranceRisqueBonus total
+{row.level}{row.gold} 💰{row.endurance} ⚡{row.risk}{row.bonus}
+
+
+

Niv. 1-2 — Succès garanti. Aucun risque.

+

Niv. 3-5 — Risque croissant. En cas d'échec : or et endurance perdus, équipement intact.

+

Bonus — +2 ATK (armes) ou +2 DEF (armures) par niveau de forge.

+
+
+ ); +} + +// ── Tab: Boutique ── + +function ShopTab({ items }: { items: Item[] }) { + const shopItems = items.filter(i => (i as any).buyPrice > 0); + + return ( +
+

Boutique

+

+ 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. +

+ {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 ( +
+

+ {zone.emoji} {zone.name} +

+
+ {zoneShop.map(item => ( +
+
+ + {item.type === 'weapon' ? '⚔️' : item.type === 'armor' ? '🛡️' : '🧪'} {item.name} + + {(item as any).buyPrice} 💰 +
+
+ {item.attackBonus > 0 && ATK +{item.attackBonus} } + {item.defenseBonus > 0 && DEF +{item.defenseBonus} } + · Niv. {(item as any).minLevel}+ +
+
+ ))} +
+
+ ); + })} + + {/* Potions */} + {(() => { + const potions = shopItems.filter(i => i.type === 'consumable'); + if (!potions.length) return null; + return ( +
+

🧪 Potions

+
+ {potions.map(item => ( +
+
{item.name}
+
{item.description}
+
{(item as any).buyPrice} 💰
+
+ ))} +
+
+ ); + })()} +
+ ); +} + +// ── 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 ( +
+ {/* Header */} +
+

+ 📖 Guide du Têtard +

+

+ Tout ce qu'il faut savoir pour survivre dans le monde de TetaRdPG +

+
+ + {/* Tab navigation */} +
+ {TABS.map(t => { + const Icon = t.icon; + const active = tab === t.id; + return ( + + ); + })} +
+ + {/* Tab content */} + {tab === 'start' && } + {tab === 'zones' && } + {tab === 'bestiary' && } + {tab === 'items' && } + {tab === 'craft' && } + {tab === 'forge' && } + {tab === 'shop' && } +
+ ); +} diff --git a/src/monster/monster.controller.ts b/src/monster/monster.controller.ts index 9b12e70..b37a2d2 100644 --- a/src/monster/monster.controller.ts +++ b/src/monster/monster.controller.ts @@ -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, ) {} + @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 } });