# Timelines Unity — architecture et création de séquences

## Rôle des Timelines Unity

Les **Timelines** sont des séquences audiovisuelles Unity basées sur `PlayableDirector`. Elles servent à présenter une animation de résultats, de première ouverture ou de lancement produit tout en bloquant temporairement l'interaction normale avec l'UI.

Elles sont différentes des **Tutoriels** : un tutoriel guide activement l'utilisateur dans l'interface et attend souvent ses clics; une Timeline joue une séquence Unity et appelle un callback lorsqu'elle se termine.

```mermaid
flowchart LR
TRIGGER[Controller / UI / State] --> TL[Timeline subclass]
TL --> BASE[TimelineBase.Play]
BASE --> CAM[Switch cameras + block raycasts]
BASE --> DIR[PlayableDirector.Play]
DIR --> STOP[PlayableDirector.stopped]
STOP --> RESTORE[TimelineBase.Stop]
RESTORE --> CB[onFinish]

```

## TimelineBase — contrat commun

Le socle est `Assets/Src/Game/Timeline/TimelineBase.cs`.

`TimelineBase` gère :

- `uiCamera`, `offscreenCamera` et éventuellement `camera3D`;
- activation de `scene3dRoot`;
- changement de `Canvas.worldCamera`;
- adaptation du `screenTransform` selon le ratio d'écran;
- `UICanvasGroup.blocksRaycasts = false` pendant la séquence;
- `PlayableDirector.Play()`;
- abonnement à `director.stopped`;
- restauration de l'UI dans `Stop()`;
- appel du callback `onFinish`.

## Ce que Play() fait réellement

1. active `scene3dRoot`;
2. désactive la caméra UI;
3. active la caméra offscreen;
4. redirige le Canvas vers cette caméra;
5. copie le viewport de la caméra UI;
6. calcule le scale horizontal entre les valeurs 4:3 et 16:9;
7. bloque les raycasts UI;
8. lance le `PlayableDirector`;
9. s'abonne à `director.stopped`.

Lorsque le director s'arrête, `OnPlayFinish()` se désabonne, appelle `Stop()`, puis déclenche le callback.

## Timelines communes existantes

<table id="bkmrk-classe-responsabilit"><thead><tr><th>Classe</th><th>Responsabilité</th></tr></thead><tbody><tr><td>`CreateProductTimeline`</td><td>Séquence générique après création/lancement d'un premier objet métier.</td></tr><tr><td>`YearResultsTimeline`</td><td>Animation de résultats annuelle By Year avec graph, profit/perte et anti-bankruptcy.</td></tr><tr><td>`FirstModuleTimeline`</td><td>Première ouverture d'un module By Module.</td></tr><tr><td>`ModuleResultsTimeline`</td><td>Animation de correction/résultats d'un module summative.</td></tr></tbody></table>

## Timelines game-specific

Les jeux peuvent spécialiser le socle :

- `Assets/GameSpecific/mk1/Src/Timeline/TimelinesMk1.cs` — extension de `YearResultsTimeline` + lancement produit;
- `Assets/GameSpecific/ent1/Src/Timeline/LaunchProductTimeline.cs` — séquences lancement/résultats ENT1;
- les assets `.playable` sont présents sous les Resources des jeux concernés.

Une Timeline qui dépend d'une structure game-specific doit rester dans le dossier du jeu, pas dans le socle commun.

## Assets .playable

Une Timeline Unity est composée de deux parties :

- le **script C#** qui choisit le director, prépare les données et appelle `Play()`;
- l'**asset .playable** qui contient les tracks, clips et animations.

Changer uniquement le C# sans vérifier l'asset peut laisser des bindings cassés. Changer uniquement le `.playable` nécessite aussi de vérifier que le prefab/scene fournit toutes les références utilisées par les tracks.

## Créer une Timeline de zéro — procédure complète

### 1. Vérifier qu'une Timeline est le bon outil

Utiliser une Timeline si le besoin est principalement une **séquence visuelle/animée**. Pour guider un utilisateur vers un bouton, attendre un clic réel ou surligner une zone UI, utiliser le système Tutoriels.

### 2. Créer l'asset Timeline

Dans Unity :

