# Flesh and Blood (FAB)

# Flesh and Blood (FAB)

> _Dernière mise à jour : 2026-05-10_

## Source

- **Package npm** : `@flesh-and-blood/cards` + `@flesh-and-blood/types` (LSS, ~3.6.246)
- **Bundle Docker** : la data est embarquée dans l'image Docker au build (pas téléchargée à runtime)
- **Collection Qdrant** : `flesh-and-blood-cards`

## Code

| Fichier | Rôle |
|---|---|
| `src/services/cards/sources/flesh-and-blood.ts` | CardSource utilisant le package npm |
| `scripts/flesh-and-blood-cards/ingest.ts` | Push Qdrant |
| `scripts/flesh-and-blood-cards/link-game.ts` | Lie une ligne `games` à `hasCardDatabase = 'flesh-and-blood-cards'` |
| `src/services/decks/parse-decklist.ts` | Parser tolérant Fabrary / FABDB / GEM |
| `src/services/decks/match-decklist.ts` | Index `byFullName` + `byBaseName` du cache cartes |
| `src/routes/decks.ts` | `POST /api/decks/parse` |

## Payload Qdrant `flesh-and-blood-cards`

```typescript
{
  id: pointId,
  name: string,
  name_en: string,
  set_label: string,
  rarity: string,
  card_type: 'Action' | 'Attack' | 'Defense Reaction' | 'Equipment' | 'Hero' | ...,
  pitch: 1 | 2 | 3 | null,    // red/yellow/blue
  attack: number,
  defense: number,
  cost: number,
  card_legal_heroes: string[],  // ['Briar', 'Lexi', 'Dash', ...]
  classes: string[],            // ['Ranger', 'Wizard']
  talents: string[],            // ['Earth', 'Lightning']
  text: string,
  image_url: string,
}
```

## Symboles UI

| Token | Symbole | PNG |
|---|---|---|
| `{r}` | Red pitch | `/fab-icons/icon_r.png` |
| `{p}` | Power | `/fab-icons/icon_p.png` |
| `{d}` | Defense | `/fab-icons/icon_d.png` |
| `{h}` | Health | `/fab-icons/icon_h.png` |
| `{i}` | Intelligence | `/fab-icons/icon_i.png` |
| `{c}` | Cost | `/fab-icons/icon_c.png` |
| `{u}` | Currency | `/fab-icons/icon_u.png` |
| `{t}` | Tap | `/fab-icons/icon_t.png` |

PNG officiels LSS depuis `rules.fabtcg.com` (cf. mémoire `project_fab_symbols_done.md`, commit `2a77d94`).

Helper : `frontend/src/lib/fab-symbols.ts`. Whitelist regex stricte `[rpdhicut]` minuscules — sinon collision avec `{R}` MTG (majuscules).

## Import de deck (Fabrary)

Workflow : Fabrary → "Copy as Text" → coller dans `DeckImportModal` → preview → attacher au chat.

**Choix de design : on ne fetch PAS Fabrary.** L'app Fabrary est une SPA React avec API GraphQL AWS AppSync protégée par Cognito Identity Pool + signature SigV4. Implémenter ce flow côté serveur serait fragile (endpoint non documenté) et lourd. Le format texte standard FAB est stable, fonctionne avec tous les builders communauté (Fabrary, FABDB, GEM), et se passe d'auth.

### Parser tolérant (`parse-decklist.ts`)

Accepte :
- `3 Card`, `(3) Card`, `3x Card`, `3 - Card`
- Suffixe pitch optionnel : `(red|yellow|blue)`
- En-têtes : `Hero:`, `Weapon:`, `Equipment:`, `Deck:`/`Mainboard:`/`Pitch 1/2/3`, `Sideboard:`, `Name:`, `Format:`
- Lignes vides et commentaires `//` ou `#` ignorés

⚠️ **Le parser exige une quantité numérique en tête** : sans ça, les lignes parasites du footer Fabrary (`Voir le deck complet @ https://...`, `Fait avec ♥`) seraient comptées comme cartes à qty=1 et pollueraient `unmatched`.

Décodage automatique `decodeURIComponent` quand le presse-papier mobile renvoie du `%20`/`%0A`.

### Matcher (`match-decklist.ts`)

- Index `byFullName` : match exact `<name> (<pitch>)`
- Index `byBaseName` : match sur le nom de base si pas de pitch
- Quand l'utilisateur ne précise pas le pitch et qu'il existe plusieurs variantes : priorité **red > yellow > blue** + flag `pitchAssumed=true` surfacé dans la preview

### Whitelist côté backend

`POST /api/decks/parse` vérifie :
- `requireAuth` + `requireConfirmed`
- Rate-limit 10/min/user
- `game.contentType === 'base'`
- `game.hasCardDatabase ∈ SUPPORTED_COLLECTIONS = ['flesh-and-blood-cards']`

Pour étendre à un autre TCG : ajouter à `SUPPORTED_COLLECTIONS` + créer parser+matcher dédié pour le format de ce TCG.

## Méta-game

`META_FAB_TOURNAMENT_ENABLED=true` active la sync `services/meta/fabtcg.ts` :

- Scrape `fabtcg.com` (officiel LSS)
- Formats configurables : `META_FAB_TOURNAMENT_FORMATS=classic-constructed,blitz,living-legend,silver-age`
- Cap : `META_FAB_TOURNAMENT_MAX_DECKS=50` decks par format
- Âge max : `META_FAB_TOURNAMENT_MAX_AGE_DAYS=60`
- Rate-limit : `META_FAB_RATE_LIMIT_MS=1500` (Cloudflare-friendly)
- User-Agent : `META_FAB_USER_AGENT` (header Cloudflare-compatible)

## Workflow update

⚠️ Cas spécial : la data FAB est **bundlée dans l'image Docker** via le package npm. Mettre à jour les cartes nécessite un rebuild + redeploy AVANT le resync. Cf. `mettre-a-jour-cartes.md`.