Laxley Grandfather Clock puzzle artwork done.

This commit is contained in:
2026-03-28 18:53:38 +00:00
parent 69306a141b
commit 83e9a35d2f
152 changed files with 12822 additions and 194 deletions

View File

@@ -1,7 +1,171 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Events.Progression;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Hints.Data;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Services.Puzzles.Base;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Game.Puzzles.ChapterOne.LaxleyHouse.Clock
{
public class LaxleyClockBasePuzzle
[Serializable]
[MemoryPackable]
public partial struct LaxleyClockPuzzleState
{
public bool HourHandPlaced;
public bool MinuteHandPlaced;
public int HourHandRotationStep;
public int MinuteHandRotationStep;
public bool IsSolved;
}
public class LaxleyClockBasePuzzle : BasePuzzle, IPuzzleStateful
{
private const int SolvedHourStep = 6;
private const int SolvedMinuteStep = 1;
[Header("Clock Face")]
[SerializeField]
private LaxleyClockFace _clockFace;
[Header("Puzzle State")]
[SerializeField]
private Image _background;
[SerializeField]
private Sprite _clockOpenSprite;
[SerializeField]
private List<BaseItem> _clockItems;
public override string PuzzleID => PuzzleIdentifiers.AllPuzzles[PuzzleKey.LaxleyClock];
public override string LevelName => "Grandfather Clock";
public override Dictionary<int, BaseHint> Hints { get; }
public bool IsCompleted => SaveManager.GetLevelFlag(LevelFlag.LaxleyClockSolved);
protected override async UniTask PostLoadInternal()
{
_clockFace.Initialise(this);
if (SaveManager.GetLevelFlag(LevelFlag.LaxleyClockSolved))
{
_clockFace.LockHands();
await OpenClock(false);
}
}
public async UniTask NotifyClockStateChanged()
{
if (IsCompleted)
return;
if (!_clockFace.AreBothHandsPlaced())
return;
if (_clockFace.HourHandRotationStep == SolvedHourStep &&
_clockFace.MinuteHandRotationStep == SolvedMinuteStep)
{
await CompletePuzzle();
}
}
public override async UniTask CompletePuzzle()
{
if (IsCompleted)
return;
SaveManager.SetLevelFlag(LevelFlag.LaxleyClockSolved, true);
SaveManager.SetPuzzleCompleted(PuzzleKey.LaxleyClock, true);
EventCoordinator.Publish(new FadeEvent(false, 0.5f));
await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
_clockFace.HideMechanism();
_clockFace.LockHands();
await OpenClock(true);
EventCoordinator.Publish(new UnlockAchievementEvent(AchievementID.LaxleyGrandfatherClockPuzzleSolved));
EventCoordinator.Publish(new FadeEvent(true, 0.5f));
await UniTask.Delay(TimeSpan.FromSeconds(0.5f));
}
public UniTask<byte[]> CaptureState()
{
var state = new LaxleyClockPuzzleState
{
HourHandPlaced = _clockFace.HourHandPlaced,
MinuteHandPlaced = _clockFace.MinuteHandPlaced,
HourHandRotationStep = _clockFace.HourHandRotationStep,
MinuteHandRotationStep = _clockFace.MinuteHandRotationStep,
IsSolved = IsCompleted
};
byte[] data = MemoryPackSerializer.Serialize(state);
return UniTask.FromResult(data);
}
public async UniTask RestoreState(byte[] state)
{
_clockFace.Initialise(this);
if (state == null || state.Length == 0)
{
if (SaveManager.GetLevelFlag(LevelFlag.LaxleyClockSolved))
{
_clockFace.LockHands();
await OpenClock(false);
}
return;
}
LaxleyClockPuzzleState restored = MemoryPackSerializer.Deserialize<LaxleyClockPuzzleState>(state);
_clockFace.RestoreState(
restored.HourHandPlaced,
restored.MinuteHandPlaced,
restored.HourHandRotationStep,
restored.MinuteHandRotationStep,
restored.IsSolved);
if (restored.IsSolved || SaveManager.GetLevelFlag(LevelFlag.LaxleyClockSolved))
{
_clockFace.LockHands();
await OpenClock(false);
}
}
private async UniTask OpenClock(bool playAudio = false)
{
_background.sprite = _clockOpenSprite;
if (playAudio)
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.ClockOpening));
await UnlockItems();
}
private UniTask UnlockItems()
{
foreach (var item in _clockItems)
{
if (SaveManager.CurrentSave.CollectedItems.All(x => x.UniqueIdentifier != item.ItemData.UniqueID))
{
item.CanvasGroup.blocksRaycasts = true;
item.CanvasGroup.interactable = true;
item.CanvasGroup.alpha = 1;
}
}
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,7 +1,192 @@
using BriarQueen.Data.Identifiers;
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;
using UnityEngine.UI;
namespace BriarQueen.Game.Puzzles.ChapterOne.LaxleyHouse.Clock
{
public class LaxleyClockFace
public class LaxleyClockFace : BaseItem
{
[Header("Hands")]
[SerializeField]
private LaxleyClockHand _hourHand;
[SerializeField]
private LaxleyClockHand _minuteHand;
[SerializeField]
private Image _hub;
private LaxleyClockBasePuzzle _owningPuzzle;
private bool _locked;
public bool HourHandPlaced => _hourHand != null && _hourHand.Placed;
public bool MinuteHandPlaced => _minuteHand != null && _minuteHand.Placed;
public int HourHandRotationStep => _hourHand != null ? _hourHand.CurrentRotationStep : 0;
public int MinuteHandRotationStep => _minuteHand != null ? _minuteHand.CurrentRotationStep : 0;
public override string InteractableName => "Clock Face";
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Interact;
public void Initialise(LaxleyClockBasePuzzle owningPuzzle)
{
_owningPuzzle = owningPuzzle;
_locked = false;
gameObject.SetActive(true);
if (_hub != null)
_hub.gameObject.SetActive(true);
_hourHand?.Initialise(this, _owningPuzzle);
_minuteHand?.Initialise(this, _owningPuzzle);
RefreshInteractionState();
}
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (!IsInteractable())
return;
if (item == null)
{
string message = string.Empty;
if (!HourHandPlaced && !MinuteHandPlaced)
{
message = InteractEventIDs.Get(EnvironmentInteractKey.LaxleyGrandfatherClockMissingBothHands);
}
else if (!HourHandPlaced && MinuteHandPlaced)
{
message = InteractEventIDs.Get(EnvironmentInteractKey.LaxleyGrandfatherClockMissingHourHand);
}
else if (HourHandPlaced && !MinuteHandPlaced)
{
message = InteractEventIDs.Get(EnvironmentInteractKey.LaxleyGrandfatherClockMissingMinuteHand);
}
if (!string.IsNullOrWhiteSpace(message))
EventCoordinator.Publish(new DisplayInteractEvent(message));
return;
}
bool isHourHandItem = item.UniqueID == ItemIDs.Get(ItemKey.LaxleyClockHourHand);
bool isMinuteHandItem = item.UniqueID == ItemIDs.Get(ItemKey.LaxleyClockMinuteHand);
if (!isHourHandItem && !isMinuteHandItem)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return;
}
await PlaceHand(item);
}
public bool AreBothHandsPlaced()
{
return HourHandPlaced && MinuteHandPlaced;
}
public void LockHands()
{
_locked = true;
_hourHand?.Lock();
_minuteHand?.Lock();
RefreshInteractionState();
}
public void HideMechanism()
{
_locked = true;
_hourHand?.Hide();
_minuteHand?.Hide();
if (_hub != null)
_hub.gameObject.SetActive(false);
gameObject.SetActive(false);
}
public void RestoreState(
bool hourPlaced,
bool minutePlaced,
int hourRotationStep,
int minuteRotationStep,
bool solved)
{
_locked = solved;
gameObject.SetActive(!solved);
if (solved)
{
_hourHand?.RestoreState(hourPlaced, hourRotationStep, true);
_minuteHand?.RestoreState(minutePlaced, minuteRotationStep, true);
HideMechanism();
return;
}
if (_hub != null)
_hub.gameObject.SetActive(true);
_hourHand?.RestoreState(hourPlaced, hourRotationStep, false);
_minuteHand?.RestoreState(minutePlaced, minuteRotationStep, false);
RefreshInteractionState();
}
internal async UniTask NotifyHandChanged()
{
RefreshInteractionState();
if (_owningPuzzle != null)
await _owningPuzzle.NotifyClockStateChanged();
}
private async UniTask PlaceHand(ItemDataSo item)
{
if (item.UniqueID == ItemIDs.Get(ItemKey.LaxleyClockHourHand))
{
await _hourHand.Place();
}
else if (item.UniqueID == ItemIDs.Get(ItemKey.LaxleyClockMinuteHand))
{
await _minuteHand.Place();
}
PlayerManager.RemoveItem(item);
RefreshInteractionState();
if (_owningPuzzle != null)
await _owningPuzzle.NotifyClockStateChanged();
}
private void RefreshInteractionState()
{
bool bothPlaced = AreBothHandsPlaced();
bool canPlaceHands = !_locked && !bothPlaced;
if (CanvasGroup != null)
{
CanvasGroup.blocksRaycasts = canPlaceHands;
CanvasGroup.interactable = canPlaceHands;
}
_hourHand?.SetCanRotate(!_locked && bothPlaced);
_minuteHand?.SetCanRotate(!_locked && bothPlaced);
}
private bool IsInteractable()
{
return !_locked && !AreBothHandsPlaced();
}
}
}

