Restructured for new direction.

This commit is contained in:
2026-05-12 12:01:09 +01:00
parent 0439b6c1d2
commit c203f836b1
1134 changed files with 125569 additions and 213519 deletions

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
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

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

View File

@@ -0,0 +1,167 @@
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

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

View File

@@ -0,0 +1,12 @@
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

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

View File

@@ -0,0 +1,182 @@
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 bool _isLocked;
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

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

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a35c6a665b664f5b89e806b13ef62510
timeCreated: 1773930345

View File

@@ -1,185 +0,0 @@
using System;
using System.Collections.Generic;
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.Managers.Player.Data;
using BriarQueen.Framework.Services.Puzzles.Base;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
namespace BriarQueen.Game.Puzzles.ChapterOne.Fountain
{
[Serializable]
[MemoryPackable]
public partial struct FountainGemPuzzleState
{
public bool IsCompleted;
public string[] InputtedGemIDs;
}
public class FountainGemBasePuzzle : BasePuzzle, IPuzzleStateful
{
[SerializeField]
private FountainGemSlot _fountainGemSlot;
public override string PuzzleID => PuzzleIdentifiers.AllPuzzles[PuzzleKey.FountainGemPuzzle];
public override string LevelName => "Hole in the side of a Fountain";
public override Dictionary<int, BaseHint> Hints { get; } = new();
private readonly List<ItemDataSo> _inputtedGems = new();
private readonly List<string> _inputtedGemIDs = new();
private readonly string[] _correctSequence =
{
ItemIDs.Get(ItemKey.Sapphire),
ItemIDs.Get(ItemKey.Emerald),
ItemIDs.Get(ItemKey.Ruby)
};
private bool _isCompleted;
public bool IsCompleted => _isCompleted;
protected override UniTask PostActivateInternal()
{
TutorialService.DisplayTutorial(TutorialPopupID.ResetPuzzles);
return UniTask.CompletedTask;
}
protected void Start()
{
if (_fountainGemSlot != null)
_fountainGemSlot.Initialize(this);
}
internal bool TryInsertGem(ItemDataSo item)
{
if (_isCompleted || item == null)
return false;
var itemID = item.UniqueID;
if (string.IsNullOrWhiteSpace(itemID))
return false;
_inputtedGems.Add(item);
_inputtedGemIDs.Add(itemID);
ConsumePlayerGem(item);
if (_inputtedGemIDs.Count < _correctSequence.Length)
return true;
if (IsCorrectSequence())
{
CompletePuzzle().Forget();
return true;
}
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.PuzzleIncorrect));
ResetPuzzleState();
return false;
}
public override UniTask CompletePuzzle()
{
if (_isCompleted)
return UniTask.CompletedTask;
_isCompleted = true;
SaveManager.SetLevelFlag(LevelFlag.VillageStreetGateOpen, true);
SaveManager.SetPuzzleCompleted(PuzzleKey.FountainGemPuzzle, true);
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.GateOpening));
EventCoordinator.Publish(new UnlockAchievementEvent(AchievementID.FountainGemPuzzleSolved));
return UniTask.CompletedTask;
}
public UniTask<byte[]> CaptureState()
{
var state = new FountainGemPuzzleState
{
IsCompleted = _isCompleted,
InputtedGemIDs = _inputtedGemIDs.ToArray()
};
return UniTask.FromResult(MemoryPackSerializer.Serialize(state));
}
public UniTask RestoreState(byte[] state)
{
_inputtedGems.Clear();
_inputtedGemIDs.Clear();
_isCompleted = false;
if (state == null || state.Length == 0)
{
_isCompleted = SaveManager.GetLevelFlag(LevelFlag.VillageStreetGateOpen);
return UniTask.CompletedTask;
}
var restoredState = MemoryPackSerializer.Deserialize<FountainGemPuzzleState>(state);
_isCompleted = restoredState.IsCompleted;
if (restoredState.InputtedGemIDs != null)
_inputtedGemIDs.AddRange(restoredState.InputtedGemIDs);
if (_isCompleted)
{
SaveManager.SetLevelFlag(LevelFlag.VillageStreetGateOpen, true, false);
SaveManager.SetPuzzleCompleted(PuzzleKey.FountainGemPuzzle, true, false);
}
return UniTask.CompletedTask;
}
public void ResetPuzzleState()
{
if (_isCompleted)
return;
for (int i = 0; i < _inputtedGems.Count; i++)
{
var gem = _inputtedGems[i];
if (gem != null)
PlayerManager.CollectItem(gem);
}
_inputtedGems.Clear();
_inputtedGemIDs.Clear();
}
public void OnResetButtonPressed()
{
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.ResetPuzzle));
ResetPuzzleState();
}
private bool IsCorrectSequence()
{
if (_inputtedGemIDs.Count != _correctSequence.Length)
return false;
for (int i = 0; i < _correctSequence.Length; i++)
{
if (_inputtedGemIDs[i] != _correctSequence[i])
return false;
}
return true;
}
private void ConsumePlayerGem(ItemDataSo item)
{
if (item == null)
return;
PlayerManager.RemoveItem(item);
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: b6d0351ea34c4c918d254f72ac186173
timeCreated: 1773930345

View File

@@ -1,50 +0,0 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using Cysharp.Threading.Tasks;
namespace BriarQueen.Game.Puzzles.ChapterOne.Fountain
{
public class FountainGemSlot : BaseItem
{
private FountainGemBasePuzzle _puzzle;
private readonly string _firstPieceKey = ItemIDs.Get(ItemKey.Sapphire);
private readonly string _secondPieceKey = ItemIDs.Get(ItemKey.Emerald);
private readonly string _thirdPieceKey = ItemIDs.Get(ItemKey.Ruby);
public override string InteractableName => "Hole";
public void Initialize(FountainGemBasePuzzle puzzle)
{
_puzzle = puzzle;
}
public override UniTask OnInteract(ItemDataSo item = null)
{
if (_puzzle == null || item == null || _puzzle.IsCompleted)
{
return UniTask.CompletedTask;
}
if (!CheckEmptyHands())
return UniTask.CompletedTask;
var itemID = item.UniqueID;
if (!IsValidGem(itemID))
{
return UniTask.CompletedTask;
}
_puzzle.TryInsertGem(item);
return UniTask.CompletedTask;
}
private bool IsValidGem(string itemID)
{
return itemID == _firstPieceKey ||
itemID == _secondPieceKey ||
itemID == _thirdPieceKey;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 40ab281620b846b3b4798a035abae950
timeCreated: 1773930669

View File

@@ -1,30 +0,0 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Game.Puzzles.ChapterOne.Fountain
{
public class FountainResetButton : MonoBehaviour
{
[SerializeField]
private Button _resetButton;
[SerializeField]
private FountainGemBasePuzzle _fountainGemBasePuzzle;
protected void Start()
{
_resetButton.onClick.AddListener(OnResetClicked);
}
protected void OnDestroy()
{
_resetButton.onClick.RemoveListener(OnResetClicked);
}
private void OnResetClicked()
{
_fountainGemBasePuzzle.OnResetButtonPressed();
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 52594bb963824a15b8916933f552312d
timeCreated: 1773933231

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6edb1657de0f4759b5563de3dd625799
timeCreated: 1774694628

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 123b22f5870243e5ba1fdbcda4bbabde
timeCreated: 1774694641

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 38875deca26847619404eb281c98f21c
timeCreated: 1774694667

View File

@@ -1,171 +0,0 @@
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
{
[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,3 +0,0 @@
fileFormatVersion: 2
guid: 7162a53d3fc7482389473ef98dbb6f11
timeCreated: 1774694726

View File

@@ -1,192 +0,0 @@
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 : 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,3 +0,0 @@
fileFormatVersion: 2
guid: 86f2d2fa28bf4e8aa2e5afdf00da6dca
timeCreated: 1774694771

View File

@@ -1,316 +0,0 @@
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 : 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

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 34468c31870143d18f90a5c285f282f9
timeCreated: 1774695987

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6f8d8ac8596d463daab5ce2a1a87f08b
timeCreated: 1774694641

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f90b81f7232344f3b46f2470cc93c933
timeCreated: 1774694641

View File

@@ -1,292 +0,0 @@
using System;
using System.Collections.Generic;
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 Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;
namespace BriarQueen.Game.Puzzles.ChapterOne.LaxleyHouse.Fireplace.LockboxPuzzle
{
[Serializable]
[MemoryPackable]
public partial struct FireplaceLockboxPuzzleState
{
public int SolutionSlotOne;
public int SolutionSlotTwo;
public int SolutionSlotThree;
public int SlotOneValue;
public int SlotTwoValue;
public int SlotThreeValue;
}
public class FireplaceLockboxBasePuzzle : BasePuzzle, IPuzzleStateful
{
private const int MinDigit = 1;
private const int MaxDigit = 9;
public override string PuzzleID => PuzzleIdentifiers.AllPuzzles[PuzzleKey.FireplaceLockboxPuzzle];
public override string LevelName => "Fireplace Lockbox";
public override Dictionary<int, BaseHint> Hints { get; }
[Header("Components")]
[SerializeField]
[SerializedDictionary(keyName: "Lockbox Number", valueName: "Number Sprite")]
private SerializedDictionary<int, Sprite> _numberSprites = new();
[Header("Puzzle Slots")]
[SerializeField]
private List<LockboxSlot> _lockboxSlots = new();
[Header("Level State")]
[SerializeField]
private Image _backgroundImage;
[SerializeField]
private Sprite _lockBoxOpenSprite;
[SerializeField]
private List<BaseItem> _lockboxItems = new();
private int _solutionSlotOne = -1;
private int _solutionSlotTwo = -1;
private int _solutionSlotThree = -1;
public bool IsCompleted { get; private set; }
private void Awake()
{
InitializeSlots();
}
protected override async UniTask PostLoadInternal()
{
if (SaveManager.GetLevelFlag(LevelFlag.LaxleyLockboxOpened))
{
DisableSlots();
await OpenLockbox();
}
}
public override async UniTask CompletePuzzle()
{
if (SaveManager.GetLevelFlag(LevelFlag.LaxleyLockboxOpened))
return;
IsCompleted = true;
SaveManager.SetLevelFlag(LevelFlag.LaxleyLockboxOpened, true);
SaveManager.SetPuzzleCompleted(PuzzleKey.FireplaceLockboxPuzzle, true);
DisableSlots();
EventCoordinator.Publish(new FadeEvent(false, 0.5f));
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.LockboxOpening));
await OpenLockbox();
EventCoordinator.Publish(new UnlockAchievementEvent(AchievementID.FireplaceLockboxPuzzleBoxSolved));
EventCoordinator.Publish(new FadeEvent(true, 0.5f));
}
private async UniTask OpenLockbox()
{
if (_backgroundImage != null)
_backgroundImage.sprite = _lockBoxOpenSprite;
await UnlockLockboxItems();
}
private async UniTask UnlockLockboxItems()
{
foreach (var item in _lockboxItems)
{
if (item == null)
continue;
if (SaveManager.CurrentSave.CollectedItems.All(x => x.UniqueIdentifier != item.ItemData.UniqueID))
{
item.CanvasGroup.alpha = 1f;
item.CanvasGroup.blocksRaycasts = true;
item.CanvasGroup.interactable = true;
}
}
}
public UniTask<byte[]> CaptureState()
{
var state = new FireplaceLockboxPuzzleState
{
SolutionSlotOne = _solutionSlotOne,
SolutionSlotTwo = _solutionSlotTwo,
SolutionSlotThree = _solutionSlotThree,
SlotOneValue = _lockboxSlots.Count > 0 ? _lockboxSlots[0].CurrentValue : MinDigit,
SlotTwoValue = _lockboxSlots.Count > 1 ? _lockboxSlots[1].CurrentValue : MinDigit,
SlotThreeValue = _lockboxSlots.Count > 2 ? _lockboxSlots[2].CurrentValue : MinDigit
};
return UniTask.FromResult(MemoryPackSerializer.Serialize(state));
}
public UniTask RestoreState(byte[] state)
{
if (state == null || state.Length == 0)
{
GenerateSolutionIfNeeded();
ApplyRandomDefaultSlotValues();
ApplySolutionToCompletionCheck();
if (SaveManager.GetLevelFlag(LevelFlag.LaxleyLockboxOpened))
DisableSlots();
return UniTask.CompletedTask;
}
var restored = MemoryPackSerializer.Deserialize<FireplaceLockboxPuzzleState>(state);
_solutionSlotOne = WrapDigit(restored.SolutionSlotOne);
_solutionSlotTwo = WrapDigit(restored.SolutionSlotTwo);
_solutionSlotThree = WrapDigit(restored.SolutionSlotThree);
if (_lockboxSlots.Count > 0)
_lockboxSlots[0].ApplyValueImmediate(restored.SlotOneValue);
if (_lockboxSlots.Count > 1)
_lockboxSlots[1].ApplyValueImmediate(restored.SlotTwoValue);
if (_lockboxSlots.Count > 2)
_lockboxSlots[2].ApplyValueImmediate(restored.SlotThreeValue);
ApplySolutionToCompletionCheck();
if (SaveManager.GetLevelFlag(LevelFlag.LaxleyLockboxOpened))
DisableSlots();
return UniTask.CompletedTask;
}
private void InitializeSlots()
{
if (_lockboxSlots == null)
return;
foreach (var slot in _lockboxSlots)
{
if (slot == null)
continue;
slot.Initialize(GetNumberSprite, OnSlotValueChanged);
}
}
private void DisableSlots()
{
if (_lockboxSlots == null)
return;
foreach (var slot in _lockboxSlots)
{
if (slot == null)
continue;
slot.gameObject.SetActive(false);
}
}
private void ApplyRandomDefaultSlotValues()
{
if (_lockboxSlots.Count > 0)
_lockboxSlots[0].ApplyValueImmediate(GetRandomDigit());
if (_lockboxSlots.Count > 1)
_lockboxSlots[1].ApplyValueImmediate(GetRandomDigit());
if (_lockboxSlots.Count > 2)
_lockboxSlots[2].ApplyValueImmediate(GetRandomDigit());
}
private int GetRandomDigit()
{
return Random.Range(MinDigit, MaxDigit + 1);
}
private Sprite GetNumberSprite(int value)
{
value = WrapDigit(value);
return _numberSprites.TryGetValue(value, out var sprite) ? sprite : null;
}
private void OnSlotValueChanged()
{
ApplySolutionToCompletionCheck();
if (!SaveManager.GetLevelFlag(LevelFlag.LaxleyLockboxOpened) && IsCurrentInputCorrect())
CompletePuzzle().Forget();
}
private void ApplySolutionToCompletionCheck()
{
IsCompleted = IsCurrentInputCorrect();
}
private bool IsCurrentInputCorrect()
{
if (_lockboxSlots == null || _lockboxSlots.Count < 3)
return false;
return _lockboxSlots[0].CurrentValue == _solutionSlotOne &&
_lockboxSlots[1].CurrentValue == _solutionSlotTwo &&
_lockboxSlots[2].CurrentValue == _solutionSlotThree;
}
private void GenerateSolutionIfNeeded()
{
if (HasValidSolution())
return;
GenerateSolutionVariation();
}
private bool HasValidSolution()
{
return _solutionSlotOne is >= MinDigit and <= MaxDigit &&
_solutionSlotTwo is >= MinDigit and <= MaxDigit &&
_solutionSlotThree is >= MinDigit and <= MaxDigit;
}
private void GenerateSolutionVariation()
{
int[] digits = { 8, 4, 7 };
for (int i = digits.Length - 1; i > 0; i--)
{
int swapIndex = Random.Range(0, i + 1);
(digits[i], digits[swapIndex]) = (digits[swapIndex], digits[i]);
}
_solutionSlotOne = digits[0];
_solutionSlotTwo = digits[1];
_solutionSlotThree = digits[2];
}
private int WrapDigit(int value)
{
value %= MaxDigit;
if (value <= 0)
value += MaxDigit;
return value;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 57793604ae2648118b1911c5b1fe3f75
timeCreated: 1774554976

View File

@@ -1,295 +0,0 @@
using System;
using System.Threading;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Game.Puzzles.ChapterOne.LaxleyHouse.Fireplace.LockboxPuzzle
{
public class LockboxSlot : BaseItem
{
private const int MinDigit = 1;
private const int MaxDigit = 9;
[Header("Components")]
[SerializeField]
private Image _currentNumberImage;
[SerializeField]
private Image _nextNumberImage;
[Header("Animation")]
[SerializeField]
private float _spinPadding = 8f;
[SerializeField]
private float _spinDuration = 0.15f;
[SerializeField]
private Ease _spinEase = Ease.OutCubic;
[SerializeField]
[Range(0f, 1f)]
private float _fadedAlpha = 0.4f;
private RectTransform _currentRect;
private RectTransform _nextRect;
private Sequence _slotSequence;
private CancellationTokenSource _slotCancellationTokenSource;
private Func<int, Sprite> _spriteResolver;
private Action _onValueChanged;
public int CurrentValue { get; private set; } = MinDigit;
public void Initialize(Func<int, Sprite> spriteResolver, Action onValueChanged)
{
_spriteResolver = spriteResolver;
_onValueChanged = onValueChanged;
_currentRect = _currentNumberImage != null ? _currentNumberImage.rectTransform : null;
_nextRect = _nextNumberImage != null ? _nextNumberImage.rectTransform : null;
SyncHelperImageLayout();
ApplyValueImmediate(CurrentValue);
}
public void ApplyValueImmediate(int value)
{
CurrentValue = WrapValue(value);
CancelTweenIfRunning();
SyncHelperImageLayout();
var sprite = ResolveSprite(CurrentValue);
if (_currentNumberImage != null)
{
_currentNumberImage.sprite = sprite;
_currentNumberImage.enabled = sprite != null;
_currentNumberImage.color = Color.white;
}
if (_nextNumberImage != null)
{
_nextNumberImage.sprite = null;
_nextNumberImage.enabled = false;
_nextNumberImage.color = Color.white;
}
if (_currentRect != null)
_currentRect.anchoredPosition = Vector2.zero;
if (_nextRect != null)
_nextRect.anchoredPosition = GetHiddenPosition();
}
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (item != null)
return;
await SpinToNextValue();
}
public async UniTask SpinToNextValue()
{
if (_currentNumberImage == null || _nextNumberImage == null || _currentRect == null || _nextRect == null)
return;
int nextValue = WrapValue(CurrentValue + 1);
var nextSprite = ResolveSprite(nextValue);
if (nextSprite == null)
{
Debug.LogWarning($"[LockboxSlot] Missing sprite for value {nextValue}.", this);
return;
}
CancelTweenIfRunning();
SyncHelperImageLayout();
_slotCancellationTokenSource = new CancellationTokenSource();
_nextNumberImage.sprite = nextSprite;
_nextNumberImage.enabled = true;
_nextNumberImage.color = new Color(1f, 1f, 1f, _fadedAlpha);
_currentNumberImage.enabled = true;
_currentNumberImage.color = Color.white;
_currentRect.anchoredPosition = Vector2.zero;
_nextRect.anchoredPosition = GetHiddenPosition();
_slotSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.UIAnchoredPosition(
_currentRect,
GetVisibleExitPosition(),
_spinDuration,
_spinEase,
useUnscaledTime: true))
.Group(Tween.UIAnchoredPosition(
_nextRect,
Vector2.zero,
_spinDuration,
_spinEase,
useUnscaledTime: true))
.Group(Tween.Alpha(
_currentNumberImage,
new TweenSettings<float>
{
endValue = _fadedAlpha,
settings = new TweenSettings
{
duration = _spinDuration,
useUnscaledTime = true
}
}))
.Group(Tween.Alpha(
_nextNumberImage,
new TweenSettings<float>
{
endValue = 1f,
settings = new TweenSettings
{
duration = _spinDuration,
useUnscaledTime = true
}
}));
try
{
await _slotSequence.ToUniTask(cancellationToken: _slotCancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
if (_slotSequence.isAlive)
_slotSequence.Stop();
_slotSequence = default;
_slotCancellationTokenSource?.Dispose();
_slotCancellationTokenSource = null;
}
SwapImages();
CurrentValue = nextValue;
PrepareHiddenImage();
_onValueChanged?.Invoke();
}
private Sprite ResolveSprite(int value)
{
return _spriteResolver?.Invoke(WrapValue(value));
}
private int WrapValue(int value)
{
value %= MaxDigit;
if (value <= 0)
value += MaxDigit;
return value;
}
private void SwapImages()
{
(_currentNumberImage, _nextNumberImage) = (_nextNumberImage, _currentNumberImage);
(_currentRect, _nextRect) = (_nextRect, _currentRect);
}
private void PrepareHiddenImage()
{
if (_currentNumberImage != null)
{
_currentNumberImage.enabled = true;
_currentNumberImage.color = Color.white;
}
if (_currentRect != null)
_currentRect.anchoredPosition = Vector2.zero;
if (_nextNumberImage != null)
{
_nextNumberImage.sprite = null;
_nextNumberImage.enabled = false;
_nextNumberImage.color = Color.white;
}
if (_nextRect != null)
_nextRect.anchoredPosition = GetHiddenPosition();
}
private void SyncHelperImageLayout()
{
if (_currentRect == null || _nextRect == null)
return;
_nextRect.anchorMin = _currentRect.anchorMin;
_nextRect.anchorMax = _currentRect.anchorMax;
_nextRect.pivot = _currentRect.pivot;
_nextRect.sizeDelta = _currentRect.sizeDelta;
_nextRect.localScale = _currentRect.localScale;
_nextRect.localRotation = _currentRect.localRotation;
}
private Vector2 GetHiddenPosition()
{
float travelDistance = GetTravelDistance();
return new Vector2(0f, -travelDistance);
}
private Vector2 GetVisibleExitPosition()
{
float travelDistance = GetTravelDistance();
return new Vector2(0f, travelDistance);
}
private float GetTravelDistance()
{
if (_currentRect == null)
return _spinPadding;
float rectHeight = _currentRect.rect.height;
if (rectHeight <= 0f && _currentNumberImage != null)
rectHeight = _currentNumberImage.preferredHeight;
return rectHeight + _spinPadding;
}
private void CancelTweenIfRunning()
{
if (_slotCancellationTokenSource != null && !_slotCancellationTokenSource.IsCancellationRequested)
_slotCancellationTokenSource.Cancel();
if (_slotSequence.isAlive)
_slotSequence.Stop();
_slotSequence = default;
_slotCancellationTokenSource?.Dispose();
_slotCancellationTokenSource = null;
}
private void OnDisable()
{
CancelTweenIfRunning();
}
private void OnDestroy()
{
CancelTweenIfRunning();
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6c4c81a879c94cb9a2eeafcd454d55e4
timeCreated: 1774555158

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 452ffcf5991344cf992d50e7b54e31df
timeCreated: 1772735921

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 687d55dc611140ada4fd8af70e83d7d9
timeCreated: 1773330168

View File

@@ -1,151 +0,0 @@
using System;
using System.Threading;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using BriarQueen.Framework.Managers.UI;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Game.Puzzles.ChapterOne.Workshop.BoxPuzzle
{
public class PuzzleBoxSlot : BaseItem
{
[SerializeField]
private WorkshopBasePuzzleBox _owner;
[SerializeField]
private Image _pictureImage;
[Header("Rules")]
[SerializeField]
private AssetItemKey _requiredPicturePieceID;
[SerializeField]
private int _slotID;
private CancellationTokenSource _selectedCts;
private Sequence _selectedSequence;
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Interact;
public override string InteractableName => IsLocked ? string.Empty : "Loose Piece";
public AssetItemKey RequiredPieceID => _requiredPicturePieceID;
public bool IsLocked { get; private set; }
public AssetItemKey CurrentPieceID { get; private set; }
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (IsLocked)
return;
if(!CheckEmptyHands())
return;
if (_owner.SelectedPiece == null)
{
_owner.SelectPiece(this);
return;
}
await _owner.TrySwap(this);
}
public void SetSelected(bool selected)
{
if (selected)
StartBreathingAsync().Forget();
else
StopBreathing();
}
public bool IsCorrect()
{
return CurrentPieceID == _requiredPicturePieceID;
}
public void Lock()
{
IsLocked = true;
StopBreathing();
}
private async UniTask StartBreathingAsync()
{
StopBreathing();
// Create a sequence with scale + alpha pulsing
_selectedSequence = Sequence.Create(cycleMode: Sequence.SequenceCycleMode.Yoyo, cycles: -1)
.Group(
Tween.Scale(_pictureImage.rectTransform, new TweenSettings<Vector3>
{
startValue = _pictureImage.rectTransform.localScale,
endValue = _pictureImage.rectTransform.localScale * 1.05f,
settings = new TweenSettings { duration = 0.5f, ease = Ease.InOutSine }
}).Group(
Tween.Alpha(_pictureImage, new TweenSettings<float>
{
startValue = 1f,
endValue = 0.8f,
settings = new TweenSettings { duration = 0.5f, ease = Ease.InOutSine }
}))
);
_selectedCts = new CancellationTokenSource();
try
{
await _selectedSequence.ToUniTask(cancellationToken: _selectedCts.Token);
}
catch (OperationCanceledException)
{
// Expected when stopping
}
finally
{
DisposeCts();
}
}
private void StopBreathing()
{
if (_selectedSequence.isAlive)
_selectedSequence.Complete();
_pictureImage.rectTransform.localScale = Vector3.one;
_pictureImage.color = new Color(_pictureImage.color.r, _pictureImage.color.g, _pictureImage.color.b, 1f);
DisposeCts();
}
private void DisposeCts()
{
if (_selectedCts == null) return;
_selectedCts.Cancel();
_selectedCts.Dispose();
_selectedCts = null;
}
public async UniTask SetPiece(AssetItemKey pieceID, bool animate)
{
CurrentPieceID = pieceID;
var sprite = await _owner.GetSpriteForPiece(AssetKeyIdentifiers.Get(pieceID));
if (sprite == null) return;
if (!animate)
{
_pictureImage.sprite = sprite;
return;
}
await Tween.Alpha(_pictureImage, 0f, 0.5f).ToUniTask();
_pictureImage.sprite = sprite;
await Tween.Alpha(_pictureImage, 1f, 0.5f).ToUniTask();
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: e93f8f355d8249f099cd3d446b40890a
timeCreated: 1773335140

View File

@@ -1,282 +0,0 @@
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.Managers.Hints.Data;
using BriarQueen.Framework.Services.Puzzles.Base;
using BriarQueen.Game.Items.HoverZones.ChapterOne.Workshop.Upstairs.PuzzleBox;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
using Random = UnityEngine.Random;
namespace BriarQueen.Game.Puzzles.ChapterOne.Workshop.BoxPuzzle
{
[Serializable]
[MemoryPackable]
public partial struct WorkshopPuzzleBoxState
{
public AssetItemKey[] SlotPieceIDs;
public bool Completed;
public bool SideEffectsFired;
}
public class WorkshopBasePuzzleBox : BasePuzzle, IPuzzleStateful
{
private static readonly HashSet<AssetItemKey> PuzzlePieceIDs =
Enum.GetValues(typeof(AssetItemKey))
.Cast<AssetItemKey>()
.Where(k => k.ToString().StartsWith("ChapterOneBoxPuzzlePiece"))
.ToHashSet();
[Header("Puzzle Setup")]
[SerializeField]
private CanvasGroup _openBoxCanvasGroup;
[Header("Slots")]
[SerializeField]
private List<PuzzleBoxSlot> _slots;
[SerializeField]
private bool _lockSlotsOnComplete = true;
private readonly Dictionary<string, Sprite> _spriteCache = new();
private bool _restoredFromSave;
private bool _sideEffectsFired;
public override string LevelName => "Puzzle Box";
public override string PuzzleID => PuzzleIdentifiers.AllPuzzles[PuzzleKey.WorkshopPuzzleBox];
public override Dictionary<int, BaseHint> Hints { get; }
public PuzzleBoxSlot SelectedPiece { get; private set; }
public bool IsCompleted { get; private set; }
public UniTask<byte[]> CaptureState()
{
var state = new WorkshopPuzzleBoxState
{
SlotPieceIDs = new AssetItemKey[_slots.Count],
Completed = IsCompleted,
SideEffectsFired = _sideEffectsFired
};
for (var i = 0; i < _slots.Count; i++)
state.SlotPieceIDs[i] = _slots[i].CurrentPieceID;
var payload = MemoryPackSerializer.Serialize(state);
return UniTask.FromResult(payload);
}
public async UniTask RestoreState(byte[] stateBlob)
{
Debug.Log($"[WorkshopBasePuzzleBox] Restoring state: {stateBlob}");
if (stateBlob == null || stateBlob.Length == 0)
{
await ShufflePieces();
UpdateInteractionLayers();
return;
}
var state = MemoryPackSerializer.Deserialize<WorkshopPuzzleBoxState>(stateBlob);
for (var i = 0; i < state.SlotPieceIDs.Length; i++)
await _slots[i].SetPiece(state.SlotPieceIDs[i], false);
IsCompleted = state.Completed;
_sideEffectsFired = state.SideEffectsFired;
if (IsCompleted && _lockSlotsOnComplete)
{
foreach (var slot in _slots)
slot.Lock();
}
if (IsCompleted)
{
SaveManager.SetLevelFlag(LevelFlag.WorkshopDownstairsDoorOpen, true, false);
SaveManager.SetPuzzleCompleted(PuzzleKey.WorkshopPuzzleBox, true, false);
}
UpdateInteractionLayers();
_restoredFromSave = true;
}
private void UpdateInteractionLayers()
{
var openEnabled = IsCompleted;
if (_openBoxCanvasGroup != null)
{
_openBoxCanvasGroup.interactable = openEnabled;
_openBoxCanvasGroup.blocksRaycasts = openEnabled;
if (openEnabled)
_openBoxCanvasGroup.GetComponent<PuzzleBoxInteractZone>().SetInteractableName();
}
foreach (var slot in _slots)
{
var group = slot.CanvasGroup;
if (group == null)
continue;
group.interactable = !openEnabled;
group.blocksRaycasts = !openEnabled;
}
}
public async UniTask<Sprite> GetSpriteForPiece(string pieceID)
{
if (_spriteCache.TryGetValue(pieceID, out var sprite))
return sprite;
if (!AssetRegistry.TryGetReference(pieceID, out var reference))
{
Debug.LogError($"[BoxPuzzle] Missing asset reference for {pieceID}");
return null;
}
sprite = await AddressableManager.LoadAssetAsync<Sprite>(reference);
if (sprite != null)
_spriteCache[pieceID] = sprite;
return sprite;
}
public void SelectPiece(PuzzleBoxSlot slot)
{
if (slot == null)
return;
if (SelectedPiece != null && SelectedPiece != slot)
SelectedPiece.SetSelected(false);
SelectedPiece = slot;
slot.SetSelected(true);
}
public async UniTask TrySwap(PuzzleBoxSlot other)
{
if (SelectedPiece == null || other == null)
return;
var first = SelectedPiece;
var second = other;
SelectedPiece = null;
first.SetSelected(false);
second.SetSelected(false);
if (first == second || first.IsLocked || second.IsLocked)
return;
await SwapSlots(first, second);
EvaluateLocks();
if (CheckCompleted() && !IsCompleted)
{
MarkAsCompleted();
await CompletePuzzle();
}
}
private async UniTask SwapSlots(PuzzleBoxSlot a, PuzzleBoxSlot b)
{
var pieceA = a.CurrentPieceID;
var pieceB = b.CurrentPieceID;
await UniTask.WhenAll(
a.SetPiece(pieceB, true),
b.SetPiece(pieceA, true)
);
}
private async UniTask ShufflePieces()
{
var ids = PuzzlePieceIDs.ToList();
var validShuffle = false;
while (!validShuffle)
{
for (var i = 0; i < ids.Count; i++)
{
var rand = Random.Range(i, ids.Count);
(ids[i], ids[rand]) = (ids[rand], ids[i]);
}
validShuffle = true;
for (var i = 0; i < _slots.Count; i++)
{
if (_slots[i].RequiredPieceID == ids[i])
{
validShuffle = false;
break;
}
}
}
for (var i = 0; i < _slots.Count; i++)
await _slots[i].SetPiece(ids[i], false);
}
private void EvaluateLocks()
{
foreach (var slot in _slots)
{
if (slot.IsCorrect())
slot.Lock();
}
}
private bool CheckCompleted()
{
return _slots.All(slot => slot.IsCorrect());
}
private void MarkAsCompleted(bool lockSlots = true)
{
if (IsCompleted)
return;
IsCompleted = true;
SaveManager.SetLevelFlag(LevelFlag.WorkshopDownstairsDoorOpen, true, false);
SaveManager.SetPuzzleCompleted(PuzzleKey.WorkshopPuzzleBox, true, false);
if (lockSlots && _lockSlotsOnComplete)
{
foreach (var slot in _slots)
slot.Lock();
}
UpdateInteractionLayers();
}
public override UniTask CompletePuzzle()
{
if (_sideEffectsFired)
return UniTask.CompletedTask;
_sideEffectsFired = true;
SaveManager.SetLevelFlag(LevelFlag.WorkshopDownstairsDoorOpen, true, false);
SaveManager.SetPuzzleCompleted(PuzzleKey.WorkshopPuzzleBox, true);
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.WorkshopPuzzleBoxUnlocked));
EventCoordinator.Publish(new UnlockAchievementEvent(AchievementID.WorkshopPuzzleBoxSolved));
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.DoorCreek));
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 5635d368ea404b5183611926e6a6cfcb
timeCreated: 1773330168

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 81caa7ed407f4737aae0a52a6b2f60f2
timeCreated: 1773330146

View File

@@ -1,281 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AYellowpaper.SerializedCollections;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Events.Progression;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Managers.Hints.Data;
using BriarQueen.Framework.Managers.Player.Data;
using BriarQueen.Framework.Services.Puzzles.Base;
using BriarQueen.Game.Items.Pickups.ChapterOne.Workshop;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
using Sprite = UnityEngine.Sprite;
namespace BriarQueen.Game.Puzzles.ChapterOne.Workshop.CandlePuzzle
{
[Serializable]
[MemoryPackable]
public partial struct CandlePuzzleState
{
public bool IsCompleted;
public string[] PlacedCandleIds;
public bool[] LockedSlots;
}
public class CandleBasePuzzle : BasePuzzle, IPuzzleStateful, IPuzzleWorldStateSync
{
[Header("Sprites")]
[SerializedDictionary("Candle Colour", "Sprite")]
[SerializeField]
private SerializedDictionary<Candle.CandleColour, Sprite> _candleSprites = new();
[Header("Slots")]
[SerializeField]
private List<CandleSlot> _candleSlots = new();
[SerializeField]
private bool _lockSlotsOnComplete = true;
public override string LevelName => "Candle Holder";
public override string PuzzleID => PuzzleIdentifiers.AllPuzzles[PuzzleKey.WorkshopCandlePuzzle];
public override Dictionary<int, BaseHint> Hints { get; } = new();
public bool IsCompleted { get; private set; }
public UniTask<byte[]> CaptureState()
{
var count = _candleSlots != null ? _candleSlots.Count : 0;
var placed = new string[count];
var locked = new bool[count];
for (var i = 0; i < count; i++)
{
var slot = _candleSlots[i];
if (slot == null)
{
placed[i] = null;
locked[i] = false;
continue;
}
placed[i] = slot.CurrentCandleID;
locked[i] = slot.IsLocked;
}
var payload = new CandlePuzzleState
{
IsCompleted = IsCompleted,
PlacedCandleIds = placed,
LockedSlots = locked
};
return UniTask.FromResult(MemoryPackSerializer.Serialize(payload));
}
public async UniTask RestoreState(byte[] state)
{
Debug.Log($"[CandlePuzzleBase] Restoring state: {state}");
if (state == null || state.Length == 0)
{
Debug.LogWarning("[CandlePuzzleBase] State is null or empty");
await RestoreFromWorldVariables();
return;
}
CandlePuzzleState payload;
try
{
payload = MemoryPackSerializer.Deserialize<CandlePuzzleState>(state);
}
catch (Exception ex)
{
Debug.LogWarning($"[CandlePuzzleBase] Failed to deserialize state: {ex}");
await RestoreFromWorldVariables();
return;
}
await RestoreSlotsAsync(payload);
}
public void SyncWorldStateToSave()
{
SyncWorldStateFromSlots();
}
public async UniTask NotifySlotChanged()
{
SyncWorldStateFromSlots();
await CheckPuzzleCompleted();
EventCoordinator.Publish(new RequestGameSaveEvent());
}
public async UniTask CheckPuzzleCompleted()
{
if (_candleSlots == null || _candleSlots.Count == 0)
{
IsCompleted = false;
return;
}
var allCorrect = _candleSlots.Where(s => s != null).All(s => s.CorrectlySet);
if (!IsCompleted && allCorrect)
{
IsCompleted = true;
SaveManager.SetLevelFlag(LevelFlag.WorkshopSafeUnlocked, true, false);
SaveManager.SetPuzzleCompleted(PuzzleKey.WorkshopCandlePuzzle, true, false);
if (_lockSlotsOnComplete)
{
foreach (var slot in _candleSlots)
{
if (slot != null)
slot.SetLocked(true);
}
}
await CompletePuzzle();
}
else
{
IsCompleted = allCorrect;
}
}
public override UniTask CompletePuzzle()
{
Debug.Log("[CandlePuzzleBase] Complete puzzle");
SaveManager.SetLevelFlag(LevelFlag.WorkshopSafeUnlocked, true, false);
SaveManager.SetPuzzleCompleted(PuzzleKey.WorkshopCandlePuzzle, true);
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.WorkshopSafeUnlocked));
EventCoordinator.Publish(new UnlockAchievementEvent(AchievementID.WorkshopSafeUnlocked));
return UniTask.CompletedTask;
}
private void SyncWorldStateFromSlots()
{
var save = SaveManager.CurrentSave;
if (save == null || save.PersistentVariables == null)
return;
save.PersistentVariables.Game.WorkshopCandleSlotsFilled ??= new Dictionary<int, string>();
var map = save.PersistentVariables.Game.WorkshopCandleSlotsFilled;
map.Clear();
foreach (var slot in _candleSlots)
{
if (slot == null)
continue;
var candleId = slot.CurrentCandleID;
if (!string.IsNullOrEmpty(candleId))
map[slot.SlotID] = candleId;
}
}
private async UniTask RestoreSlotsAsync(CandlePuzzleState payload)
{
var count = _candleSlots != null ? _candleSlots.Count : 0;
for (var i = 0; i < count; i++)
{
var slot = _candleSlots[i];
if (slot == null)
continue;
var candleId = payload.PlacedCandleIds != null && i < payload.PlacedCandleIds.Length
? payload.PlacedCandleIds[i]
: null;
var locked = payload.LockedSlots != null && i < payload.LockedSlots.Length && payload.LockedSlots[i];
ItemDataSo candleSo = null;
if (!string.IsNullOrEmpty(candleId))
candleSo = ItemRegistry.FindItemTemplateByID(candleId);
await slot.RestorePlacedCandle(candleSo, locked);
}
IsCompleted = payload.IsCompleted;
if (IsCompleted)
{
SaveManager.SetLevelFlag(LevelFlag.WorkshopSafeUnlocked, true, false);
SaveManager.SetPuzzleCompleted(PuzzleKey.WorkshopCandlePuzzle, true, false);
}
if (IsCompleted && _lockSlotsOnComplete)
{
foreach (var slot in _candleSlots)
{
if (slot != null)
slot.SetLocked(true);
}
}
}
private async UniTask RestoreFromWorldVariables()
{
Debug.Log("[CandlePuzzleBase] RestoreFromWorldVariables()");
var save = SaveManager.CurrentSave;
if (save == null || save.PersistentVariables == null)
return;
var map = save.PersistentVariables.Game.WorkshopCandleSlotsFilled;
if (map == null || map.Count == 0)
{
Debug.LogWarning("[CandlePuzzleBase] WorkshopCandleSlotsFilled is empty");
IsCompleted = false;
return;
}
foreach (var kvp in map)
Debug.Log($"[CandlePuzzleBase] World slot {kvp.Key} -> {kvp.Value}");
foreach (var slot in _candleSlots)
{
if (slot == null)
continue;
if (!map.TryGetValue(slot.SlotID, out var candleId) || string.IsNullOrEmpty(candleId))
{
await slot.RestorePlacedCandle(null, false);
continue;
}
Debug.Log($"Looking up candle template for ID: {candleId}");
var candleSo = ItemRegistry.FindItemTemplateByID(candleId);
if (candleSo == null)
Debug.LogError($"Candle template NOT FOUND for ID: {candleId}");
if (candleSo == null)
Debug.LogWarning($"[CandlePuzzleBase] Could not find candle template for ID '{candleId}'");
await slot.RestorePlacedCandle(candleSo, false);
}
await CheckPuzzleCompleted();
}
internal Sprite GetCandleSprite(Candle.CandleColour colour)
{
return _candleSprites != null && _candleSprites.TryGetValue(colour, out var sprite) ? sprite : null;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 74e3381d84864aafb2c4c1c4a90a84f4
timeCreated: 1772735921

View File

@@ -1,255 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
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 BriarQueen.Game.Items.Pickups.ChapterOne.Workshop;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Game.Puzzles.ChapterOne.Workshop.CandlePuzzle
{
public class CandleSlot : BaseItem
{
private static readonly HashSet<string> CandleIDs = new()
{
ItemIDs.Pickups[ItemKey.RedCandle],
ItemIDs.Pickups[ItemKey.OrangeCandle],
ItemIDs.Pickups[ItemKey.YellowCandle],
ItemIDs.Pickups[ItemKey.GreenCandle],
ItemIDs.Pickups[ItemKey.BlueCandle],
ItemIDs.Pickups[ItemKey.IndigoCandle],
ItemIDs.Pickups[ItemKey.VioletCandle]
};
private static readonly Dictionary<ItemKey, Candle.CandleColour> CandleKeyToColour = new()
{
{ ItemKey.RedCandle, Candle.CandleColour.Red },
{ ItemKey.OrangeCandle, Candle.CandleColour.Orange },
{ ItemKey.YellowCandle, Candle.CandleColour.Yellow },
{ ItemKey.GreenCandle, Candle.CandleColour.Green },
{ ItemKey.BlueCandle, Candle.CandleColour.Blue },
{ ItemKey.IndigoCandle, Candle.CandleColour.Indigo },
{ ItemKey.VioletCandle, Candle.CandleColour.Violet }
};
[SerializeField]
private CandleBasePuzzle _basePuzzleOwner;
[Header("Visuals")]
[SerializeField]
private Image _candleImage;
[Header("Rules")]
[SerializeField]
private ItemKey _requiredCandleID;
[SerializeField]
private int _slotID;
[SerializeField]
private bool _preventRemovingCorrectCandle = true;
private CancellationTokenSource _cancellationTokenSource;
[SerializeField]
private ItemDataSo _filledCandle;
private Sequence _sequence;
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Interact;
public override string InteractableName => IsEmpty ? "Empty Holder" : _filledCandle.ItemName;
private bool IsEmpty => _filledCandle == null;
public int SlotID => _slotID;
public string CurrentCandleID => _filledCandle != null ? _filledCandle.UniqueID : null;
public bool CorrectlySet => !IsEmpty && _filledCandle.ItemKey == _requiredCandleID;
public bool IsLocked { get; private set; }
public void SetLocked(bool locked)
{
IsLocked = locked;
}
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (IsLocked)
return;
if (_preventRemovingCorrectCandle && CorrectlySet)
return;
if (!CheckEmptyHands())
return;
if (item == null)
{
if (IsEmpty)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(EnvironmentInteractKey.FindCandle)));
return;
}
await PickupCandle();
await _basePuzzleOwner.NotifySlotChanged();
return;
}
if (!IsCandle(item))
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(EnvironmentInteractKey.DoesntBelong)));
return;
}
if (!IsEmpty)
await PickupCandle();
await PlaceCandle(item);
await _basePuzzleOwner.NotifySlotChanged();
}
public async UniTask RestorePlacedCandle(ItemDataSo candleSo, bool locked)
{
IsLocked = locked;
EnsureCanvasGroup();
CancelTweenIfRunning();
_filledCandle = candleSo;
if (_candleImage != null)
_candleImage.sprite = _filledCandle != null ? GetSpriteForCandle(_filledCandle) : null;
_canvasGroup.alpha = _filledCandle != null ? 1f : 0f;
await UniTask.CompletedTask;
}
private async UniTask PlaceCandle(ItemDataSo candleSo)
{
if (candleSo == null) return;
EnsureCanvasGroup();
CancelTweenIfRunning();
_filledCandle = candleSo;
if (_candleImage != null)
_candleImage.sprite = GetSpriteForCandle(_filledCandle);
_canvasGroup.alpha = 0f;
_sequence = Sequence.Create().Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
{
startValue = _canvasGroup.alpha,
endValue = 1f,
settings = new TweenSettings { duration = 0.3f }
}));
_cancellationTokenSource = new CancellationTokenSource();
try
{
await _sequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
DisposeCts();
}
PlayerManager.RemoveItem(candleSo);
}
private async UniTask PickupCandle()
{
if (IsEmpty) return;
EnsureCanvasGroup();
CancelTweenIfRunning();
_sequence = Sequence.Create().Group(Tween.Alpha(_canvasGroup, new TweenSettings<float>
{
startValue = _canvasGroup.alpha,
endValue = 0f,
settings = new TweenSettings { duration = 0.3f }
}));
_cancellationTokenSource = new CancellationTokenSource();
try
{
await _sequence.ToUniTask(cancellationToken: _cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
return;
}
finally
{
DisposeCts();
}
PlayerManager.CollectItem(_filledCandle);
_filledCandle = null;
if (_candleImage != null)
_candleImage.sprite = null;
}
private void EnsureCanvasGroup()
{
if (_canvasGroup == null)
_canvasGroup = GetComponent<CanvasGroup>();
}
private void CancelTweenIfRunning()
{
if (_sequence.isAlive)
_sequence.Complete();
DisposeCts();
}
private void DisposeCts()
{
if (_cancellationTokenSource == null) return;
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
}
private Sprite GetSpriteForCandle(ItemDataSo candle)
{
if (candle == null || _basePuzzleOwner == null)
{
Debug.Log($"[Candle Puzzle] Candle is null.");
return null;
}
if (CandleKeyToColour.TryGetValue(candle.ItemKey, out var colour))
{
Debug.Log($"[Candle Puzzle] {candle.ItemKey} : {colour}");
return _basePuzzleOwner.GetCandleSprite(colour);
}
// fallback if candle ID not found
return _basePuzzleOwner.GetCandleSprite(Candle.CandleColour.Red);
}
private bool IsCandle(ItemDataSo itemSo)
{
return itemSo != null && CandleIDs.Contains(itemSo.UniqueID);
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 8cdb672cabe34a5c820eef6dfebfef72
timeCreated: 1772736131