Add subtitle UI for voice playback

This commit is contained in:
2026-05-16 21:33:00 +01:00
parent 58050abded
commit 3174079e37
81 changed files with 8657 additions and 1231 deletions

View File

@@ -25,7 +25,7 @@ mainly on vines and thorns.
9. **Tokens** - Cancellation Tokens should be re-used where possible.
10. **Versions** - We're using C# 9 with Unity 6.3
11. **Workflow** - You're not to make any code changes until you've shown me the proposed changes, I'll then either approve, deny, or modify them before you make the change.
12. **Events** - We use C# events, actions, and delegates rather than UnityEvents, except when dealing with raw/stock ui components.
## Documentation

View File

@@ -27,7 +27,6 @@ namespace BriarQueen.Data.Identifiers
ChapterOneArrivalRoad,
ChapterOneAshwickOutskirts,
ChapterOneInsideBrokenDownCar,
ChapterOneAshwickOutskirtsKeypad,
}
public enum AssetItemKey

View File

@@ -13,6 +13,7 @@ namespace BriarQueen.Data.Identifiers
None = 0,
CarDoorOpening,
ItemPickup,
AshwickGateOpening
}
public enum UIFXKey
@@ -30,6 +31,22 @@ namespace BriarQueen.Data.Identifiers
public enum VoiceKey
{
None = 0,
EmptyHands,
CantUseItem,
SomethingMissing,
CarefulInteract,
LooksImportant,
WrongTool,
CodexLocked,
Locked,
CantGoThere,
DoesntBelong,
FireHot,
AshwickHallowSign,
FirstSkeleton,
ClockTower,
}
public static class AudioNameIdentifiers
@@ -46,16 +63,17 @@ namespace BriarQueen.Data.Identifiers
new ReadOnlyDictionary<SFXKey, string>(
new Dictionary<SFXKey, string>
{
{ SFXKey.CarDoorOpening, "SFX:CarDoorOpening" },
{ SFXKey.ItemPickup, "SFX:ItemPickup" },
{ SFXKey.CarDoorOpening, "SFX:General:CarDoorOpening" },
{ SFXKey.ItemPickup, "SFX:General:ItemPickup" },
{ SFXKey.AshwickGateOpening, "SFX:Level:AshwickOutskirts:GateOpening" },
});
public static readonly IReadOnlyDictionary<UIFXKey, string> UIFX =
new ReadOnlyDictionary<UIFXKey, string>(
new Dictionary<UIFXKey, string>
{
{ UIFXKey.AchievementUnlocked, "UIFX:AchievementUnlocked" },
{ UIFXKey.CodexEntryUnlocked, "UIFX:CodexEntryUnlocked" },
{ UIFXKey.AchievementUnlocked, "UIFX:General:AchievementUnlocked" },
{ UIFXKey.CodexEntryUnlocked, "UIFX:General:CodexEntryUnlocked" },
});
public static readonly IReadOnlyDictionary<AmbienceKey, string> Ambience =
@@ -69,7 +87,21 @@ namespace BriarQueen.Data.Identifiers
new ReadOnlyDictionary<VoiceKey, string>(
new Dictionary<VoiceKey, string>
{
// Add voice mappings here
{ VoiceKey.EmptyHands, "Voice:Item:EmptyHands" },
{ VoiceKey.CantUseItem, "Voice:Item:CantUseItem" },
{ VoiceKey.SomethingMissing, "Voice:Item:SomethingMissing" },
{ VoiceKey.CarefulInteract, "Voice:Item:CarefulInteract" },
{ VoiceKey.LooksImportant, "Voice:Item:LooksImportant" },
{ VoiceKey.WrongTool, "Voice:Item:WrongTool" },
{ VoiceKey.CodexLocked, "Voice:Item:CodexLocked" },
{ VoiceKey.Locked, "Voice:Environment:Locked" },
{ VoiceKey.CantGoThere, "Voice:Environment:CantGoThere" },
{ VoiceKey.DoesntBelong, "Voice:Environment:DoesntBelong" },
{ VoiceKey.FireHot, "Voice:Environment:FireHot" },
{ VoiceKey.AshwickHallowSign, "Voice:Environment:AshwickHallowSign" },
{ VoiceKey.FirstSkeleton, "Voice:Environment:FirstSkeleton" },
{ VoiceKey.ClockTower, "Voice:Environment:ClockTower" },
});
public static string Get(MusicKey key)

View File

@@ -12,7 +12,7 @@ namespace BriarQueen.Data.Identifiers
public enum ClueEntryID
{
None = 0,
AshwickMarketGate,
JasonsNote,
}
public enum PhotoEntryID
@@ -37,7 +37,7 @@ namespace BriarQueen.Data.Identifiers
new ReadOnlyDictionary<ClueEntryID, string>(
new Dictionary<ClueEntryID, string>
{
{ ClueEntryID.AshwickMarketGate, $"{PHOTO_PREFIX}:AshwickMarketGate" },
{ ClueEntryID.JasonsNote, $"{PHOTO_PREFIX}:AshwickMarketGate" },
});
public static readonly IReadOnlyDictionary<PhotoEntryID, string> Photos =

View File

@@ -18,7 +18,6 @@ namespace BriarQueen.Data.Identifiers
public enum LevelInteractKey
{
None = 0,
MarketplaceFirstEntry
}
public enum EnvironmentInteractKey
@@ -29,11 +28,8 @@ namespace BriarQueen.Data.Identifiers
DoesntBelong = 3,
FireHot = 4,
AshwickHallowSign = 5,
AshwickRidgewayStatue = 6,
AshwickBlockedRidgwayRoad = 7,
AshwickRidgewaySkeleton = 8,
AskwickMarketplaceSign = 9,
FirstSkeleton = 10,
ClockTower = 11,
}
public enum UIInteractKey
@@ -62,7 +58,6 @@ namespace BriarQueen.Data.Identifiers
new ReadOnlyDictionary<LevelInteractKey, string>(
new Dictionary<LevelInteractKey, string>
{
{ LevelInteractKey.MarketplaceFirstEntry, "I wonder if any of these are unlocked."}
});
public static readonly IReadOnlyDictionary<EnvironmentInteractKey, string> EnvironmentInteractions =
@@ -74,9 +69,8 @@ namespace BriarQueen.Data.Identifiers
{ EnvironmentInteractKey.DoesntBelong, "This feels out of place." },
{ EnvironmentInteractKey.FireHot, "Too hot to get close." },
{ EnvironmentInteractKey.AshwickHallowSign, "Ashwick Hallow… Even the name feels like a warning."},
{ EnvironmentInteractKey.AshwickRidgewayStatue, "Lovely sculpture. Mildly terrifying, but lovely."},
{ EnvironmentInteractKey.AshwickBlockedRidgwayRoad, "Right. Closed road. Of course it is."},
{ EnvironmentInteractKey.FirstSkeleton, "What the hell happened here?"}
{ EnvironmentInteractKey.FirstSkeleton, "What the hell happened here?"},
{ EnvironmentInteractKey.ClockTower, "Even from here, something about that clock tower feels wrong."}
});
public static readonly IReadOnlyDictionary<UIInteractKey, string> UIInteractions =
@@ -106,4 +100,4 @@ namespace BriarQueen.Data.Identifiers
return UIInteractions.TryGetValue(key, out var value) ? value : string.Empty;
}
}
}
}

