# Système d'équipement instancié — Design Document

## Vue d'ensemble

Chaque arme ou armure droppée ou achetée en boutique est une **instance unique** générée procéduralement. La rareté de l'instance est héritée de l'équipement parent (un item Légendaire reste Légendaire). Chaque instance reçoit entre **0 et 2 affixes** tirés aléatoirement parmi les affixes compatibles.

Les objets consommables (`ItemMmo`) ne sont **jamais instanciés** — ils continuent à fonctionner comme aujourd'hui.

---

## Entités à créer / modifier

### 1. `AffixMmo` (nouvelle entité)

| Colonne | Type | Description |
|---|---|---|
| `id` | int PK | |
| `name` | varchar(120) | Label affiché au joueur (ex. "Fureur") |
| `nameFragment` | varchar(80) | Mot utilisé dans le nom généré (ex. "Furieuse" / "de la Fureur") |
| `placement` | varchar(10) | `prefix` ou `suffix` |
| `effectType` | varchar(40) | Voir liste ci-dessous |
| `statKey` | varchar(20) \| null | Pour `effectType = stat` : quelle stat (`attaque`, `defense`…) |
| `minValue` | int | Borne basse du roll |
| `maxValue` | int | Borne haute du roll |
| `rarityWeight` | int | Poids pour la sélection aléatoire (plus élevé = plus fréquent) |
| `compatibleWeaponTypes` | json \| null | Tableau d'IDs de `WeaponType`. `null` = compatible toutes armes |
| `compatibleArmorSlots` | json \| null | Tableau d'indices de slots (0–5). `null` = compatible toutes armures |
| `appliesToWeapons` | bool | Peut apparaître sur les armes |
| `appliesToArmors` | bool | Peut apparaître sur les armures |

#### `effectType` supportés

| Valeur | Effet |
|---|---|
| `stat` | Bonus sur une stat (`statKey` requis) |
| `regen_pv` | Régénération de PV par tour |
| `regen_eg` | Régénération d'EG par tour |
| `counter_physical` | Bonus % chances de contre physique |
| `counter_magical` | Bonus % chances de contre magique |
| `critical_rate` | Bonus % taux de critique |
| `elemental_resist` | Résistance élémentaire (valeur = % bonus) |
| `status_inflict` | Inflige un statut (valeur = probabilité %) — à étendre si besoin |

---

### 2. `EquipmentInstance` (nouvelle entité)

| Colonne | Type | Description |
|---|---|---|
| `id` | int PK | |
| `baseType` | varchar(10) | `weapon` ou `armor` |
| `baseId` | int | ID du `WeaponMmo` ou `ArmorMmo` parent |
| `name` | varchar(255) | Nom généré (ex. "Épée Furieuse de la Précision") |
| `affixes` | json | `[{affix_id, value, effectType, statKey, name}]` |
| `statsSnapshot` | json | Stats finales calculées à la génération (base + affixes) |
| `price` | int | Prix en Dollawrs (= `baseItem.value`, voir note) |
| `createdAt` | datetime | |

> **Note prix** : le prix reste `baseItem.value` pour l'instant. Un multiplicateur par rareté pourra être ajouté ultérieurement.

#### Exemple de `affixes`
```json
[
  { "affix_id": 3, "value": 12, "effectType": "stat", "statKey": "attaque", "name": "Fureur" },
  { "affix_id": 7, "value": 5,  "effectType": "critical_rate", "statKey": null, "name": "Précision" }
]
```

#### Exemple de `statsSnapshot`
```json
{ "pvMax": 0, "egMax": 0, "attaque": 22, "defense": 0, "arcane": 0, "sagesse": 0, "vitesse": 0, "finesse": 5 }
```
(base arme : +10 ATQ ; affixes : +12 ATQ, +5 FIN → snapshot ATQ = 22, FIN = 5)

---

### 3. `EnemyMmo` — nouvelle colonne `dropTable`

```php
// src/Entity/Mmo/EnemyMmo.php
#[ORM\Column(type: 'json', nullable: true)]
private ?array $dropTable = null;
```

Structure du JSON :
```json
[
  { "type": "weapon", "id": 3, "chance": 15 },
  { "type": "armor",  "id": 7, "chance": 10 },
  { "type": "item",   "id": 2, "chance": 40 }
]
```
- `type` : `weapon`, `armor` ou `item`
- `id` : ID du `WeaponMmo` / `ArmorMmo` / `ItemMmo` parent (le générateur s'occupe de produire l'instance)
- `chance` : probabilité en % par ennemi vaincu (0–100, indépendants entre eux)

Admin : champ JSON textarea dans le formulaire `EnemyMmo` (même pattern que les champs JSON existants).

---

### 4. `Player` — nouvelle colonne `shopCaches`

```php
// src/Entity/Player.php
#[ORM\Column(type: 'json')]
private array $shopCaches = [];
```

Structure du JSON :
```json
{
  "12": {
    "instanceIds": [45, 46, 47],
    "directItems": [{"type":"item","id":3},{"type":"item","id":7}],
    "refreshAt": "2026-06-01 14:00:00"
  }
}
```
- Clé = `zoneId` (string)
- `instanceIds` : IDs d'`EquipmentInstance` réservées pour ce joueur dans cette boutique
- `directItems` : objets consommables du pool (non instanciés, servis tels quels)
- `refreshAt` : prochain renouvellement

---

### 4. `Player` — modification de `inventory`

Les instances achetées/droppées s'ajoutent avec le type `weapon_instance` ou `armor_instance` :
```json
{ "type": "weapon_instance", "id": 45, "qty": 1 }
```
`syncInventory()` et les routes existantes gèrent déjà les types arbitraires — seule la **résolution** (affichage du nom, icône, stats) devra distinguer les instances des items de base.

---

## Service de génération — `EquipmentGeneratorService`

```
src/Service/EquipmentGeneratorService.php
```

### Algorithme `generateFromBase(string $type, int $baseId): EquipmentInstance`

1. Charger le `WeaponMmo` ou `ArmorMmo` parent
2. Tirer le nombre d'affixes : `rand(0, 2)`
3. Si n > 0 :
   - Filtrer les `AffixMmo` compatibles avec ce type/slot (`appliesToWeapons` ou `appliesToArmors` + `compatibleWeaponTypes`/`compatibleArmorSlots` si renseignés)
   - **Sélection pondérée sans remise** :
     1. Construire un tableau `[affix_id repeated rarityWeight times]` (pool pondéré)
     2. Mélanger (`shuffle`)
     3. Piocher n éléments distincts (ignorer les doublons d'affix_id)
   - Pour chaque affix sélectionné : `value = rand(affix.minValue, affix.maxValue)`
   - Garantir qu'un même affix n'apparaît pas deux fois (sans remise)
4. Calculer `statsSnapshot` :
   - Partir des 8 stats du base item
   - Appliquer les affixes de type `stat` (`statKey` + `value`)
   - Les autres effectTypes (`regen_pv`, etc.) sont stockés dans `affixes` mais n'impactent pas le snapshot (ils sont lus séparément à l'application en combat)
5. Générer le nom :
   - Préfixes (placement=`prefix`) : ajoutés **avant** le nom de base
   - Suffixes (placement=`suffix`) : ajoutés **après** avec "de"/"du" selon genre à définir
   - Exemple : "Furieuse Épée de la Précision"
6. Calculer le prix (`baseItem.getValue()`)
7. Persister et retourner l'instance

### Méthode `generateForShop(MapZone $zone, Player $player): void`

1. Lire `zone.shopItems` (pool complet)
2. Séparer armes/armures (→ instances) et objets (→ directs)
3. Tirer au maximum 10 items aléatoires dans le pool total
4. Pour chaque arme/armure tirée : appeler `generateFromBase()`
5. Écrire `player.shopCaches[zoneId]` avec les IDs d'instances + directItems + `refreshAt = now + 30min`
6. Purger les anciennes instances du cache précédent qui ne sont plus dans l'inventaire du joueur (nettoyage orphelins)

---

## Drop en combat — `BattleService::applyVictory()`

### Structure `dropTable` sur `EnemyMmo`

```json
[
  { "type": "weapon", "id": 3, "chance": 15 },
  { "type": "armor",  "id": 7, "chance": 10 },
  { "type": "item",   "id": 2, "chance": 40 }
]
```
`chance` = probabilité en % (0–100) par ennemi vaincu.

### Logique dans `applyVictory()`

```php
foreach ($battle['enemies'] as $enemy) {
    foreach ($enemy['dropTable'] as $drop) {
        if (rand(0, 99) < $drop['chance']) {
            if ($drop['type'] === 'weapon' || $drop['type'] === 'armor') {
                $instance = $this->generator->generateFromBase($drop['type'], $drop['id']);
                $player->addToInventory($drop['type'] . '_instance', $instance->getId());
            } else {
                $player->addToInventory($drop['type'], $drop['id']);
            }
        }
    }
}
```

### Affichage dans l'écran de victoire

