git : Merge branch 'feat/level/create-level-1' into fix/dev-room

This commit is contained in:
Thibault Pouch
2026-03-11 11:37:33 +01:00
36 changed files with 23476 additions and 7835 deletions

View File

@@ -0,0 +1,189 @@
using System;
using UnityEngine;
using UnityEngine.Events;
public class ButtonSequenceDoorPuzzle : MonoBehaviour
{
[Header("References")]
[Tooltip("All available buttons for this puzzle.")]
[SerializeField] private WallInteractButton[] buttons;
[Tooltip("Door to open when the sequence is correct.")]
[SerializeField] private SlidingDoor targetDoor;
[Tooltip("Optional blocks controlled by this puzzle (reset with SetOff on start/wrong input).")]
[SerializeField] private TestBlock[] puzzleBlocks;
[Header("Sequence")]
[Tooltip("Button indices (from the buttons array) that must be pressed in order. Example: 2,0,3")]
[SerializeField] private int[] requiredSequence = { 0, 1, 2 };
[Tooltip("If true, wrong input resets progress back to 0.")]
[SerializeField] private bool resetOnWrongPress = true;
[Tooltip("If true, puzzle can only be solved once.")]
[SerializeField] private bool lockAfterSolved = true;
[Header("Debug")]
[SerializeField] private bool enableDebugLogs = true;
private int m_progress;
private bool m_isSolved;
private UnityAction[] m_cachedListeners;
private void OnEnable()
{
SetAllBlocksOff();
RegisterAllButtons();
}
private void OnDisable()
{
UnregisterAllButtons();
}
private void RegisterAllButtons()
{
if (buttons == null)
return;
m_cachedListeners = new UnityAction[buttons.Length];
for (int i = 0; i < buttons.Length; i++)
{
WallInteractButton button = buttons[i];
if (button == null)
continue;
int buttonIndex = i;
UnityAction action = () => OnButtonPressed(buttonIndex);
m_cachedListeners[i] = action;
button.OnInteract.AddListener(action);
}
}
private void UnregisterAllButtons()
{
if (buttons == null)
return;
for (int i = 0; i < buttons.Length; i++)
{
WallInteractButton button = buttons[i];
if (button == null)
continue;
if (m_cachedListeners != null && i < m_cachedListeners.Length && m_cachedListeners[i] != null)
button.OnInteract.RemoveListener(m_cachedListeners[i]);
}
m_cachedListeners = null;
m_progress = 0;
}
private void OnButtonPressed(int buttonIndex)
{
Log($"Button pressed: index {buttonIndex}");
if (m_isSolved && lockAfterSolved)
{
Log("Puzzle already solved and locked.");
return;
}
if (!IsSequenceValid())
{
Log("Invalid sequence configuration.");
return;
}
int expectedIndex = requiredSequence[m_progress];
Log($"Expected button index: {expectedIndex} (step {m_progress + 1}/{requiredSequence.Length})");
if (buttonIndex == expectedIndex)
{
m_progress++;
Log($"Correct input. Progress: {m_progress}/{requiredSequence.Length}");
if (m_progress >= requiredSequence.Length)
{
SolvePuzzle();
}
return;
}
if (resetOnWrongPress)
{
Log("Wrong input. Resetting sequence and turning puzzle blocks OFF.");
m_progress = 0;
SetAllBlocksOff();
return;
}
Log("Wrong input, but resetOnWrongPress is disabled.");
}
private bool IsSequenceValid()
{
if (requiredSequence == null || requiredSequence.Length == 0)
return false;
if (buttons == null || buttons.Length == 0)
return false;
for (int i = 0; i < requiredSequence.Length; i++)
{
int index = requiredSequence[i];
if (index < 0 || index >= buttons.Length)
return false;
}
return true;
}
private void SolvePuzzle()
{
m_isSolved = true;
m_progress = 0;
Log("Sequence completed. Opening door.");
if (targetDoor != null)
targetDoor.Open();
}
private void SetAllBlocksOff()
{
if (puzzleBlocks == null)
return;
for (int i = 0; i < puzzleBlocks.Length; i++)
{
if (puzzleBlocks[i] == null)
continue;
puzzleBlocks[i].SetOff();
}
}
private void Log(string message)
{
if (!enableDebugLogs)
return;
Debug.Log($"[{nameof(ButtonSequenceDoorPuzzle)}] {message}", this);
}
#if UNITY_EDITOR
private void OnValidate()
{
if (requiredSequence == null)
return;
for (int i = 0; i < requiredSequence.Length; i++)
{
requiredSequence[i] = Math.Max(0, requiredSequence[i]);
}
}
#endif
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 920802292ba9a49d2bee3519a905717d

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2e7b9c54377674993a7922f84e9cfcce
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,262 @@
using System;
using System.Collections;
using UnityEngine;
/// <summary>
/// Reusable subtitle player that renders and plays subtitle lines loaded from JSON.
/// </summary>
public class SubtitleSequencePlayer : MonoBehaviour
{
[Serializable]
private struct SubtitleLine
{
public string speaker;
public string text;
public float duration;
}
[Serializable]
private struct SubtitleFile
{
public SubtitleLine[] lines;
}
[Header("Optional Default Data")]
[Tooltip("Used only if trigger zone calls PlayDefault().")]
[SerializeField] private TextAsset defaultSubtitleJson;
[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 string m_currentSpeaker;
private string m_currentText;
private GUIStyle m_textStyle;
private GUIStyle m_speakerStyle;
private Texture2D m_background;
private bool m_isShowing;
private bool m_isPlaying;
private float m_alpha;
private SubtitleLine[] m_runtimeLines = Array.Empty<SubtitleLine>();
public bool IsPlaying => m_isPlaying;
public bool TryPlay(TextAsset subtitleJson, float initialDelay = 0f)
{
if (m_isPlaying)
return false;
if (!TryReadLinesFromJson(subtitleJson, out SubtitleLine[] parsedLines))
return false;
m_runtimeLines = parsedLines;
StartCoroutine(PlaySequence(initialDelay));
return true;
}
public bool PlayDefault(float initialDelay = 0f)
{
return TryPlay(defaultSubtitleJson, initialDelay);
}
private IEnumerator PlaySequence(float initialDelay)
{
m_isPlaying = true;
if (m_runtimeLines == null || m_runtimeLines.Length == 0)
{
m_isPlaying = false;
yield break;
}
if (initialDelay > 0f)
yield return new WaitForSeconds(initialDelay);
for (int i = 0; i < m_runtimeLines.Length; i++)
{
if (string.IsNullOrWhiteSpace(m_runtimeLines[i].text) || m_runtimeLines[i].duration <= 0f)
continue;
yield return StartCoroutine(ShowLine(m_runtimeLines[i]));
if (gapBetweenLines > 0f)
yield return new WaitForSeconds(gapBetweenLines);
}
m_currentSpeaker = string.Empty;
m_currentText = string.Empty;
m_isPlaying = false;
}
private bool TryReadLinesFromJson(TextAsset subtitleJson, out SubtitleLine[] parsedLines)
{
parsedLines = Array.Empty<SubtitleLine>();
if (subtitleJson == null || string.IsNullOrWhiteSpace(subtitleJson.text))
return false;
SubtitleFile file;
try
{
file = JsonUtility.FromJson<SubtitleFile>(subtitleJson.text);
}
catch
{
return false;
}
if (file.lines == null || file.lines.Length == 0)
return false;
parsedLines = file.lines;
return true;
}
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);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4947743d7bc9b4589b9932d429517d3a

View File

@@ -0,0 +1,51 @@
using UnityEngine;
/// <summary>
/// Trigger zone that starts a subtitle JSON sequence on a linked SubtitleSequencePlayer.
/// Put this on the zone collider object, and link the player on your empty object.
/// </summary>
[RequireComponent(typeof(Collider))]
public class SubtitleTriggerZone : MonoBehaviour
{
[Header("References")]
[SerializeField] private SubtitleSequencePlayer subtitlePlayer;
[SerializeField] private TextAsset subtitleJson;
[Header("Playback")]
[SerializeField] private float initialDelay = 0f;
[SerializeField] private bool oneShot = true;
private bool m_hasPlayed;
private void Reset()
{
Collider col = GetComponent<Collider>();
col.isTrigger = true;
}
private void OnTriggerEnter(Collider other)
{
if (!IsPlayer(other))
return;
if (oneShot && m_hasPlayed)
return;
if (subtitlePlayer == null)
return;
if (subtitlePlayer.TryPlay(subtitleJson, initialDelay))
m_hasPlayed = true;
}
private bool IsPlayer(Collider other)
{
if (other.CompareTag("Player"))
return true;
if (other.GetComponentInParent<PlayerMovement>() != null)
return true;
return false;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 33a33c65f75e2443383c2e29bd6bf5f1

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3952fe191e7e945b3ba35d76408a51a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
{
"lines": [
{
"speaker": "SYSTEME",
"text": "...Ici, quelque chose cloche.",
"duration": 2.5
},
{
"speaker": "SYSTEME",
"text": "Reste calme. Observe la piece.",
"duration": 2.5
},
{
"speaker": "SYSTEME",
"text": "Trouve une sortie.",
"duration": 2.2
}
]
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c4b9d13b29337441dbdb06a8a45e32c3
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: