Additional Levels added. Mostly just base artwork so far.

This commit is contained in:
2026-03-26 21:31:29 +00:00
parent 644b56282e
commit b3b569e98f
131 changed files with 6151 additions and 6008 deletions

View File

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

View File

@@ -1,7 +1,295 @@
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.UI;
using BriarQueen.Framework.Managers.Hints.Data;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Services.Puzzles.Base;
using BriarQueen.Game.Levels.ChapterOne.LaxleyHouse.FireplaceLockbox;
using Cysharp.Threading.Tasks;
using MemoryPack;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;
namespace BriarQueen.Game.Levels.ChapterOne.LaxleyHouse
{
public class FireplaceLockbox
[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 FadeEvent(true, 0.5f));
}
private async UniTask OpenLockbox()
{
if (_backgroundImage != null)
_backgroundImage.sprite = _lockBoxOpenSprite;
await GenerateLockboxItems();
}
private async UniTask GenerateLockboxItems()
{
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;
}
else
{
await DestructionService.Destroy(item.gameObject);
}
}
}
public UniTask<byte[]> CaptureState()
{
var state = new FireplaceLockboxPuzzleState
{
SolutionSlotOne = _solutionSlotOne,
SolutionSlotTwo = _solutionSlotTwo,
SolutionSlotThree = _solutionSlotThree,
SlotOneValue = _lockboxSlots.Count > 0 ? _lockboxSlots[0].CurrentValue : MinDigit,
SlotTwoValue = _lockboxSlots.Count > 1 ? _lockboxSlots[1].CurrentValue : MinDigit,
SlotThreeValue = _lockboxSlots.Count > 2 ? _lockboxSlots[2].CurrentValue : MinDigit
};
return UniTask.FromResult(MemoryPackSerializer.Serialize(state));
}
public UniTask RestoreState(byte[] state)
{
if (state == null || state.Length == 0)
{
GenerateSolutionIfNeeded();
ApplyRandomDefaultSlotValues();
ApplySolutionToCompletionCheck();
if (SaveManager.GetLevelFlag(LevelFlag.LaxleyLockboxOpened))
DisableSlots();
return UniTask.CompletedTask;
}
var restored = MemoryPackSerializer.Deserialize<FireplaceLockboxPuzzleState>(state);
_solutionSlotOne = WrapDigit(restored.SolutionSlotOne);
_solutionSlotTwo = WrapDigit(restored.SolutionSlotTwo);
_solutionSlotThree = WrapDigit(restored.SolutionSlotThree);
if (_lockboxSlots.Count > 0)
_lockboxSlots[0].ApplyValueImmediate(restored.SlotOneValue);
if (_lockboxSlots.Count > 1)
_lockboxSlots[1].ApplyValueImmediate(restored.SlotTwoValue);
if (_lockboxSlots.Count > 2)
_lockboxSlots[2].ApplyValueImmediate(restored.SlotThreeValue);
ApplySolutionToCompletionCheck();
if (SaveManager.GetLevelFlag(LevelFlag.LaxleyLockboxOpened))
DisableSlots();
return UniTask.CompletedTask;
}
private void InitializeSlots()
{
if (_lockboxSlots == null)
return;
foreach (var slot in _lockboxSlots)
{
if (slot == null)
continue;
slot.Initialize(GetNumberSprite, OnSlotValueChanged);
}
}
private void DisableSlots()
{
if (_lockboxSlots == null)
return;
foreach (var slot in _lockboxSlots)
{
if (slot == null)
continue;
slot.gameObject.SetActive(false);
}
}
private void ApplyRandomDefaultSlotValues()
{
if (_lockboxSlots.Count > 0)
_lockboxSlots[0].ApplyValueImmediate(GetRandomDigit());
if (_lockboxSlots.Count > 1)
_lockboxSlots[1].ApplyValueImmediate(GetRandomDigit());
if (_lockboxSlots.Count > 2)
_lockboxSlots[2].ApplyValueImmediate(GetRandomDigit());
}
private int GetRandomDigit()
{
return Random.Range(MinDigit, MaxDigit + 1);
}
private Sprite GetNumberSprite(int value)
{
value = WrapDigit(value);
return _numberSprites.TryGetValue(value, out var sprite) ? sprite : null;
}
private void OnSlotValueChanged()
{
ApplySolutionToCompletionCheck();
if (!SaveManager.GetLevelFlag(LevelFlag.LaxleyLockboxOpened) && IsCurrentInputCorrect())
CompletePuzzle().Forget();
}
private void ApplySolutionToCompletionCheck()
{
IsCompleted = IsCurrentInputCorrect();
}
private bool IsCurrentInputCorrect()
{
if (_lockboxSlots == null || _lockboxSlots.Count < 3)
return false;
return _lockboxSlots[0].CurrentValue == _solutionSlotOne &&
_lockboxSlots[1].CurrentValue == _solutionSlotTwo &&
_lockboxSlots[2].CurrentValue == _solutionSlotThree;
}
private void GenerateSolutionIfNeeded()
{
if (HasValidSolution())
return;
GenerateSolutionVariation();
}
private bool HasValidSolution()
{
return _solutionSlotOne is >= MinDigit and <= MaxDigit &&
_solutionSlotTwo is >= MinDigit and <= MaxDigit &&
_solutionSlotThree is >= MinDigit and <= MaxDigit;
}
private void GenerateSolutionVariation()
{
int[] digits = { 8, 4, 7 };
for (int i = digits.Length - 1; i > 0; i--)
{
int swapIndex = Random.Range(0, i + 1);
(digits[i], digits[swapIndex]) = (digits[swapIndex], digits[i]);
}
_solutionSlotOne = digits[0];
_solutionSlotTwo = digits[1];
_solutionSlotThree = digits[2];
}
private int WrapDigit(int value)
{
value %= MaxDigit;
if (value <= 0)
value += MaxDigit;
return value;
}
}
}

