524 lines
15 KiB
C#
524 lines
15 KiB
C#
using System;
|
|
using System.Threading;
|
|
using BriarQueen.Data.Identifiers;
|
|
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;
|
|
using PrimeTween;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
using VContainer;
|
|
|
|
namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
|
|
{
|
|
[MemoryPackable]
|
|
public partial class AshwickGateKeypadPuzzleState
|
|
{
|
|
public string Digits;
|
|
}
|
|
|
|
public class AshwickGateKeypadPuzzle : BasePuzzle, IPuzzleStateful, IUIWindow, IUIBackHandler, IUIOverlayHost
|
|
{
|
|
private const string CORRECT_CODE = "312";
|
|
private const int REQUIRED_DIGITS = 3;
|
|
|
|
[Header("Scene References")]
|
|
[SerializeField] private AshwickOutskirts _outskirts;
|
|
[SerializeField] private CanvasGroup _panelGroup;
|
|
[SerializeField] private TMP_InputField _displayField;
|
|
[SerializeField] private UILightGlow _statusGlow;
|
|
[SerializeField] private GraphicRaycaster _graphicRaycaster;
|
|
[SerializeField] private Button _closeButton;
|
|
[SerializeField] private Button[] _digitButtons = new Button[9];
|
|
|
|
[Header("Light Colors")]
|
|
[SerializeField] private Color _incorrectGlowColor = new(0.85f, 0.2f, 0.2f, 1f);
|
|
[SerializeField] private Color _correctGlowColor = new(0.2f, 0.85f, 0.35f, 1f);
|
|
|
|
[Header("Timing")]
|
|
[SerializeField] private float _feedbackDuration = 0.35f;
|
|
|
|
[SerializeField] private TweenSettings _panelFadeTweenSettings = new()
|
|
{
|
|
duration = 0.8f,
|
|
ease = Ease.OutQuad,
|
|
useUnscaledTime = true
|
|
};
|
|
|
|
private readonly UnityAction[] _digitCallbacks = new UnityAction[9];
|
|
|
|
private InteractManager _interactManager;
|
|
private CancellationTokenSource _panelCts;
|
|
private Sequence _panelSequence;
|
|
|
|
private string _currentDigits = string.Empty;
|
|
private bool _isCompleted;
|
|
private bool _isEvaluating;
|
|
private bool _isOpen;
|
|
private bool _raycasterRegistered;
|
|
private bool _skipSaveOnHide;
|
|
|
|
private TutorialService _tutorialService;
|
|
private UIManager _uiManager;
|
|
|
|
public override string PuzzleID => PuzzleIdentifiers.Get(PuzzleKey.AshwickOutskirtsGate);
|
|
public bool IsCompleted => _isCompleted || SaveManager.GetLevelFlag(LevelFlag.AshwickGateOpen);
|
|
public WindowType WindowType => WindowType.AshwickGateKeypadWindow;
|
|
public UIPauseBehavior PauseBehavior => UIPauseBehavior.OpenPauseOverlay;
|
|
|
|
[Inject]
|
|
public void ConstructKeypad(
|
|
InteractManager interactManager,
|
|
UIManager uiManager,
|
|
TutorialService tutorialService)
|
|
{
|
|
_interactManager = interactManager;
|
|
_uiManager = uiManager;
|
|
_tutorialService = tutorialService;
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
if (_displayField != null)
|
|
{
|
|
_displayField.readOnly = true;
|
|
_displayField.text = string.Empty;
|
|
}
|
|
|
|
BindButtons();
|
|
SetPanelState(0f, false, false);
|
|
SyncDisplay();
|
|
}
|
|
|
|
private void Start()
|
|
{
|
|
_uiManager?.RegisterWindow(this);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
_uiManager?.UnregisterWindow(this);
|
|
UnbindButtons();
|
|
TryUnregisterRaycaster();
|
|
CancelPanelTween();
|
|
}
|
|
|
|
public override UniTask PostLoad()
|
|
{
|
|
_isCompleted = SaveManager.GetLevelFlag(LevelFlag.AshwickGateOpen);
|
|
_isOpen = false;
|
|
_isEvaluating = false;
|
|
_skipSaveOnHide = false;
|
|
|
|
return UniTask.CompletedTask;
|
|
}
|
|
|
|
public override UniTask CompletePuzzle()
|
|
{
|
|
return CompletePuzzleInternal();
|
|
}
|
|
|
|
public void Open()
|
|
{
|
|
if (IsCompleted || _uiManager == null)
|
|
return;
|
|
|
|
_uiManager.OpenWindow(WindowType);
|
|
}
|
|
|
|
public void Close()
|
|
{
|
|
_uiManager?.CloseWindow(WindowType);
|
|
}
|
|
|
|
public UniTask<byte[]> CaptureState()
|
|
{
|
|
var state = new AshwickGateKeypadPuzzleState
|
|
{
|
|
Digits = IsCompleted ? string.Empty : _currentDigits
|
|
};
|
|
|
|
return UniTask.FromResult(MemoryPackSerializer.Serialize(state));
|
|
}
|
|
|
|
public UniTask RestoreState(byte[] state)
|
|
{
|
|
_isCompleted = SaveManager.GetLevelFlag(LevelFlag.AshwickGateOpen);
|
|
_currentDigits = string.Empty;
|
|
|
|
if (!_isCompleted && state is { Length: > 0 })
|
|
{
|
|
var restored = MemoryPackSerializer.Deserialize<AshwickGateKeypadPuzzleState>(state);
|
|
_currentDigits = restored?.Digits ?? string.Empty;
|
|
|
|
if (_currentDigits.Length > REQUIRED_DIGITS)
|
|
_currentDigits = _currentDigits[..REQUIRED_DIGITS];
|
|
}
|
|
|
|
SyncDisplay();
|
|
ResetFeedbackState();
|
|
return UniTask.CompletedTask;
|
|
}
|
|
|
|
private async UniTask CompletePuzzleInternal()
|
|
{
|
|
if (IsCompleted || _outskirts == null)
|
|
return;
|
|
|
|
_isCompleted = true;
|
|
_currentDigits = string.Empty;
|
|
SyncDisplay();
|
|
|
|
SaveManager.SetPuzzleCompleted(PuzzleKey.AshwickOutskirtsGate, true, requestSave: false);
|
|
_skipSaveOnHide = true;
|
|
_uiManager?.CloseWindow(WindowType);
|
|
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.AshwickGateOpening));
|
|
await _outskirts.OpenGate();
|
|
}
|
|
|
|
public async UniTask Show()
|
|
{
|
|
if (IsCompleted || _isEvaluating || _isOpen)
|
|
return;
|
|
|
|
_isOpen = true;
|
|
ResetPanelTween();
|
|
|
|
SetPanelState(0f, false, true);
|
|
SyncDisplay();
|
|
ResetFeedbackState();
|
|
EnsureExclusiveRaycaster();
|
|
|
|
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, true, true);
|
|
_tutorialService?.DisplayTutorial(TutorialPopupID.LeavingPuzzles);
|
|
}
|
|
|
|
public async UniTask Hide()
|
|
{
|
|
if (!_isOpen)
|
|
return;
|
|
|
|
_isOpen = false;
|
|
ResetPanelTween();
|
|
ResetFeedbackState();
|
|
|
|
if (_panelGroup != null)
|
|
{
|
|
_panelGroup.interactable = false;
|
|
_panelGroup.blocksRaycasts = true;
|
|
}
|
|
|
|
try
|
|
{
|
|
_panelSequence = Sequence.Create(useUnscaledTime: true)
|
|
.Group(Tween.Alpha(_panelGroup, new TweenSettings<float>
|
|
{
|
|
startValue = _panelGroup != null ? _panelGroup.alpha : 0f,
|
|
endValue = 0f,
|
|
settings = _panelFadeTweenSettings
|
|
}));
|
|
|
|
await _panelSequence.ToUniTask(cancellationToken: _panelCts.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
finally
|
|
{
|
|
_panelSequence = default;
|
|
}
|
|
|
|
SetPanelState(0f, false, false);
|
|
TryUnregisterRaycaster();
|
|
|
|
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)
|
|
{
|
|
if (_isEvaluating || IsCompleted || !_isOpen || _currentDigits.Length >= REQUIRED_DIGITS)
|
|
return;
|
|
|
|
if (_currentDigits.Length == 0)
|
|
ResetFeedbackState();
|
|
|
|
_currentDigits += digit.ToString();
|
|
SyncDisplay();
|
|
|
|
if (_currentDigits.Length == REQUIRED_DIGITS)
|
|
EvaluateCode().Forget();
|
|
}
|
|
|
|
private async UniTaskVoid EvaluateCode()
|
|
{
|
|
_isEvaluating = true;
|
|
|
|
if (_panelGroup != null)
|
|
_panelGroup.interactable = false;
|
|
|
|
if (_currentDigits == CORRECT_CODE)
|
|
{
|
|
if (_statusGlow != null)
|
|
{
|
|
_statusGlow.SetLightColor(_correctGlowColor);
|
|
await _statusGlow.TurnOn();
|
|
}
|
|
|
|
await CompletePuzzleInternal();
|
|
}
|
|
else
|
|
{
|
|
if (_statusGlow != null)
|
|
{
|
|
_statusGlow.SetLightColor(_incorrectGlowColor);
|
|
await _statusGlow.TurnOn();
|
|
}
|
|
|
|
_currentDigits = string.Empty;
|
|
SyncDisplay();
|
|
|
|
if (_statusGlow != null)
|
|
await _statusGlow.TurnOff();
|
|
|
|
if (_isOpen && _panelGroup != null)
|
|
_panelGroup.interactable = true;
|
|
}
|
|
|
|
_isEvaluating = false;
|
|
}
|
|
|
|
private void BindButtons()
|
|
{
|
|
for (var i = 0; i < _digitButtons.Length; i++)
|
|
{
|
|
var button = _digitButtons[i];
|
|
if (button == null)
|
|
continue;
|
|
|
|
var digit = i + 1;
|
|
_digitCallbacks[i] = () => OnDigitPressed(digit);
|
|
button.onClick.AddListener(_digitCallbacks[i]);
|
|
}
|
|
|
|
if (_closeButton != null)
|
|
_closeButton.onClick.AddListener(Close);
|
|
}
|
|
|
|
private void UnbindButtons()
|
|
{
|
|
for (var i = 0; i < _digitButtons.Length; i++)
|
|
{
|
|
var button = _digitButtons[i];
|
|
if (button == null || _digitCallbacks[i] == null)
|
|
continue;
|
|
|
|
button.onClick.RemoveListener(_digitCallbacks[i]);
|
|
_digitCallbacks[i] = null;
|
|
}
|
|
|
|
if (_closeButton != null)
|
|
_closeButton.onClick.RemoveListener(Close);
|
|
}
|
|
|
|
private void SyncDisplay()
|
|
{
|
|
if (_displayField == null)
|
|
return;
|
|
|
|
_displayField.text = _currentDigits;
|
|
}
|
|
|
|
private void ResetFeedbackState()
|
|
{
|
|
_statusGlow?.TurnOff().Forget();
|
|
ResetCloseButtonSelection();
|
|
}
|
|
|
|
private void ResetCloseButtonSelection()
|
|
{
|
|
if (_closeButton == null || EventSystem.current == null)
|
|
return;
|
|
|
|
if (EventSystem.current.currentSelectedGameObject == _closeButton.gameObject)
|
|
EventSystem.current.SetSelectedGameObject(null);
|
|
}
|
|
|
|
private void SetPanelState(float alpha, bool interactable, bool blocksRaycasts)
|
|
{
|
|
if (_panelGroup == null)
|
|
return;
|
|
|
|
_panelGroup.alpha = alpha;
|
|
_panelGroup.interactable = interactable;
|
|
_panelGroup.blocksRaycasts = blocksRaycasts;
|
|
}
|
|
|
|
private void ResetPanelTween()
|
|
{
|
|
CancelPanelTween();
|
|
_panelCts = new CancellationTokenSource();
|
|
}
|
|
|
|
private void CancelPanelTween()
|
|
{
|
|
if (_panelSequence.isAlive)
|
|
_panelSequence.Stop();
|
|
|
|
if (_panelCts != null)
|
|
{
|
|
try
|
|
{
|
|
_panelCts.Cancel();
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
|
|
_panelCts.Dispose();
|
|
_panelCts = null;
|
|
}
|
|
}
|
|
|
|
private void EnsureExclusiveRaycaster()
|
|
{
|
|
if (_interactManager == null || _graphicRaycaster == null)
|
|
return;
|
|
|
|
if (!_raycasterRegistered)
|
|
{
|
|
_interactManager.AddUIRaycaster(_graphicRaycaster);
|
|
_raycasterRegistered = true;
|
|
}
|
|
|
|
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
|
|
}
|
|
|
|
private void TryUnregisterRaycaster()
|
|
{
|
|
if (!_raycasterRegistered || _interactManager == null || _graphicRaycaster == null)
|
|
return;
|
|
|
|
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
|
|
_interactManager.ReleaseExclusiveRaycaster(_graphicRaycaster);
|
|
_raycasterRegistered = false;
|
|
}
|
|
}
|
|
}
|