View File

@@ -11,24 +11,45 @@ namespace BriarQueen.Data.Identifiers
// TutorialTip = 2
}
public static class SubtitleIdentifiers
public readonly struct SubtitleEntry
{
public static readonly IReadOnlyDictionary<SubtitleKey, string> Subtitles =
new ReadOnlyDictionary<SubtitleKey, string>(
new Dictionary<SubtitleKey, string>
{
// { SubtitleKey.IntroLine, "Subtitle:IntroLine" },
// { SubtitleKey.TutorialTip, "Subtitle:TutorialTip" }
});
public static string Get(SubtitleKey key)
public SubtitleEntry(string text, float preferredDurationSeconds = 0f)
{
return Subtitles.TryGetValue(key, out var value) ? value : string.Empty;
Text = text;
PreferredDurationSeconds = preferredDurationSeconds;
}
public static IEnumerable<string> GetAll()
public string Text { get; }
public float PreferredDurationSeconds { get; }
}
public static class SubtitleIdentifiers
{
public static readonly IReadOnlyDictionary<SubtitleKey, SubtitleEntry> Subtitles =
new ReadOnlyDictionary<SubtitleKey, SubtitleEntry>(
new Dictionary<SubtitleKey, SubtitleEntry>
{
// { SubtitleKey.IntroLine, new SubtitleEntry("Example subtitle.", 2.5f) },
});
public static bool TryGet(SubtitleKey key, out SubtitleEntry entry)
{
return Subtitles.Values;
return Subtitles.TryGetValue(key, out entry);
}
public static string GetText(SubtitleKey key)
{
return Subtitles.TryGetValue(key, out var entry) ? entry.Text : string.Empty;
}
public static float GetPreferredDuration(SubtitleKey key)
{
return Subtitles.TryGetValue(key, out var entry) ? entry.PreferredDurationSeconds : 0f;
}
public static IEnumerable<SubtitleKey> GetAllKeys()
{
return Subtitles.Keys;
}
}
}

View File

@@ -7,12 +7,12 @@ namespace BriarQueen.Data.Identifiers
ReturnToPreviousLevel,
UsingItemsTogether,
HideHUD,
ExitItems,
MultipleUseItems,
DarkRooms,
Codex,
HiddenItems,
ResetPuzzles,
LeavingPuzzles,
Tools,
ItemsAway,
ItemCycling,
@@ -25,7 +25,7 @@ namespace BriarQueen.Data.Identifiers
{
{
TutorialPopupID.ReturnToPreviousLevel,
"Click the bottom corners to return to the previous area."
"Click the lower corners to return to the previous area."
},
{
TutorialPopupID.UsingItemsTogether,
@@ -35,13 +35,9 @@ namespace BriarQueen.Data.Identifiers
TutorialPopupID.HideHUD,
"Press '{Hide_HUD}' to hide the HUD."
},
{
TutorialPopupID.ExitItems,
"Press '{Right_Click}' to exit the current interaction."
},
{
TutorialPopupID.MultipleUseItems,
"Some items can be used multiple times, but they may wear out."
"Some items can be used more than once, but they may wear out."
},
{
TutorialPopupID.DarkRooms,
@@ -49,7 +45,7 @@ namespace BriarQueen.Data.Identifiers
},
{
TutorialPopupID.Codex,
"The Codex is used to collect any documents you encounter. Press '{Codex}' to open it."
"Documents you find are stored in the Codex. Press '{Codex}' to open it."
},
{
TutorialPopupID.HiddenItems,
@@ -57,26 +53,31 @@ namespace BriarQueen.Data.Identifiers
},
{
TutorialPopupID.ResetPuzzles,
"Some puzzles can be reset."
"Some puzzles can be reset if you get stuck."
},
{
TutorialPopupID.LeavingPuzzles,
"When you leave a puzzle, your progress is saved."
},
{
TutorialPopupID.Tools,
"You'll find tools as you explore. Try them on different objects. Press '{Show_Tools}' to view your tools."
"You'll find tools as you explore. Try them on different objects. Press '{Show_Tools}' to open your tools."
},
{
TutorialPopupID.ItemsAway,
"Press '{Right_Click}' to put away the current item."
"Press '{Right_Click}' to put away the selected item."
},
{
TutorialPopupID.ItemCycling,
"Press '{Previous_Item}' or '{Next_Item}' to cycle through your backpack."
"Press '{Previous_Item}' or '{Next_Item}' to cycle through the items in your backpack."
},
{
TutorialPopupID.ToolCycling,
"Press '{Previous_Tool}' or '{Next_Tool}' to cycle through your tools."
}
},
};
public static IEnumerable<string> GetAllTexts() => AllPopups.Values;
}
}
}

View File