View File

@@ -1,7 +1,316 @@
using System.Threading;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
namespace BriarQueen.Game.Puzzles.ChapterOne.LaxleyHouse.Clock
{
public class LaxleyClockHand
public class LaxleyClockHand : BaseItem
{
[Header("State")]
[SerializeField]
private bool _placed;
[SerializeField]
[Range(0, 11)]
private int _currentRotationStep;
[SerializeField]
private bool _locked;
[SerializeField]
private bool _isRotating;
[SerializeField]
private bool _canRotate;
[Header("Rotation")]
[SerializeField]
private float[] _rotationSteps = new float[12]
{
0f, // 12
-30f, // 1
-60f, // 2
-90f, // 3
-120f, // 4
-150f, // 5
-180f, // 6
-210f, // 7
-240f, // 8
-270f, // 9
-300f, // 10
-330f // 11
};
[SerializeField]
private float _rotateDuration = 0.15f;
[SerializeField]
private Ease _rotateEase = Ease.Linear;
[Header("Placement")]
[SerializeField]
private float _placeDuration = 0.2f;
[SerializeField]
private Ease _placeEase = Ease.OutSine;
[Header("Components")]
[SerializeField]
private GameObject _parentPivot;
[SerializeField]
private CanvasGroup _parentCanvasGroup;
[Header("Placement Behaviour")]
[SerializeField]
private bool _randomiseRotationOnFirstPlace = true;
private Sequence _placeSequence;
private Sequence _rotateSequence;
private CancellationTokenSource _placeCTS;
private CancellationTokenSource _rotateCTS;
private LaxleyClockFace _clockFace;
public bool Placed => _placed;
public int CurrentRotationStep => _currentRotationStep;
public void Initialise(LaxleyClockFace clockFace, LaxleyClockBasePuzzle owningPuzzle)
{
_clockFace = clockFace;
_canRotate = false;
if (_placed)
{
if (_parentPivot != null)
_parentPivot.SetActive(true);
if (_parentCanvasGroup != null)
_parentCanvasGroup.alpha = 1f;
ApplyRotationImmediate(_currentRotationStep);
}
else
{
if (_parentPivot != null)
_parentPivot.SetActive(false);
if (_parentCanvasGroup != null)
_parentCanvasGroup.alpha = 0f;
}
RefreshVisualState();
}
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (!IsInteractable())
return;
await RotateToNextStep();
await _clockFace.NotifyHandChanged();
}
public async UniTask Place()
{
if (_placed)
return;
_placed = true;
_locked = false;
_canRotate = false;
CancelPlaceTween();
_placeCTS = new CancellationTokenSource();
if (_randomiseRotationOnFirstPlace)
_currentRotationStep = Random.Range(0, _rotationSteps.Length);
if (_parentPivot != null)
_parentPivot.SetActive(true);
ApplyRotationImmediate(_currentRotationStep);
if (_parentCanvasGroup != null)
{
_parentCanvasGroup.alpha = 0f;
_parentCanvasGroup.blocksRaycasts = false;
_parentCanvasGroup.interactable = false;
}
_placeSequence.Stop();
_placeSequence = Sequence.Create()
.Group(Tween.Alpha(_parentCanvasGroup, 1f, _placeDuration, _placeEase));
try
{
await _placeSequence.ToUniTask(cancellationToken: _placeCTS.Token);
}
catch
{
}
RefreshVisualState();
}
public void RestoreState(bool placed, int rotationStep, bool locked)
{
CancelPlaceTween();
CancelRotateTween();
_placed = placed;
_locked = locked;
_isRotating = false;
_canRotate = false;
_currentRotationStep = Mathf.Clamp(rotationStep, 0, 11);
if (_parentPivot != null)
_parentPivot.SetActive(_placed);
if (_placed)
{
ApplyRotationImmediate(_currentRotationStep);
if (_parentCanvasGroup != null)
_parentCanvasGroup.alpha = 1f;
}
else
{
if (_parentCanvasGroup != null)
_parentCanvasGroup.alpha = 0f;
}
RefreshVisualState();
}
public void SetCanRotate(bool canRotate)
{
_canRotate = canRotate;
RefreshVisualState();
}
public void Lock()
{
_locked = true;
_canRotate = false;
RefreshVisualState();
}
public void Hide()
{
CancelPlaceTween();
CancelRotateTween();
_canRotate = false;
_isRotating = false;
if (_parentCanvasGroup != null)
{
_parentCanvasGroup.blocksRaycasts = false;
_parentCanvasGroup.interactable = false;
_parentCanvasGroup.alpha = 0f;
}
if (_parentPivot != null)
_parentPivot.SetActive(false);
}
private async UniTask RotateToNextStep()
{
if (!IsInteractable())
return;
_isRotating = true;
RefreshVisualState();
CancelRotateTween();
_rotateCTS = new CancellationTokenSource();
_currentRotationStep = (_currentRotationStep + 1) % 12;
Vector3 targetEuler = _parentPivot.transform.localEulerAngles;
targetEuler.z = _rotationSteps[_currentRotationStep];
_rotateSequence.Stop();
_rotateSequence = Sequence.Create()
.Group(
Tween.LocalRotation(
_parentPivot.transform,
Quaternion.Euler(targetEuler),
_rotateDuration,
_rotateEase));
try
{
await _rotateSequence.ToUniTask(cancellationToken: _rotateCTS.Token);
}
catch
{
}
_isRotating = false;
RefreshVisualState();
}
private void ApplyRotationImmediate(int step)
{
if (_parentPivot == null)
return;
Vector3 euler = _parentPivot.transform.localEulerAngles;
euler.z = _rotationSteps[Mathf.Clamp(step, 0, 11)];
_parentPivot.transform.localEulerAngles = euler;
}
private void RefreshVisualState()
{
bool interactable = IsInteractable();
if (_parentCanvasGroup != null)
{
_parentCanvasGroup.blocksRaycasts = interactable;
_parentCanvasGroup.interactable = interactable;
}
}
private bool IsInteractable()
{
return _placed && !_locked && !_isRotating && _canRotate;
}
private void CancelPlaceTween()
{
_placeSequence.Stop();
if (_placeCTS != null)
{
_placeCTS.Cancel();
_placeCTS.Dispose();
_placeCTS = null;
}
}
private void CancelRotateTween()
{
_rotateSequence.Stop();
if (_rotateCTS != null)
{
_rotateCTS.Cancel();
_rotateCTS.Dispose();
_rotateCTS = null;
}
}
private void OnDestroy()
{
CancelPlaceTween();
CancelRotateTween();
}
}
}

