# Chantier — plan compact du restaurant

> Dossier de reprise, écrit le 28/08/2026. Travail interrompu en cours d'étape 2.
> Branche de travail : **`big-egg-3d/`**. Lire `../CLAUDE.md` avant d'agir.

## La demande

Redimensionner le restaurant et refaire son plan :

1. Murs trois fois plus minces, salle deux fois moins large, hauteur × 0,75.
2. Tous les meubles transposés pour rester **à l'intérieur des murs**.
3. Trottoir trois fois plus large, clients qui contournent la file d'attente.
4. Puis, en complément : meubles **réduits** pour la circulation, étagère à **0**
   au départ, salle **élargie minimement**, maximum **5 tables**, console
   pouvant être **adossée au mur**.
5. Trottoir en **L** : une bande nord (est-ouest) **plus** la bande est
   existante. Les clients arrivent au coin **nord-ouest**, longent le nord
   jusqu'au coin **nord-est**, puis descendent vers le coin **sud-est** (ou
   l'inverse).

Les points 1 à 3 ont été faits par un agent précédent (Codex), qui s'est arrêté
avant de valider. Les points 4 et 5 sont en cours.

---

## État du dépôt à l'interruption

| | |
|---|---|
| `smoke_test` | **2 échecs + un blocage** (code de sortie 124) |
| `bridge_3d_check` | vert à la fin de l'étape 1, **non rejoué** depuis |
| `bridge_3d_flow_check` | non rejoué depuis l'étape 1 |
| Suites JS / PHP | **non rejouées** — les données ont changé, elles doivent l'être |

**Sauvegardes disponibles** dans `_backups/` :

- `big-egg-3d_avant-plan-compact_20260827-1747` — l'état laissé par Codex,
  10 échecs, cause connue (voir plus bas). **Point de retour sûr.**
- `big-egg-3d_TENTATIVE-plan-compact-incomplete_20260827-1806` — une première
  tentative abandonnée (58 échecs). À ne consulter que pour mémoire.
- `big-egg-3d_avant-effets-attaque_20260827-1121` — antérieure au travail de
  Codex, utile pour un diff.

---

## Ce qui est fait

### Étape 1 — tailles, salle, assertions (terminée, validée)

- Les 9 meubles réduits de **15 %** (`× 0,85`) dans
  `data/game_data_master.json` **et** `tools/game-data-manager/data/draft.json`.
  Four 96×80 → 82×68, comptoir 142×72 → 121×61, ardoise 210×150 → 178×128, etc.
- `ROOM_RECT` : `Rect2(510, 200, 900, 652.5)` → `Rect2(510, 200, 1040, 652.5)`.
- Les deux assertions de dimensions de Codex réécrites dans `smoke_test.gd` :
  elles figeaient « 900 de large » et « centrée sur 960 ». La seconde est
  remplacée par la contrainte réelle — la salle **et son trottoir** tiennent dans
  les 1920 de large.
- **Correctif de fond** : `_build_takeout_counter()` écrivait
  `takeout_prop.size = Vector2(142, 72)` **en dur**. `apply_layout()` corrigeait
  la taille ensuite, mais trop tard : les deux zones de service se taillent sur
  `takeout_prop.size` juste après, et restaient dimensionnées pour l'ancien
  comptoir. La taille vient maintenant de la fiche
  (`DataCatalog.RESTAURANT_ELEMENTS`). C'est la règle « aucune position de meuble
  n'est une constante » appliquée aussi aux tailles — **chercher d'autres
  littéraux du même genre**.

Fin d'étape 1 : retour exact aux 10 échecs hérités, aucun nouveau.

### Étape 2 — le plan (aux trois quarts)

Constantes réécrites dans `scripts/restaurant/restaurant.gd` :

| Constante | Avant | Après |
|---|---|---|
| `MAX_TABLES` | 25 | **5** |
| `TABLE_ORIGIN` | (920, 325) | (920, 470) |
| `TABLE_SPACING` | (100, 100) | (120, 140) |
| `STATION_ORIGIN` | (590, 310) | (580, 330) |
| `STATION_SPACING` | 100 | 90 |
| `CONSOLE_POSITION` | (800, 320) | (795, 470) |
| `TAKEOUT_CENTER_X` | 700 | **900** |
| `DINING_RECT` | (875, 250, 505, 575) | (870, 400, 650, 425) |
| `ORDER_BOARD_POSITION` | (680, 740) | (680, 745) |
| plonge (`_default_position`) | (700, 510) | (700, 540) |
| poubelle | (700, 610) | (700, 640) |
| casier | (770, 480) | (795, 340) |

Données : `basic_table.max_count` et `game_variables.table_count.maximum`
passés à **5**, dans les deux fichiers.

