Bundles in-flight work on the campaign/missions system (CampaignDef, MissionDef, TerrainPatch, TransformerDef, MissionChecker, CampaignLoader, FlavorBanner, transformer rules), plan files, and matching tests. Baseline commit so the upcoming automation testing harness lands on a clean tree.
371 lines
18 KiB
Markdown
371 lines
18 KiB
Markdown
# Plan : Puzzle → Logistique incrémentale (Missions)
|
|
|
|
## Vision
|
|
|
|
Remplacer la série de niveaux indépendants par un plateau persistant où les **missions** s'enchaînent. Chaque mission complétée débloque une extension du terrain (nouvelles cases, productions, demandes, murs) sans casser la solution en place. Le jeu devient un jeu de **logistique** et non de **puzzle**.
|
|
|
|
### Fil narratif (lore GDD §12)
|
|
|
|
Les pions manquent d'un roi. Mission après mission, ils construisent des pièces de plus en plus puissantes pour atteindre des ressources plus lointaines, jusqu'à fabriquer le roi — qui exécute tout le monde.
|
|
|
|
Chaque mission débloque de nouveaux **types de pièces** : on commence avec les Pions seuls, puis les Tours, les Fous, les Cavaliers, et enfin la Dame. Le lore justifie la progression mécanique.
|
|
|
|
---
|
|
|
|
## Concepts clés
|
|
|
|
| Ancien | Nouveau |
|
|
|--------|---------|
|
|
| Level (plateau isolé) | **Campaign** : un plateau persistant avec N missions |
|
|
| Victoire → charger le niveau suivant | Victoire → le plateau **s'étend**, la mission suivante se débloque |
|
|
| Stock fixe par niveau | Stock cumulé : chaque mission donne un budget additionnel |
|
|
| Reset complet entre niveaux | Les pièces posées et les productions **restent actives** |
|
|
| Stop (retour en Edit) | **Supprimé** — la simulation tourne en continu, le joueur édite en temps réel |
|
|
| Toutes pièces disponibles dès le début | Pièces **débloquées progressivement** par mission (lore) |
|
|
| Pièces à niveau fixe | **Niveaux de pièces** (I, II, III) débloqués par la progression |
|
|
|
|
### Règle d'or : pas de régression
|
|
Les murs ne peuvent apparaître que sur les **nouvelles cases** débloquées par la mission. Le terrain existant ne change jamais de façon bloquante.
|
|
|
|
---
|
|
|
|
## Changements de game design
|
|
|
|
### Suppression du Stop — édition en temps réel
|
|
|
|
Il n'y a plus de phase Edit séparée. La simulation tourne en continu. Le joueur peut :
|
|
- **Pause** (barre espace) : l'animation termine le tour en cours puis se fige.
|
|
- **Placer/retirer des pièces** à tout moment (en pause ou pendant que ça tourne).
|
|
- **Réoptimiser** les missions précédentes — essentiel pour un jeu de logistique.
|
|
|
|
Conséquence engine : `PlacePieceCommand` et `RemovePieceCommand` fonctionnent quel que soit le `SimPhase` (plus de guard `phase == Edit`).
|
|
|
|
### Auto-pause pendant le placement
|
|
|
|
Sélectionner un type de pièce à placer met automatiquement la simulation en pause. La pause est levée quand le placement est confirmé (clic arrivée) ou annulé (Échap). Le joueur ne pense pas "pause" — il pense "placement". C'est transparent.
|
|
|
|
### Retirer une pièce — touche Suppr
|
|
|
|
Le clic droit est réservé au pan de caméra. Pour retirer une pièce :
|
|
- Clic gauche sur la pièce → sélection + panneau de détail
|
|
- **Touche Suppr** ou **bouton [Retirer]** dans le panneau de détail → retourne au stock
|
|
|
|
### Collisions → retour au stock + pause auto
|
|
|
|
Quand une pièce est détruite par collision, elle retourne dans le stock du joueur (au lieu d'être perdue). Cela évite un soft-lock où le joueur n'a plus assez de pièces pour résoudre la mission.
|
|
|
|
La simulation se met en **pause automatique** sur collision :
|
|
- La caméra effectue un **pan + zoom** vers la zone de collision.
|
|
- Une **notification** apparaît dans un coin de l'écran pour expliciter ce qui s'est passé (ex: "Tour II détruite par Dame — retournée au stock").
|
|
- Le joueur peut reprendre la simulation (Espace) après avoir pris connaissance de la situation.
|
|
|
|
### Undo (Ctrl+Z)
|
|
|
|
Annule le dernier placement ou retrait de pièce. L'architecture event-sourcing de l'engine rend l'implémentation naturelle : on conserve un historique de commandes et on les rejoue sans la dernière. Essentiel pour l'itération rapide sur un réseau en temps réel.
|
|
|
|
### Productions — pas d'intervalle, production variable
|
|
|
|
Chaque bâtiment de production a un champ `amount` (1 à 4) : il produit ce nombre de cargaisons **à chaque tour**. Le buffer max = `amount`. La surproduction non récupérée est écrasée au tour suivant (perdue). Pas de champ `interval`.
|
|
|
|
### Niveaux de pièces (I, II, III)
|
|
|
|
Chaque type de pièce a un niveau qui affecte :
|
|
- **Portée** : Tour I = 1 case, Tour II = 2 cases, Tour III = 3 cases
|
|
- **Priorité de transfert** : à statut social égal, le niveau départage
|
|
- **Collisions** : à statut égal, le niveau supérieur survit
|
|
|
|
Les missions débloquent des niveaux supérieurs progressivement.
|
|
|
|
### Drag & drop de pièces
|
|
|
|
Le joueur peut glisser une pièce déjà placée pour déplacer son point de départ (les arrivées possibles se recalculent). Quality-of-life essentiel quand on itère sur un réseau en temps réel.
|
|
|
|
---
|
|
|
|
## Architecture (Black-Box Sim)
|
|
|
|
### Nouveaux modèles (engine)
|
|
|
|
```
|
|
CampaignDef # remplace la liste de LevelDef
|
|
├── name: string
|
|
├── initialWidth / initialHeight
|
|
├── missions: MissionDef[]
|
|
|
|
MissionDef
|
|
├── id: int
|
|
├── name: string
|
|
├── description: string
|
|
├── terrainPatch: TerrainPatch # extension du plateau
|
|
├── stock: PieceStock[] # stock additionnel offert
|
|
├── demands: DemandDef[] # nouveaux objectifs
|
|
├── unlockedPieces: PieceKind[] # types de pièces débloqués par cette mission
|
|
├── unlockedLevels: PieceUpgrade[] # niveaux de pièces débloqués
|
|
│
|
|
PieceUpgrade
|
|
├── kind: PieceKind
|
|
├── level: int # ex: Tour niveau 2
|
|
|
|
TerrainPatch
|
|
├── newWidth / newHeight # nouvelle taille du plateau (>= précédente)
|
|
├── cells: PatchCell[] # cases ajoutées/modifiées
|
|
│ ├── col, row
|
|
│ └── type: Empty | Wall | Production(def) | Demand(def)
|
|
```
|
|
|
|
### Nouveau state (engine)
|
|
|
|
```
|
|
CampaignState
|
|
├── campaignDef: CampaignDef
|
|
├── currentMissionIndex: int
|
|
├── completedMissions: int[]
|
|
├── boardState: BoardState # persistant entre missions
|
|
├── availablePieces: Set<(PieceKind, int level)> # pièces débloquées
|
|
```
|
|
|
|
`BoardState` évolue :
|
|
- Ajout d'un champ `missionIndex` sur chaque `DemandState` pour savoir à quelle mission appartient une demande.
|
|
- Les demandes complétées restent dans le state (marquées `Satisfied`), les nouvelles s'ajoutent.
|
|
- `ProductionDef` reçoit un champ `amount` (1-4) au lieu de `interval`.
|
|
|
|
### Modification de SimPhase
|
|
|
|
```
|
|
Running ←→ Paused
|
|
↓ ↓
|
|
MissionComplete (la sim continue mais un overlay félicite)
|
|
↓
|
|
AdvanceMission → Running (terrain étendu, nouvelles demandes)
|
|
```
|
|
|
|
Phases supprimées : `Edit`, `Victory`, `Defeat`.
|
|
- Plus de `Edit` : le joueur place des pièces à tout moment.
|
|
- Plus de `Victory` séparé : `MissionComplete` pour chaque mission, la dernière affiche un écran de fin.
|
|
- Plus de `Defeat` : pas de deadline punitive (jeu de logistique, pas de puzzle).
|
|
|
|
> **Note** : la suppression des deadlines est une conséquence de la suppression du Stop. Sans pouvoir reset, un deadline qui expire sans recours serait frustrant. Les demandes ont un `amount` cible mais pas de date limite. Les métriques (tours pour compléter) servent de score optionnel.
|
|
|
|
### Nouvelles Commands
|
|
|
|
| Command | Effet |
|
|
|---------|-------|
|
|
| `LoadCampaignCommand` | Charge la campagne, initialise le plateau avec la mission 0, démarre Running |
|
|
| `AdvanceMissionCommand` | Applique le `TerrainPatch` de la mission suivante, ajoute stock/demandes/pièces |
|
|
| `MovePieceCommand` | Drag & drop : déplace une pièce existante (nouveau start + end) |
|
|
|
|
Commands modifiées :
|
|
| Command | Changement |
|
|
|---------|------------|
|
|
| `PlacePieceCommand` | Fonctionne en Running et Paused (plus de guard Edit) |
|
|
| `RemovePieceCommand` | Idem |
|
|
| `PauseSimulationCommand` | Termine le tour en cours avant de figer |
|
|
|
|
Commands supprimées :
|
|
| Command | Raison |
|
|
|---------|--------|
|
|
| `StopSimulationCommand` | Plus de Stop |
|
|
| `StartSimulationCommand` | La sim démarre automatiquement au load |
|
|
| `ResetLevelCommand` | Plus de reset complet |
|
|
|
|
### Nouveaux Events
|
|
|
|
| Event | Données |
|
|
|-------|---------|
|
|
| `MissionCompleteEvent` | missionIndex |
|
|
| `MissionStartedEvent` | missionIndex, terrainPatch appliqué |
|
|
| `TerrainExpandedEvent` | nouvelles cases, nouvelle taille |
|
|
| `PieceUnlockedEvent` | kind, level |
|
|
| `PieceMovedByPlayerEvent` | pieceId, oldStart, oldEnd, newStart, newEnd |
|
|
| `PieceReturnedToStockEvent` | pieceId, kind, reason (collision) |
|
|
|
|
### Modifications du flow de victoire
|
|
|
|
1. `VictoryChecker` → renommé `MissionChecker`.
|
|
2. Vérifie les demandes **de la mission courante** uniquement.
|
|
3. Si toutes satisfaites → `MissionCompleteEvent`.
|
|
4. La sim continue de tourner (les pièces continuent de produire/livrer).
|
|
5. Le joueur déclenche `AdvanceMissionCommand` quand il est prêt.
|
|
6. Dernière mission complétée → écran de fin de campagne.
|
|
|
|
### Collisions — retour au stock + pause auto + caméra
|
|
|
|
`CollisionResolver` modifié :
|
|
- La pièce détruite émet `PieceReturnedToStockEvent` au lieu de `PieceDestroyedEvent`.
|
|
- Le stock dans `BoardState` est incrémenté.
|
|
- La pièce est retirée du plateau mais le joueur peut la replacer.
|
|
|
|
Côté présentation (Godot) :
|
|
- La simulation se met en **pause automatique** à la détection d'une collision.
|
|
- La caméra effectue un **pan + zoom** vers la zone de collision.
|
|
- Une **notification** apparaît dans un coin de l'écran (ex: "Tour II détruite par Dame — retournée au stock").
|
|
- Le joueur reprend avec Espace après avoir pris connaissance.
|
|
|
|
---
|
|
|
|
## Format JSON de campagne
|
|
|
|
```json
|
|
{
|
|
"name": "La Quête du Roi",
|
|
"initialWidth": 4,
|
|
"initialHeight": 4,
|
|
"missions": [
|
|
{
|
|
"id": 1,
|
|
"name": "Premier Convoi",
|
|
"description": "Les pions découvrent une scierie. Il faut acheminer le bois.",
|
|
"terrainPatch": {
|
|
"newWidth": 4,
|
|
"newHeight": 4,
|
|
"cells": [
|
|
{ "col": 0, "row": 0, "type": "production", "production": { "name": "Scierie", "cargo": "wood", "amount": 1 } },
|
|
{ "col": 3, "row": 0, "type": "demand", "demand": { "name": "Dépôt", "cargo": "wood", "amount": 3 } }
|
|
]
|
|
},
|
|
"unlockedPieces": ["pawn"],
|
|
"unlockedLevels": [{ "kind": "pawn", "level": 1 }],
|
|
"stock": [
|
|
{ "kind": "pawn", "count": 6 }
|
|
]
|
|
},
|
|
{
|
|
"id": 2,
|
|
"name": "Forger les Tours",
|
|
"description": "Les pions ont forgé des Tours. De nouveaux territoires s'ouvrent à l'est.",
|
|
"terrainPatch": {
|
|
"newWidth": 6,
|
|
"newHeight": 4,
|
|
"cells": [
|
|
{ "col": 4, "row": 0, "type": "empty" },
|
|
{ "col": 4, "row": 1, "type": "wall" },
|
|
{ "col": 4, "row": 2, "type": "empty" },
|
|
{ "col": 4, "row": 3, "type": "empty" },
|
|
{ "col": 5, "row": 0, "type": "empty" },
|
|
{ "col": 5, "row": 1, "type": "empty" },
|
|
{ "col": 5, "row": 2, "type": "production", "production": { "name": "Carrière", "cargo": "stone", "amount": 1 } },
|
|
{ "col": 5, "row": 3, "type": "demand", "demand": { "name": "Chantier", "cargo": "stone", "amount": 5 } }
|
|
]
|
|
},
|
|
"unlockedPieces": ["rook"],
|
|
"unlockedLevels": [{ "kind": "rook", "level": 1 }],
|
|
"stock": [
|
|
{ "kind": "pawn", "count": 2 },
|
|
{ "kind": "rook", "count": 3 }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phases d'implémentation
|
|
|
|
### Phase 1 — Modèles engine (pas de Godot)
|
|
|
|
1. Créer `CampaignDef`, `MissionDef`, `TerrainPatch`, `PatchCell`, `PieceUpgrade` dans `chessistics-engine/Model/`.
|
|
2. Créer `CampaignState` dans `chessistics-engine/Model/`.
|
|
3. Modifier `SimPhase` : supprimer `Edit`, `Victory`, `Defeat`. Ajouter `MissionComplete`.
|
|
4. Ajouter `missionIndex` à `DemandState`.
|
|
5. Ajouter `amount` à `ProductionDef` (remplace le concept d'intervalle). Buffer max = amount.
|
|
6. Ajouter `level` à `PieceStock` et aux modèles de pièces pour supporter les niveaux I/II/III.
|
|
|
|
### Phase 2 — Refactor SimPhase et édition en temps réel
|
|
|
|
1. Supprimer les guards `phase == Edit` dans `PlacePieceCommand` et `RemovePieceCommand`.
|
|
2. Supprimer `StopSimulationCommand`, `StartSimulationCommand`, `ResetLevelCommand`.
|
|
3. Modifier `PauseSimulationCommand` pour terminer le tour en cours avant de figer.
|
|
4. La sim démarre automatiquement au chargement (état initial = `Running` en pause).
|
|
|
|
### Phase 3 — Collisions → retour au stock + pause auto
|
|
|
|
1. Modifier `CollisionResolver` : la pièce détruite retourne au stock.
|
|
2. Créer `PieceReturnedToStockEvent` (avec coordonnées de la collision pour le pan caméra).
|
|
3. Incrémenter le stock dans `BoardState` quand une pièce est détruite.
|
|
4. L'engine émet un event de pause automatique après une collision.
|
|
5. Tests : vérifier qu'après collision la pièce réapparaît dans le stock et la sim est en pause.
|
|
|
|
### Phase 4 — Commands & Events campagne
|
|
|
|
1. Créer `LoadCampaignCommand` : initialise `BoardState` depuis mission 0, applique `unlockedPieces`.
|
|
2. Créer `AdvanceMissionCommand` : applique `TerrainPatch`, ajoute stock/demandes/pièces débloquées.
|
|
3. Créer `MissionCompleteEvent`, `MissionStartedEvent`, `TerrainExpandedEvent`, `PieceUnlockedEvent`.
|
|
4. Renommer `VictoryChecker` → `MissionChecker` : ne vérifie que les demandes de la mission courante.
|
|
|
|
### Phase 5 — Drag & drop engine
|
|
|
|
1. Créer `MovePieceCommand` : valide le nouveau placement, met à jour start/end.
|
|
2. Créer `PieceMovedByPlayerEvent`.
|
|
3. Tests : déplacer une pièce en Running, vérifier qu'elle reprend sa trajectoire.
|
|
|
|
### Phase 6 — Production variable
|
|
|
|
1. Modifier la production pour utiliser `amount` (1-4) au lieu de toujours produire 1.
|
|
2. Le buffer max = `amount`. La surproduction écrase le buffer.
|
|
3. Tests : production amount=3 remplit 3 slots, non récupérés → écrasés au tour suivant.
|
|
|
|
### Phase 7 — CampaignLoader
|
|
|
|
1. Créer `CampaignLoader` dans `chessistics-engine/Loading/` : parse le JSON campagne.
|
|
2. Valider la cohérence : pas de régression de taille, `unlockedPieces` cohérent.
|
|
3. Les anciens JSON de niveaux restent compatibles (mode puzzle legacy optionnel).
|
|
|
|
### Phase 8 — Tests engine intégration
|
|
|
|
1. Test : charger une campagne, compléter mission 1, avancer, vérifier terrain étendu.
|
|
2. Test : les pièces de la mission 1 restent en place après `AdvanceMissionCommand`.
|
|
3. Test : `MissionCompleteEvent` émis au bon moment.
|
|
4. Test : placer une pièce pendant Running fonctionne.
|
|
5. Test : collision retourne la pièce au stock.
|
|
6. Test : pièces non débloquées ne sont pas plaçables.
|
|
7. Test : niveaux de pièces affectent portée et priorité.
|
|
|
|
### Phase 9 — GameSim facade
|
|
|
|
1. `GameSim` accepte un `CampaignDef` (mode principal) ou un `LevelDef` (legacy).
|
|
2. Exposer `CampaignSnapshot` avec `currentMissionIndex`, `completedMissions`, `availablePieces`.
|
|
3. Plus de `StopSimulationCommand` dans la facade.
|
|
|
|
### Phase 10 — Présentation Godot
|
|
|
|
1. `Main.cs` : charger une campagne, supprimer le flow Stop/Start.
|
|
2. Contrôles : Espace = pause/resume, plus de bouton Stop. Barre de contrôle : `[⏸/▶] [x1] [x2] [x4] Tour: N Mission: M/T`.
|
|
3. **Auto-pause placement** : sélectionner un type de pièce dans le stock met la sim en pause. La pause est levée quand le placement est confirmé ou annulé (Échap).
|
|
4. **Retrait de pièce** : supprimer le clic droit pour retirer. Clic gauche sélectionne → touche Suppr ou bouton [Retirer] dans le panneau de détail.
|
|
5. **Collision — pause + caméra + notification** :
|
|
- Sur `PieceReturnedToStockEvent` : pause auto de la simulation.
|
|
- Pan + zoom caméra vers la zone de collision (tween animé).
|
|
- Notification dans un coin de l'écran (ex: "Tour II détruite par Dame — retournée au stock").
|
|
- Le joueur reprend avec Espace.
|
|
6. **Transition de mission** (cinématique) :
|
|
- Titre "Nouvelle mission" en plein écran, fade-in.
|
|
- Lock du pan/zoom joueur — la caméra se déplace pour montrer la zone de la prochaine mission.
|
|
- Les nouvelles cases apparaissent sur le plateau avec animation d'expansion.
|
|
- Le titre de mission se déplace vers la zone d'objectif du panneau latéral avant de disparaître (guide l'œil).
|
|
- Unlock pan/zoom, la simulation reprend.
|
|
7. `ObjectivePanel` : objectifs de la mission courante + badge ✓ pour les missions complétées.
|
|
8. `PieceStockPanel` : stock cumulatif, n'affiche que les pièces débloquées.
|
|
9. Notification `PieceUnlockedEvent` : animation de déblocage d'une nouvelle pièce.
|
|
10. Drag & drop visuel : le joueur glisse une pièce, les destinations légales s'affichent.
|
|
11. **Undo (Ctrl+Z)** : annule le dernier placement ou retrait. Conserver un historique de commandes côté présentation, rejouer l'état sans la dernière.
|
|
|
|
### Phase 11 — Données de campagne
|
|
|
|
1. Créer `Data/campaigns/campaign_01.json` — "La Quête du Roi".
|
|
2. Concevoir 6-8 missions avec progression de pièces (Pion → Tour → Fou → Cavalier → Dame).
|
|
3. Chaque mission étend le terrain sans casser les solutions précédentes.
|
|
4. Équilibrer les stocks cumulatifs et les `amount` de production.
|
|
|
|
---
|
|
|
|
## Risques et mitigations
|
|
|
|
| Risque | Mitigation |
|
|
|--------|------------|
|
|
| Un patch de terrain casse une solution existante | Validation loader : les patches ne peuvent que **ajouter** des cases ou modifier des cases hors de la zone initiale |
|
|
| Le stock cumulé rend les missions triviales | Playtesting + stock additionnel calibré mission par mission |
|
|
| Placer des pièces en Running cause des bugs de timing | La commande s'applique **entre deux tours** (fin du tour courant → placement → tour suivant) |
|
|
| Collision = perte de pièce frustrante | Retour au stock — le joueur peut replacer immédiatement |
|
|
| Drag & drop complexe pendant la simulation | Le drag ne modifie la pièce qu'au **relâchement** (atomique), appliqué entre deux tours |
|
|
| Niveaux de pièces explosent la complexité | Introduction graduelle : mission 1-3 = niveau I seul, niveaux II/III arrivent plus tard |
|