diff --git a/Assets/Code/Scripts/Level.meta b/Assets/Code/Scripts/Level.meta
new file mode 100644
index 0000000..9210c49
--- /dev/null
+++ b/Assets/Code/Scripts/Level.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2e7b9c54377674993a7922f84e9cfcce
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs b/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs
new file mode 100644
index 0000000..c2878f5
--- /dev/null
+++ b/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs
@@ -0,0 +1,244 @@
+using System.Collections;
+using UnityEngine;
+using UnityEngine.SceneManagement;
+
+///
+/// Auto-spawns in Level01 and displays an intro subtitle sequence.
+///
+public class Level01IntroSubtitles : MonoBehaviour
+{
+ [System.Serializable]
+ private struct SubtitleLine
+ {
+ public string speaker;
+ public string text;
+ public float duration;
+ }
+
+ [Header("Trigger")]
+ [SerializeField] private string targetSceneName = "Level01";
+ [SerializeField] private float initialDelay = 2.5f;
+
+ [Header("Subtitle Sequence")]
+ [SerializeField] private SubtitleLine[] lines =
+ {
+ new SubtitleLine { speaker = "SYSTEME", text = "...Ici, quelque chose cloche.", duration = 2.5f },
+ new SubtitleLine { speaker = "SYSTEME", text = "Reste calme. Observe la piece.", duration = 2.5f },
+ new SubtitleLine { speaker = "SYSTEME", text = "Trouve une sortie.", duration = 2.2f },
+ };
+
+ [SerializeField] private float typewriterCharsPerSecond = 40f;
+ [SerializeField] private float fadeDuration = 0.2f;
+ [SerializeField] private float gapBetweenLines = 0.15f;
+
+ [Header("Visual")]
+ [SerializeField] private int fontSize = 28;
+ [SerializeField] private int speakerFontSize = 18;
+ [SerializeField] private float horizontalPadding = 28f;
+ [SerializeField] private float bottomOffset = 56f;
+ [SerializeField] private Color textColor = new Color(1f, 1f, 1f, 1f);
+ [SerializeField] private Color speakerColor = new Color(1f, 0.85f, 0.35f, 1f);
+ [SerializeField] private Color backgroundColor = new Color(0f, 0f, 0f, 0.62f);
+
+ private static bool s_bootstrapped;
+
+ private string m_currentSpeaker;
+ private string m_currentText;
+ private GUIStyle m_textStyle;
+ private GUIStyle m_speakerStyle;
+ private Texture2D m_background;
+ private bool m_isShowing;
+ private float m_alpha;
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
+ private static void ResetBootstrapFlag()
+ {
+ s_bootstrapped = false;
+ }
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
+ private static void Bootstrap()
+ {
+ if (s_bootstrapped)
+ return;
+
+ s_bootstrapped = true;
+
+ GameObject go = new GameObject(nameof(Level01IntroSubtitles));
+ go.hideFlags = HideFlags.DontSave;
+ go.AddComponent();
+ }
+
+ private void Start()
+ {
+ Scene activeScene = SceneManager.GetActiveScene();
+ if (activeScene.name != targetSceneName)
+ {
+ Destroy(gameObject);
+ return;
+ }
+
+ StartCoroutine(PlaySequence());
+ }
+
+ private IEnumerator PlaySequence()
+ {
+ if (initialDelay > 0f)
+ yield return new WaitForSeconds(initialDelay);
+
+ for (int i = 0; i < lines.Length; i++)
+ {
+ if (string.IsNullOrWhiteSpace(lines[i].text) || lines[i].duration <= 0f)
+ continue;
+
+ yield return StartCoroutine(ShowLine(lines[i]));
+
+ if (gapBetweenLines > 0f)
+ yield return new WaitForSeconds(gapBetweenLines);
+ }
+
+ m_currentSpeaker = string.Empty;
+ m_currentText = string.Empty;
+ Destroy(gameObject);
+ }
+
+ private IEnumerator ShowLine(SubtitleLine line)
+ {
+ m_currentSpeaker = line.speaker;
+ m_currentText = string.Empty;
+ m_isShowing = true;
+
+ if (fadeDuration > 0f)
+ {
+ float fadeIn = 0f;
+ while (fadeIn < fadeDuration)
+ {
+ fadeIn += Time.deltaTime;
+ m_alpha = Mathf.Clamp01(fadeIn / fadeDuration);
+ yield return null;
+ }
+ }
+ else
+ {
+ m_alpha = 1f;
+ }
+
+ float typeTime = 0f;
+ int totalChars = line.text.Length;
+ if (typewriterCharsPerSecond > 0f)
+ {
+ while (m_currentText.Length < totalChars)
+ {
+ typeTime += Time.deltaTime;
+ int visibleChars = Mathf.Clamp(Mathf.FloorToInt(typeTime * typewriterCharsPerSecond), 0, totalChars);
+ m_currentText = line.text.Substring(0, visibleChars);
+ yield return null;
+ }
+ }
+ else
+ {
+ m_currentText = line.text;
+ }
+
+ float holdDuration = Mathf.Max(0f, line.duration - (typewriterCharsPerSecond > 0f ? typeTime : 0f));
+ if (holdDuration > 0f)
+ yield return new WaitForSeconds(holdDuration);
+
+ if (fadeDuration > 0f)
+ {
+ float fadeOut = fadeDuration;
+ while (fadeOut > 0f)
+ {
+ fadeOut -= Time.deltaTime;
+ m_alpha = Mathf.Clamp01(fadeOut / fadeDuration);
+ yield return null;
+ }
+ }
+
+ m_alpha = 0f;
+ m_isShowing = false;
+ }
+
+ private void OnGUI()
+ {
+ if (!m_isShowing || string.IsNullOrEmpty(m_currentText))
+ return;
+
+ EnsureStyles();
+
+ float maxWidth = Mathf.Min(Screen.width - 24f, 940f);
+ float textWidth = maxWidth - horizontalPadding * 2f;
+ float speakerHeight = string.IsNullOrEmpty(m_currentSpeaker)
+ ? 0f
+ : m_speakerStyle.CalcHeight(new GUIContent(m_currentSpeaker), textWidth);
+ float textHeight = m_textStyle.CalcHeight(new GUIContent(string.IsNullOrEmpty(m_currentText) ? " " : m_currentText), textWidth);
+
+ float boxWidth = maxWidth;
+ float boxHeight = speakerHeight + textHeight + 28f;
+ float boxX = (Screen.width - boxWidth) * 0.5f;
+ float boxY = Screen.height - bottomOffset - boxHeight;
+
+ Rect boxRect = new Rect(boxX, boxY, boxWidth, boxHeight);
+ Color previousColor = GUI.color;
+ GUI.color = new Color(1f, 1f, 1f, m_alpha);
+ GUI.DrawTexture(boxRect, m_background);
+
+ float yOffset = boxRect.y + 10f;
+ if (!string.IsNullOrEmpty(m_currentSpeaker))
+ {
+ Rect speakerRect = new Rect(
+ boxRect.x + horizontalPadding,
+ yOffset,
+ textWidth,
+ speakerHeight);
+ GUI.Label(speakerRect, m_currentSpeaker, m_speakerStyle);
+ yOffset += speakerHeight + 2f;
+ }
+
+ Rect textRect = new Rect(
+ boxRect.x + horizontalPadding,
+ yOffset,
+ textWidth,
+ textHeight);
+
+ GUI.Label(textRect, m_currentText, m_textStyle);
+ GUI.color = previousColor;
+ }
+
+ private void EnsureStyles()
+ {
+ if (m_textStyle != null)
+ return;
+
+ m_background = new Texture2D(1, 1);
+ m_background.SetPixel(0, 0, backgroundColor);
+ m_background.Apply();
+
+ m_textStyle = new GUIStyle(GUI.skin.label)
+ {
+ alignment = TextAnchor.MiddleCenter,
+ fontSize = fontSize,
+ wordWrap = true,
+ richText = false,
+ clipping = TextClipping.Clip,
+ };
+ m_textStyle.normal.textColor = textColor;
+
+ m_speakerStyle = new GUIStyle(GUI.skin.label)
+ {
+ alignment = TextAnchor.MiddleCenter,
+ fontSize = speakerFontSize,
+ fontStyle = FontStyle.Bold,
+ wordWrap = false,
+ clipping = TextClipping.Clip,
+ };
+ m_speakerStyle.normal.textColor = speakerColor;
+
+ }
+
+ private void OnDestroy()
+ {
+ if (m_background != null)
+ Destroy(m_background);
+ }
+}
diff --git a/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs.meta b/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs.meta
new file mode 100644
index 0000000..727a995
--- /dev/null
+++ b/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a315c9c77ad8a49238033181ece48806
\ No newline at end of file