View File

@@ -4,18 +4,18 @@ using System.Linq;
using AYellowpaper.SerializedCollections;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Events.Progression;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Hints.Data;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Services.Puzzles.Base;
using BriarQueen.Game.Levels.ChapterOne.LaxleyHouse.FireplaceLockbox;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;
namespace BriarQueen.Game.Levels.ChapterOne.LaxleyHouse
namespace BriarQueen.Game.Puzzles.ChapterOne.LaxleyHouse.Fireplace.LockboxPuzzle
{
[Serializable]
[MemoryPackable]
@@ -94,6 +94,7 @@ namespace BriarQueen.Game.Levels.ChapterOne.LaxleyHouse
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.LockboxOpening));
await OpenLockbox();
EventCoordinator.Publish(new UnlockAchievementEvent(AchievementID.FireplaceLockboxPuzzleBoxSolved));
EventCoordinator.Publish(new FadeEvent(true, 0.5f));
}
@@ -103,10 +104,10 @@ namespace BriarQueen.Game.Levels.ChapterOne.LaxleyHouse
if (_backgroundImage != null)
_backgroundImage.sprite = _lockBoxOpenSprite;
await GenerateLockboxItems();
await UnlockLockboxItems();
}
private async UniTask GenerateLockboxItems()
private async UniTask UnlockLockboxItems()
{
foreach (var item in _lockboxItems)
{
@@ -119,10 +120,6 @@ namespace BriarQueen.Game.Levels.ChapterOne.LaxleyHouse
item.CanvasGroup.blocksRaycasts = true;
item.CanvasGroup.interactable = true;
}
else
{
await DestructionService.Destroy(item.gameObject);
}
}
}

View File

@@ -7,7 +7,7 @@ using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Game.Levels.ChapterOne.LaxleyHouse.FireplaceLockbox
namespace BriarQueen.Game.Puzzles.ChapterOne.LaxleyHouse.Fireplace.LockboxPuzzle
{
public class LockboxSlot : BaseItem
{