Refine UI stack and add Ashwick keypad puzzle

This commit is contained in:
2026-05-15 13:02:12 +01:00
parent 806cf80110
commit 58050abded
69 changed files with 17470 additions and 2752 deletions

View File

@@ -14,7 +14,7 @@ using VContainer;
namespace BriarQueen.Game.Items.HoverZones
{
public class InteractZone : MonoBehaviour, IInteractable
public class TransitionZone : MonoBehaviour, IInteractable
{
[Header("Level Setup")]
[SerializeField]

View File

@@ -0,0 +1,57 @@
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Game.Items.HoverZones;
using BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Game.Levels.ChapterOne.Ashwick
{
public class AshwickOutskirts : BaseLevel
{
[Header("Background")]
[SerializeField]
private Image _background;
[SerializeField]
private Sprite _backgroundOpenSprite;
[Header("Keypad Puzzle")]
[SerializeField]
private AshwickGate _ashwickGate;
[SerializeField]
private TransitionZone _keypadZone;
[SerializeField]
private TransitionZone _nextLevelZone;
private Sprite _defaultSprite;
protected async override UniTask PostLoadInternal()
{
if (SaveManager.GetLevelFlag(LevelFlag.AshwickGateOpen))
{
_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

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b17e98637f5042b4b70962875c373f5f
timeCreated: 1778704389

View File

@@ -0,0 +1,24 @@
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
{
public class AshwickGate : BaseItem
{
[SerializeField] private AshwickGateKeypadPuzzle _keypadPuzzle;
public override string InteractableName => "Iron Gate";
public override UniTask OnInteract(ItemDataSo item = null)
{
if (!CheckEmptyHands())
return UniTask.CompletedTask;
_keypadPuzzle?.Open();
return UniTask.CompletedTask;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b7b31f5b3d0844359def22f68ccea856
timeCreated: 1778781649

View File

@@ -0,0 +1,393 @@
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.Services.Puzzles.Base;
using BriarQueen.Game.Levels.ChapterOne.Ashwick;
using Cysharp.Threading.Tasks;
using MemoryPack;
using PrimeTween;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using VContainer;
namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow
{
[MemoryPackable]
public partial class AshwickGateKeypadPuzzleState
{
public string Digits;
}
public class AshwickGateKeypadPuzzle : BasePuzzle, IPuzzleStateful
{
private const string CorrectCode = "312";
private const int RequiredDigits = 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 = 1.5f,
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;
public override string PuzzleID => PuzzleIdentifiers.AllPuzzles[PuzzleKey.AshwickMarketGate];
public bool IsCompleted => _isCompleted || SaveManager.GetLevelFlag(LevelFlag.AshwickGateOpen);
[Inject]
public void ConstructKeypad(InteractManager interactManager)
{
_interactManager = interactManager;
}
private void Awake()
{
if (_displayField != null)
{
_displayField.readOnly = true;
_displayField.text = string.Empty;
}
BindButtons();
SetPanelState(0f, false, false);
SyncDisplay();
}
private void OnDestroy()
{
UnbindButtons();
TryUnregisterRaycaster();
CancelPanelTween();
}
public override UniTask PostLoad()
{
_isCompleted = SaveManager.GetLevelFlag(LevelFlag.AshwickGateOpen);
_isOpen = false;
_isEvaluating = false;
SetPanelState(0f, false, false);
SyncDisplay();
_statusGlow?.TurnOff().Forget();
return UniTask.CompletedTask;
}
public override UniTask CompletePuzzle()
{
return CompletePuzzleInternal();
}
public void Open()
{
OpenInternal().Forget();
}
public void Close()
{
CloseInternal(requestSave: true).Forget();
}
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 > RequiredDigits)
_currentDigits = _currentDigits[..RequiredDigits];
}
SyncDisplay();
return UniTask.CompletedTask;
}
private async UniTask CompletePuzzleInternal()
{
if (IsCompleted || _outskirts == null)
return;
_isCompleted = true;
_currentDigits = string.Empty;
SyncDisplay();
SaveManager.SetPuzzleCompleted(PuzzleKey.AshwickMarketGate, true, requestSave: false);
await CloseInternal(requestSave: false);
await _outskirts.OpenGate();
}
private async UniTaskVoid OpenInternal()
{
if (IsCompleted || _isEvaluating || _isOpen)
return;
_isOpen = true;
ResetPanelTween();
SetPanelState(0f, false, true);
SyncDisplay();
TryRegisterRaycaster();
_statusGlow?.TurnOff().Forget();
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);
}
private async UniTask CloseInternal(bool requestSave)
{
if (!_isOpen)
return;
_isOpen = false;
ResetPanelTween();
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 (requestSave)
EventCoordinator.PublishImmediate(new RequestGameSaveEvent());
}
private void OnDigitPressed(int digit)
{
if (_isEvaluating || IsCompleted || !_isOpen || _currentDigits.Length >= RequiredDigits)
return;
_currentDigits += digit.ToString();
SyncDisplay();
if (_currentDigits.Length == RequiredDigits)
EvaluateCode().Forget();
}
private async UniTaskVoid EvaluateCode()
{
_isEvaluating = true;
if (_panelGroup != null)
_panelGroup.interactable = false;
if (_currentDigits == CorrectCode)
{
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 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 TryRegisterRaycaster()
{
if (_raycasterRegistered || _interactManager == null || _graphicRaycaster == null)
return;
_interactManager.AddUIRaycaster(_graphicRaycaster);
_interactManager.SetExclusiveRaycaster(_graphicRaycaster);
_raycasterRegistered = true;
}
private void TryUnregisterRaycaster()
{
if (!_raycasterRegistered || _interactManager == null || _graphicRaycaster == null)
return;
_interactManager.RemoveUIRaycaster(_graphicRaycaster);
_interactManager.ClearExclusiveRaycaster();
_raycasterRegistered = false;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 229b8c87b487340b8b0a02349675f70c

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 9afe762158da4f8c8bb8a1bcb98d62b1
timeCreated: 1778242706

View File

@@ -1,26 +0,0 @@
using System.Linq;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using Cysharp.Threading.Tasks;
namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow.GatePuzzle
{
public class AshwickGate : BaseItem
{
public override UniTask OnInteract(ItemDataSo item = null)
{
var codex = PlayerManager.GetDiscoveredCodexEntriesByType(CodexType.PuzzleClue);
if (codex.Any(x => x.UniqueID == CodexEntryIDs.Get(ClueEntryID.AshwickMarketGate)))
{
EventCoordinator.Publish(new DisplayInteractEvent($"The note said to use the lights."));
return UniTask.CompletedTask;
}
EventCoordinator.Publish(new DisplayInteractEvent($"It's locked."));
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1bc3c6697e5b4c029e807116f930e9a3
timeCreated: 1778247353

View File

@@ -1,167 +0,0 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Effects;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Services.Puzzles.Base;
using BriarQueen.Game.Items.HoverZones;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow.GatePuzzle
{
public class AshwickMarketGatePuzzle : BasePuzzle, IPuzzleStateful
{
[Header("Lights")]
[SerializeField]
private StreetlightGlow _leftLight;
[SerializeField]
private StreetlightGlow _rightLight;
[Header("Solution")]
[SerializeField]
private StreetlightGlowState _leftLightRequiredState = StreetlightGlowState.Blue;
[SerializeField]
private StreetlightGlowState _rightLightRequiredState = StreetlightGlowState.Blue;
[Header("Gate")]
[SerializeField]
private UIDissolveImage _gateImage;
[Header("Transition")]
[SerializeField]
private InteractZone _marketplaceZone;
private bool _isCompleted;
private bool _isCompleting;
public override string PuzzleID => PuzzleIdentifiers.AllPuzzles[PuzzleKey.AshwickMarketGate];
public bool IsCompleted => _isCompleted;
public override UniTask PostLoad()
{
_leftLight?.Initialize(this);
_rightLight?.Initialize(this);
return UniTask.CompletedTask;
}
public async UniTask EvaluateCompletion()
{
if (_isCompleted || _isCompleting)
{
return;
}
if (!CheckComplete())
{
return;
}
await CompletePuzzle();
}
public override async UniTask CompletePuzzle()
{
if (_isCompleted || _isCompleting)
{
return;
}
_isCompleting = true;
try
{
_isCompleted = true;
_leftLight?.Lock();
_rightLight?.Lock();
_marketplaceZone.gameObject.SetActive(true);
SaveManager.SetPuzzleCompleted(PuzzleKey.AshwickMarketGate, true, false);
SaveManager.SetLevelFlag(LevelFlag.MarketGateOpen, true, false);
EventCoordinator.PublishImmediate(new RequestGameSaveEvent());
if (_gateImage != null)
{
await _gateImage.DissolveOutAndDestroy();
}
}
finally
{
_isCompleting = false;
}
}
public bool CheckComplete()
{
if (_leftLight == null || _rightLight == null)
{
return false;
}
return _leftLight.CurrentState == _leftLightRequiredState &&
_rightLight.CurrentState == _rightLightRequiredState;
}
public UniTask<byte[]> CaptureState()
{
var payload = new AshwickMarketGatePuzzleStatePayload
{
LeftLightState = _leftLight != null ? _leftLight.CurrentState : StreetlightGlowState.Off,
RightLightState = _rightLight != null ? _rightLight.CurrentState : StreetlightGlowState.Off,
IsCompleted = _isCompleted
};
return UniTask.FromResult(MemoryPackSerializer.Serialize(payload));
}
public async UniTask RestoreState(byte[] state)
{
var isMarkedComplete = SaveManager.CurrentSave?.PersistentVariables?.Game
?.IsPuzzleCompleted(PuzzleKey.AshwickMarketGate) == true;
var payload = new AshwickMarketGatePuzzleStatePayload
{
LeftLightState = StreetlightGlowState.Off,
RightLightState = StreetlightGlowState.Off,
IsCompleted = isMarkedComplete
};
if (state is { Length: > 0 })
{
payload = MemoryPackSerializer.Deserialize<AshwickMarketGatePuzzleStatePayload>(state);
payload.IsCompleted |= isMarkedComplete;
}
if (_leftLight != null)
{
await _leftLight.SetState(payload.LeftLightState, false);
}
if (_rightLight != null)
{
await _rightLight.SetState(payload.RightLightState, false);
}
if (!payload.IsCompleted)
{
_isCompleted = false;
_leftLight?.Unlock();
_rightLight?.Unlock();
return;
}
_isCompleted = true;
_leftLight?.Lock();
_rightLight?.Lock();
if (_gateImage != null && DestructionService != null)
{
await DestructionService.Destroy(_gateImage.gameObject);
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 701653a82bc1487d8aff6ea83a9abaeb
timeCreated: 1778243473

View File

@@ -1,12 +0,0 @@
using MemoryPack;
namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow.GatePuzzle
{
[MemoryPackable]
public partial struct AshwickMarketGatePuzzleStatePayload
{
public StreetlightGlowState LeftLightState;
public StreetlightGlowState RightLightState;
public bool IsCompleted;
}
}

View File

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

View File

@@ -1,181 +0,0 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Effects;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using BriarQueen.Framework.Managers.UI;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace BriarQueen.Game.Puzzles.ChapterOne.AshwickHallow.GatePuzzle
{
public enum StreetlightGlowState
{
Off = 0,
Red = 1,
Orange = 2,
Green = 3,
Blue = 4
}
public class StreetlightGlow : BaseItem
{
[Header("Puzzle")]
[SerializeField]
private AshwickMarketGatePuzzle _puzzle;
[Header("Light")]
[SerializeField]
private UILightGlow _light;
[SerializeField]
private float _changeDuration = 0.25f;
[SerializeField]
private float _activeIntensity = 1.5f;
[Header("Colours")]
[SerializeField]
private Color _red = new(0.872f, 0.08f, 0.1704798f, 1f);
[SerializeField]
private Color _orange = new(0.8666667f, 0.5109999f, 0f, 1f);
[SerializeField]
private Color _green = new(0.12f, 0.865f, 0.1f, 1f);
[SerializeField]
private Color _blue = new(0.1490196f, 0.6001954f, 1f, 1f);
private bool _isChanging;
private StreetlightGlowState _currentState = StreetlightGlowState.Off;
public StreetlightGlowState CurrentState => _currentState;
public override string InteractableName
{
get
{
if (_isLocked)
{
return string.Empty;
}
return _currentState == StreetlightGlowState.Off
? "Turn On"
: "Switch Colour";
}
}
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Interact;
public void Initialize(AshwickMarketGatePuzzle puzzle)
{
if (_puzzle == null)
{
_puzzle = puzzle;
}
SetState(_currentState, false).Forget();
}
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (item != null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return;
}
if (_isLocked || _isChanging)
{
return;
}
if (!CheckEmptyHands())
{
return;
}
await CycleNext();
if (_puzzle != null)
{
await _puzzle.EvaluateCompletion();
}
}
public async UniTask SetState(StreetlightGlowState state, bool animate)
{
_currentState = state;
if (_light == null)
{
return;
}
var targetColor = GetColorForState(state);
var targetIntensity = state == StreetlightGlowState.Off ? 0f : _activeIntensity;
if (!animate)
{
_light.SetLightColor(targetColor);
_light.SetIntensity(targetIntensity);
return;
}
_isChanging = true;
try
{
await _light.TweenTo(targetColor, targetIntensity, _changeDuration);
}
finally
{
_isChanging = false;
}
}
public void Lock()
{
_isLocked = true;
}
public void Unlock()
{
_isLocked = false;
}
public Color GetCurrentColor()
{
return GetColorForState(_currentState);
}
private UniTask CycleNext()
{
var nextState = _currentState switch
{
StreetlightGlowState.Off => StreetlightGlowState.Red,
StreetlightGlowState.Red => StreetlightGlowState.Green,
StreetlightGlowState.Green => StreetlightGlowState.Orange,
StreetlightGlowState.Orange => StreetlightGlowState.Blue,
StreetlightGlowState.Blue => StreetlightGlowState.Off,
_ => StreetlightGlowState.Off
};
return SetState(nextState, true);
}
private Color GetColorForState(StreetlightGlowState state)
{
return state switch
{
StreetlightGlowState.Red => _red,
StreetlightGlowState.Orange => _orange,
StreetlightGlowState.Green => _green,
StreetlightGlowState.Blue => _blue,
_ => Color.black
};
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 74c68ae5d328428f87e9fb87424df340
timeCreated: 1778242706