Le comptoir takeout a été **sorti de la colonne cuisine** (x 700 → 900) : c'était
la cause de la moitié des échecs hérités.

---

## Ce qui reste

### Finir l'étape 2 — deux assertions de places

Les deux derniers échecs :

```
Ouvrir deux places n'a pas posé deux tabourets: 5
Le dernier tabouret ouvert ne se retire pas
```

Le contrôle ajoute `+2` à `table_count` et attend `seats_open + 2` posés. Avec un
plafond à 5, l'addition est écrêtée. **Ne pas remonter le plafond** : c'est la
demande. Faire lire au contrôle ce que la statistique ouvre réellement. Le
remplacement exact, dans `smoke_test.gd` (bloc « Palette, retrait et bonus
passifs ») :

```gdscript
	GameState.add_modifier("test:seats", "table_count", "flat", 2.0)
	restaurant.apply_layout()
	# On lit ce que la statistique ouvre **réellement** au lieu de supposer que
	# « +2 » passe: `table_count` a un plafond, et le contrôle mesurerait sinon
	# une addition que la règle interdit.
	var seats_after: int = restaurant.resolved_max_count("basic_table")
	_check(seats_after > seats_open, "Ouvrir des places n'en a ouvert aucune: %d puis %d" % [seats_open, seats_after])
	_check(restaurant.placed_count("basic_table") == seats_after, "Les places ouvertes ne sont pas toutes posées: %d pour %d ouvertes" % [restaurant.placed_count("basic_table"), seats_after])
	_check(restaurant.remaining_count("basic_table") == 0, "Des tabourets ouverts restent en réserve")
	var last_open: RestaurantElement = restaurant.element_of(RestaurantElement.instance_id("basic_table", seats_after - 1))
	_check(last_open != null and restaurant.remove_element(last_open), "Le dernier tabouret ouvert ne se retire pas")
	restaurant.apply_layout()
	_check(last_open != null and not last_open.placed and last_open.dismissed, "Un tabouret retiré à la main a été reposé d'office")
	_check(restaurant.remaining_count("basic_table") == 1, "Le tabouret retiré n'est pas revenu dans la palette")
	_check(not restaurant.visible_table_indices().has(seats_after - 1), "Le tabouret retiré est encore dessiné dans la salle")
```

**Puis localiser le blocage** (sortie 124). Le smoke test tourne normalement en
~6 s ; au-delà c'est un blocage réel. Règle connue : *un test qui lit une
référence nulle bloque au lieu d'échouer* — garder les gardes. Chercher après le
dernier `OK` imprimé.

### Étape 3 — étagère à zéro, console adossée

**Étagère.** Chaîne canonique imposée par `docs/contrat-deblocage-meubles.md` :

1. Ajouter la statistique `shelf_count` : `base_value 0`, `minimum 0`,
   `maximum 1`, catégorie `restaurant`.
2. `basic_shelf.max_count_variable = "shelf_count"`. **Ne pas toucher à
   `max_count`**, qui reste la borne absolue.
3. Ajouter un nœud d'upgrade qui fait `{variable: "shelf_count", layer: "flat",
   operation: "add", value: 1}`. Copier la forme de `second_table`.
4. Les deux fichiers de données, puis les 12 points du §5 de `CLAUDE.md`.

> **Conséquence à assumer** : `employee_locker` a
> `placement_requires: ["basic_shelf", "basic_order_board"]`. Étagère à 0 ⇒ pas
> de casier ⇒ **aucune embauche au départ**. C'est peut-être la progression
> voulue, mais ça doit être un choix, pas une surprise.

> **Piège déjà payé** : ne **pas** accorder `shelf_count` dans
> `_open_order_board()`. Ce helper est partagé, et le faire désarme les contrôles
> qui éprouvent justement l'absence d'un prérequis (« La pose du casier n'exige
> pas l'ardoise »). L'accorder au **seul** endroit qui installe le casier pour
> les tests de brigade, par un modificateur `test:shelf` distinct.

**Console adossée au mur.** `placement_blocker()` refuse tout meuble qui touche un
mur — *« Fixtures cannot touch a wall »* — **sauf s'il est ancré**. Il faut donc
donner un `anchor` à `basic_console` dans la donnée, comme
`basic_takeout_counter` a `north_wall`. Vérifier au passage que la console reste
**non retirable** (`removal_blocker`) : c'est le seul chemin vers l'interrupteur
et vers le mode construction.

### Étape 4 — trottoir en L et trajet des clients

Aujourd'hui : `STREET_SIDE = "east"`, une seule bande, `SIDEWALK_BAND = 156`.
`street_span()`, `street_line()`, `street_rect()`, `street_door()` supposent tous
**une seule façade**.

À faire :

1. Une bande **nord** (est-ouest) en plus de la bande **est** (nord-sud), qui se
   rejoignent au coin nord-est.
2. Le trajet des passants : coin **nord-ouest** → est le long du nord → coin
   **nord-est** → sud le long de l'est → coin **sud-est**. Et l'inverse.
3. Le comptoir takeout reste au **mur nord** : avec une bande nord, il borde
   enfin sa clientèle. **Recaler `TAKEOUT_QUEUE_Y` et `SIDEWALK_Y`**, qui valent
   encore 145 — une coordonnée nord héritée de l'époque où la rue passait au
   nord, alors que la rue est à l'est. La file takeout flotte donc hors trottoir.
4. Nettoyer `restaurant.gd` : il porte **deux blocs de commentaires
   contradictoires** sur le comptoir, l'un disant qu'il suit la rue, l'autre
   qu'il reste au mur nord.

Codex a déjà posé la couche de collision distincte pour les passants et un test
de contournement — les reprendre, ne pas les refaire.

---

## Le vérificateur géométrique

Il attrape chevauchements, sorties de murs et points d'accès bloqués **avant** de
lancer Godot. Le tenir à jour avec les constantes du plan.

```python
# -*- coding: utf-8 -*-
INT = (524.67, 214.67, 1535.33, 837.83)   # interieur des murs, salle 1040x652,5
ST_X, ST_Y, ST_PAS = 580.0, 330.0, 90.0
m = {}
for i in range(4):
    m['four %d' % i] = (ST_X, ST_Y + i * ST_PAS, 82, 68)