1. créer un asset Timeline `.playable` dans le dossier Resources approprié;
2. ajouter les tracks nécessaires;
3. configurer clips, animations et bindings;
4. tester la séquence directement dans l'éditeur.

### 3. Créer la classe C#

```csharp
public class MyTimeline : TimelineBase
{
    public static MyTimeline Instance;

    [SerializeField]
    private PlayableDirector director;

    private void Awake()
    {
        Instance = this;
    }

    public void PlaySequence(
        string message,
        Action onFinish
    )
    {
        character3dText.text = message;
        Play(onFinish, director);
    }
}
```

### 4. Brancher les références du prefab/scene

Le composant hérité de `TimelineBase` doit avoir des références valides vers :

- `uiCamera`;
- `offscreenCamera`;
- `scene3dRoot`;
- `screenTransform`;
- `UICanvasGroup`;
- `character3dText` si la Timeline affiche une bulle;
- le ou les `PlayableDirector`.

### 5. Choisir le bon point de déclenchement

Exemples réels :

- `DecisionsControlByModule` déclenche `FirstModuleTimeline`;
- le même contrôle déclenche `ModuleResultsTimeline` lorsque le module summative est généré;
- `StudentState` et plusieurs TeamControllers déclenchent `YearResultsTimeline`;
- les contrôles de produit/posting peuvent déclencher `CreateProductTimeline` ou une Timeline game-specific.

### 6. Préparer les données avant Play()

Une Timeline ne devrait pas aller chercher arbitrairement toutes ses données elle-même. Le trigger prépare les valeurs nécessaires, puis les passe à la Timeline.

Exemple `YearResultsTimeline` :

- reçoit `teamUid`;
- reçoit les `RankingsData`;
- reçoit `yearId`;
- calcule profit/perte;
- choisit `goodResultsDirector` ou `badResultsDirector`.

### 7. Gérer la fin dans onFinish

Le code qui ouvre l'écran suivant ou change la progression doit être exécuté dans le callback.

```csharp
MyTimeline.Instance.PlaySequence(
    message,
    () =>
    {
        OpenNextPanel();
    }
);
```

Ne pas écrire :

```csharp
MyTimeline.Instance.PlaySequence(message, null);
OpenNextPanel(); // s'exécute immédiatement
```

## Empêcher une Timeline de rejouer

`TimelineBase` ne persiste aucun état par lui-même. Le mécanisme anti-replay dépend du contexte.

<table id="bkmrk-cas-m%C3%A9canisme-premi%C3%A8"><thead><tr><th>Cas</th><th>Mécanisme</th></tr></thead><tbody><tr><td>Première ouverture de module</td><td>`Scenario.eScenarioStep.eStudentFirstModule`.</td></tr><tr><td>Résultats annuels</td><td>`SetYearSequenceSeen(yearId)`.</td></tr><tr><td>Résultats d'un module</td><td>`UserState.ModuleUserState.STUDENT_RESULTS`.</td></tr><tr><td>Premier produit / premier launch</td><td>Condition métier + éventuellement ScenarioStep.</td></tr></tbody></table>

## FirstModuleTimeline

`FirstModuleTimeline.PlayFirstModule()` :

1. injecte le texte;
2. joue `firstModuleDirector`;
3. à la fin, sauvegarde `eStudentFirstModule`;
4. appelle le callback qui ouvre le module.

Le trigger est présent dans les contrôles By Module lorsque l'étudiant n'a jamais vu cette séquence.

## ModuleResultsTimeline

Cette Timeline est plus complexe car les données peuvent ne pas être encore disponibles lorsqu'on veut la jouer.

### Attente des résultats

Si `TryGetTeamByModuleResult()` ne trouve pas encore les résultats :

1. la Timeline s'abonne à `onNewTeamModuleResults`;
2. attend le bon `actModuleUid` / module type;
3. se désabonne dès réception;
4. prépare puis joue la séquence.

Ce pattern est à réutiliser lorsqu'une nouvelle Timeline dépend d'une donnée asynchrone : abonnement temporaire + filtre strict + désabonnement immédiat.

### Choix du director

Le score moyen du module choisit entre :

- `goodResultsDirector` à partir de 85 %;
- `okayResultsDirector` à partir de 70 %;
- `badResultsDirector` sinon.

### Pause et report