@@ -32,6 +32,9 @@ namespace BriarQueen.Framework.Effects
[SerializeField]
private bool _randomizeFlickerOffset = true;
[SerializeField]
private bool _useStartingValues;
[Header("Tween")]
[SerializeField]
@@ -81,8 +84,12 @@ namespace BriarQueen.Framework.Effects
}
CreateRuntimeMaterial();
SetLightColor(_startingColor);
SetIntensity(_startingIntensity);
if(_useStartingValues)
{
SetLightColor(_startingColor);
SetIntensity(_startingIntensity);
}
if (_randomizeFlickerOffset && _runtimeMaterial != null)
{

View File

@@ -0,0 +1,9 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.Audio
{
public record VoicePlaybackFinishedEvent(
VoiceKey VoiceKey,
SubtitleKey SubtitleKey) : IEvent;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a5c701b2f1b56482cb423352feaecd6b

View File

@@ -0,0 +1,10 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.Audio
{
public record VoicePlaybackStartedEvent(
VoiceKey VoiceKey,
SubtitleKey SubtitleKey,
float ClipLengthSeconds) : IEvent;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7b2afa1c54b63413293f433a9490a998

View File

@@ -0,0 +1,6 @@
using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.UI
{
public record SubtitleDisplayChangedEvent(string Text, bool Visible) : IEvent;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d606965a42b7a4bc29e67528e42120e2

View File

@@ -45,7 +45,8 @@ namespace BriarQueen.Framework.Managers.Audio
private AudioSource _musicSourceB;
private AudioSource _voiceSource;
private string _activeVoiceSubtitleId;
private VoiceKey _activeVoiceKey = VoiceKey.None;
private SubtitleKey _activeSubtitleKey = SubtitleKey.None;
private AudioFileSo _currentMusicTrack;
private CancellationTokenSource _musicDuckCts;
@@ -140,7 +141,8 @@ namespace BriarQueen.Framework.Managers.Audio
_musicSourceB = null;
_voiceSource = null;
_currentMusicTrack = null;
_activeVoiceSubtitleId = null;
_activeVoiceKey = VoiceKey.None;
_activeSubtitleKey = SubtitleKey.None;
_voiceFinishedPublished = false;
Initialized = false;
}
@@ -205,8 +207,7 @@ namespace BriarQueen.Framework.Managers.Audio
if (_audioMixer == null || string.IsNullOrWhiteSpace(parameter))
return;
if (!_baseDb.TryGetValue(parameter, out var baseDb))
baseDb = 0f;
var baseDb = _baseDb.GetValueOrDefault(parameter, 0f);
var effective = baseDb;
@@ -340,10 +341,14 @@ namespace BriarQueen.Framework.Managers.Audio
_voiceCts = new CancellationTokenSource();
var token = _voiceCts.Token;
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
_activeVoiceKey = audioData.VoiceKey;
_activeSubtitleKey = audioData.MatchingSubtitleID;
_voiceFinishedPublished = false;
_eventCoordinator.Publish(new VoiceLineStartedEvent(_activeVoiceSubtitleId));
_eventCoordinator.Publish(new VoicePlaybackStartedEvent(
_activeVoiceKey,
_activeSubtitleKey,
audioData.Clip.length));
_voiceSource.clip = audioData.Clip;
_voiceSource.pitch = audioData.Pitch;
@@ -369,11 +374,16 @@ namespace BriarQueen.Framework.Managers.Audio
{
if (_voiceFinishedPublished) return;
if (!string.IsNullOrEmpty(_activeVoiceSubtitleId))
_eventCoordinator.Publish(new VoiceLineFinishedEvent(_activeVoiceSubtitleId));
if (_activeVoiceKey != VoiceKey.None || _activeSubtitleKey != SubtitleKey.None)
{
_eventCoordinator.Publish(new VoicePlaybackFinishedEvent(
_activeVoiceKey,
_activeSubtitleKey));
}
_voiceFinishedPublished = true;
_activeVoiceSubtitleId = null;
_activeVoiceKey = VoiceKey.None;
_activeSubtitleKey = SubtitleKey.None;
}
public void StopVoice()
@@ -666,4 +676,4 @@ namespace BriarQueen.Framework.Managers.Audio
public float StartedAtUnscaled;
}
}
}
}

View File

@@ -449,13 +449,12 @@ namespace BriarQueen.Framework.Managers.Input
private void OnPause(InputAction.CallbackContext ctx)
{
var isMainMenu = _gameService != null && _gameService.IsMainMenuSceneLoaded;
if (isMainMenu || _isAnyUIOpen)
if (isMainMenu)
{
_eventCoordinator?.PublishImmediate(new UIBackRequestedEvent());
return;
}
_isPaused = true;
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
}

View File

@@ -163,6 +163,22 @@ namespace BriarQueen.Framework.Managers.Interaction
Debug.Log($"[InteractManager] SetExclusiveRaycaster set to {raycaster.gameObject.name}.");
}
public void ReleaseExclusiveRaycaster(GraphicRaycaster raycaster)
{
if (raycaster == null)
return;
if (_exclusiveRaycaster != raycaster)
return;
_exclusiveRaycaster = null;
if (_currentHovered != null)
ClearHover().Forget();
Debug.Log($"[InteractManager] Released exclusive raycaster {raycaster.gameObject.name}.");
}
/// <summary>
/// Clear exclusive mode and return to using all registered raycasters.
/// </summary>
@@ -453,4 +469,4 @@ namespace BriarQueen.Framework.Managers.Interaction
_selectedItem = evt.Item;
}
}
}
}

View File

@@ -15,10 +15,15 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
[Header("Codex")]
[SerializeField]
private CodexEntrySo _codexEntry;
[SerializeField]
private bool _removeTrigger;
[Header("Events")]
[SerializeField]
private SFXKey _soundEffect;
[SerializeField]
private VoiceKey _voiceLine;
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Inspect;
public override string InteractableName =>
@@ -40,12 +45,23 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
return;
}
PlayerManager.UnlockCodexEntry(_codexEntry);
if (_removeTrigger)
{
await Remove();
}
if (_soundEffect != SFXKey.None)
{
AudioManager.Play(AudioNameIdentifiers.Get(_soundEffect));
}
if (_voiceLine != VoiceKey.None)
{
AudioManager.Play(AudioNameIdentifiers.Get(_voiceLine));
}
}
protected override void UpdateSaveGameOnRemoval()

View File

@@ -4,6 +4,7 @@ namespace BriarQueen.Framework.Managers.UI.Base
{
public interface IUIOverlayHost
{
bool CanSuspendFor(WindowType incomingWindowType);
UniTask SuspendForOverlay();
UniTask ResumeFromOverlay();
}

View File

@@ -1,16 +1,19 @@
using System.Threading;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
namespace BriarQueen.Framework.Managers.UI.Base
{
public enum UIPauseBehavior
{
TreatAsBackRequest,
OpenPauseOverlay
}
public interface IUIWindow
{
UniTask Show();
UniTask Hide();
WindowType WindowType { get; }
UIPauseBehavior PauseBehavior { get; }
}
}
}

View File

@@ -4,6 +4,7 @@ namespace BriarQueen.Framework.Managers.UI.Base
{
PauseMenuWindow,
SettingsWindow,
CodexWindow
CodexWindow,
AshwickGateKeypadWindow
}
}
}

View File

