First commit for private source control. Older commits available on Github.

This commit is contained in:
2026-03-26 12:52:52 +00:00
parent a04c602626
commit 2d449c4a17
2176 changed files with 408185 additions and 0 deletions

View File

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

View File

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

View File

@@ -0,0 +1,185 @@
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.StreetGateOpen, 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.StreetGateOpen);
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.StreetGateOpen, 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

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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

View File

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

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