`ChangePlayableSpeed(false)` met le root playable à vitesse 0 et affiche les options. La Timeline peut alors montrer les notes via `reportControl.ShowGrades()` avant de continuer.

## YearResultsTimeline

`PlayNewResults()` :

1. récupère le `RankingsData` de l'année;
2. met à jour le whiteboard/graph;
3. compare la valeur actuelle à l'année précédente ou à `GraphFirstValue`;
4. détermine profit/perte;
5. choisit le director;
6. construit le texte localisé;
7. marque la séquence annuelle comme vue via `SetEndOfYearSequenceSeen(yearId)`.

## Créer une Timeline avec plusieurs variantes

Pour un même événement avec plusieurs résultats visuels, garder une seule classe et plusieurs `PlayableDirector` lorsque les inputs/lifecycle sont identiques.

```csharp
PlayableDirector director =
    score >= threshold
        ? goodDirector
        : badDirector;

Play(onFinish, director);
```

Créer plusieurs classes uniquement si les séquences ont des données et un lifecycle réellement différents.

## Interaction avec les caméras et l'UI

`TimelineBase.Play()` désactive la caméra UI et empêche les raycasts. Un bug pendant la séquence peut donc laisser toute l'application visuellement ou fonctionnellement bloquée si `Stop()` n'est jamais exécuté.

### Stop()

`Stop()` doit toujours rétablir :

- `uiCamera.enabled = true`;
- `offscreenCamera.enabled = false`;
- `scene3dRoot.SetActive(false)`;
- `Canvas.worldCamera = uiCamera`;
- `UICanvasGroup.blocksRaycasts = true`.

## Gestion du ratio d'écran

`TimelineBase` interpole le scale horizontal entre `scale_4_3` et `scale_16_9` selon `Screen.width / Screen.height`, puis clamp le résultat.

Si une séquence est correctement cadrée en 16:9 mais coupée sur un autre ratio, vérifier ces deux valeurs sur le prefab avant de modifier la caméra ou les clips.

## Timelines game-specific

### MK1

`TimelinesMk1` hérite de `YearResultsTimeline` et ajoute un `launchProductDirector` ainsi qu'un whiteboard MK1.

### ENT1

`LaunchProductTimeline` gère un whiteboard de lancement produit et ses propres directors de résultats.

## Build et assets

Modifier :

- un script Timeline;
- un prefab qui la contient;
- un asset `.playable`;
- un binding d'animation;

nécessite un nouveau build Unity/WebGL.

## Debug Timeline

<table id="bkmrk-sympt%C3%B4me-%C3%80-v%C3%A9rifier-"><thead><tr><th>Symptôme</th><th>À vérifier</th></tr></thead><tbody><tr><td>Écran noir après la séquence</td><td>`director.stopped` a-t-il été déclenché ? `Stop()` a-t-il tourné ?</td></tr><tr><td>UI visible mais non cliquable</td><td>`UICanvasGroup.blocksRaycasts`.</td></tr><tr><td>Timeline rejouée à chaque ouverture</td><td>Mécanisme anti-replay non persisté.</td></tr><tr><td>Callback plusieurs fois</td><td>Abonnement `director.stopped` ou event résultats non désabonné.</td></tr><tr><td>Mauvaise variante good/okay/bad</td><td>Valeur métier passée à la Timeline et seuils de sélection.</td></tr><tr><td>Animation avec anciennes données</td><td>Le trigger passe-t-il les données actuelles avant `Play()` ?</td></tr><tr><td>Résultats module jamais affichés</td><td>`onNewTeamModuleResults`, filtres actModuleUid/moduleType.</td></tr><tr><td>Layout déformé</td><td>`scale_4_3`, `scale_16_9`, `screenTransform`.</td></tr><tr><td>Playable ne joue pas</td><td>Director / asset `.playable` / bindings Unity.</td></tr></tbody></table>

## Checklist avant merge

- asset `.playable` sauvegardé;
- tous les bindings valides;
- références caméra/canvas/director non null;
- séquence testée en Editor;
- `Stop()` restaure l'UI;
- callback de fin continue correctement le flow;
- anti-replay sauvegardé;
- relog ne rejoue pas une séquence déjà vue sauf si voulu;
- résolutions/ratios d'écran vérifiés;
- WebGL testé.