Les drops sont inclus dans la réponse `applyVictory` et affichés dans `.party-xp-grid` ou une section dédiée "Butin" :
- Arme/armure instanciée : nom coloré selon la rareté du parent + affixes en sous-texte
- Objet : nom simple

---

## Boutiques avec instances

### Flux d'ouverture (`apiShopShow`)

```
Joueur ouvre boutique (zoneId = 12)
  └─ Lire player.shopCaches["12"]
       ├─ Absent ou refreshAt dépassé ?
       │    └─ generateForShop() → écrire le cache → flush
       └─ Lire instanceIds + directItems depuis le cache
            └─ Charger les EquipmentInstance + ItemMmo
                 └─ Retourner {instances, directItems, refreshAt}
```

### Achat d'une instance (`apiShopBuy` — adaptée)

1. Vérifier que `instanceId` est dans `player.shopCaches[zoneId].instanceIds`
2. Vérifier fonds suffisants
3. Débiter `instance.price`
4. Retirer l'instanceId du cache joueur
5. Ajouter `{type: 'weapon_instance'|'armor_instance', id: instanceId}` à l'inventaire
6. Flush

---

## Frontend — `show.html.twig`

### Sous-onglets dans l'onglet Acheter

Structure des onglets après ouverture d'une boutique :

```
[Acheter]  [Vendre]
  └─ [Armes ●]  [Armures]  [Objets ●]   ← apparaissent uniquement si la catégorie est présente
```

- L'onglet actif par défaut = premier sous-onglet non vide
- Les données de chaque sous-catégorie sont séparées dans `data.buyTabs` :
  ```json
  {
    "weapons":  [...instances...],
    "armors":   [...instances...],
    "items":    [...directItems...]
  }
  ```

### Affichage d'une instance dans le panneau détail (`#shop-desc`)

Au survol d'une carte d'instance (arme/armure) :
- Nom coloré selon la rareté
- Stats snapshot (grille 8 stats, identique au panneau encyclopédie existant)
- Section affixes : liste des affixes avec leur valeur rolled
  - Ex. `⚡ Fureur : +12 ATQ`
  - Ex. `✦ Précision : +5% critique`
- Description du base item

---

## Commande console

```
src/Command/GenerateEquipmentCommand.php
```

```bash
php bin/console mmo:equipment:generate --type=weapon --base-id=3 [--persist]
```

- Sans `--persist` : affiche l'instance dans le terminal (dry run)
- Avec `--persist` : enregistre en base et affiche l'ID

Affichage exemple :
```
Nom         : Épée Furieuse de la Précision
Rareté      : Rare (hérité)
Affixes (2) :
  - Fureur (prefix) : +12 ATQ  [pool: 5–15]
  - Précision (suffix) : +5% crit  [pool: 3–8]
Stats snapshot :
  ATQ +22  FIN +0  ...
Prix        : 450 Dollawrs
```

---

## Ordre d'implémentation

1. **Migration** — tables `affix_mmo`, `equipment_instance` ; colonne `shop_caches` sur `player`
2. **Entités** — `AffixMmo`, `EquipmentInstance`, méthodes `Player::getShopCaches()` etc.
3. **Admin CRUD** — `AffixMmo` (liste, new, edit) avec aperçu des compatibilités
4. **`EquipmentGeneratorService`** — génération d'instance + calcul snapshot + nommage
5. **Commande console** — `mmo:equipment:generate` pour valider le générateur
6. **Drop en combat** — `dropTable` sur `EnemyMmo` + intégration dans `applyVictory()`
7. **Boutiques instanciées** — `apiShopShow` + `generateForShop` + cache joueur + `apiShopBuy` adapté
8. **Frontend** — sous-onglets Armes/Armures/Objets + panneau détail avec affixes

---

## Points en suspens

- **Genre grammatical** pour la construction du nom (Épée *Furieuse* vs Bouclier *Furieux*) — pourrait être un champ `nameFragmentFeminine` / `nameFragmentMasculine` sur l'affix, ou géré par le nom de base de l'item
- **Affixes de type `status_inflict`** — nécessite une FK vers `StatusMmo` en plus de la `value` ; à étendre si besoin
- **Inventaire — résolution des instances** : `MapController::apiInventory()` et `syncInventory()` devront distinguer `weapon_instance` pour charger depuis `EquipmentInstance` plutôt que `WeaponMmo`
- **Vente d'instances** : l'onglet Vendre de la boutique doit aussi afficher les instances de l'inventaire (avec le même prix = `instance.price / 2`)