View File

@@ -1,7 +1,40 @@
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Game.Items.Environment.General;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Game.Levels.ChapterOne.LaxleyHouse
{
public class LaxleyFireplace
public class LaxleyFireplace : BaseLevel
{
[Header("Fireplace")]
[SerializeField]
private Image _background;
[SerializeField]
private Sprite _firePlaceExtinguishedSprite;
[SerializeField]
private Fire _fireplaceFire;
protected override async UniTask PostLoadInternal()
{
bool fireExtinguished = SaveManager.GetLevelFlag(LevelFlag.LaxleyFireplaceExtinguished);
if (fireExtinguished)
{
await ExtinguishFire();
}
}
private async UniTask ExtinguishFire()
{
_background.sprite = _firePlaceExtinguishedSprite;
await DestructionService.Destroy(_fireplaceFire.gameObject);
}
}
}

View File

@@ -26,6 +26,7 @@ namespace BriarQueen.Game.Levels.ChapterOne.VillageStreet
[Header("Zones")]
[SerializeField]
private StreetVines _vinesZone;
[SerializeField]
private InteractZone _plaqueZone;
@@ -35,8 +36,8 @@ namespace BriarQueen.Game.Levels.ChapterOne.VillageStreet
protected override UniTask PostLoadInternal()
{
bool gateOpen = SaveManager.GetLevelFlag(LevelFlag.StreetGateOpen);
bool vinesCut = SaveManager.GetLevelFlag(LevelFlag.StreetVinesCut) || gateOpen;
bool gateOpen = SaveManager.GetLevelFlag(LevelFlag.VillageStreetGateOpen);
bool vinesCut = SaveManager.GetLevelFlag(LevelFlag.VillageStreetVinesCut) || gateOpen;
if (gateOpen)
{
@@ -57,11 +58,12 @@ namespace BriarQueen.Game.Levels.ChapterOne.VillageStreet
await UniTask.Delay(TimeSpan.FromSeconds(1));
EventCoordinator.Publish(new FadeEvent(false, 0.8f));
await UniTask.Delay(TimeSpan.FromSeconds(0.8f));
_background.sprite = _vinesCutSprite;
SetZoneState(_plaqueZone, true);
SaveManager.SetLevelFlag(LevelFlag.StreetVinesCut, true);
await DeactivateVines();
SaveManager.SetLevelFlag(LevelFlag.VillageStreetVinesCut, true);
EventCoordinator.Publish(new FadeEvent(true, 0.8f));
}
@@ -69,9 +71,9 @@ namespace BriarQueen.Game.Levels.ChapterOne.VillageStreet
{
SetZoneState(_plaqueZone, vinesCut);
SetZoneState(_gateZone, gateOpen);
if(vinesCut)
DeactivateVines();
if (vinesCut)
DeactivateVines().Forget();
}
private void SetZoneState(InteractZone zone, bool active)
@@ -84,16 +86,9 @@ namespace BriarQueen.Game.Levels.ChapterOne.VillageStreet
zone.CanvasGroup.interactable = active;
}
private void DeactivateVines()
private async UniTask DeactivateVines()
{
var canvasGroup = _vinesZone.CanvasGroup;
if (!canvasGroup)
return;
canvasGroup.alpha = 0;
canvasGroup.blocksRaycasts = false;
canvasGroup.interactable = false;
await DestructionService.Destroy(_vinesZone.gameObject);
}
}
}