@@ -41,12 +41,14 @@ namespace BriarQueen.Framework.Managers.UI
public bool Initialized { get; private set; }
private sealed record OverlayResumeContext(WindowType OverlayWindowType, IUIOverlayHost Host);
private IHud _hudContainer;
private IPopup _infoPopup;
private IPopup _tutorialPopup;
private IScreenFader _screenFader;
private IUIOverlayHost _mainMenuOverlayHost;
private IUIOverlayHost _activeSettingsOverlayHost;
private readonly Stack<OverlayResumeContext> _overlayResumeStack = new();
[Inject]
public UIManager(
@@ -127,6 +129,18 @@ namespace BriarQueen.Framework.Managers.UI
window.Hide().Forget();
}
public void UnregisterWindow(IUIWindow window)
{
if (window == null)
return;
if (_windows.TryGetValue(window.WindowType, out var registered) && ReferenceEquals(registered, window))
_windows.Remove(window.WindowType);
if (window is IUIOverlayHost overlayHost)
RemoveOverlayResumeContextsForHost(overlayHost);
}
public void RegisterHUD(IHud hudContainer)
{
_hudContainer = hudContainer;
@@ -169,9 +183,7 @@ namespace BriarQueen.Framework.Managers.UI
if (!ReferenceEquals(_mainMenuOverlayHost, host))
return;
if (ReferenceEquals(_activeSettingsOverlayHost, host))
_activeSettingsOverlayHost = null;
RemoveOverlayResumeContextsForHost(host);
_mainMenuOverlayHost = null;
}
@@ -186,6 +198,51 @@ namespace BriarQueen.Framework.Managers.UI
return target != null && _windowStack.Contains(target);
}
private void RemoveOverlayResumeContextsForHost(IUIOverlayHost host)
{
if (host == null || _overlayResumeStack.Count == 0)
return;
var contextsToKeep = new List<OverlayResumeContext>();
foreach (var context in _overlayResumeStack)
{
if (!ReferenceEquals(context.Host, host))
contextsToKeep.Add(context);
}
_overlayResumeStack.Clear();
for (var i = contextsToKeep.Count - 1; i >= 0; i--)
_overlayResumeStack.Push(contextsToKeep[i]);
}
private async UniTask<bool> TrySuspendActiveWindowFor(WindowType incomingWindowType)
{
if (ActiveWindow is IUIOverlayHost overlayHost &&
overlayHost.CanSuspendFor(incomingWindowType))
{
await overlayHost.SuspendForOverlay();
_overlayResumeStack.Push(new OverlayResumeContext(incomingWindowType, overlayHost));
return true;
}
return false;
}
private async UniTask RestoreUnderlyingUi(WindowType closedWindowType)
{
if (_overlayResumeStack.Count > 0 &&
_overlayResumeStack.Peek().OverlayWindowType == closedWindowType)
{
var resumeContext = _overlayResumeStack.Pop();
await resumeContext.Host.ResumeFromOverlay();
return;
}
if (ActiveWindow != null)
await ActiveWindow.Show();
}
private async UniTask ApplyHudVisibility(bool visible)
{
if (_disposed || _hudContainer == null)
@@ -206,13 +263,25 @@ namespace BriarQueen.Framework.Managers.UI
private void OnPauseClickReceived(PauseButtonClickedEvent _)
{
if (_windowStack.Count > 0)
if (ActiveWindow == null)
{
OpenWindow(WindowType.PauseMenuWindow);
return;
}
if (ActiveWindow.WindowType == WindowType.PauseMenuWindow)
{
TryHandleBackRequest();
return;
}
OpenWindow(WindowType.PauseMenuWindow);
if (ActiveWindow.PauseBehavior == UIPauseBehavior.OpenPauseOverlay)
{
OpenWindow(WindowType.PauseMenuWindow);
return;
}
TryHandleBackRequest();
}
private void OnBackRequested(UIBackRequestedEvent _)
@@ -350,28 +419,22 @@ namespace BriarQueen.Framework.Managers.UI
if (_windowStack.Contains(window))
return;
_activeSettingsOverlayHost = null;
var suspended = false;
var openingSettingsOverPause =
source == SettingsOpenSource.PauseMenu &&
ActiveWindow?.WindowType == WindowType.PauseMenuWindow &&
ActiveWindow is IUIOverlayHost;
var openingSettingsOverMainMenu =
source == SettingsOpenSource.MainMenu &&
_mainMenuOverlayHost != null;
if (openingSettingsOverPause)
if (source == SettingsOpenSource.MainMenu &&
_mainMenuOverlayHost != null &&
_mainMenuOverlayHost.CanSuspendFor(WindowType.SettingsWindow))
{
_activeSettingsOverlayHost = (IUIOverlayHost)ActiveWindow;
await _activeSettingsOverlayHost.SuspendForOverlay();
await _mainMenuOverlayHost.SuspendForOverlay();
_overlayResumeStack.Push(new OverlayResumeContext(WindowType.SettingsWindow, _mainMenuOverlayHost));
suspended = true;
}
else if (openingSettingsOverMainMenu)
else
{
_activeSettingsOverlayHost = _mainMenuOverlayHost;
await _activeSettingsOverlayHost.SuspendForOverlay();
suspended = await TrySuspendActiveWindowFor(WindowType.SettingsWindow);
}
else if (ActiveWindow != null)
if (!suspended && ActiveWindow != null)
{
await ActiveWindow.Hide();
}
@@ -407,7 +470,9 @@ namespace BriarQueen.Framework.Managers.UI
if (_windowStack.Contains(window))
return;
if (ActiveWindow != null)
var suspended = await TrySuspendActiveWindowFor(windowType);
if (!suspended && ActiveWindow != null)
await ActiveWindow.Hide();
_windowStack.Push(window);
@@ -452,15 +517,7 @@ namespace BriarQueen.Framework.Managers.UI
break;
}
if (target.WindowType == WindowType.SettingsWindow && _activeSettingsOverlayHost != null)
{
await _activeSettingsOverlayHost.ResumeFromOverlay();
_activeSettingsOverlayHost = null;
}
else if (ActiveWindow != null)
{
await ActiveWindow.Show();
}
await RestoreUnderlyingUi(target.WindowType);
NotifyUIStackChanged();
}
@@ -506,17 +563,8 @@ namespace BriarQueen.Framework.Managers.UI
NotifyWindowStateChanged(top.WindowType, false);
}
if (top != null &&
top.WindowType == WindowType.SettingsWindow &&
_activeSettingsOverlayHost != null)
{
await _activeSettingsOverlayHost.ResumeFromOverlay();
_activeSettingsOverlayHost = null;
}
else if (ActiveWindow != null)
{
await ActiveWindow.Show();
}
if (top != null)
await RestoreUnderlyingUi(top.WindowType);
NotifyUIStackChanged();
}
@@ -536,17 +584,12 @@ namespace BriarQueen.Framework.Managers.UI
await _windowTransitionGate.WaitAsync();
try
{
var shouldResumeSettingsHost = false;
while (_windowStack.Count > 0)
{
var window = _windowStack.Pop();
if (window == null)
continue;
if (window.WindowType == WindowType.SettingsWindow && _activeSettingsOverlayHost != null)
shouldResumeSettingsHost = true;
try
{
await window.Hide();
@@ -557,18 +600,7 @@ namespace BriarQueen.Framework.Managers.UI
}
}
if (shouldResumeSettingsHost)
{
try
{
await _activeSettingsOverlayHost.ResumeFromOverlay();
}
catch
{
}
}
_activeSettingsOverlayHost = null;
_overlayResumeStack.Clear();
if (_tutorialPopup != null)
{
@@ -621,7 +653,7 @@ namespace BriarQueen.Framework.Managers.UI
faderComponent.gameObject.SetActive(false);
_windowStack.Clear();
_activeSettingsOverlayHost = null;
_overlayResumeStack.Clear();
_mainMenuOverlayHost = null;
}

View File

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

View File

@@ -0,0 +1,148 @@
using System;
using System.Threading;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.Audio;
using BriarQueen.Framework.Events.UI;
using Cysharp.Threading.Tasks;
using VContainer;
namespace BriarQueen.Framework.Services.Subtitles
{
public class SubtitleService : IDisposable
{
private readonly EventCoordinator _eventCoordinator;
private CancellationTokenSource _subtitleCts;
private SubtitleKey _activeSubtitleKey = SubtitleKey.None;
private string _currentText = string.Empty;
public bool IsVisible => _activeSubtitleKey != SubtitleKey.None && !string.IsNullOrWhiteSpace(_currentText);
public string CurrentText => _currentText;
[Inject]
public SubtitleService(EventCoordinator eventCoordinator)
{
_eventCoordinator = eventCoordinator;
}
public void Initialize()
{
_eventCoordinator.Subscribe<VoicePlaybackStartedEvent>(OnVoicePlaybackStarted);
_eventCoordinator.Subscribe<VoicePlaybackFinishedEvent>(OnVoicePlaybackFinished);
}
public void Dispose()
{
_eventCoordinator.Unsubscribe<VoicePlaybackStartedEvent>(OnVoicePlaybackStarted);
_eventCoordinator.Unsubscribe<VoicePlaybackFinishedEvent>(OnVoicePlaybackFinished);
CancelCurrentSubtitle();
ClearSubtitle();
}
public void PlayScriptedSubtitle(SubtitleKey subtitleKey, float durationOverrideSeconds = 0f)
{
if (subtitleKey == SubtitleKey.None)
{
ClearScriptedSubtitle();
return;
}
if (!SubtitleIdentifiers.TryGet(subtitleKey, out var entry) || string.IsNullOrWhiteSpace(entry.Text))
return;
var duration = durationOverrideSeconds > 0f
? durationOverrideSeconds
: entry.PreferredDurationSeconds;
ShowSubtitle(subtitleKey, entry.Text, duration).Forget();
}
public void ClearScriptedSubtitle()
{
CancelCurrentSubtitle();
ClearSubtitle();
}
private void OnVoicePlaybackStarted(VoicePlaybackStartedEvent evt)
{
if (evt.SubtitleKey == SubtitleKey.None)
return;
if (!SubtitleIdentifiers.TryGet(evt.SubtitleKey, out var entry) || string.IsNullOrWhiteSpace(entry.Text))
return;
var duration = entry.PreferredDurationSeconds > 0f
? entry.PreferredDurationSeconds
: evt.ClipLengthSeconds;
ShowSubtitle(evt.SubtitleKey, entry.Text, duration).Forget();
}
private void OnVoicePlaybackFinished(VoicePlaybackFinishedEvent evt)
{
if (evt.SubtitleKey == SubtitleKey.None)
return;
if (_activeSubtitleKey != evt.SubtitleKey)
return;
CancelCurrentSubtitle();
ClearSubtitle();
}
private async UniTaskVoid ShowSubtitle(SubtitleKey subtitleKey, string text, float durationSeconds)
{
CancelCurrentSubtitle();
_activeSubtitleKey = subtitleKey;
_currentText = text;
_subtitleCts = new CancellationTokenSource();
_eventCoordinator.PublishImmediate(new SubtitleDisplayChangedEvent(text, true));
var safeDuration = Math.Max(0f, durationSeconds);
if (safeDuration <= 0f)
return;
try
{
await UniTask.Delay(
TimeSpan.FromSeconds(safeDuration),
DelayType.UnscaledDeltaTime,
cancellationToken: _subtitleCts.Token);
}
catch (OperationCanceledException)
{
return;
}
if (_activeSubtitleKey == subtitleKey)
ClearSubtitle();
}
private void CancelCurrentSubtitle()
{
if (_subtitleCts == null)
return;
try
{
_subtitleCts.Cancel();
}
catch
{
}
_subtitleCts.Dispose();
_subtitleCts = null;
}
private void ClearSubtitle()
{
_activeSubtitleKey = SubtitleKey.None;
_currentText = string.Empty;
_eventCoordinator.PublishImmediate(new SubtitleDisplayChangedEvent(string.Empty, false));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61b6e034a002e4319a76ebe6bd90f50f

View File

@@ -92,15 +92,9 @@ namespace BriarQueen.Game.Items.Environment.General.Book
_bookInterface.CanvasGroup.blocksRaycasts = true;
_bookInterface.CanvasGroup.interactable = true;
ShowTutorialIfNeeded();
UnlockCodexEntry();
}
private void ShowTutorialIfNeeded()
{
TutorialService.DisplayTutorial(TutorialPopupID.ExitItems);
}
private void UnlockCodexEntry()
{
PlayerManager.UnlockCodexEntry(CodexEntryIDs.Get(_documentEntryID));

View File

@@ -16,6 +16,13 @@ namespace BriarQueen.Game.Items.Environment.General
[SerializeField]
private bool _removeTrigger;
[Header("Events")]
[SerializeField]
private SFXKey _soundEffect;
[SerializeField]
private VoiceKey _voiceLine;
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Inspect;
public override string InteractableName => InteractableTooltip;
@@ -29,6 +36,16 @@ namespace BriarQueen.Game.Items.Environment.General
if (_removeTrigger)
await Remove();
if (_soundEffect != SFXKey.None)
{
AudioManager.Play(AudioNameIdentifiers.Get(_soundEffect));
}
if (_voiceLine != VoiceKey.None)
{
AudioManager.Play(AudioNameIdentifiers.Get(_voiceLine));
}
}
}
}

View File

@@ -21,9 +21,6 @@ namespace BriarQueen.Game.Levels.ChapterOne.Ashwick
[SerializeField]
private AshwickGate _ashwickGate;
[SerializeField]
private TransitionZone _keypadZone;
[SerializeField]
private TransitionZone _nextLevelZone;
@@ -36,22 +33,17 @@ namespace BriarQueen.Game.Levels.ChapterOne.Ashwick
_background.sprite = _backgroundOpenSprite;
_nextLevelZone.Unlock();
await DestructionService.Destroy(_ashwickGate.gameObject);
await DestructionService.Destroy(_keypadZone.gameObject);
}
}
public async UniTask OpenGate()
{
EventCoordinator.PublishImmediate(new FadeEvent(false));
_background.sprite = _backgroundOpenSprite;
_nextLevelZone.Unlock();
await DestructionService.Destroy(_ashwickGate.gameObject);
await DestructionService.Destroy(_keypadZone.gameObject);
SaveManager.SetLevelFlag(LevelFlag.AshwickGateOpen, true);
EventCoordinator.PublishImmediate(new FadeEvent(true));
}
}
}