m['etagere']  = (700.0, (ST_Y + (ST_Y + ST_PAS)) * 0.5, 60, 162)
m['plonge']   = (700.0, 540.0, 82, 68)
m['poubelle'] = (700.0, 640.0, 73, 68)
m['casier']   = (795.0, 340.0, 75, 102)
m['console']  = (795.0, 470.0, 88, 65)
m['ardoise']  = (680.0, 745.0, 178, 128)
m['comptoir'] = (900.0, 220.0, 121, 61)
for i in range(5):
    m['table %d' % i] = (920.0 + i * 120.0, 470.0, 71, 111)

def r(v):
    x, y, w, h = v
    return (x - w / 2.0, y - h / 2.0, x + w / 2.0, y + h / 2.0)

noms, soucis = list(m), 0
for i in range(len(noms)):
    for j in range(i + 1, len(noms)):
        a, b = r(m[noms[i]]), r(m[noms[j]])
        ox, oy = min(a[2], b[2]) - max(a[0], b[0]), min(a[3], b[3]) - max(a[1], b[1])
        if ox > 0 and oy > 0:
            print('CHEVAUCHE %-9s x %-9s %.0f x %.0f' % (noms[i], noms[j], ox, oy)); soucis += 1
for nom, v in m.items():
    if nom == 'comptoir':      # ancre au mur nord: il a le droit de le toucher
        continue
    a = r(v)
    if a[0] < INT[0] or a[1] < INT[1] or a[2] > INT[2] or a[3] > INT[3]:
        print('HORS MURS %-9s %s' % (nom, [round(x) for x in a])); soucis += 1
for nom, p in {'prep takeout': (838.0, 282.0), 'service takeout': (962.0, 282.0)}.items():
    for autre, v in m.items():
        if autre == 'comptoir':
            continue
        a = r(v)
        if a[0] <= p[0] <= a[2] and a[1] <= p[1] <= a[3]:
            print('ACCES %-16s dans %s' % (nom, autre)); soucis += 1
print('--- soucis:', soucis)
```

---

## Méthode — ce qui a raté la première fois

La première tentative a fait passer les échecs de **10 → 32 → 58**, parce que
tailles, salle, plan, données et attentes des tests bougeaient **en même temps**.
Chaque correction en révélait deux autres.

**Une étape, une validation, avant la suivante.** L'étape 1 refaite ainsi est
revenue proprement à zéro échec nouveau, et a même mis au jour le littéral
`Vector2(142, 72)` que personne ne cherchait.

Rappels de `CLAUDE.md` qui se sont vérifiés sur ce chantier :

- **Ne jamais figer un numéro ni une dimension dans un contrôle.** Trois tests
  ont cassé sans qu'aucune règle ne bouge, parce qu'ils écrivaient `2.5`, `900`,
  `960` ou « la case 14 ».
- **Imposer ce dont on dépend.** Un contrôle du butin mesurait la hauteur au
  repos sans la fixer, alors que le magnétisme pouvait déjà avoir lancé un
  rebond : il comparait un saut à lui-même.
- **Un échec isolé se revérifie** avant d'être traité comme une régression.
- La validation se fait **au niveau du changement** (§2 de `CLAUDE.md`). Ce
  chantier touche schéma, données et outil : c'est le **niveau 3**, donc toute la
  batterie — mais **une fois le chantier fini**, pas à chaque itération.
