Restructured for new direction.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb3430590f19429d8fc83b83cd519f9f
|
||||
timeCreated: 1778242706
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9afe762158da4f8c8bb8a1bcb98d62b1
|
||||
timeCreated: 1778242706
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bc3c6697e5b4c029e807116f930e9a3
|
||||
timeCreated: 1778247353
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 701653a82bc1487d8aff6ea83a9abaeb
|
||||
timeCreated: 1778243473
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de15a1ddef77473cb145cd59500cde71
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74c68ae5d328428f87e9fb87424df340
|
||||
timeCreated: 1778242706
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a35c6a665b664f5b89e806b13ef62510
|
||||
timeCreated: 1773930345
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6d0351ea34c4c918d254f72ac186173
|
||||
timeCreated: 1773930345
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40ab281620b846b3b4798a035abae950
|
||||
timeCreated: 1773930669
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52594bb963824a15b8916933f552312d
|
||||
timeCreated: 1773933231
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6edb1657de0f4759b5563de3dd625799
|
||||
timeCreated: 1774694628
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 123b22f5870243e5ba1fdbcda4bbabde
|
||||
timeCreated: 1774694641
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38875deca26847619404eb281c98f21c
|
||||
timeCreated: 1774694667
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7162a53d3fc7482389473ef98dbb6f11
|
||||
timeCreated: 1774694726
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 86f2d2fa28bf4e8aa2e5afdf00da6dca
|
||||
timeCreated: 1774694771
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34468c31870143d18f90a5c285f282f9
|
||||
timeCreated: 1774695987
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f8d8ac8596d463daab5ce2a1a87f08b
|
||||
timeCreated: 1774694641
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f90b81f7232344f3b46f2470cc93c933
|
||||
timeCreated: 1774694641
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57793604ae2648118b1911c5b1fe3f75
|
||||
timeCreated: 1774554976
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c4c81a879c94cb9a2eeafcd454d55e4
|
||||
timeCreated: 1774555158
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 452ffcf5991344cf992d50e7b54e31df
|
||||
timeCreated: 1772735921
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 687d55dc611140ada4fd8af70e83d7d9
|
||||
timeCreated: 1773330168
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e93f8f355d8249f099cd3d446b40890a
|
||||
timeCreated: 1773335140
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5635d368ea404b5183611926e6a6cfcb
|
||||
timeCreated: 1773330168
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81caa7ed407f4737aae0a52a6b2f60f2
|
||||
timeCreated: 1773330146
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74e3381d84864aafb2c4c1c4a90a84f4
|
||||
timeCreated: 1772735921
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8cdb672cabe34a5c820eef6dfebfef72
|
||||
timeCreated: 1772736131
|
||||
Reference in New Issue
Block a user