View File

@@ -1,24 +0,0 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Levels.Data;
using Cysharp.Threading.Tasks;
namespace BriarQueen.Game.Levels.ChapterOne.Ashwick
{
public class Marketplace : BaseLevel
{
protected override UniTask PostActivateInternal()
{
if (SaveManager.GetLevelFlag(LevelFlag.MarketplaceFirstEntry))
return UniTask.CompletedTask;
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(LevelInteractKey.MarketplaceFirstEntry)));
SaveManager.SetLevelFlag(LevelFlag.MarketplaceFirstEntry, true);
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 2d9beb847a184e2e897942fff18a396e
timeCreated: 1778696777

View File

@@ -10,6 +10,7 @@ using BriarQueen.Framework.Managers.Levels;
using BriarQueen.Framework.Managers.Player;
using BriarQueen.Framework.Managers.UI;
using BriarQueen.Framework.Services.Settings;
using BriarQueen.Framework.Services.Subtitles;
using BriarQueen.Framework.Services.Tutorials;
using BriarQueen.Game.Cinematics;
using Cysharp.Threading.Tasks;
@@ -30,6 +31,7 @@ namespace BriarQueen.Game.Misc
private readonly SettingsService _settingsService;
private readonly SplashScreens _splashScreens;
private readonly SteamManager _steamManager;
private readonly SubtitleService _subtitleService;
private readonly TutorialService _tutorialService;
private readonly UIManager _uiManager;
@@ -45,6 +47,7 @@ namespace BriarQueen.Game.Misc
SettingsService settingsService,
SteamManager steamManager,
SplashScreens splashScreens,
SubtitleService subtitleService,
TutorialService tutorialService)
{
_audioManager = audioManager;
@@ -58,6 +61,8 @@ namespace BriarQueen.Game.Misc
_settingsService = settingsService;
_steamManager = steamManager;
_splashScreens = splashScreens;
_subtitleService = subtitleService;
_tutorialService = tutorialService;
}
public async UniTask StartAsync(CancellationToken cancellationToken)
@@ -67,6 +72,9 @@ namespace BriarQueen.Game.Misc
Debug.Log("[Bootstrap] Audio...");
_audioManager.Initialize();
Debug.Log("[Bootstrap] Subtitles...");
_subtitleService.Initialize();
Debug.Log("[Bootstrap] Settings...");
await _settingsService.InitializeAsync();
@@ -99,4 +107,4 @@ namespace BriarQueen.Game.Misc
Debug.Log("[Bootstrap] Called SplashScreens.Play()");
}
}
}
}

View File

@@ -1,5 +1,6 @@
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using BriarQueen.Framework.Managers.UI;
using Cysharp.Threading.Tasks;
using UnityEngine;
@@ -7,10 +8,13 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
{
public class AshwickGate : BaseItem
{
[Header("Keypad")]
[SerializeField] private AshwickGateKeypadPuzzle _keypadPuzzle;
public override string InteractableName => "Iron Gate";
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Interact;
public override UniTask OnInteract(ItemDataSo item = null)
{
if (!CheckEmptyHands())

View File

@@ -5,7 +5,10 @@ using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Effects;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Managers.Interaction;
using BriarQueen.Framework.Managers.UI;
using BriarQueen.Framework.Managers.UI.Base;
using BriarQueen.Framework.Services.Puzzles.Base;
using BriarQueen.Framework.Services.Tutorials;
using BriarQueen.Game.Levels.ChapterOne.Ashwick;
using Cysharp.Threading.Tasks;
using MemoryPack;
@@ -24,7 +27,7 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
public string Digits;
}
public class AshwickGateKeypadPuzzle : BasePuzzle, IPuzzleStateful
public class AshwickGateKeypadPuzzle : BasePuzzle, IPuzzleStateful, IUIWindow, IUIBackHandler, IUIOverlayHost
{
private const string CorrectCode = "312";
private const int RequiredDigits = 3;
@@ -63,14 +66,25 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
private bool _isEvaluating;
private bool _isOpen;
private bool _raycasterRegistered;
private bool _skipSaveOnHide;
private TutorialService _tutorialService;
private UIManager _uiManager;
public override string PuzzleID => PuzzleIdentifiers.AllPuzzles[PuzzleKey.AshwickMarketGate];
public bool IsCompleted => _isCompleted || SaveManager.GetLevelFlag(LevelFlag.AshwickGateOpen);
public WindowType WindowType => WindowType.AshwickGateKeypadWindow;
public UIPauseBehavior PauseBehavior => UIPauseBehavior.OpenPauseOverlay;
[Inject]
public void ConstructKeypad(InteractManager interactManager)
public void ConstructKeypad(
InteractManager interactManager,
UIManager uiManager,
TutorialService tutorialService)
{
_interactManager = interactManager;
_uiManager = uiManager;
_tutorialService = tutorialService;
}
private void Awake()
@@ -86,8 +100,14 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
SyncDisplay();
}
private void Start()
{
_uiManager?.RegisterWindow(this);
}
private void OnDestroy()
{
_uiManager?.UnregisterWindow(this);
UnbindButtons();
TryUnregisterRaycaster();
CancelPanelTween();
@@ -98,10 +118,7 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
_isCompleted = SaveManager.GetLevelFlag(LevelFlag.AshwickGateOpen);
_isOpen = false;
_isEvaluating = false;
SetPanelState(0f, false, false);
SyncDisplay();
_statusGlow?.TurnOff().Forget();
_skipSaveOnHide = false;
return UniTask.CompletedTask;
}
@@ -113,12 +130,15 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
public void Open()
{
OpenInternal().Forget();
if (IsCompleted || _uiManager == null)
return;
_uiManager.OpenWindow(WindowType);
}
public void Close()
{
CloseInternal(requestSave: true).Forget();
_uiManager?.CloseWindow(WindowType);
}
public UniTask<byte[]> CaptureState()
@@ -159,12 +179,13 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
SyncDisplay();
SaveManager.SetPuzzleCompleted(PuzzleKey.AshwickMarketGate, true, requestSave: false);
await CloseInternal(requestSave: false);
_skipSaveOnHide = true;
_uiManager?.CloseWindow(WindowType);
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.AshwickGateOpening));
await _outskirts.OpenGate();
}
private async UniTaskVoid OpenInternal()
public async UniTask Show()
{
if (IsCompleted || _isEvaluating || _isOpen)
return;
@@ -174,8 +195,7 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
SetPanelState(0f, false, true);
SyncDisplay();
TryRegisterRaycaster();
_statusGlow?.TurnOff().Forget();
EnsureExclusiveRaycaster();
try
{
@@ -199,9 +219,10 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
}
SetPanelState(1f, true, true);
_tutorialService?.DisplayTutorial(TutorialPopupID.LeavingPuzzles);
}
private async UniTask CloseInternal(bool requestSave)
public async UniTask Hide()
{
if (!_isOpen)
return;
@@ -239,8 +260,91 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
SetPanelState(0f, false, false);
TryUnregisterRaycaster();
if (requestSave)
if (!_skipSaveOnHide && !_isCompleted)
EventCoordinator.PublishImmediate(new RequestGameSaveEvent());
_skipSaveOnHide = false;
}
public bool HandleBackRequest()
{
Close();
return true;
}
public bool CanSuspendFor(WindowType incomingWindowType)
{
return incomingWindowType == WindowType.PauseMenuWindow;
}
public async UniTask SuspendForOverlay()
{
if (!_isOpen)
return;
ResetPanelTween();
if (_panelGroup != null)
{
_panelGroup.interactable = false;
_panelGroup.blocksRaycasts = false;
}
try
{
_panelSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_panelGroup, new TweenSettings<float>
{
startValue = _panelGroup != null ? _panelGroup.alpha : 1f,
endValue = 0f,
settings = _panelFadeTweenSettings
}));
await _panelSequence.ToUniTask(cancellationToken: _panelCts.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
_panelSequence = default;
}
SetPanelState(0f, false, false);
}
public async UniTask ResumeFromOverlay()
{
if (!_isOpen)
return;
ResetPanelTween();
EnsureExclusiveRaycaster();
SetPanelState(0f, false, true);
try
{
_panelSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_panelGroup, new TweenSettings<float>
{
startValue = 0f,
endValue = 1f,
settings = _panelFadeTweenSettings
}));
await _panelSequence.ToUniTask(cancellationToken: _panelCts.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
_panelSequence = default;
}
SetPanelState(1f, !_isEvaluating, true);
}
private void OnDigitPressed(int digit)
@@ -370,14 +474,18 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
}
}
private void TryRegisterRaycaster()
private void EnsureExclusiveRaycaster()
{
if (_raycasterRegistered || _interactManager == null || _graphicRaycaster == null)
if (_interactManager == null || _graphicRaycaster == null)
return;
_interactManager.AddUIRaycaster(_graphicRaycaster);
if (!_raycasterRegistered)
{
_interactManager.AddUIRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
private void TryUnregisterRaycaster()
@@ -386,7 +494,7 @@ namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
return;
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
_interactManager.ClearExclusiveRaycaster();
_interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = false;
}
}

View File

@@ -15,6 +15,7 @@ using BriarQueen.Framework.Services.Destruction;
using BriarQueen.Framework.Services.Game;
using BriarQueen.Framework.Services.Puzzles;
using BriarQueen.Framework.Services.Settings;
using BriarQueen.Framework.Services.Subtitles;
using BriarQueen.Framework.Services.Tutorials;
using BriarQueen.Game.Cinematics;
using BriarQueen.Game.Misc;
@@ -89,8 +90,9 @@ namespace BriarQueen.Game.Scopes
builder.Register<SaveManager>(Lifetime.Singleton);
builder.Register<SettingsService>(Lifetime.Singleton);
builder.Register<SteamManager>(Lifetime.Singleton);
builder.Register<SubtitleService>(Lifetime.Singleton);
builder.Register<TutorialService>(Lifetime.Singleton);
builder.Register<UIManager>(Lifetime.Singleton);
}
}
}
}

View File

@@ -255,6 +255,7 @@ namespace BriarQueen.UI.Codex
}
public WindowType WindowType => WindowType.CodexWindow;
public UIPauseBehavior PauseBehavior => UIPauseBehavior.TreatAsBackRequest;
// ── IUIWindow ─────────────────────────────────────────────────
@@ -263,7 +264,7 @@ namespace BriarQueen.UI.Codex
ResetOperationCts();
gameObject.SetActive(true);
TryRegisterRaycaster();
EnsureExclusiveRaycaster();
if (_canvasGroup != null)
{
@@ -380,7 +381,7 @@ namespace BriarQueen.UI.Codex
// ── Raycaster ─────────────────────────────────────────────────
private void TryRegisterRaycaster()
private void EnsureExclusiveRaycaster()
{
Debug.Log($"[CodexWindow] TryRegisterRaycaster " +
@@ -392,10 +393,15 @@ namespace BriarQueen.UI.Codex
Debug.Log("[CodexWindow] Try register raycaster.");
if (_raycasterRegistered || _interactManager == null || _graphicRaycaster == null) return;
_interactManager.AddUIRaycaster(_graphicRaycaster);
if (_interactManager == null || _graphicRaycaster == null) return;
if (!_raycasterRegistered)
{
_interactManager.AddUIRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
Debug.Log("[CodexWindow] Registered raycaster.");
}
@@ -406,7 +412,7 @@ namespace BriarQueen.UI.Codex
if (!_raycasterRegistered || _interactManager == null || _graphicRaycaster == null) return;
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
_interactManager.ClearExclusiveRaycaster();
_interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = false;
Debug.Log("[CodexWindow] Raycaster unregistered.");

View File

@@ -0,0 +1,212 @@
using System;
using System.Threading;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Services.Subtitles;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using VContainer;
namespace BriarQueen.UI.HUD
{
public class SubtitleUI : MonoBehaviour
{
[Header("UI")]
[SerializeField]
private InteractTextUI _interactTextUI;
[SerializeField]
private CanvasGroup _canvasGroup;
[Header("Tweens")]
[SerializeField]
private TweenSettings _tweenSettings = new()
{
duration = 0.2f,
ease = Ease.InOutSine,
useUnscaledTime = true
};
private EventCoordinator _eventCoordinator;
private SubtitleService _subtitleService;
private CancellationTokenSource _cancellationTokenSource;
private Sequence _sequence;
[Inject]
public void Construct(EventCoordinator eventCoordinator, SubtitleService subtitleService)
{
_eventCoordinator = eventCoordinator;
_subtitleService = subtitleService;
}
private void Awake()
{
ApplyImmediate(string.Empty, false);
}
private void OnEnable()
{
_eventCoordinator?.Subscribe<SubtitleDisplayChangedEvent>(OnSubtitleDisplayChanged);
SyncFromService();
}
private void OnDisable()
{
_eventCoordinator?.Unsubscribe<SubtitleDisplayChangedEvent>(OnSubtitleDisplayChanged);
StopTween();
}
private void OnDestroy()
{
StopTween();
}
private void SyncFromService()
{
if (_subtitleService != null && _subtitleService.IsVisible)
{
ApplyImmediate(_subtitleService.CurrentText, true);
return;
}
ApplyImmediate(string.Empty, false);
}
private void OnSubtitleDisplayChanged(SubtitleDisplayChangedEvent eventData)
{
if (!eventData.Visible || string.IsNullOrWhiteSpace(eventData.Text))
{
HideSubtitle().Forget();
return;
}
ShowSubtitle(eventData.Text).Forget();
}
private async UniTaskVoid ShowSubtitle(string text)
{
if (_canvasGroup == null || _interactTextUI == null)
return;
StopTween();
_interactTextUI.SetText(text);
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
_cancellationTokenSource = new CancellationTokenSource();
var tween = new TweenSettings<float>
{
startValue = _canvasGroup.alpha,
endValue = 1f,
settings = _tweenSettings
};
_sequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_canvasGroup, tween));
try
{
await _sequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
DisposeTweenState();
}
_canvasGroup.alpha = 1f;
}
private async UniTaskVoid HideSubtitle()
{
if (_canvasGroup == null || _interactTextUI == null)
return;
StopTween();
_cancellationTokenSource = new CancellationTokenSource();
var tween = new TweenSettings<float>
{
startValue = _canvasGroup.alpha,
endValue = 0f,
settings = _tweenSettings
};
_sequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Alpha(_canvasGroup, tween));
try
{
await _sequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
DisposeTweenState();
}
ApplyImmediate(string.Empty, false);
}
private void ApplyImmediate(string text, bool visible)
{
if (_interactTextUI != null)
{
if (visible)
_interactTextUI.SetText(text);
else
_interactTextUI.ClearText();
}
if (_canvasGroup == null)
return;
_canvasGroup.alpha = visible ? 1f : 0f;
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
}
private void StopTween()
{
if (_sequence.isAlive)
_sequence.Stop();
_sequence = default;
if (_cancellationTokenSource == null)
return;
try
{
_cancellationTokenSource.Cancel();
}
catch
{
}
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
}
private void DisposeTweenState()
{
_sequence = default;
if (_cancellationTokenSource == null)
return;
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a7225bc6a3f524fb8b820162cd937c1e

View File

@@ -635,6 +635,11 @@ namespace BriarQueen.UI.Menus
group.blocksRaycasts = inputEnabled;
}
public bool CanSuspendFor(WindowType incomingWindowType)
{
return incomingWindowType == WindowType.SettingsWindow;
}
public async UniTask SuspendForOverlay()
{
if (_mainMenuGroup == null)

View File

@@ -79,18 +79,25 @@ namespace BriarQueen.UI.Menus
public bool IsModal => true;
public WindowType WindowType => WindowType.PauseMenuWindow;
private void TryRegisterRaycaster()
{
if (_raycasterRegistered)
return;
public UIPauseBehavior PauseBehavior => UIPauseBehavior.TreatAsBackRequest;
public bool CanSuspendFor(WindowType incomingWindowType)
{
return incomingWindowType == WindowType.SettingsWindow;
}
private void EnsureExclusiveRaycaster()
{
if (_interactManager == null || _graphicRaycaster == null)
return;
_interactManager.AddUIRaycaster(_graphicRaycaster);
if (!_raycasterRegistered)
{
_interactManager.AddUIRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
private void TryUnregisterRaycaster()
@@ -102,7 +109,7 @@ namespace BriarQueen.UI.Menus
return;
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
_interactManager.ClearExclusiveRaycaster();
_interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = false;
}
@@ -149,7 +156,7 @@ namespace BriarQueen.UI.Menus
SetLevelName();
gameObject.SetActive(true);
TryRegisterRaycaster();
EnsureExclusiveRaycaster();
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
@@ -233,6 +240,7 @@ namespace BriarQueen.UI.Menus
public async UniTask ResumeFromOverlay()
{
StopAndResetCancellation();
EnsureExclusiveRaycaster();
_buttonsGroup.blocksRaycasts = true;
_buttonsGroup.interactable = true;

View File

@@ -147,6 +147,7 @@ namespace BriarQueen.UI.Menus
// ── IUIWindow ─────────────────────────────────────────────────
public bool IsModal => true;
public WindowType WindowType => WindowType.SettingsWindow;
public UIPauseBehavior PauseBehavior => UIPauseBehavior.TreatAsBackRequest;
// ── Unity lifecycle ───────────────────────────────────────────
private void Awake()
@@ -155,9 +156,9 @@ namespace BriarQueen.UI.Menus
if (_backButton != null) _backButton.onClick.AddListener(OnBackClicked);
// Individual buttons drive panel switching via SelectionRequested
if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested += _ => SwitchCategory(Category.Game);
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested += _ => SwitchCategory(Category.Visual);
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested += _ => SwitchCategory(Category.Audio);
if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested += OnGameCategorySelected;
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested += OnVisualCategorySelected;
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested += OnAudioCategorySelected;
HookSlider(_masterVolumeSlider, _masterVolumeText, v => _draftAudio.MasterVolume = v);
HookSlider(_musicVolumeSlider, _musicVolumeText, v => _draftAudio.MusicVolume = v);
@@ -210,9 +211,9 @@ namespace BriarQueen.UI.Menus
if (_applyButton != null) _applyButton.onClick.RemoveListener(OnApplyClicked);
if (_backButton != null) _backButton.onClick.RemoveListener(OnBackClicked);
if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested -= _ => SwitchCategory(Category.Game);
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested -= _ => SwitchCategory(Category.Visual);
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested -= _ => SwitchCategory(Category.Audio);
if (_gameCategoryButton != null) _gameCategoryButton.SelectionRequested -= OnGameCategorySelected;
if (_visualCategoryButton != null) _visualCategoryButton.SelectionRequested -= OnVisualCategorySelected;
if (_audioCategoryButton != null) _audioCategoryButton.SelectionRequested -= OnAudioCategorySelected;
if (_popupDisplayDurationSlider != null)
_popupDisplayDurationSlider.onValueChanged.RemoveListener(OnPopupDisplayDurationChanged);
@@ -246,7 +247,7 @@ namespace BriarQueen.UI.Menus
StopAndResetCancellation();
gameObject.SetActive(true);
TryRegisterRaycaster();
EnsureExclusiveRaycaster();
_canvasGroup.alpha = 1f;
_canvasGroup.blocksRaycasts = false;
@@ -368,6 +369,21 @@ namespace BriarQueen.UI.Menus
return true;
}
private void OnGameCategorySelected(AnimatedSelectionButton _)
{
SwitchCategory(Category.Game);
}
private void OnVisualCategorySelected(AnimatedSelectionButton _)
{
SwitchCategory(Category.Visual);
}
private void OnAudioCategorySelected(AnimatedSelectionButton _)
{
SwitchCategory(Category.Audio);
}
// ── Category switching ────────────────────────────────────────
private void SwitchCategory(Category category)
@@ -909,17 +925,18 @@ namespace BriarQueen.UI.Menus
};
}
private void TryRegisterRaycaster()
private void EnsureExclusiveRaycaster()
{
if (_raycasterRegistered)
return;
if (_interactManager == null || _graphicRaycaster == null)
return;
_interactManager.AddUIRaycaster(_graphicRaycaster);
if (!_raycasterRegistered)
{
_interactManager.AddUIRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
private void TryUnregisterRaycaster()
@@ -931,7 +948,7 @@ namespace BriarQueen.UI.Menus
return;
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
_interactManager.ClearExclusiveRaycaster();
_interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = false;
}
}

View File

@@ -40,6 +40,9 @@ namespace BriarQueen.UI.Scopes
[SerializeField]
private InventoryBar _inventoryBar;
[SerializeField]
private SubtitleUI _subtitleUI;
protected override void Configure(IContainerBuilder builder)
@@ -70,6 +73,9 @@ namespace BriarQueen.UI.Scopes
if (_inventoryBar != null)
builder.RegisterComponent(_inventoryBar);
if (_subtitleUI != null)
builder.RegisterComponent(_subtitleUI);
builder.RegisterBuildCallback(container =>
@@ -101,4 +107,4 @@ namespace BriarQueen.UI.Scopes
});
}
}
}
}