Restructured for new direction.

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

37
Assets/Scripts/AGENTS.md Normal file
View File

@@ -0,0 +1,37 @@
# A Fairytale Gone Bad: The Briar Queen
Unity based Point and Click Adventure.
## Summary
The Briar Queen is the first entry in a series of horror/thriller based Point and Click Adventure.
It revolves around our central heroine, who's returned to her birthplace after having been adopted 20 years prior.
But not all is what she thought, she was expecting a wondrous and vibrant land of mysticism and magicks, but instead
she comes across a desolate land of despair and terror. Vines have overgrown the villages, houses sit abandoned, and
the streets desolate. Her task (and the players) is to discover what happened to the lands once known as prosperous
and wonderful, and perhaps, just perhaps, discover herself in the process.
## Gameplay Design
1. **Canvas** - Our game is entirely canvas based and pre-drawn artwork. No 3D Objects or 2D objects. Users interact by clicking on objects, solving puzzles.
2. **Puzzles** - We use Resident-Evil styled puzzles which can be composed of as many parts as necessary. Items get picked up and used to complete or interact with these puzzles.
3. **Registries** - Scriptable Objects should live in a registry of somekind. Asset References, Audio, etc.
4. **Braces required on all control flow**`if`, `else`, `for`, `foreach`, `while`, `do`, `switch` must always have braces, including two-line statements.
5. **Naming**`_camelCase` private fields, `PascalCase` properties/methods/classes.
6. **Player** - We consider everything from the player's point of view. How Audio is going to sound or how a puzzle plays out.
7. **UniTask** - Any async-capable code should be put into UniTasks and awaited. See below for documentation link.
8. **PrimeTween** - Tweening is done using the PrimeTween Library. Each class that has a Sequence should also have a cancellation token.
9. **Tokens** - Cancellation Tokens should be re-used where possible.
10. **Versions** - We're using C# 9 with Unity 6.3
## Documentation
UniTask - https://github.com/Cysharp/UniTask
PrimeTween - https://github.com/KyryloKuzyk/PrimeTween
## AI Direction
1. Never just apply code. Always present it to me first so I can review it. Prefer full classes over simple methods so I can get the full context.
2. I am autisic, so please present explanations for any code changes, what it'll fix or achieve, and why we should do it.
3.

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f3851666fa8045b6985cd88cbc091a30
timeCreated: 1777915419

BIN
Assets/Scripts/Archive.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2424908ceda2f438a98e74be97301a50
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -15,6 +15,9 @@ namespace BriarQueen.Data.IO.Saves
{
public string SaveVersion = "0.0.2-alpha";
public string SaveFileName;
// Key Unlocks
public bool CodexUnlocked = false;
// Inventory & item tracking
public List<ItemSaveData> InventoryData = new();
@@ -95,22 +98,7 @@ namespace BriarQueen.Data.IO.Saves
public enum LevelFlag
{
None = 0,
FountainVinesCut,
PumpHouseOpened,
PumpHousePipesFixed,
PumpWaterRestored,
WorkshopBagHoleDug,
WorkshopSafeUnlocked,
WorkshopDownstairsDoorOpen,
WorkshopDownstairsLightOn,
WorkshopGrindstoneRepaired,
VillageStreetGateOpen,
VillageStreetVinesCut,
LaxleyFireplaceExtinguished,
LaxleyLockboxOpened,
LaxleyClockSolved,
LaxleyHourHandRetrieved,
LaxleyMinuteHandRetrieved,
MarketGateOpen,
}
[Serializable]
@@ -123,14 +111,6 @@ namespace BriarQueen.Data.IO.Saves
// Tracks completed puzzles
public Dictionary<string, bool> PuzzleCompleted = new();
// Candle slots
public Dictionary<int, string> WorkshopCandleSlotsFilled = new()
{
{ 0, ItemIDs.Pickups[ItemKey.BlueCandle] },
{ 3, ItemIDs.Pickups[ItemKey.OrangeCandle] },
{ 5, ItemIDs.Pickups[ItemKey.RedCandle] }
};
// -------- Helper Methods --------
public bool IsPuzzleCompleted(PuzzleKey puzzle)
{

View File

@@ -24,56 +24,16 @@ namespace BriarQueen.Data.Identifiers
public enum LevelKey
{
None = 0,
ChapterOneVillageEdge,
ChapterOneVillage,
ChapterOneVillageFurther,
ChapterOnePumphouse,
ChapterOneFountain,
ChapterOneWorkshop,
ChapterOnePumphousePipes,
ChapterOneWorkshopDrawer,
ChapterOneWorkshopUpstairs,
ChapterOneWorkshopCandlePuzzle,
ChapterOneWorkshopJewelleryBox,
ChapterOneWorkshopJewelleryBoxOpen,
ChapterOneWorkshopSafe,
ChapterOneWorkshopBag,
ChapterOneWorkshopDownstairs,
ChapterOnePumphouseTable,
ChapterOneWorkshopBookcase,
ChapterOneFountainPuzzle,
ChapterOneStreetGateSign,
ChapterOneLaxleyHouse,
ChapterOneLaxleyHouseClock,
ChapterOneLaxleyHouseClockPuzzle,
ChapterOneLaxleyHouseTable,
ChapterOneLaxleyHouseUpstairs,
ChapterOneLaxleyHouseFireplace,
ChapterOneVillageMarketSquare,
ChapterOneVillageMarketSquareStatue,
ChapterOneVillageMarketSquareFirepit,
ChapterOneVillageEnd,
ChapterOneVillageEndChurch,
ChapterOneLaxleyHouseLockbox,
ChapterOneLaxleyHouseUpstairsStudy,
ChapterOneLaxleyUpstairsPortrait,
ChapterOneLaxleyUpstairsTable,
ChapterOneLaxleyUpstairsStorage,
ChapterOneStreetCart,
ChapterOneArrivalRoad,
ChapterOneAshwickRidgeway,
ChapterOneInsideBrokenDownCar,
ChapterOneAshwickMarketplace,
ChapterOneHarrowVale,
}
public enum AssetItemKey
{
None = 0,
ChapterOneBoxPuzzlePiece1,
ChapterOneBoxPuzzlePiece2,
ChapterOneBoxPuzzlePiece3,
ChapterOneBoxPuzzlePiece4,
ChapterOneBoxPuzzlePiece5,
ChapterOneBoxPuzzlePiece6,
ChapterOneBoxPuzzlePiece7,
ChapterOneBoxPuzzlePiece8,
ChapterOneWorkshopWornBook
}
public static class AssetKeyIdentifiers

View File

@@ -11,17 +11,8 @@ namespace BriarQueen.Data.Identifiers
public enum SFXKey
{
None = 0,
WorkshopSafeUnlocked,
WorkshopPuzzleBoxUnlocked,
DoorCreek,
ItemCollected,
GateOpening,
PuzzleIncorrect,
ResetPuzzle,
SharpenKnife,
LockBoxNumberReel,
LockboxOpening,
ClockOpening,
CarDoorOpening,
ItemPickup,
}
public enum UIFXKey
@@ -48,32 +39,23 @@ namespace BriarQueen.Data.Identifiers
new Dictionary<MusicKey, string>
{
// Add when you have music
// { MusicKey.SomeTrack, "MUSIC_SomeTrack" }
// { MusicKey.SomeTrack, "Music:SomeTrack" }
});
public static readonly IReadOnlyDictionary<SFXKey, string> SFX =
new ReadOnlyDictionary<SFXKey, string>(
new Dictionary<SFXKey, string>
{
{ SFXKey.WorkshopSafeUnlocked, "SFX_WorkshopSafeUnlocked" },
{ SFXKey.WorkshopPuzzleBoxUnlocked, "SFX_WorkshopPuzzleBoxUnlocked" },
{ SFXKey.DoorCreek, "SFX_DoorCreek" },
{ SFXKey.ItemCollected, "SFX_ItemCollected" },
{ SFXKey.GateOpening, "SFX_GateOpening" },
{ SFXKey.PuzzleIncorrect, "SFX_PuzzleIncorrect" },
{ SFXKey.ResetPuzzle, "SFX_ResetPuzzle" },
{ SFXKey.SharpenKnife, "SFX_SharpenKnife"},
{ SFXKey.LockBoxNumberReel, "SFX_LockBoxNumberReel" },
{ SFXKey.LockboxOpening, "SFX_LockboxOpening" },
{ SFXKey.ClockOpening, "SFX_ClockOpening" },
{ SFXKey.CarDoorOpening, "SFX:CarDoorOpening" },
{ SFXKey.ItemPickup, "SFX:ItemPickup" },
});
public static readonly IReadOnlyDictionary<UIFXKey, string> UIFX =
new ReadOnlyDictionary<UIFXKey, string>(
new Dictionary<UIFXKey, string>
{
{ UIFXKey.AchievementUnlocked, "UIFX_AchievementUnlocked" },
{ UIFXKey.CodexEntryUnlocked, "UIFX_CodexEntryUnlocked" },
{ UIFXKey.AchievementUnlocked, "UIFX:AchievementUnlocked" },
{ UIFXKey.CodexEntryUnlocked, "UIFX:CodexEntryUnlocked" },
});
public static readonly IReadOnlyDictionary<AmbienceKey, string> Ambience =
@@ -115,4 +97,4 @@ namespace BriarQueen.Data.Identifiers
return Voice.TryGetValue(key, out var value) ? value : string.Empty;
}
}
}
}

View File

@@ -3,20 +3,16 @@ namespace BriarQueen.Data.Identifiers
public static class AudioMixerParameters
{
public const string MASTER_VOLUME = "Master_Volume";
public const string MUSIC_VOLUME = "Music_Volume";
public const string SFX_VOLUME = "SFX_Volume";
public const string AMBIENCE_VOLUME = "Ambience_Volume";
public const string VOICE_VOLUME = "Voice_Volume";
public const string UI_VOLUME = "UI_Volume";
public const string MUSIC_VOLUME = "Music_Volume";
public const string SFX_VOLUME = "SFX_Volume";
public const string VOICE_VOLUME = "Voice_Volume";
}
public static class AudioMixerGroups
{
public const string MASTER_GROUP = "Master";
public const string MUSIC_GROUP = "Music";
public const string SFX_GROUP = "SFX";
public const string AMBIENCE_GROUP = "Ambience";
public const string VOICE_GROUP = "Voice";
public const string UI_GROUP = "UI";
public const string MUSIC_GROUP = "Music";
public const string SFX_GROUP = "SFX";
public const string VOICE_GROUP = "Voice";
}
}

View File

@@ -3,62 +3,52 @@ using System.Collections.ObjectModel;
namespace BriarQueen.Data.Identifiers
{
public enum BookEntryID
public enum DocumentEntryID
{
None = 0,
WorkshopDiary = 1,
LaxleyHouseBillOfSale = 2,
GranddfatherClockPlaque = 3
C1CarNewspaper,
}
public enum ClueEntryID
{
None = 0,
WorkshopBookshelfClue = 1,
WorkshopRainbowClue = 2,
PumphouseScratchedTable = 3,
StreetGatePlaque = 4,
GranddfatherClockPlaqueClue = 5
AshwickMarketGate,
}
public enum PhotoEntryID
{
None = 0,
WorkshopFadedPhoto = 1
}
public static class CodexEntryIDs
{
public static readonly IReadOnlyDictionary<BookEntryID, string> Books =
new ReadOnlyDictionary<BookEntryID, string>(
new Dictionary<BookEntryID, string>
private const string DOCUMENT_PREFIX = "Codex:Document:";
private const string CLUE_PREFIX = "Codex:Clue:";
private const string PHOTO_PREFIX = "Codex:Photo:";
public static readonly IReadOnlyDictionary<DocumentEntryID, string> Documents =
new ReadOnlyDictionary<DocumentEntryID, string>(
new Dictionary<DocumentEntryID, string>
{
{ BookEntryID.WorkshopDiary, "BOOK_WorkshopDiary" },
{ BookEntryID.LaxleyHouseBillOfSale, "BOOK_LaxleyHouseBillOfSale" },
{ BookEntryID.GranddfatherClockPlaque, "BOOK_GranddfatherClockPlaque" },
{ DocumentEntryID.C1CarNewspaper, GetDocumentIdentifier(DocumentEntryID.C1CarNewspaper) },
});
public static readonly IReadOnlyDictionary<ClueEntryID, string> Clues =
new ReadOnlyDictionary<ClueEntryID, string>(
new Dictionary<ClueEntryID, string>
{
{ ClueEntryID.WorkshopBookshelfClue, "CLUE_WorkshopBookshelf" },
{ ClueEntryID.WorkshopRainbowClue , "CLUE_WorkshopRainbow" },
{ ClueEntryID.PumphouseScratchedTable, "CLUE_PumphouseScratchedTable" },
{ ClueEntryID.StreetGatePlaque, "CLUE_StreetGatePlaque" },
{ ClueEntryID.GranddfatherClockPlaqueClue, "CLUE_GranddfatherClockPlaque" },
{ ClueEntryID.AshwickMarketGate, $"{PHOTO_PREFIX}:AshwickMarketGate" },
});
public static readonly IReadOnlyDictionary<PhotoEntryID, string> Photos =
new ReadOnlyDictionary<PhotoEntryID, string>(
new Dictionary<PhotoEntryID, string>
{
{ PhotoEntryID.WorkshopFadedPhoto, "PHOTO_WorkshopFadedPhoto" }
});
public static string Get(BookEntryID id)
public static string Get(DocumentEntryID id)
{
return Books.TryGetValue(id, out var value) ? value : string.Empty;
return Documents.TryGetValue(id, out var value) ? value : string.Empty;
}
public static string Get(ClueEntryID id)
@@ -70,5 +60,20 @@ namespace BriarQueen.Data.Identifiers
{
return Photos.TryGetValue(id, out var value) ? value : string.Empty;
}
private static string GetDocumentIdentifier(DocumentEntryID id)
{
return $"{DOCUMENT_PREFIX}{id}";
}
private static string GetClueIdentifier(ClueEntryID id)
{
return $"{CLUE_PREFIX}{id}";
}
private static string GetPhotoIdentifier(PhotoEntryID id)
{
return $"{PHOTO_PREFIX}{id}";
}
}
}
}

View File

@@ -3,7 +3,7 @@ namespace BriarQueen.Data.Identifiers
public enum CodexType
{
None = 0,
BookEntry = 1,
DocumentEntry = 1,
PuzzleClue = 2,
Photo = 3
}

View File

@@ -8,53 +8,30 @@ namespace BriarQueen.Data.Identifiers
None = 0,
EmptyHands = 1,
CantUseItem = 2,
RustyKnife = 3,
SomethingMissing = 4,
PliersSnapped = 5,
CarefulInteract = 6,
RagFallsApart = 7,
LooksImportant = 8,
WrongTool = 9,
CollectEndlessGoblets = 10,
SomethingMissing = 3,
CarefulInteract = 4,
LooksImportant = 5,
WrongTool = 6,
CodexLocked = 7
}
public enum LevelInteractKey
{
None = 0,
WaterValve = 1,
ClearVinesOutside = 2,
PumphouseChain = 3,
CutVines = 4,
WorkshopLockedSafe = 5,
UnlockedPumphouse = 6,
}
public enum EnvironmentInteractKey
{
None = 0,
BrokenLantern = 1,
WorkshopWriting = 2,
UseGrindstone = 3,
WorkshopBookDisintegrating = 4,
UsingKnife = 5,
AlreadySharpened = 6,
Locked = 7,
CantGoThere = 8,
DirtyWindow = 9,
WorkshopBagNoItems = 10,
FindCandle = 11,
DoesntBelong = 12,
SharpGlass = 13,
FreshAndCoolWater = 14,
WorkshopBooks = 15,
PumpTurnOn = 16,
FireHot = 17,
ExtinguishFire = 18,
CauldronBoiledAway = 19,
LaxleyHouseBrokenClock = 20,
LaxleyGrandfatherClockMissingBothHands = 21,
LaxleyGrandfatherClockMissingHourHand = 22,
LaxleyGrandfatherClockMissingMinuteHand = 23,
Locked = 1,
CantGoThere = 2,
DoesntBelong = 3,
FireHot = 4,
AshwickHallowSign = 5,
AshwickRidgewayStatue = 6,
AshwickBlockedRidgwayRoad = 7,
AshwickRidgewaySkeleton = 8,
AskwickMarketplaceSign = 9,
}
public enum UIInteractKey
@@ -71,14 +48,11 @@ namespace BriarQueen.Data.Identifiers
{
{ ItemInteractKey.EmptyHands, "My hands are too full for that." },
{ ItemInteractKey.CantUseItem, "That wont work here." },
{ ItemInteractKey.RustyKnife, "Too dull to be of any use." },
{ ItemInteractKey.SomethingMissing, "Something isnt right." },
{ ItemInteractKey.PliersSnapped, "The pliers snap. Thats the end of them." },
{ ItemInteractKey.CarefulInteract, "I should take care with this." },
{ ItemInteractKey.RagFallsApart, "It fell apart in my hands." },
{ ItemInteractKey.LooksImportant, "This feels important." },
{ ItemInteractKey.WrongTool, "This isnt the right tool." },
{ ItemInteractKey.CollectEndlessGoblets, "Faint symbols coil across the goblets surface." },
{ ItemInteractKey.CodexLocked, "I have nowhere to put that." },
});
@@ -86,41 +60,19 @@ namespace BriarQueen.Data.Identifiers
new ReadOnlyDictionary<LevelInteractKey, string>(
new Dictionary<LevelInteractKey, string>
{
{ LevelInteractKey.WaterValve, "The water is already flowing." },
{ LevelInteractKey.ClearVinesOutside, "The vines still block the way outside." },
{ LevelInteractKey.PumphouseChain, "Its locked by something more than rust." },
{ LevelInteractKey.CutVines, "These wont give way by hand." },
{ LevelInteractKey.WorkshopLockedSafe, "Sealed tight." },
{ LevelInteractKey.UnlockedPumphouse, "The lock gives with a dull click." },
});
public static readonly IReadOnlyDictionary<EnvironmentInteractKey, string> EnvironmentInteractions =
new ReadOnlyDictionary<EnvironmentInteractKey, string>(
new Dictionary<EnvironmentInteractKey, string>
{
{ EnvironmentInteractKey.BrokenLantern, "Beyond repair." },
{ EnvironmentInteractKey.WorkshopWriting, "At least it isnt blood." },
{ EnvironmentInteractKey.UseGrindstone, "This could still sharpen an edge." },
{ EnvironmentInteractKey.WorkshopBookDisintegrating, "It crumbles at a touch." },
{ EnvironmentInteractKey.UsingKnife, "Careful… one slip." },
{ EnvironmentInteractKey.AlreadySharpened, "That edge will do." },
{ EnvironmentInteractKey.Locked, "Locked." },
{ EnvironmentInteractKey.Locked, "It's locked." },
{ EnvironmentInteractKey.CantGoThere, "No way through." },
{ EnvironmentInteractKey.DirtyWindow, "Nothing visible through the grime." },
{ EnvironmentInteractKey.WorkshopBagNoItems, "Picked clean." },
{ EnvironmentInteractKey.FindCandle, "I need the candle that goes here." },
{ EnvironmentInteractKey.DoesntBelong, "This feels out of place." },
{ EnvironmentInteractKey.SharpGlass, "Still sharp enough to bite." },
{ EnvironmentInteractKey.FreshAndCoolWater, "Cool. Clean. Unexpected." },
{ EnvironmentInteractKey.WorkshopBooks, "Time hasnt been kind to these." },
{ EnvironmentInteractKey.PumpTurnOn, "The pumps shudder back to life." },
{ EnvironmentInteractKey.FireHot, "Too hot to get close." },
{ EnvironmentInteractKey.CauldronBoiledAway, "Whatever it held is long gone." },
{ EnvironmentInteractKey.ExtinguishFire, "The symbols begin to glow as the goblet fills." },
{ EnvironmentInteractKey.LaxleyHouseBrokenClock, "The clock stopped at three-thirty-three." },
{ EnvironmentInteractKey.LaxleyGrandfatherClockMissingBothHands, "It's missing both hands. "},
{ EnvironmentInteractKey.LaxleyGrandfatherClockMissingHourHand, "The hour hand is missing."},
{ EnvironmentInteractKey.LaxleyGrandfatherClockMissingMinuteHand, "The minute hand is missing."},
{ EnvironmentInteractKey.AshwickHallowSign, "Ashwick Hallow… Even the name feels like a warning."},
{ EnvironmentInteractKey.AshwickRidgewayStatue, "Lovely sculpture. Mildly terrifying, but lovely."},
{ EnvironmentInteractKey.AshwickBlockedRidgwayRoad, "Right. Closed road. Of course it is."}
});
public static readonly IReadOnlyDictionary<UIInteractKey, string> UIInteractions =

View File

@@ -6,71 +6,20 @@ namespace BriarQueen.Data.Identifiers
public enum ItemKey
{
None = 0,
RustedKnife = 1,
SharpenedKnife = 2,
EmeraldAmulet = 3,
DustyMirror = 4,
SmallRag = 5,
GreenCandle = 6,
IndigoCandle = 7,
DirtyMagnifyingGlass = 8,
PumphouseKey = 9,
RedCandle = 10,
OrangeCandle = 11,
YellowCandle = 12,
BlueCandle = 13,
VioletCandle = 14,
Pliers = 15,
Emerald = 16,
Sapphire = 17,
Ruby = 18,
RubyRing = 19,
SilverCoin = 20,
GoldCoin = 21,
GrindstoneAxlePin = 22,
Diamond = 23,
DiamondTiara = 24,
DustySapphire = 25,
TornPage1 = 26,
TornPage2 = 27,
TornPage3 = 28,
TornPage4 = 29,
TornPage5 = 30,
IncompleteBook = 31,
CompleteBook = 32,
Stamp = 33,
LaxleyClockHourHand = 34,
LaxleyClockMinuteHand = 35,
S_Key,
DirtyTeddyBear,
GoldAmulet,
}
public enum EnvironmentKey
{
None = 0,
ChainLock = 1,
FountainVines = 2,
GrindingStone = 3,
WorkshopWindow = 4,
WorkshopDamagedBook = 5,
WorkshopBookSlot = 6,
WorkshopDiary = 7,
PumphouseWaterValve = 8,
WorkshopFadedPhoto = 9,
WorkshopDownstairsLight = 10,
WorkshopBrokenLantern = 11,
WorkshopWriting = 12,
StreetVines = 13,
GrandfatherClockPlaque = 14,
C1CarNewspaper,
}
public enum PuzzleSlotKey
{
None = 0,
WorkshopCandleSlot = 1,
WorkshopPuzzleBoxSlot = 2,
FountainGemSlot = 3,
FireplaceLockboxSlot = 4,
GrandfatherClockFace = 5,
GrandfatherClockHand = 6,
}
public static class ItemIDs
@@ -79,73 +28,21 @@ namespace BriarQueen.Data.Identifiers
new ReadOnlyDictionary<PuzzleSlotKey, string>(
new Dictionary<PuzzleSlotKey, string>
{
{ PuzzleSlotKey.WorkshopCandleSlot, "PUZ_WorkshopCandleSlot" },
{ PuzzleSlotKey.WorkshopPuzzleBoxSlot, "PUZ_WorkshopPuzzleBoxSlot" },
{ PuzzleSlotKey.FountainGemSlot, "PUZ_FountainGemSlot" },
{ PuzzleSlotKey.FireplaceLockboxSlot, "PUZ_FireplaceLockboxSlot" },
{ PuzzleSlotKey.GrandfatherClockFace, "PUZ_GrandfatherClockFace" },
{ PuzzleSlotKey.GrandfatherClockHand, "PUZ_GrandfatherClockHand" },
});
public static readonly IReadOnlyDictionary<EnvironmentKey, string> Environment =
new ReadOnlyDictionary<EnvironmentKey, string>(
new Dictionary<EnvironmentKey, string>
{
{ EnvironmentKey.ChainLock, "ENV_ChainLock" },
{ EnvironmentKey.FountainVines, "ENV_FountainVines" },
{ EnvironmentKey.GrindingStone, "ENV_GrindingStone" },
{ EnvironmentKey.WorkshopWindow, "ENV_WorkshopWindow" },
{ EnvironmentKey.WorkshopDamagedBook, "ENV_WorkshopDamagedBook" },
{ EnvironmentKey.WorkshopBookSlot, "ENV_WorkshopBookSlot" },
{ EnvironmentKey.WorkshopDiary, "ENV_WorkshopDiary" },
{ EnvironmentKey.PumphouseWaterValve, "ENV_PumphouseWaterValve" },
{ EnvironmentKey.WorkshopFadedPhoto, "ENV_WorkshopFadedPhoto" },
{ EnvironmentKey.WorkshopDownstairsLight, "ENV_WorkshopDownstairsLight" },
{ EnvironmentKey.WorkshopBrokenLantern, "ENV_WorkshopBrokenLantern" },
{ EnvironmentKey.WorkshopWriting, "ENV_WorkshopWriting" },
{ EnvironmentKey.StreetVines, "ENV_StreetVines" },
{ EnvironmentKey.GrandfatherClockPlaque, "ENV_GrandfatherClockPlaque" },
});
public static readonly IReadOnlyDictionary<ItemKey, string> Pickups =
new ReadOnlyDictionary<ItemKey, string>(
new Dictionary<ItemKey, string>
{
{ ItemKey.RustedKnife, "01_RustedKnife" },
{ ItemKey.SharpenedKnife, "02_SharpenedKnife" },
{ ItemKey.EmeraldAmulet, "03_EmeraldAmulet" },
{ ItemKey.DustyMirror, "04_DustyMirror" },
{ ItemKey.SmallRag, "05_SmallRag" },
{ ItemKey.GreenCandle, "06_GreenCandle" },
{ ItemKey.IndigoCandle, "07_IndigoCandle" },
{ ItemKey.DirtyMagnifyingGlass, "08_DirtyMagnifyingGlass" },
{ ItemKey.PumphouseKey, "09_PumphouseKey" },
{ ItemKey.RedCandle, "10_RedCandle" },
{ ItemKey.OrangeCandle, "11_OrangeCandle" },
{ ItemKey.YellowCandle, "12_YellowCandle" },
{ ItemKey.BlueCandle, "13_BlueCandle" },
{ ItemKey.VioletCandle, "14_VioletCandle" },
{ ItemKey.Pliers, "15_Pliers" },
{ ItemKey.Emerald, "16_Emerald" },
{ ItemKey.Sapphire, "17_Sapphire" },
{ ItemKey.Ruby, "18_Ruby" },
{ ItemKey.RubyRing, "19_RubyRing" },
{ ItemKey.SilverCoin, "20_SilverCoin" },
{ ItemKey.GoldCoin, "21_GoldCoin" },
{ ItemKey.GrindstoneAxlePin, "22_GrindstoneAxlePin" },
{ ItemKey.Diamond, "23_Diamond" },
{ ItemKey.DiamondTiara, "24_DiamondTiara" },
{ ItemKey.DustySapphire, "25_DustySapphire" },
{ ItemKey.TornPage1, "26_TornPage1" },
{ ItemKey.TornPage2, "27_TornPage2" },
{ ItemKey.TornPage3, "28_TornPage3" },
{ ItemKey.TornPage4, "29_TornPage4" },
{ ItemKey.TornPage5, "30_TornPage5" },
{ ItemKey.IncompleteBook, "31_IncompleteBook" },
{ ItemKey.CompleteBook, "32_CompleteBook" },
{ ItemKey.Stamp, "33_Stamp" },
{ ItemKey.LaxleyClockHourHand, "34_LaxleyClockHourHand" },
{ ItemKey.LaxleyClockMinuteHand, "35_LaxleyClockMinuteHand" },
{ ItemKey.S_Key, "Item:Pickup:S_Key" },
{ ItemKey.DirtyTeddyBear, "Item:Pickup:DirtyTeddyBear" },
{ ItemKey.GoldAmulet, "Item:Pickup:GoldAmulet" },
});
public static string Get(ItemKey key)
@@ -183,4 +80,4 @@ namespace BriarQueen.Data.Identifiers
return Pickups.Values;
}
}
}
}

View File

@@ -3,9 +3,7 @@ namespace BriarQueen.Data.Identifiers
public enum Location
{
None = 0,
Village = 1,
Workshop = 2,
LaxleyHouse = 3,
Pumphouse = 4,
TheOldRidgeway,
AshwickHallow,
}
}

View File

@@ -4,22 +4,14 @@ namespace BriarQueen.Data.Identifiers
{
public enum PuzzleKey
{
WorkshopCandlePuzzle,
WorkshopPuzzleBox,
FountainGemPuzzle,
FireplaceLockboxPuzzle,
LaxleyClock
AshwickMarketGate,
}
public static class PuzzleIdentifiers
{
public static readonly Dictionary<PuzzleKey, string> AllPuzzles = new()
{
{ PuzzleKey.WorkshopCandlePuzzle, "CH1:Puzzle:WorkshopCandles" },
{ PuzzleKey.WorkshopPuzzleBox, "CH1:Puzzle:WorkshopBox" },
{ PuzzleKey.FountainGemPuzzle , "CH1:Puzzle:FountainGems" },
{ PuzzleKey.FireplaceLockboxPuzzle, "CH1:Puzzle:FireplaceLockboxPuzzle" },
{ PuzzleKey.LaxleyClock, "CH1:Puzzle:LaxleyClock" },
{ PuzzleKey.AshwickMarketGate, "CH1:Puzzle:AshwickMarketGate" },
};
// Optional helper to get all puzzle IDs

View File

@@ -17,8 +17,8 @@ namespace BriarQueen.Data.Identifiers
new ReadOnlyDictionary<SubtitleKey, string>(
new Dictionary<SubtitleKey, string>
{
// { SubtitleKey.IntroLine, "SUB_IntroLine" },
// { SubtitleKey.TutorialTip, "SUB_TutorialTip" }
// { SubtitleKey.IntroLine, "Subtitle:IntroLine" },
// { SubtitleKey.TutorialTip, "Subtitle:TutorialTip" }
});
public static string Get(SubtitleKey key)
@@ -31,4 +31,4 @@ namespace BriarQueen.Data.Identifiers
return Subtitles.Values;
}
}
}
}

View File

@@ -8,8 +8,6 @@ namespace BriarQueen.Data.Identifiers
None = 0,
[DisplayName("Sharpened Knife")]
Knife = 1,
[DisplayName("Water Goblet")]
EndlessGoblet,
}

View File

@@ -6,11 +6,11 @@ namespace BriarQueen.Data.Identifiers
{
ReturnToPreviousLevel,
UsingItemsTogether,
HideHUD,
HideHUDKeyboard,
ExitItems,
MultipleUseItems,
DarkRooms,
Codex,
CodexKeyboard,
HiddenItems,
ResetPuzzles,
Tools,
@@ -32,12 +32,12 @@ namespace BriarQueen.Data.Identifiers
"Select one item, then click another to use them together."
},
{
TutorialPopupID.HideHUD,
"Press 'H' to hide the HUD."
TutorialPopupID.HideHUDKeyboard,
"Press '{Hide_HUD}' to hide the HUD."
},
{
TutorialPopupID.ExitItems,
"Right-click to exit the current interaction."
"Press '{Right_Click}' to exit the current interaction."
},
{
TutorialPopupID.MultipleUseItems,
@@ -48,8 +48,8 @@ namespace BriarQueen.Data.Identifiers
"Dark rooms can hide important details. Use light to reveal them."
},
{
TutorialPopupID.Codex,
"The Codex stores information you've discovered. Press 'C' to open it."
TutorialPopupID.CodexKeyboard,
"The Codex is used to collect any documents you encounter. Press '{Codex}' to open it."
},
{
TutorialPopupID.HiddenItems,
@@ -61,25 +61,22 @@ namespace BriarQueen.Data.Identifiers
},
{
TutorialPopupID.Tools,
"You'll find tools as you explore. Try them on different objects. Press 'Y' to view your tools."
"You'll find tools as you explore. Try them on different objects. Press '{Show_Tools}' to view your tools."
},
{
TutorialPopupID.ItemsAway,
"Right-click to put away the current item."
"Press '{Right_Click}' to put away the current item."
},
{
TutorialPopupID.ItemCycling,
"Press '[' or ']' to cycle through your backpack."
"Press '{Previous_Item}' or '{Next_Item}' to cycle through your backpack."
},
{
TutorialPopupID.ToolCycling,
"Press 'Q' or 'E' to cycle through your tools."
"Press '{Previous_Tool}' or '{Next_Tool}' to cycle through your tools."
}
};
public static IEnumerable<string> GetAllTexts()
{
return AllPopups.Values;
}
public static IEnumerable<string> GetAllTexts() => AllPopups.Values;
}
}

View File

@@ -40,7 +40,7 @@ namespace BriarQueen.Editor.Drawers.Registries
switch (entry.EntryType)
{
case CodexType.BookEntry:
case CodexType.DocumentEntry:
books.Add(entry);
break;

View File

@@ -435,17 +435,17 @@ namespace BriarQueen.Editor.Windows
if (_createBookEntries)
{
ProcessEnum<BookEntryID, CodexEntrySo>(
ProcessEnum<DocumentEntryID, CodexEntrySo>(
rootFolder: _codexRootFolder,
folderName: "Books",
noneValue: BookEntryID.None,
folderName: "Documents",
noneValue: DocumentEntryID.None,
assetExtensionName: key => key.ToString(),
loadExistingAsset: path => AssetDatabase.LoadAssetAtPath<CodexEntrySo>(path),
createAsset: () => CreateInstance<CodexEntrySo>(),
applyValues: (so, key) =>
{
SetEnumField(so, "_codexType", (int)CodexType.BookEntry);
SetEnumField(so, "_bookEntryID", (int)key);
SetEnumField(so, "_codexType", (int)CodexType.DocumentEntry);
SetEnumField(so, "_documentEntryID", (int)key);
SetEnumField(so, "_clueEntryID", (int)ClueEntryID.None);
SetEnumField(so, "_photoEntryID", (int)PhotoEntryID.None);
SetStringFieldIfEmpty(so, "_title", ObjectNames.NicifyVariableName(key.ToString()));
@@ -467,7 +467,7 @@ namespace BriarQueen.Editor.Windows
applyValues: (so, key) =>
{
SetEnumField(so, "_codexType", (int)CodexType.PuzzleClue);
SetEnumField(so, "_bookEntryID", (int)BookEntryID.None);
SetEnumField(so, "_documentEntryID", (int)DocumentEntryID.None);
SetEnumField(so, "_clueEntryID", (int)key);
SetEnumField(so, "_photoEntryID", (int)PhotoEntryID.None);
SetStringFieldIfEmpty(so, "_title", ObjectNames.NicifyVariableName(key.ToString()));
@@ -489,7 +489,7 @@ namespace BriarQueen.Editor.Windows
applyValues: (so, key) =>
{
SetEnumField(so, "_codexType", (int)CodexType.Photo);
SetEnumField(so, "_bookEntryID", (int)BookEntryID.None);
SetEnumField(so, "_documentEntryID", (int)DocumentEntryID.None);
SetEnumField(so, "_clueEntryID", (int)ClueEntryID.None);
SetEnumField(so, "_photoEntryID", (int)key);
SetStringFieldIfEmpty(so, "_title", ObjectNames.NicifyVariableName(key.ToString()));
@@ -652,4 +652,4 @@ namespace BriarQueen.Editor.Windows
}
}
}
}
}

View File

@@ -10,7 +10,8 @@
"GUID:593a5b492d29ac6448b1ebf7f035ef33",
"GUID:84651a3751eca9349aac36a66bba901b",
"GUID:75469ad4d38634e559750d17036d5f7c",
"GUID:776d03a35f1b52c4a9aed9f56d7b4229"
"GUID:776d03a35f1b52c4a9aed9f56d7b4229",
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eca349fcd212e4ac18902c56b71847e0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,499 @@
using System;
using System.Collections.Generic;
using System.Threading;
using BriarQueen.Framework.Services.Destruction;
using Cysharp.Threading.Tasks;
using PrimeTween;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
namespace BriarQueen.Framework.Effects
{
[ExecuteAlways]
public class UIDissolveImage : MonoBehaviour
{
private static readonly int _dissolveAmountId = Shader.PropertyToID("_DissolveAmount");
private static readonly int _reverseDirectionId = Shader.PropertyToID("_ReverseDirection");
[Header("References")]
[SerializeField]
private Image _image;
[SerializeField]
private CanvasGroup _canvasGroup;
[SerializeField]
private Material _dissolveMaterialTemplate;
[Header("Targeting")]
[SerializeField]
private bool _dissolveChildGraphics;
[SerializeField]
private bool _includeInactiveChildGraphics = true;
[SerializeField]
private bool _includeTextGraphics;
[Header("Tween")]
[SerializeField]
private float _duration = 0.75f;
[SerializeField]
private Ease _ease = Ease.InOutSine;
[SerializeField]
private bool _useUnscaledTime = true;
[Header("Direction")]
[SerializeField]
private bool _reverseDirection;
[Header("Destruction")]
[SerializeField]
private bool _destroyWhenFullyDissolved = true;
[Header("Editor Preview")]
[SerializeField]
private bool _previewInEditMode;
[SerializeField]
[Range(0f, 1f)]
private float _previewDissolveAmount;
private readonly List<Graphic> _targetGraphics = new();
private readonly List<Material> _originalMaterials = new();
private CancellationTokenSource _dissolveCts;
private DestructionService _destructionService;
private bool _hasOriginalMaterials;
private bool _isPreviewMaterial;
private Material _runtimeMaterial;
private Sequence _dissolveSequence;
public float DissolveAmount
{
get
{
if (_runtimeMaterial == null)
{
return 0f;
}
return _runtimeMaterial.GetFloat(_dissolveAmountId);
}
set => SetDissolveAmount(value);
}
[Inject]
public void Construct(DestructionService destructionService)
{
_destructionService = destructionService;
}
private void Awake()
{
ResolveReferences();
if (Application.isPlaying)
{
CreateRuntimeMaterial(false);
SetDissolveAmount(0f);
return;
}
#if UNITY_EDITOR
RefreshEditModePreview();
#endif
}
private void OnEnable()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
RefreshEditModePreview();
}
#endif
}
private void OnDisable()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
RestoreEditModeMaterial();
}
#endif
}
private void OnDestroy()
{
CancelDissolve();
if (Application.isPlaying)
{
DestroyRuntimeMaterial(false);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
#endif
}
private void OnValidate()
{
ResolveReferences();
if (Application.isPlaying)
{
SetReverseDirection(_reverseDirection);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
RefreshEditModePreview();
#endif
}
public UniTask DissolveIn()
{
gameObject.SetActive(true);
return TweenDissolve(1f, 0f, _duration, false);
}
public UniTask DissolveOut()
{
return TweenDissolve(0f, 1f, _duration, _destroyWhenFullyDissolved);
}
public UniTask DissolveOut(bool destroyWhenComplete)
{
return TweenDissolve(0f, 1f, _duration, destroyWhenComplete);
}
public UniTask DissolveOutAndDestroy()
{
return TweenDissolve(0f, 1f, _duration, true);
}
public UniTask TweenDissolve(float from, float to, float duration)
{
return TweenDissolve(from, to, duration, false);
}
public async UniTask TweenDissolve(float from, float to, float duration, bool destroyWhenComplete)
{
if (_runtimeMaterial == null)
{
CreateRuntimeMaterial(false);
}
CancelDissolve();
_dissolveCts = new CancellationTokenSource();
SetDissolveAmount(from);
_dissolveSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
.Group(Tween.Custom(
from,
to,
Mathf.Max(0f, duration),
SetDissolveAmount,
_ease,
useUnscaledTime: _useUnscaledTime));
try
{
await _dissolveSequence.ToUniTask(cancellationToken: _dissolveCts.Token);
SetDissolveAmount(to);
if (destroyWhenComplete && Mathf.Approximately(to, 1f))
{
await DestroyAfterDissolve();
}
}
catch (OperationCanceledException)
{
// Interrupted by another dissolve request or object destruction.
}
finally
{
_dissolveSequence = default;
if (_dissolveCts != null)
{
_dissolveCts.Dispose();
_dissolveCts = null;
}
}
}
public void SetDissolveAmount(float amount)
{
if (_runtimeMaterial == null)
{
return;
}
_runtimeMaterial.SetFloat(_dissolveAmountId, Mathf.Clamp01(amount));
SetTargetsMaterialDirty();
}
public void SetReverseDirection(bool reverseDirection)
{
_reverseDirection = reverseDirection;
if (_runtimeMaterial == null)
{
return;
}
_runtimeMaterial.SetFloat(_reverseDirectionId, _reverseDirection ? 1f : 0f);
SetTargetsMaterialDirty();
}
public void CancelDissolve()
{
if (_dissolveSequence.isAlive)
{
_dissolveSequence.Stop();
_dissolveSequence = default;
}
if (_dissolveCts != null)
{
_dissolveCts.Cancel();
_dissolveCts.Dispose();
_dissolveCts = null;
}
}
private async UniTask DestroyAfterDissolve()
{
if (_destructionService != null)
{
await _destructionService.Destroy(gameObject);
return;
}
Debug.LogWarning($"[{nameof(UIDissolveImage)}] Missing {nameof(DestructionService)}. Destroying directly.");
Destroy(gameObject);
}
private void ResolveReferences()
{
if (_image == null)
{
_image = GetComponent<Image>();
}
if (_canvasGroup == null)
{
_canvasGroup = GetComponent<CanvasGroup>();
}
}
private void ResolveTargetGraphics()
{
ResolveReferences();
_targetGraphics.Clear();
if (_dissolveChildGraphics)
{
var root = _canvasGroup != null ? _canvasGroup.transform : transform;
var graphics = root.GetComponentsInChildren<Graphic>(_includeInactiveChildGraphics);
foreach (var graphic in graphics)
{
if (IsValidTargetGraphic(graphic))
{
_targetGraphics.Add(graphic);
}
}
return;
}
if (_image != null)
{
_targetGraphics.Add(_image);
}
}
private bool IsValidTargetGraphic(Graphic graphic)
{
if (graphic == null)
{
return false;
}
// TMP needs a TMP-compatible dissolve shader; replacing its SDF material breaks text rendering.
if (!_includeTextGraphics && graphic is TMP_Text)
{
return false;
}
return true;
}
private void CreateRuntimeMaterial(bool isPreviewMaterial)
{
ResolveTargetGraphics();
if (_targetGraphics.Count == 0)
{
return;
}
var sourceMaterial = _dissolveMaterialTemplate != null
? _dissolveMaterialTemplate
: _targetGraphics[0].material;
if (sourceMaterial == null)
{
return;
}
if (_runtimeMaterial != null)
{
ApplyRuntimeMaterialToTargets();
return;
}
SaveOriginalMaterials();
_runtimeMaterial = Instantiate(sourceMaterial);
_runtimeMaterial.name = isPreviewMaterial
? $"{nameof(UIDissolveImage)} Preview Material"
: $"{nameof(UIDissolveImage)} Runtime Material";
_isPreviewMaterial = isPreviewMaterial;
if (isPreviewMaterial)
{
_runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
}
ApplyRuntimeMaterialToTargets();
SetReverseDirection(_reverseDirection);
}
private void ApplyRuntimeMaterialToTargets()
{
foreach (var graphic in _targetGraphics)
{
if (graphic == null)
{
continue;
}
graphic.material = _runtimeMaterial;
graphic.SetMaterialDirty();
}
}
private void SaveOriginalMaterials()
{
_originalMaterials.Clear();
foreach (var graphic in _targetGraphics)
{
_originalMaterials.Add(graphic != null ? graphic.material : null);
}
_hasOriginalMaterials = true;
}
private void RestoreOriginalMaterials()
{
if (!_hasOriginalMaterials)
{
return;
}
var count = Mathf.Min(_targetGraphics.Count, _originalMaterials.Count);
for (var i = 0; i < count; i++)
{
var graphic = _targetGraphics[i];
if (graphic == null)
{
continue;
}
graphic.material = _originalMaterials[i];
graphic.SetMaterialDirty();
}
_originalMaterials.Clear();
_hasOriginalMaterials = false;
}
private void SetTargetsMaterialDirty()
{
foreach (var graphic in _targetGraphics)
{
if (graphic != null)
{
graphic.SetMaterialDirty();
}
}
}
private void DestroyRuntimeMaterial(bool immediate)
{
if (_runtimeMaterial == null)
{
return;
}
RestoreOriginalMaterials();
if (immediate)
{
DestroyImmediate(_runtimeMaterial);
}
else
{
Destroy(_runtimeMaterial);
}
_runtimeMaterial = null;
_isPreviewMaterial = false;
}
#if UNITY_EDITOR
private void RefreshEditModePreview()
{
if (Application.isPlaying)
{
return;
}
if (!_previewInEditMode)
{
RestoreEditModeMaterial();
return;
}
CreateRuntimeMaterial(true);
SetReverseDirection(_reverseDirection);
SetDissolveAmount(_previewDissolveAmount);
}
private void RestoreEditModeMaterial()
{
if (!_isPreviewMaterial)
{
return;
}
DestroyRuntimeMaterial(true);
}
#endif
}
}

View File

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

View File

@@ -0,0 +1,502 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using PrimeTween;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Framework.Effects
{
[ExecuteAlways]
public class UIEdgeDarken : MonoBehaviour
{
private static readonly int _amountId = Shader.PropertyToID("_Amount");
private static readonly int _centerDarknessId = Shader.PropertyToID("_CenterDarkness");
private static readonly int _colorId = Shader.PropertyToID("_Color");
private static readonly int _edgeDarknessId = Shader.PropertyToID("_EdgeDarkness");
private static readonly int _edgeWidthId = Shader.PropertyToID("_EdgeWidth");
private static readonly int _rectPivotId = Shader.PropertyToID("_RectPivot");
private static readonly int _rectSizeId = Shader.PropertyToID("_RectSize");
private static readonly int _softnessId = Shader.PropertyToID("_Softness");
[Header("References")]
[SerializeField]
private Graphic _graphic;
[SerializeField]
private CanvasGroup _canvasGroup;
[SerializeField]
private Material _edgeDarkenMaterialTemplate;
[Header("Targeting")]
[SerializeField]
private bool _targetChildGraphics;
[SerializeField]
private bool _includeInactiveChildGraphics = true;
[SerializeField]
private bool _includeTextGraphics;
[Header("Darken")]
[SerializeField]
[Range(0f, 1f)]
private float _amount = 1f;
[SerializeField]
private Color _color = new(0f, 0f, 0f, 0.65f);
[SerializeField]
[Range(0f, 1f)]
private float _edgeDarkness = 1f;
[SerializeField]
[Range(0f, 1f)]
private float _centerDarkness;
[SerializeField]
[Range(0.001f, 0.5f)]
private float _edgeWidth = 0.22f;
[SerializeField]
[Range(0.001f, 0.5f)]
private float _softness = 0.18f;
[Header("Tween")]
[SerializeField]
private float _duration = 0.35f;
[SerializeField]
private Ease _ease = Ease.InOutSine;
[SerializeField]
private bool _useUnscaledTime = true;
[Header("Editor Preview")]
[SerializeField]
private bool _previewInEditMode;
[SerializeField]
[Range(0f, 1f)]
private float _previewAmount = 1f;
private readonly List<Graphic> _targetGraphics = new();
private readonly List<Material> _originalMaterials = new();
private readonly List<Material> _runtimeMaterials = new();
private CancellationTokenSource _darkenCts;
private bool _hasOriginalMaterials;
private bool _isPreviewMaterial;
private Sequence _darkenSequence;
public float Amount
{
get => _amount;
set => SetAmount(value);
}
private void Awake()
{
ResolveReferences();
if (Application.isPlaying)
{
CreateRuntimeMaterial(false);
ApplyMaterialProperties(_amount);
return;
}
#if UNITY_EDITOR
RefreshEditModePreview();
#endif
}
private void OnEnable()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
RefreshEditModePreview();
}
#endif
}
private void OnDisable()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
RestoreEditModeMaterial();
}
#endif
}
private void OnDestroy()
{
CancelTween();
if (Application.isPlaying)
{
DestroyRuntimeMaterial(false);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
#endif
}
private void OnValidate()
{
ResolveReferences();
if (Application.isPlaying)
{
ApplyMaterialProperties(_amount);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
RefreshEditModePreview();
#endif
}
public UniTask DarkenIn()
{
return TweenAmount(0f, 1f, _duration);
}
public UniTask DarkenOut()
{
return TweenAmount(1f, 0f, _duration);
}
public async UniTask TweenAmount(float from, float to, float duration)
{
if (_runtimeMaterials.Count == 0)
{
CreateRuntimeMaterial(false);
}
CancelTween();
_darkenCts = new CancellationTokenSource();
SetAmount(from);
_darkenSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
.Group(Tween.Custom(
from,
to,
Mathf.Max(0f, duration),
SetAmount,
_ease,
useUnscaledTime: _useUnscaledTime));
try
{
await _darkenSequence.ToUniTask(cancellationToken: _darkenCts.Token);
SetAmount(to);
}
catch (OperationCanceledException)
{
// Interrupted by another darken request or object destruction.
}
finally
{
_darkenSequence = default;
if (_darkenCts != null)
{
_darkenCts.Dispose();
_darkenCts = null;
}
}
}
public void SetAmount(float amount)
{
_amount = Mathf.Clamp01(amount);
ApplyMaterialProperties(_amount);
}
public void CancelTween()
{
if (_darkenSequence.isAlive)
{
_darkenSequence.Stop();
_darkenSequence = default;
}
if (_darkenCts != null)
{
_darkenCts.Cancel();
_darkenCts.Dispose();
_darkenCts = null;
}
}
private void ResolveReferences()
{
if (_graphic == null)
{
_graphic = GetComponent<Graphic>();
}
if (_canvasGroup == null)
{
_canvasGroup = GetComponent<CanvasGroup>();
}
}
private void ResolveTargetGraphics()
{
ResolveReferences();
_targetGraphics.Clear();
if (_targetChildGraphics)
{
var root = _canvasGroup != null ? _canvasGroup.transform : transform;
var graphics = root.GetComponentsInChildren<Graphic>(_includeInactiveChildGraphics);
foreach (var graphic in graphics)
{
if (IsValidTargetGraphic(graphic))
{
_targetGraphics.Add(graphic);
}
}
return;
}
if (_graphic != null)
{
_targetGraphics.Add(_graphic);
}
}
private bool IsValidTargetGraphic(Graphic graphic)
{
if (graphic == null)
{
return false;
}
// TMP needs a TMP-compatible shader; replacing its SDF material breaks text rendering.
if (!_includeTextGraphics && graphic is TMP_Text)
{
return false;
}
return true;
}
private void CreateRuntimeMaterial(bool isPreviewMaterial)
{
ResolveTargetGraphics();
if (_targetGraphics.Count == 0)
{
return;
}
var sourceMaterial = _edgeDarkenMaterialTemplate != null
? _edgeDarkenMaterialTemplate
: _targetGraphics[0].material;
if (sourceMaterial == null)
{
return;
}
if (_runtimeMaterials.Count > 0)
{
ApplyRuntimeMaterialToTargets();
return;
}
SaveOriginalMaterials();
_isPreviewMaterial = isPreviewMaterial;
foreach (var graphic in _targetGraphics)
{
if (graphic == null)
{
_runtimeMaterials.Add(null);
continue;
}
var runtimeMaterial = Instantiate(sourceMaterial);
runtimeMaterial.name = isPreviewMaterial
? $"{nameof(UIEdgeDarken)} Preview Material"
: $"{nameof(UIEdgeDarken)} Runtime Material";
if (isPreviewMaterial)
{
runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
}
_runtimeMaterials.Add(runtimeMaterial);
}
ApplyRuntimeMaterialToTargets();
}
private void ApplyRuntimeMaterialToTargets()
{
var count = Mathf.Min(_targetGraphics.Count, _runtimeMaterials.Count);
for (var i = 0; i < count; i++)
{
var graphic = _targetGraphics[i];
if (graphic == null)
{
continue;
}
graphic.material = _runtimeMaterials[i];
graphic.SetMaterialDirty();
}
}
private void ApplyMaterialProperties(float amount)
{
if (_runtimeMaterials.Count == 0)
{
return;
}
var count = Mathf.Min(_targetGraphics.Count, _runtimeMaterials.Count);
for (var i = 0; i < count; i++)
{
var material = _runtimeMaterials[i];
var graphic = _targetGraphics[i];
if (material == null || graphic == null)
{
continue;
}
var rectTransform = graphic.rectTransform;
material.SetFloat(_amountId, Mathf.Clamp01(amount));
material.SetColor(_colorId, _color);
material.SetFloat(_edgeDarknessId, _edgeDarkness);
material.SetFloat(_centerDarknessId, _centerDarkness);
material.SetFloat(_edgeWidthId, _edgeWidth);
material.SetFloat(_softnessId, _softness);
material.SetVector(_rectSizeId, rectTransform.rect.size);
material.SetVector(_rectPivotId, rectTransform.pivot);
}
SetTargetsMaterialDirty();
}
private void SaveOriginalMaterials()
{
_originalMaterials.Clear();
foreach (var graphic in _targetGraphics)
{
_originalMaterials.Add(graphic != null ? graphic.material : null);
}
_hasOriginalMaterials = true;
}
private void RestoreOriginalMaterials()
{
if (!_hasOriginalMaterials)
{
return;
}
var count = Mathf.Min(_targetGraphics.Count, _originalMaterials.Count);
for (var i = 0; i < count; i++)
{
var graphic = _targetGraphics[i];
if (graphic == null)
{
continue;
}
graphic.material = _originalMaterials[i];
graphic.SetMaterialDirty();
}
_originalMaterials.Clear();
_hasOriginalMaterials = false;
}
private void SetTargetsMaterialDirty()
{
foreach (var graphic in _targetGraphics)
{
if (graphic != null)
{
graphic.SetMaterialDirty();
}
}
}
private void DestroyRuntimeMaterial(bool immediate)
{
if (_runtimeMaterials.Count == 0)
{
return;
}
RestoreOriginalMaterials();
foreach (var runtimeMaterial in _runtimeMaterials)
{
if (runtimeMaterial == null)
{
continue;
}
if (immediate)
{
DestroyImmediate(runtimeMaterial);
}
else
{
Destroy(runtimeMaterial);
}
}
_runtimeMaterials.Clear();
_isPreviewMaterial = false;
}
#if UNITY_EDITOR
private void RefreshEditModePreview()
{
if (Application.isPlaying)
{
return;
}
if (!_previewInEditMode)
{
RestoreEditModeMaterial();
return;
}
CreateRuntimeMaterial(true);
ApplyMaterialProperties(_previewAmount);
}
private void RestoreEditModeMaterial()
{
if (!_isPreviewMaterial)
{
return;
}
DestroyRuntimeMaterial(true);
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7cb0728fb51e41b2aa93ec51993d9150
timeCreated: 1770379821

View File

@@ -0,0 +1,399 @@
using System;
using System.Threading;
using BriarQueen.Framework.Services.Destruction;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
namespace BriarQueen.Game.Effects
{
[ExecuteAlways]
public class UIFogReveal : MonoBehaviour
{
// ── Shader property IDs ───────────────────────────────────────
private static readonly int _fogAmountId = Shader.PropertyToID("_FogAmount");
private static readonly int _fogColorId = Shader.PropertyToID("_FogColor");
private static readonly int _fogIntensityId = Shader.PropertyToID("_FogIntensity");
private static readonly int _edgeSoftnessId = Shader.PropertyToID("_EdgeSoftness");
private static readonly int _noiseScaleId = Shader.PropertyToID("_NoiseScale");
private static readonly int _driftSpeedId = Shader.PropertyToID("_DriftSpeed");
private static readonly int _densityVariationId = Shader.PropertyToID("_DensityVariation");
private static readonly int _aspectRatioId = Shader.PropertyToID("_AspectRatio");
private static readonly int _fogMotionId = Shader.PropertyToID("_FogMotion");
private static readonly int _useSpriteId = Shader.PropertyToID("_UseSprite");
// ── Inspector ─────────────────────────────────────────────────
[Header("References")]
[SerializeField] private Image _image;
[SerializeField] private Material _fogMaterialTemplate;
[Header("Fog Settings")]
[SerializeField] private Color _fogColor = new(0.18f, 0.20f, 0.26f, 1f);
[SerializeField][Range(0f, 1f)] private float _fogIntensity = 0.95f;
[SerializeField][Range(0.05f, 1f)] private float _edgeSoftness = 0.55f;
[SerializeField][Range(1f, 20f)] private float _noiseScale = 5f;
[SerializeField][Range(0f, 0.5f)] private float _driftSpeed = 0.04f;
[SerializeField][Range(0f, 1f)] private float _densityVariation = 0.28f;
[SerializeField] private bool _fogMotion = true;
[SerializeField] private bool _useSprite = false;
[Header("Fog Range")]
[SerializeField][Range(0f, 1f)] private float _startFog = 0f;
[SerializeField][Range(0f, 1f)] private float _maxFog = 0.6f;
[Header("Screen")]
[SerializeField] private bool _autoAspectRatio = true;
[SerializeField] private float _aspectRatio = 1.777f;
[Header("Tween")]
[SerializeField] private float _duration = 1.5f;
[SerializeField] private Ease _ease = Ease.InOutSine;
[SerializeField] private bool _useUnscaledTime = true;
[Header("Delay")]
[SerializeField][Range(0f, 5f)] private float _fogInDelay = 0f;
[SerializeField][Range(0f, 5f)] private float _fogOutDelay = 0f;
[Header("Editor Preview")]
[SerializeField] private bool _previewInEditMode;
[SerializeField][Range(0f, 1f)] private float _previewFogAmount;
// ── Runtime state ─────────────────────────────────────────────
private Material _runtimeMaterial;
private bool _isPreviewMaterial;
private Sequence _fogSequence;
private CancellationTokenSource _fogCts;
private DestructionService _destructionService;
// ── Public properties ─────────────────────────────────────────
public float FogAmount
{
get => _runtimeMaterial != null ? _runtimeMaterial.GetFloat(_fogAmountId) : 0f;
set => SetFogAmount(value);
}
public float FogInDuration => _fogInDelay + _duration;
public float FogOutDuration => _fogOutDelay + _duration;
public bool FogMotion
{
get => _fogMotion;
set
{
_fogMotion = value;
_runtimeMaterial?.SetFloat(_fogMotionId, value ? 1f : 0f);
}
}
public bool UseSprite
{
get => _useSprite;
set
{
_useSprite = value;
_runtimeMaterial?.SetFloat(_useSpriteId, value ? 1f : 0f);
}
}
public float MaxFog
{
get => _maxFog;
set => _maxFog = value;
}
// ── DI ────────────────────────────────────────────────────────
[Inject]
public void Construct(DestructionService destructionService)
{
_destructionService = destructionService;
}
// ── Unity lifecycle ───────────────────────────────────────────
private void Awake()
{
ResolveReferences();
if (Application.isPlaying)
{
CreateRuntimeMaterial(false);
SetFogAmount(_startFog);
return;
}
#if UNITY_EDITOR
RefreshEditModePreview();
#endif
}
private void OnEnable()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
RefreshEditModePreview();
#endif
}
private void OnDisable()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
RestoreEditModeMaterial();
#endif
}
private void Update()
{
if (!Application.isPlaying) return;
if (!_autoAspectRatio) return;
var canvas = GetComponentInParent<Canvas>();
if (canvas == null) return;
var rt = canvas.GetComponent<RectTransform>();
var ratio = rt.rect.width / Mathf.Max(rt.rect.height, 0.001f);
_runtimeMaterial?.SetFloat(_aspectRatioId, ratio);
}
private void OnDestroy()
{
CancelFog();
if (Application.isPlaying)
{
DestroyRuntimeMaterial(false);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
#endif
}
private void OnValidate()
{
ResolveReferences();
if (Application.isPlaying)
{
ApplyShaderSettings();
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
RefreshEditModePreview();
#endif
}
// ── Public API ────────────────────────────────────────────────
/// <summary>Animate fog in (startFog → maxFog). Respects _fogInDelay.</summary>
public UniTask FogIn() => TweenFogWithDelay(_startFog, _maxFog, _duration, _fogInDelay);
/// <summary>Animate fog in over a custom duration, no delay.</summary>
public UniTask FogIn(float duration) => TweenFog(_startFog, _maxFog, duration);
/// <summary>Animate fog out (maxFog → startFog). Respects _fogOutDelay.</summary>
public UniTask FogOut() => TweenFogWithDelay(_maxFog, _startFog, _duration, _fogOutDelay);
/// <summary>Animate fog out over a custom duration, no delay.</summary>
public UniTask FogOut(float duration) => TweenFog(_maxFog, _startFog, duration);
/// <summary>Animate fog to an arbitrary target amount, no delay.</summary>
public UniTask FogTo(float target, float duration) => TweenFog(FogAmount, target, duration);
/// <summary>Snap fog to a value immediately, cancels any running tween.</summary>
public void FogSet(float amount)
{
CancelFog();
SetFogAmount(amount);
}
/// <summary>Snap fog to startFog immediately.</summary>
public void FogReset()
{
CancelFog();
SetFogAmount(_startFog);
}
public void CancelFog()
{
if (_fogSequence.isAlive)
{
_fogSequence.Stop();
_fogSequence = default;
}
if (_fogCts != null)
{
_fogCts.Cancel();
_fogCts.Dispose();
_fogCts = null;
}
}
public async UniTask TweenFog(float from, float to, float duration)
{
await TweenFogWithDelay(from, to, duration, 0f);
}
public async UniTask TweenFogWithDelay(float from, float to, float duration, float delay)
{
if (_runtimeMaterial == null)
CreateRuntimeMaterial(false);
CancelFog();
_fogCts = new CancellationTokenSource();
SetFogAmount(from);
try
{
if (delay > 0f)
{
await UniTask.Delay(
TimeSpan.FromSeconds(delay),
ignoreTimeScale: _useUnscaledTime,
cancellationToken: _fogCts.Token);
}
_fogSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
.Group(Tween.Custom(
from,
to,
Mathf.Max(0f, duration),
SetFogAmount,
_ease,
useUnscaledTime: _useUnscaledTime));
await _fogSequence.ToUniTask(cancellationToken: _fogCts.Token);
SetFogAmount(to);
}
catch (OperationCanceledException)
{
// Interrupted — normal, swallow it.
}
finally
{
_fogSequence = default;
if (_fogCts != null)
{
_fogCts.Dispose();
_fogCts = null;
}
}
}
// ── Internal ──────────────────────────────────────────────────
private void SetFogAmount(float amount)
{
if (_runtimeMaterial == null) return;
_runtimeMaterial.SetFloat(_fogAmountId, Mathf.Clamp01(amount));
_image?.SetMaterialDirty();
}
private void ApplyShaderSettings()
{
if (_runtimeMaterial == null) return;
_runtimeMaterial.SetColor(_fogColorId, _fogColor);
_runtimeMaterial.SetFloat(_fogIntensityId, _fogIntensity);
_runtimeMaterial.SetFloat(_edgeSoftnessId, _edgeSoftness);
_runtimeMaterial.SetFloat(_noiseScaleId, _noiseScale);
_runtimeMaterial.SetFloat(_driftSpeedId, _driftSpeed);
_runtimeMaterial.SetFloat(_densityVariationId, _densityVariation);
_runtimeMaterial.SetFloat(_fogMotionId, _fogMotion ? 1f : 0f);
_runtimeMaterial.SetFloat(_useSpriteId, _useSprite ? 1f : 0f);
if (!_autoAspectRatio)
_runtimeMaterial.SetFloat(_aspectRatioId, _aspectRatio);
_image?.SetMaterialDirty();
}
private void ResolveReferences()
{
if (_image == null)
_image = GetComponent<Image>();
}
private void CreateRuntimeMaterial(bool isPreviewMaterial)
{
if (_image == null) return;
var source = _fogMaterialTemplate != null
? _fogMaterialTemplate
: _image.material;
if (source == null) return;
if (_runtimeMaterial != null)
{
_image.material = _runtimeMaterial;
_image.SetMaterialDirty();
return;
}
_runtimeMaterial = Instantiate(source);
_runtimeMaterial.name = isPreviewMaterial
? $"{nameof(UIFogReveal)} Preview Material"
: $"{nameof(UIFogReveal)} Runtime Material";
_isPreviewMaterial = isPreviewMaterial;
if (isPreviewMaterial)
_runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
_image.material = _runtimeMaterial;
_image.SetMaterialDirty();
ApplyShaderSettings();
}
private void DestroyRuntimeMaterial(bool immediate)
{
if (_runtimeMaterial == null) return;
if (_image != null)
{
_image.material = null;
_image.SetMaterialDirty();
}
if (immediate)
DestroyImmediate(_runtimeMaterial);
else
Destroy(_runtimeMaterial);
_runtimeMaterial = null;
_isPreviewMaterial = false;
}
#if UNITY_EDITOR
private void RefreshEditModePreview()
{
if (Application.isPlaying) return;
if (!_previewInEditMode)
{
RestoreEditModeMaterial();
return;
}
CreateRuntimeMaterial(true);
ApplyShaderSettings();
SetFogAmount(_previewFogAmount);
}
private void RestoreEditModeMaterial()
{
if (!_isPreviewMaterial) return;
DestroyRuntimeMaterial(true);
}
#endif
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 912c8bc1a5f84113848a078f0581c8ce
timeCreated: 1778334335

View File

@@ -0,0 +1,249 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Framework.Effects
{
[RequireComponent(typeof(Image))]
public class UILightGlow : MonoBehaviour
{
private const string _lightShaderName = "BriarQueen/UI/Light Glow";
private static readonly int _lightColorId = Shader.PropertyToID("_LightColor");
private static readonly int _intensityId = Shader.PropertyToID("_Intensity");
private static readonly int _flickerOffsetId = Shader.PropertyToID("_FlickerOffset");
[Header("References")]
[SerializeField]
private Image _image;
[SerializeField]
private Material _lightMaterialTemplate;
[Header("Light")]
[SerializeField]
private Color _startingColor = new(1f, 0.78f, 0.35f, 1f);
[SerializeField]
private float _startingIntensity = 1.5f;
[SerializeField]
private bool _randomizeFlickerOffset = true;
[Header("Tween")]
[SerializeField]
private float _defaultTweenDuration = 0.35f;
[SerializeField]
private Ease _ease = Ease.InOutSine;
[SerializeField]
private bool _useUnscaledTime = true;
private Material _runtimeMaterial;
private Sequence _lightSequence;
private CancellationTokenSource _lightCts;
public Color LightColor
{
get
{
if (_runtimeMaterial == null)
{
return _startingColor;
}
return _runtimeMaterial.GetColor(_lightColorId);
}
}
public float Intensity
{
get
{
if (_runtimeMaterial == null)
{
return _startingIntensity;
}
return _runtimeMaterial.GetFloat(_intensityId);
}
}
private void Awake()
{
if (_image == null)
{
_image = GetComponent<Image>();
}
CreateRuntimeMaterial();
SetLightColor(_startingColor);
SetIntensity(_startingIntensity);
if (_randomizeFlickerOffset && _runtimeMaterial != null)
{
_runtimeMaterial.SetFloat(_flickerOffsetId, UnityEngine.Random.Range(0f, 100f));
}
}
private void OnDestroy()
{
CancelTween();
if (_runtimeMaterial != null)
{
Destroy(_runtimeMaterial);
_runtimeMaterial = null;
}
}
public UniTask ChangeColor(Color targetColor)
{
return ChangeColor(targetColor, _defaultTweenDuration);
}
public UniTask ChangeColor(Color targetColor, float duration)
{
return TweenTo(targetColor, Intensity, duration);
}
public UniTask ChangeIntensity(float targetIntensity)
{
return ChangeIntensity(targetIntensity, _defaultTweenDuration);
}
public UniTask ChangeIntensity(float targetIntensity, float duration)
{
return TweenTo(LightColor, targetIntensity, duration);
}
public UniTask TurnOff()
{
return ChangeIntensity(0f, _defaultTweenDuration);
}
public UniTask TurnOn()
{
return ChangeIntensity(_startingIntensity, _defaultTweenDuration);
}
public async UniTask TweenTo(Color targetColor, float targetIntensity, float duration)
{
if (_runtimeMaterial == null)
{
CreateRuntimeMaterial();
}
CancelTween();
var fromColor = LightColor;
var fromIntensity = Intensity;
var safeDuration = Mathf.Max(0f, duration);
_lightCts = new CancellationTokenSource();
_lightSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
.Group(Tween.Custom(
0f,
1f,
safeDuration,
progress =>
{
SetLightColor(Color.LerpUnclamped(fromColor, targetColor, progress));
SetIntensity(Mathf.LerpUnclamped(fromIntensity, targetIntensity, progress));
},
_ease,
useUnscaledTime: _useUnscaledTime));
try
{
await _lightSequence.ToUniTask(cancellationToken: _lightCts.Token);
SetLightColor(targetColor);
SetIntensity(targetIntensity);
}
catch (OperationCanceledException)
{
// Interrupted by another light tween or object destruction.
}
finally
{
_lightSequence = default;
if (_lightCts != null)
{
_lightCts.Dispose();
_lightCts = null;
}
}
}
public void SetLightColor(Color color)
{
if (_runtimeMaterial == null)
{
return;
}
_runtimeMaterial.SetColor(_lightColorId, color);
}
public void SetIntensity(float intensity)
{
if (_runtimeMaterial == null)
{
return;
}
_runtimeMaterial.SetFloat(_intensityId, Mathf.Max(0f, intensity));
}
public void CancelTween()
{
if (_lightSequence.isAlive)
{
_lightSequence.Stop();
_lightSequence = default;
}
if (_lightCts != null)
{
_lightCts.Cancel();
_lightCts.Dispose();
_lightCts = null;
}
}
private void CreateRuntimeMaterial()
{
if (_image == null)
{
return;
}
if (_lightMaterialTemplate != null)
{
_runtimeMaterial = Instantiate(_lightMaterialTemplate);
_image.material = _runtimeMaterial;
return;
}
var shader = Shader.Find(_lightShaderName);
if (shader == null)
{
Debug.LogWarning($"[{nameof(UILightGlow)}] Could not find shader '{_lightShaderName}'.");
return;
}
_runtimeMaterial = new Material(shader)
{
name = $"{nameof(UILightGlow)} Runtime Material"
};
_image.material = _runtimeMaterial;
}
}
}

View File

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

View File

@@ -3,5 +3,5 @@ using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.UI
{
public record DisplayTutorialPopupEvent(TutorialPopupID TutorialID) : IEvent;
public record DisplayTutorialPopupEvent(TutorialPopupID TutorialID, string ResolvedText) : IEvent;
}

View File

@@ -8,9 +8,5 @@ namespace BriarQueen.Framework.Events.UI
SolidColor = 0, // Left for Compat.
}
/// <summary>
/// Publish with color != Color.clear to fade in.
/// Publish with color == Color.clear to fade out (uses last active style).
/// </summary>
public record FadeEvent(bool Hidden, float Duration = 0.25f) : IEvent;
}

View File

@@ -0,0 +1,6 @@
using BriarQueen.Framework.Events.System;
namespace BriarQueen.Framework.Events.UI
{
public record UIBackRequestedEvent : IEvent;
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4cf9029b5f1a4c4f8740cb7394c1b5f8
timeCreated: 1778300000

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using BriarQueen.Framework.Assets.Components;
using BriarQueen.Framework.Managers.Assets.Components;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
@@ -11,7 +11,7 @@ using UnityEngine.SceneManagement;
using VContainer;
using VContainer.Unity;
namespace BriarQueen.Framework.Assets
namespace BriarQueen.Framework.Managers.Assets
{
public class AddressableManager : IDisposable
{
@@ -31,14 +31,18 @@ namespace BriarQueen.Framework.Assets
lock (_lock)
{
foreach (var handle in _instanceHandles.Values)
{
if (handle.IsValid())
Addressables.ReleaseInstance(handle);
}
_instanceHandles.Clear();
foreach (var handle in _assetHandles.Values)
{
if (handle.IsValid())
Addressables.Release(handle);
}
_assetHandles.Clear();
}
@@ -67,7 +71,8 @@ namespace BriarQueen.Framework.Assets
}
catch (OperationCanceledException)
{
if (handle.IsValid()) Addressables.Release(handle);
if (handle.IsValid())
Addressables.Release(handle);
throw;
}
@@ -81,7 +86,8 @@ namespace BriarQueen.Framework.Assets
{
if (_assetHandles.TryGetValue(asset, out var handle))
{
if (handle.IsValid()) Addressables.Release(handle);
if (handle.IsValid())
Addressables.Release(handle);
_assetHandles.Remove(asset);
}
@@ -97,8 +103,20 @@ namespace BriarQueen.Framework.Assets
)
{
var handle = Addressables.LoadSceneAsync(assetReference, loadSceneMode, autoLoad);
await handle.ToUniTask(progress, cancellationToken: cancellationToken);
return handle;
try
{
await handle.ToUniTask(progress, cancellationToken: cancellationToken);
return handle;
}
catch (OperationCanceledException)
{
if (handle.IsValid())
Addressables.Release(handle);
throw;
}
}
public async UniTask UnloadSceneAsync(AsyncOperationHandle<SceneInstance> sceneHandle)
@@ -116,6 +134,9 @@ namespace BriarQueen.Framework.Assets
)
{
var handle = Addressables.InstantiateAsync(reference, position, rotation, parent);
GameObject go = null;
NotifyOnDestruction notify = null;
Action onInstanceDestroyed = null;
try
{
@@ -127,33 +148,32 @@ namespace BriarQueen.Framework.Assets
return null;
}
var go = handle.Result;
lock (_lock)
{
_instanceHandles[go] = handle;
}
go = handle.Result;
var prefabScope = go.GetComponent<LifetimeScope>();
var injectionScope = scope ?? prefabScope?.Container ?? _lifetimeContainer;
injectionScope.InjectGameObject(go);
var notify = go.GetComponent<NotifyOnDestruction>();
notify = go.GetComponent<NotifyOnDestruction>();
if (!notify)
{
notify = go.AddComponent<NotifyOnDestruction>();
injectionScope.Inject(notify);
}
void OnInstanceDestroyed()
onInstanceDestroyed = () =>
{
TryReleaseInstance(go);
notify.OnDestroyedCalled -= OnInstanceDestroyed;
}
notify.OnDestroyedCalled -= onInstanceDestroyed;
};
notify.OnDestroyedCalled += OnInstanceDestroyed;
notify.OnDestroyedCalled += onInstanceDestroyed;
lock (_lock)
{
_instanceHandles[go] = handle;
}
return go;
}
@@ -161,6 +181,17 @@ namespace BriarQueen.Framework.Assets
{
if (handle.IsValid()) Addressables.ReleaseInstance(handle);
throw;
}
catch (Exception)
{
if (notify != null && onInstanceDestroyed != null)
{
notify.OnDestroyedCalled -= onInstanceDestroyed;
}
if (handle.IsValid()) Addressables.ReleaseInstance(handle);
throw;
}
}
@@ -201,4 +232,4 @@ namespace BriarQueen.Framework.Assets
}
}
}
}
}

View File

@@ -2,33 +2,42 @@ using System;
using BriarQueen.Framework.Services.Destruction;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VContainer;
namespace BriarQueen.Framework.Assets.Components
namespace BriarQueen.Framework.Managers.Assets.Components
{
public class NotifyOnDestruction : MonoBehaviour, IDestructible
{
private DestructionService _destructionService;
private bool _destroyedNotified;
public UniTask OnPreDestroy()
{
OnPreDestroyCalled?.Invoke();
return UniTask.CompletedTask; // No async operation needed, just a notification
return UniTask.CompletedTask;
}
public UniTask OnDestroyed()
{
OnDestroyedCalled?.Invoke();
return UniTask.CompletedTask; // No async operation needed, just a notification
}
[Inject]
public void Construct(DestructionService destructionService)
{
_destructionService = destructionService;
RaiseDestroyedOnce();
return UniTask.CompletedTask;
}
public event Action OnPreDestroyCalled;
public event Action OnDestroyedCalled;
private void OnDestroy()
{
RaiseDestroyedOnce();
}
private void RaiseDestroyedOnce()
{
if (_destroyedNotified)
{
return;
}
_destroyedNotified = true;
OnDestroyedCalled?.Invoke();
}
}
}
}

View File

@@ -1,4 +1,4 @@
namespace BriarQueen.Framework.Assets
namespace BriarQueen.Framework.Managers.Assets
{
public interface IAssetProvider
{

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Coordinators.Events;
@@ -23,41 +22,38 @@ namespace BriarQueen.Framework.Managers.Audio
/// - Settings set "base" volumes (in dB) per mixer parameter.
/// - Runtime states (Pause duck, Voice duck) apply "modifiers" (extra dB offsets).
/// - Effective mixer value is always: effectiveDb = baseDb + modifiersDb
/// - UI and Ambience route through SFX channel/group.
/// - SFX pool is transient — new channels are spawned on demand and
/// finished channels are reaped before each play.
/// </summary>
public class AudioManager : IDisposable, IManager
{
private const int INITIAL_AMBIENCE_SOURCES = 3;
private const int INITIAL_SFX_SOURCES = 6;
private const float PAUSE_DUCK_TARGET_DB = -18f;
private const float PAUSE_DUCK_FADE_SECONDS = 0.25f;
private const int INITIAL_SFX_SOURCES = 8;
private const float PAUSE_DUCK_TARGET_DB = -18f;
private const float PAUSE_DUCK_FADE_SECONDS = 0.25f;
private const float DEFAULT_VOICE_DUCK_TARGET_DB = -20f;
private readonly AudioMixer _audioMixer;
private readonly AudioRegistry _audioRegistry;
private readonly AudioMixer _audioMixer;
private readonly AudioRegistry _audioRegistry;
private readonly EventCoordinator _eventCoordinator;
private readonly Dictionary<string, float> _baseDb = new();
private readonly List<GameObject> _createdAudioObjects = new();
private readonly List<AudioSource> _ambienceSources = new();
private readonly List<AudioFileSo> _currentAmbienceTracks = new();
private readonly List<SfxChannel> _sfxChannels = new();
private readonly Dictionary<string, float> _baseDb = new();
private readonly List<GameObject> _createdAudioObjects = new();
private readonly List<SfxChannel> _sfxChannels = new();
private AudioSource _musicSourceA;
private AudioSource _musicSourceB;
private AudioSource _voiceSource;
private AudioSource _uiSource;
private string _activeVoiceSubtitleId;
private string _activeVoiceSubtitleId;
private AudioFileSo _currentMusicTrack;
private CancellationTokenSource _musicDuckCts;
private CancellationTokenSource _musicFadeCts;
private CancellationTokenSource _voiceCts;
private float _musicDuckDbCurrent;
private float _pauseDuckDbCurrent;
private float _musicDuckDbCurrent;
private float _pauseDuckDbCurrent;
private Sequence _musicDuckSequence;
private Sequence _pauseDuckSequence;
@@ -69,8 +65,8 @@ namespace BriarQueen.Framework.Managers.Audio
[Inject]
public AudioManager(AudioMixer mainMixer, AudioRegistry audioRegistry, EventCoordinator eventCoordinator)
{
_audioMixer = mainMixer;
_audioRegistry = audioRegistry;
_audioMixer = mainMixer;
_audioRegistry = audioRegistry;
_eventCoordinator = eventCoordinator;
}
@@ -137,53 +133,41 @@ namespace BriarQueen.Framework.Managers.Audio
}
_createdAudioObjects.Clear();
_ambienceSources.Clear();
_sfxChannels.Clear();
_currentAmbienceTracks.Clear();
_baseDb.Clear();
_musicSourceA = null;
_musicSourceB = null;
_voiceSource = null;
_uiSource = null;
_currentMusicTrack = null;
_activeVoiceSubtitleId = null;
_musicSourceA = null;
_musicSourceB = null;
_voiceSource = null;
_currentMusicTrack = null;
_activeVoiceSubtitleId = null;
_voiceFinishedPublished = false;
Initialized = false;
Initialized = false;
}
// ── Source creation ───────────────────────────────────────────
private void CreateSources()
{
_musicSourceA = CreateAudioSource("Music_Source_A", AudioMixerGroups.MUSIC_GROUP);
_musicSourceB = CreateAudioSource("Music_Source_B", AudioMixerGroups.MUSIC_GROUP);
_voiceSource = CreateAudioSource("Voice_Source", AudioMixerGroups.VOICE_GROUP);
_uiSource = CreateAudioSource("UI_Source", AudioMixerGroups.UI_GROUP);
_voiceSource = CreateAudioSource("Voice_Source", AudioMixerGroups.VOICE_GROUP);
for (var i = 0; i < INITIAL_SFX_SOURCES; i++)
{
var src = CreateAudioSource($"SFX_Source_{i}", AudioMixerGroups.SFX_GROUP);
_sfxChannels.Add(new SfxChannel
{
Source = src,
StartedAtUnscaled = -999f
});
}
for (var i = 0; i < INITIAL_AMBIENCE_SOURCES; i++)
{
_ambienceSources.Add(CreateAudioSource($"Ambience_Source_{i}", AudioMixerGroups.AMBIENCE_GROUP));
_sfxChannels.Add(new SfxChannel { Source = src, StartedAtUnscaled = -999f });
}
}
// ── Volume ────────────────────────────────────────────────────
private void PrimeMixerBaseValues()
{
PrimeBaseFromMixer(AudioMixerParameters.MASTER_VOLUME);
PrimeBaseFromMixer(AudioMixerParameters.MUSIC_VOLUME);
PrimeBaseFromMixer(AudioMixerParameters.SFX_VOLUME);
PrimeBaseFromMixer(AudioMixerParameters.AMBIENCE_VOLUME);
PrimeBaseFromMixer(AudioMixerParameters.VOICE_VOLUME);
PrimeBaseFromMixer(AudioMixerParameters.UI_VOLUME);
}
public void SetVolume(string parameter, float value01)
@@ -194,25 +178,18 @@ namespace BriarQueen.Framework.Managers.Audio
return;
}
var linear = Mathf.Clamp01(value01);
var db = Linear01ToDb(linear);
_baseDb[parameter] = db;
_baseDb[parameter] = Linear01ToDb(Mathf.Clamp01(value01));
ApplyEffectiveVolume(parameter);
}
private static float Linear01ToDb(float linear01)
{
var lin = Mathf.Max(linear01, 0.0001f);
return Mathf.Log10(lin) * 20f;
return Mathf.Log10(Mathf.Max(linear01, 0.0001f)) * 20f;
}
private void PrimeBaseFromMixer(string parameter)
{
if (_audioMixer != null && _audioMixer.GetFloat(parameter, out var db))
_baseDb[parameter] = db;
else
_baseDb[parameter] = 0f;
_baseDb[parameter] = _audioMixer != null && _audioMixer.GetFloat(parameter, out var db) ? db : 0f;
}
private void ApplyAllEffectiveVolumes()
@@ -220,9 +197,7 @@ namespace BriarQueen.Framework.Managers.Audio
ApplyEffectiveVolume(AudioMixerParameters.MASTER_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.VOICE_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.UI_VOLUME);
}
private void ApplyEffectiveVolume(string parameter)
@@ -236,8 +211,7 @@ namespace BriarQueen.Framework.Managers.Audio
var effective = baseDb;
if (parameter == AudioMixerParameters.MUSIC_VOLUME ||
parameter == AudioMixerParameters.SFX_VOLUME ||
parameter == AudioMixerParameters.AMBIENCE_VOLUME)
parameter == AudioMixerParameters.SFX_VOLUME)
{
effective += _pauseDuckDbCurrent;
}
@@ -248,10 +222,11 @@ namespace BriarQueen.Framework.Managers.Audio
_audioMixer.SetFloat(parameter, effective);
}
// ── UI stack / pause duck ─────────────────────────────────────
private void OnUIStackChanged(UIStackChangedEvent e)
{
if (!Initialized)
return;
if (!Initialized) return;
if (e.AnyUIOpen)
OnGamePausedInternal().Forget();
@@ -279,20 +254,18 @@ namespace BriarQueen.Framework.Managers.Audio
_pauseDuckSequence = default;
}
seconds = Mathf.Max(0f, seconds);
var from = _pauseDuckDbCurrent;
_pauseDuckSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Custom(
from,
targetDb,
seconds,
Mathf.Max(0f, seconds),
v =>
{
_pauseDuckDbCurrent = v;
ApplyEffectiveVolume(AudioMixerParameters.MUSIC_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.SFX_VOLUME);
ApplyEffectiveVolume(AudioMixerParameters.AMBIENCE_VOLUME);
},
Ease.OutCubic,
useUnscaledTime: true));
@@ -303,15 +276,13 @@ namespace BriarQueen.Framework.Managers.Audio
public void PauseVoiceSource(bool paused)
{
if (!Initialized || _voiceSource == null)
return;
if (paused)
_voiceSource.Pause();
else
_voiceSource.UnPause();
if (!Initialized || _voiceSource == null) return;
if (paused) _voiceSource.Pause();
else _voiceSource.UnPause();
}
// ── Play ──────────────────────────────────────────────────────
public void Play(string audioName)
{
if (!Initialized)
@@ -344,21 +315,11 @@ namespace BriarQueen.Framework.Managers.Audio
break;
case TrackType.Ambience:
if (!_currentAmbienceTracks.Contains(audioData))
{
_currentAmbienceTracks.Add(audioData);
PlayOnAvailableAmbienceSource(audioData);
}
break;
case TrackType.UIFX:
case TrackType.Sfx:
PlaySfx(audioData);
break;
case TrackType.UIFX:
PlayOneShotAsync(_uiSource, audioData).Forget();
break;
case TrackType.Voice:
PlayVoiceLine(audioData).Forget();
break;
@@ -368,6 +329,8 @@ namespace BriarQueen.Framework.Managers.Audio
DuckMusicAsync(audioData.Clip.length, audioData.FadeTime).Forget();
}
// ── Voice ─────────────────────────────────────────────────────
private async UniTaskVoid PlayVoiceLine(AudioFileSo audioData)
{
if (!Initialized || _voiceSource == null || audioData?.Clip == null)
@@ -377,15 +340,15 @@ namespace BriarQueen.Framework.Managers.Audio
_voiceCts = new CancellationTokenSource();
var token = _voiceCts.Token;
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
_activeVoiceSubtitleId = SubtitleIdentifiers.Get(audioData.MatchingSubtitleID);
_voiceFinishedPublished = false;
_eventCoordinator.Publish(new VoiceLineStartedEvent(_activeVoiceSubtitleId));
_voiceSource.clip = audioData.Clip;
_voiceSource.pitch = audioData.Pitch;
_voiceSource.volume = audioData.Volume;
_voiceSource.loop = false;
_voiceSource.clip = audioData.Clip;
_voiceSource.pitch = audioData.Pitch;
_voiceSource.volume = audioData.Volume;
_voiceSource.loop = false;
_voiceSource.priority = audioData.Priority;
_voiceSource.Play();
@@ -404,31 +367,210 @@ namespace BriarQueen.Framework.Managers.Audio
private void PublishVoiceFinishedIfNeeded()
{
if (_voiceFinishedPublished)
return;
if (_voiceFinishedPublished) return;
if (!string.IsNullOrEmpty(_activeVoiceSubtitleId))
_eventCoordinator.Publish(new VoiceLineFinishedEvent(_activeVoiceSubtitleId));
_voiceFinishedPublished = true;
_activeVoiceSubtitleId = null;
_activeVoiceSubtitleId = null;
}
public void StopVoice()
{
if (!Initialized) return;
StopAndDispose(ref _voiceCts);
if (_voiceSource != null && _voiceSource.isPlaying)
_voiceSource.Stop();
PublishVoiceFinishedIfNeeded();
}
// ── SFX (transient pool) ──────────────────────────────────────
private void PlaySfx(AudioFileSo audioData)
{
if (!Initialized || audioData == null || audioData.Clip == null)
return;
// Reap finished channels first so we don't accumulate stale entries
ReapFinishedSfxChannels();
// Try to find a free channel from the existing pool
AudioSource src = null;
var channelIndex = -1;
for (var i = 0; i < _sfxChannels.Count; i++)
{
var s = _sfxChannels[i].Source;
if (s != null && !s.isPlaying)
{
src = s;
channelIndex = i;
break;
}
}
// No free channel — spawn a transient one
if (src == null)
{
src = CreateAudioSource($"SFX_Source_Transient_{_sfxChannels.Count}", AudioMixerGroups.SFX_GROUP);
_sfxChannels.Add(new SfxChannel { Source = src, StartedAtUnscaled = -999f });
channelIndex = _sfxChannels.Count - 1;
Debug.Log($"[AudioManager] SFX pool expanded to {_sfxChannels.Count} channels.");
}
src.priority = audioData.Priority;
src.pitch = audioData.Pitch;
src.loop = audioData.Loopable;
src.PlayOneShot(audioData.Clip, audioData.Volume);
_sfxChannels[channelIndex] = new SfxChannel
{
Source = src,
StartedAtUnscaled = Time.unscaledTime
};
}
/// <summary>
/// Removes finished transient channels from the pool to prevent unbounded growth.
/// Preserves the initial pool channels even when idle.
/// </summary>
private void ReapFinishedSfxChannels()
{
for (var i = _sfxChannels.Count - 1; i >= INITIAL_SFX_SOURCES; i--)
{
var src = _sfxChannels[i].Source;
if (src == null || src.isPlaying)
continue;
// Destroy the transient GameObject and remove from pool
if (src.gameObject != null)
{
_createdAudioObjects.Remove(src.gameObject);
Object.Destroy(src.gameObject);
}
_sfxChannels.RemoveAt(i);
}
}
public void StopAllSfx()
{
if (!Initialized) return;
for (var i = _sfxChannels.Count - 1; i >= 0; i--)
{
var src = _sfxChannels[i].Source;
if (src == null) continue;
src.Stop();
// Destroy transient channels, reset initial ones
if (i >= INITIAL_SFX_SOURCES)
{
if (src.gameObject != null)
{
_createdAudioObjects.Remove(src.gameObject);
Object.Destroy(src.gameObject);
}
_sfxChannels.RemoveAt(i);
}
else
{
_sfxChannels[i] = new SfxChannel { Source = src, StartedAtUnscaled = -999f };
}
}
}
// ── Music ─────────────────────────────────────────────────────
public async UniTask CrossfadeMusic(AudioFileSo newTrack, float duration)
{
if (!Initialized || !newTrack || !newTrack.Clip) return;
if (_currentMusicTrack == newTrack) return;
StopAndDispose(ref _musicFadeCts);
_musicFadeCts = new CancellationTokenSource();
var token = _musicFadeCts.Token;
var activeSource = _musicSourceA.isPlaying ? _musicSourceA
: _musicSourceB.isPlaying ? _musicSourceB
: null;
var inactiveSource = activeSource == _musicSourceA ? _musicSourceB : _musicSourceA;
PlayOnSource(inactiveSource, newTrack);
if (activeSource == null)
{
inactiveSource.volume = newTrack.Volume;
_currentMusicTrack = newTrack;
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
return;
}
duration = Mathf.Max(0.0001f, duration);
var elapsed = 0f;
var startVolume = activeSource.volume;
try
{
while (elapsed < duration)
{
token.ThrowIfCancellationRequested();
var t = elapsed / duration;
activeSource.volume = Mathf.Lerp(startVolume, 0f, t);
inactiveSource.volume = Mathf.Lerp(0f, newTrack.Volume, t);
elapsed += Time.unscaledDeltaTime;
await UniTask.Yield(PlayerLoopTiming.Update, token);
}
activeSource.volume = 0f;
inactiveSource.volume = newTrack.Volume;
}
catch (OperationCanceledException)
{
return;
}
activeSource.Stop();
activeSource.volume = startVolume;
_currentMusicTrack = newTrack;
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
}
public void StopMusic()
{
if (!Initialized) return;
StopAndDispose(ref _musicFadeCts);
if (_musicSourceA != null) { _musicSourceA.Stop(); _musicSourceA.clip = null; _musicSourceA.volume = 0f; }
if (_musicSourceB != null) { _musicSourceB.Stop(); _musicSourceB.clip = null; _musicSourceB.volume = 0f; }
_currentMusicTrack = null;
}
private async UniTask DuckMusicAsync(float clipLengthSeconds, float fadeTimeSeconds)
{
if (!Initialized)
return;
if (!Initialized) return;
StopAndDispose(ref _musicDuckCts);
_musicDuckCts = new CancellationTokenSource();
var token = _musicDuckCts.Token;
fadeTimeSeconds = Mathf.Max(0.0001f, fadeTimeSeconds);
var duckTarget = DEFAULT_VOICE_DUCK_TARGET_DB;
try
{
await TweenMusicDuckTo(duckTarget, fadeTimeSeconds, token);
await TweenMusicDuckTo(DEFAULT_VOICE_DUCK_TARGET_DB, fadeTimeSeconds, token);
var hold = clipLengthSeconds - fadeTimeSeconds * 2f;
if (hold > 0.01f)
@@ -450,15 +592,13 @@ namespace BriarQueen.Framework.Managers.Audio
_musicDuckSequence = default;
}
seconds = Mathf.Max(0f, seconds);
var from = _musicDuckDbCurrent;
_musicDuckSequence = Sequence.Create(useUnscaledTime: true)
.Group(Tween.Custom(
from,
targetDb,
seconds,
Mathf.Max(0f, seconds),
v =>
{
_musicDuckDbCurrent = v;
@@ -471,258 +611,22 @@ namespace BriarQueen.Framework.Managers.Audio
_musicDuckSequence = default;
}
public async UniTask CrossfadeMusic(AudioFileSo newTrack, float duration)
{
if (!Initialized || !newTrack || !newTrack.Clip)
return;
if (_currentMusicTrack == newTrack)
return;
StopAndDispose(ref _musicFadeCts);
_musicFadeCts = new CancellationTokenSource();
var token = _musicFadeCts.Token;
var activeSource = _musicSourceA.isPlaying
? _musicSourceA
: _musicSourceB.isPlaying
? _musicSourceB
: null;
var inactiveSource = activeSource == _musicSourceA ? _musicSourceB : _musicSourceA;
PlayOnSource(inactiveSource, newTrack);
if (activeSource == null)
{
inactiveSource.volume = newTrack.Volume;
_currentMusicTrack = newTrack;
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
return;
}
duration = Mathf.Max(0.0001f, duration);
var elapsed = 0f;
var startVolume = activeSource.volume;
try
{
while (elapsed < duration)
{
token.ThrowIfCancellationRequested();
var t = elapsed / duration;
activeSource.volume = Mathf.Lerp(startVolume, 0f, t);
inactiveSource.volume = Mathf.Lerp(0f, newTrack.Volume, t);
elapsed += Time.unscaledDeltaTime;
await UniTask.Yield(PlayerLoopTiming.Update, token);
}
activeSource.volume = 0f;
inactiveSource.volume = newTrack.Volume;
}
catch (OperationCanceledException)
{
return;
}
activeSource.Stop();
activeSource.volume = startVolume;
_currentMusicTrack = newTrack;
_eventCoordinator.Publish(new MusicTrackChangedEvent(newTrack));
}
private void PlaySfx(AudioFileSo audioData)
{
if (!Initialized || audioData == null || audioData.Clip == null)
return;
var channelIndex = GetBestSfxChannelIndex(audioData.Priority);
if (channelIndex < 0 || channelIndex >= _sfxChannels.Count)
return;
var src = _sfxChannels[channelIndex].Source;
if (src == null)
return;
if (src.isPlaying)
src.Stop();
src.priority = audioData.Priority;
src.pitch = audioData.Pitch;
src.PlayOneShot(audioData.Clip, audioData.Volume);
_sfxChannels[channelIndex] = new SfxChannel
{
Source = src,
StartedAtUnscaled = Time.unscaledTime
};
}
private int GetBestSfxChannelIndex(int incomingPriority)
{
for (var i = 0; i < _sfxChannels.Count; i++)
{
var src = _sfxChannels[i].Source;
if (src == null)
continue;
if (!src.isPlaying)
return i;
}
var bestIndex = -1;
var worstPriority = int.MinValue;
var oldestStart = float.MaxValue;
for (var i = 0; i < _sfxChannels.Count; i++)
{
var src = _sfxChannels[i].Source;
if (src == null)
continue;
var p = src.priority;
var started = _sfxChannels[i].StartedAtUnscaled;
if (p > worstPriority || (p == worstPriority && started < oldestStart))
{
worstPriority = p;
oldestStart = started;
bestIndex = i;
}
}
return bestIndex;
}
public void StopAmbience(AudioFileSo audioData)
{
if (!Initialized || !audioData || !audioData.Clip || audioData.Type != TrackType.Ambience)
return;
if (_currentAmbienceTracks.Remove(audioData))
{
foreach (var source in _ambienceSources.Where(s => s != null && s.clip == audioData.Clip))
source.Stop();
}
}
public void StopAllAmbience()
{
if (!Initialized)
return;
foreach (var s in _ambienceSources)
{
if (s != null)
s.Stop();
}
_currentAmbienceTracks.Clear();
}
private void PlayOnAvailableAmbienceSource(AudioFileSo audioData)
{
var source = _ambienceSources.FirstOrDefault(s => s != null && !s.isPlaying);
if (source == null)
{
source = CreateAudioSource(
$"Ambience_Source_{_ambienceSources.Count}",
AudioMixerGroups.AMBIENCE_GROUP);
_ambienceSources.Add(source);
}
PlayOnSource(source, audioData);
}
public void StopMusic()
{
if (!Initialized)
return;
StopAndDispose(ref _musicFadeCts);
if (_musicSourceA != null)
{
_musicSourceA.Stop();
_musicSourceA.clip = null;
_musicSourceA.volume = 0f;
}
if (_musicSourceB != null)
{
_musicSourceB.Stop();
_musicSourceB.clip = null;
_musicSourceB.volume = 0f;
}
_currentMusicTrack = null;
}
public void StopVoice()
{
if (!Initialized)
return;
StopAndDispose(ref _voiceCts);
if (_voiceSource != null && _voiceSource.isPlaying)
_voiceSource.Stop();
PublishVoiceFinishedIfNeeded();
}
public void StopAllSfx()
{
if (!Initialized)
return;
for (var i = 0; i < _sfxChannels.Count; i++)
{
var src = _sfxChannels[i].Source;
if (src == null)
continue;
src.Stop();
_sfxChannels[i] = new SfxChannel
{
Source = src,
StartedAtUnscaled = -999f
};
}
}
// ── Stop all ──────────────────────────────────────────────────
public void StopAllAudio()
{
if (!Initialized)
return;
if (!Initialized) return;
StopMusic();
StopVoice();
StopAllSfx();
StopAllAmbience();
if (_uiSource != null)
_uiSource.Stop();
}
// ── Helpers ───────────────────────────────────────────────────
private static void StopAndDispose(ref CancellationTokenSource cts)
{
if (cts == null)
return;
try
{
cts.Cancel();
}
catch
{
}
if (cts == null) return;
try { cts.Cancel(); } catch { }
cts.Dispose();
cts = null;
}
@@ -733,7 +637,7 @@ namespace BriarQueen.Framework.Managers.Audio
Object.DontDestroyOnLoad(obj);
_createdAudioObjects.Add(obj);
var src = obj.AddComponent<AudioSource>();
var src = obj.AddComponent<AudioSource>();
var group = _audioMixer.FindMatchingGroups(groupName);
if (group != null && group.Length > 0)
@@ -744,28 +648,14 @@ namespace BriarQueen.Framework.Managers.Audio
return src;
}
private async UniTaskVoid PlayOneShotAsync(AudioSource source, AudioFileSo audioData)
{
if (!Initialized || source == null || audioData == null || audioData.Clip == null)
return;
source.priority = audioData.Priority;
source.pitch = audioData.Pitch;
source.PlayOneShot(audioData.Clip, audioData.Volume);
var seconds = audioData.Clip.length / Mathf.Max(audioData.Pitch, 0.0001f);
await UniTask.Delay(TimeSpan.FromSeconds(seconds));
}
private void PlayOnSource(AudioSource source, AudioFileSo audioData)
{
if (!Initialized || source == null || audioData == null)
return;
if (!Initialized || source == null || audioData == null) return;
source.clip = audioData.Clip;
source.loop = audioData.Loopable;
source.volume = audioData.Volume;
source.pitch = audioData.Pitch;
source.clip = audioData.Clip;
source.loop = audioData.Loopable;
source.volume = audioData.Volume;
source.pitch = audioData.Pitch;
source.priority = audioData.Priority;
source.Play();
}
@@ -773,7 +663,7 @@ namespace BriarQueen.Framework.Managers.Audio
private struct SfxChannel
{
public AudioSource Source;
public float StartedAtUnscaled;
public float StartedAtUnscaled;
}
}
}

View File

@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Managers.Input;
using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Managers.Player;
using NaughtyAttributes;
using UnityEngine;
using UnityEngine.EventSystems;
using VContainer;
namespace BriarQueen.Framework.Managers
@@ -13,6 +16,7 @@ namespace BriarQueen.Framework.Managers
{
private SaveManager _saveManager;
private PlayerManager _playerManager;
private InputManager _inputManager;
[Header("Current Loaded Save")]
[SerializeField, ReadOnly]
@@ -23,10 +27,11 @@ namespace BriarQueen.Framework.Managers
private ItemKey _itemToGive;
[Inject]
public void Construct(SaveManager saveManager, PlayerManager playerManager)
public void Construct(SaveManager saveManager, PlayerManager playerManager, InputManager inputManager)
{
_saveManager = saveManager;
_playerManager = playerManager;
_inputManager = inputManager;
}
public void Start()
@@ -38,6 +43,7 @@ namespace BriarQueen.Framework.Managers
{
_currentSave = save;
}
[Button]
private void GiveItem()

View File

@@ -26,7 +26,6 @@ namespace BriarQueen.Framework.Managers.IO
private readonly object _saveLock = new();
private CancellationTokenSource _currentSaveCts;
private DateTime _lastSaveTime;
[Inject]
public SaveManager(EventCoordinator eventCoordinator)
@@ -112,12 +111,6 @@ namespace BriarQueen.Framework.Managers.IO
private async UniTask SaveGameDataInternal(CancellationToken ct)
{
if ((DateTime.UtcNow - _lastSaveTime).TotalMilliseconds < 250)
{
Debug.Log("[SaveManager] Last save within 250ms, skipping.");
return;
}
if (CurrentSave == null)
CurrentSave = new SaveGame { SaveFileName = "NewGame" };
@@ -186,7 +179,6 @@ namespace BriarQueen.Framework.Managers.IO
CurrentSave = saveClone;
IsGameLoaded = true;
_lastSaveTime = DateTime.UtcNow;
OnSaveGameSaved?.Invoke();
Debug.Log($"[SaveManager] Save complete: {CurrentSave.SaveFileName}");
@@ -272,8 +264,7 @@ namespace BriarQueen.Framework.Managers.IO
if (loadedSave != null)
{
CurrentSave = loadedSave;
await SaveGameDataLatest();
RestoreBackupToMain(mainPath, backupPath);
Debug.Log("[SaveManager] Restored save from backup.");
}
}
@@ -285,6 +276,41 @@ namespace BriarQueen.Framework.Managers.IO
OnSaveGameLoaded?.Invoke(CurrentSave);
}
private void RestoreBackupToMain(string mainPath, string backupPath)
{
if (string.IsNullOrWhiteSpace(mainPath) || string.IsNullOrWhiteSpace(backupPath))
return;
try
{
var mainDirectory = Path.GetDirectoryName(mainPath);
if (!string.IsNullOrWhiteSpace(mainDirectory))
Directory.CreateDirectory(mainDirectory);
var tempRestorePath = mainPath + ".restoretmp";
if (File.Exists(tempRestorePath))
File.Delete(tempRestorePath);
File.Copy(backupPath, tempRestorePath, overwrite: true);
if (File.Exists(mainPath))
File.Replace(tempRestorePath, mainPath, null, ignoreMetadataErrors: true);
else
File.Move(tempRestorePath, mainPath);
}
catch (Exception ex)
{
Debug.LogError($"[SaveManager] Failed to restore backup '{backupPath}' to '{mainPath}': {ex}");
}
finally
{
var tempRestorePath = mainPath + ".restoretmp";
if (File.Exists(tempRestorePath))
File.Delete(tempRestorePath);
}
}
private async UniTask<SaveGame> LoadFromFileAsync(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
@@ -432,4 +458,4 @@ namespace BriarQueen.Framework.Managers.IO
return collected.Any(x => x.UniqueIdentifier == uniqueIdentifier);
}
}
}
}

View File

@@ -8,7 +8,6 @@ using BriarQueen.Framework.Managers.UI;
using BriarQueen.Framework.Services.Game;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.UI;
using VContainer;
namespace BriarQueen.Framework.Managers.Input
@@ -48,6 +47,7 @@ namespace BriarQueen.Framework.Managers.Input
private bool _initialized;
private bool _isPaused;
private bool _isAnyUIOpen;
private InputAction _pauseAction;
private InputAction _pointAction;
@@ -58,7 +58,7 @@ namespace BriarQueen.Framework.Managers.Input
private InputAction _nextItemAction;
private InputAction _previousItemAction;
private InputAction _virtualMouseAction;
private InputAction _submitAction;
private InputAction _submitAction;
private UICursorService _uiCursorService;
@@ -77,6 +77,8 @@ namespace BriarQueen.Framework.Managers.Input
public bool IsPaused => _isPaused;
public bool UsingControllerCursor => DeviceInputType != DeviceInputType.KeyboardAndMouse;
public string CurrentControlScheme => _playerInput?.currentControlScheme ?? string.Empty;
private void Awake()
{
@@ -109,6 +111,7 @@ namespace BriarQueen.Framework.Managers.Input
_eventCoordinator.Unsubscribe<UIToggleHudEvent>(OnHudStateChanged);
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(OnCodexStateChanged);
_eventCoordinator.Unsubscribe<ToggleToolScreenEvent>(OnToolScreenStateChanged);
_eventCoordinator.Unsubscribe<UIStackChangedEvent>(OnUIStackChanged);
}
UnbindCoreInputs();
@@ -148,28 +151,18 @@ namespace BriarQueen.Framework.Managers.Input
return;
}
if (_playerInput.actions == null)
{
Debug.LogWarning("[InputManager] PlayerInput.actions is null");
return;
}
Debug.Log($"[InputManager] Current map before cache: {ActiveActionMap}");
CacheActions();
Debug.Log($"[InputManager] Point action: {_pointAction}");
Debug.Log($"[InputManager] Click action: {_clickAction}");
Debug.Log($"[InputManager] Virtual_Mouse action: {_virtualMouseAction}");
BindCoreInputs();
DeviceInputType = GetDeviceInputType(_playerInput);
ApplyCursorModeForCurrentScheme();
_initialized = true;
_eventCoordinator.Subscribe<UIToggleHudEvent>(OnHudStateChanged);
_eventCoordinator.Subscribe<ToggleCodexEvent>(OnCodexStateChanged);
_eventCoordinator.Subscribe<ToggleToolScreenEvent>(OnToolScreenStateChanged);
_eventCoordinator.Subscribe<UIStackChangedEvent>(OnUIStackChanged);
Debug.Log("[InputManager] Initialization complete");
_initialized = true;
}
private void CacheActions()
@@ -205,14 +198,12 @@ namespace BriarQueen.Framework.Managers.Input
{
if (_pointAction != null)
{
Debug.Log("[InputManager] Binding Point");
_pointAction.performed += OnPoint;
_pointAction.canceled += OnPoint;
}
else
{
Debug.LogWarning("[InputManager] Required action 'Point' not found.");
}
if (_virtualMouseAction != null)
@@ -223,53 +214,33 @@ namespace BriarQueen.Framework.Managers.Input
if (_pauseAction != null)
_pauseAction.performed += OnPause;
else
Debug.LogWarning("[InputManager] Action 'Pause' not found.");
if (_clickAction != null)
_clickAction.performed += OnClick;
else
Debug.LogWarning("[InputManager] Action 'Click' not found.");
if (_rightClickAction != null)
_rightClickAction.performed += OnRightClick;
else
Debug.LogWarning("[InputManager] Action 'Right_Click' not found.");
if (_hideHudAction != null)
_hideHudAction.performed += OnHideHUD;
else
Debug.LogWarning("[InputManager] Action 'Hide_HUD' not found.");
if (_codexAction != null)
_codexAction.performed += OnCodex;
else
Debug.LogWarning("[InputManager] Action 'Codex' not found.");
if (_openToolsAction != null)
_openToolsAction.performed += OnOpenTools;
else
Debug.LogWarning("[InputManager] Action 'Show_Tools' not found.");
if (_nextToolAction != null)
_nextToolAction.performed += OnNextToolClicked;
else
Debug.LogWarning("[InputManager] Action 'Next_Tool' not found.");
if (_previousToolAction != null)
_previousToolAction.performed += OnPreviousToolClicked;
else
Debug.LogWarning("[InputManager] Action 'Previous_Tool' not found.");
if (_nextItemAction != null)
_nextItemAction.performed += OnNextItemClicked;
else
Debug.LogWarning("[InputManager] Action 'Next_Item' not found.");
if (_previousItemAction != null)
_previousItemAction.performed += OnPreviousItemClicked;
else
Debug.LogWarning("[InputManager] Action 'Previous_Item' not found.");
if (_playerInput != null)
_playerInput.onControlsChanged += OnControlsChanged;
@@ -343,7 +314,7 @@ namespace BriarQueen.Framework.Managers.Input
}
private void OnControlsChanged(PlayerInput playerInput)
{ Debug.Log($"Controls changed. Scheme: {playerInput.currentControlScheme}");
{
DeviceInputType = GetDeviceInputType(playerInput);
ApplyCursorModeForCurrentScheme();
}
@@ -391,7 +362,7 @@ namespace BriarQueen.Framework.Managers.Input
{
if (_submitAction == null || callback == null)
return;
_submitAction.performed -= callback;
}
@@ -401,11 +372,6 @@ namespace BriarQueen.Framework.Managers.Input
return;
var action = GetCachedAction(actionName);
if (action == null)
{
Debug.LogWarning($"[InputManager] Action '{actionName}' not found.");
return;
}
action.performed -= callback;
action.performed += callback;
@@ -417,11 +383,6 @@ namespace BriarQueen.Framework.Managers.Input
return;
var action = GetCachedAction(actionName);
if (action == null)
{
Debug.LogWarning($"[InputManager] Action '{actionName}' not found.");
return;
}
action.performed -= callback;
}
@@ -471,18 +432,28 @@ namespace BriarQueen.Framework.Managers.Input
_toolScreenShown = evt.Shown;
}
private void OnUIStackChanged(UIStackChangedEvent evt)
{
_isAnyUIOpen = evt.AnyUIOpen;
_isPaused = evt.AnyUIOpen && _gameService != null && !_gameService.IsMainMenuSceneLoaded;
}
private void OnHideHUD(InputAction.CallbackContext ctx)
{
_hudHidden = !_hudHidden;
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(_hudHidden));
_eventCoordinator?.PublishImmediate(new UIToggleHudEvent(!_hudHidden));
}
private void OnPause(InputAction.CallbackContext ctx)
{
if(_gameService.IsMainMenuSceneLoaded)
var isMainMenu = _gameService != null && _gameService.IsMainMenuSceneLoaded;
if (isMainMenu || _isAnyUIOpen)
{
_eventCoordinator?.PublishImmediate(new UIBackRequestedEvent());
return;
_isPaused = !_isPaused;
}
_isPaused = true;
_eventCoordinator?.Publish(new PauseButtonClickedEvent());
}
@@ -556,5 +527,6 @@ namespace BriarQueen.Framework.Managers.Input
return null;
}
}
}
}
}

View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Assets;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Assets;
using BriarQueen.Framework.Managers.Audio;
using BriarQueen.Framework.Managers.Interaction.Data;
using BriarQueen.Framework.Managers.IO;
@@ -31,11 +32,16 @@ namespace BriarQueen.Framework.Managers.Levels.Data
[Tooltip("Used for custom tooltip. Defaults to Item Name")]
[SerializeField]
private string _interactableTooltip = string.Empty;
protected string _interactableTooltip = string.Empty;
[Tooltip("Optional. Used for custom interaction.")]
[SerializeField]
private string _pickupText = string.Empty;
[Header("Object Setup")]
[SerializeField]
protected CanvasGroup _canvasGroup;
protected bool _isLocked;
protected AddressableManager AddressableManager;
protected AssetRegistry AssetRegistry;
@@ -68,8 +74,23 @@ namespace BriarQueen.Framework.Managers.Levels.Data
public virtual UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Pickup;
public virtual string InteractableName =>
!string.IsNullOrWhiteSpace(_interactableTooltip) ? _interactableTooltip : _itemData.ItemName;
public virtual string InteractableName
{
get
{
if (!string.IsNullOrWhiteSpace(_interactableTooltip))
{
return _interactableTooltip;
}
if (_itemData != null && !string.IsNullOrWhiteSpace(_itemData.ItemName))
{
return _itemData.ItemName;
}
return string.Empty;
}
}
/// <summary>
@@ -91,6 +112,15 @@ namespace BriarQueen.Framework.Managers.Levels.Data
await Pickup();
await OnInteracted();
if (!string.IsNullOrWhiteSpace(_pickupText))
{
EventCoordinator.Publish(new DisplayInteractEvent(_pickupText));
}
else
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.LooksImportant)));
}
}
public virtual UniTask EnterHover()
@@ -147,8 +177,8 @@ namespace BriarQueen.Framework.Managers.Levels.Data
protected virtual async UniTask Remove()
{
// TODO - Play Cut Vines SFX
if (_canvasGroup == null) _canvasGroup = GetComponent<CanvasGroup>();
if (_canvasGroup == null)
_canvasGroup = GetComponent<CanvasGroup>();
if (PickupSequence.isAlive)
{
@@ -184,7 +214,7 @@ namespace BriarQueen.Framework.Managers.Levels.Data
}
}
private void UpdateSaveGameOnRemoval()
protected virtual void UpdateSaveGameOnRemoval()
{
var save = SaveManager.CurrentSave;
Debug.Log($"[Base Item] Found save - {save.SaveFileName}");
@@ -240,5 +270,35 @@ namespace BriarQueen.Framework.Managers.Levels.Data
await DestructionService.Destroy(gameObject);
}
}
public void Lock()
{
_isLocked = true;
_canvasGroup.blocksRaycasts = false;
_canvasGroup.interactable = false;
}
public void Unlock()
{
_isLocked = false;
_canvasGroup.blocksRaycasts = true;
_canvasGroup.interactable = true;
}
public void OnValidate()
{
#if UNITY_EDITOR
var canvasGroup = GetComponent<CanvasGroup>();
if (!canvasGroup)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
_canvasGroup = canvasGroup;
}
#endif
}
}
}
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.UI;
@@ -8,12 +10,19 @@ using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Managers.Player;
using BriarQueen.Framework.Managers.Player.Data.Codex;
using BriarQueen.Framework.Services.Destruction;
using BriarQueen.Framework.Services.Puzzles.Base;
using BriarQueen.Framework.Services.Settings;
using BriarQueen.Framework.Services.Tutorials;
using Cysharp.Threading.Tasks;
using NaughtyAttributes;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
using SettingsService = BriarQueen.Framework.Services.Settings.SettingsService;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace BriarQueen.Framework.Managers.Levels.Data
{
@@ -30,6 +39,10 @@ namespace BriarQueen.Framework.Managers.Levels.Data
public List<CodexTrigger> CodexTriggers;
[Header("Puzzles")]
[SerializeField]
public List<BasePuzzle> Puzzles;
[Header("Setup")]
[SerializeField]
protected GraphicRaycaster _raycaster;
@@ -47,8 +60,6 @@ namespace BriarQueen.Framework.Managers.Levels.Data
public virtual string LevelName => _levelName;
public virtual bool IsPuzzleLevel { get; }
public virtual int CurrentLevelHintStage { get; set; }
public virtual Dictionary<int, BaseHint> Hints { get; }
@@ -112,5 +123,72 @@ namespace BriarQueen.Framework.Managers.Levels.Data
{
return UniTask.CompletedTask;
}
#if UNITY_EDITOR
[Button("Discover Level References")]
private void DiscoverLevelReferences()
{
Undo.RecordObject(this, "Discover Level References");
var discoveredCodexTriggers = GetComponentsInChildren<CodexTrigger>(true)
.Where(trigger => trigger != null)
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
.ToList();
var discoveredPickups = GetComponentsInChildren<BaseItem>(true)
.Where(item => item != null && item is not CodexTrigger)
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
.ToList();
var discoveredPuzzles = GetComponentsInChildren<BasePuzzle>(true)
.Where(puzzle => puzzle != null)
.OrderBy(GetHierarchyPath, StringComparer.Ordinal)
.ToList();
Pickups = discoveredPickups;
CodexTriggers = discoveredCodexTriggers;
Puzzles = discoveredPuzzles;
EditorUtility.SetDirty(this);
PrefabUtility.RecordPrefabInstancePropertyModifications(this);
Debug.Log(
$"[BaseLevel] Discovery complete for '{name}'. Pickups: {Pickups.Count}, CodexTriggers: {CodexTriggers.Count}, Puzzles: {Puzzles.Count}",
this);
}
private static string GetHierarchyPath(Component component)
{
if (component == null)
{
return string.Empty;
}
var names = new Stack<string>();
var current = component.transform;
while (current != null)
{
names.Push(current.name);
current = current.parent;
}
return string.Join("/", names);
}
#endif
public void OnValidate()
{
#if UNITY_EDITOR
CanvasScaler scaler = GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.matchWidthOrHeight = 0.5f;
scaler.referenceResolution = new Vector2(1920, 1200);
GraphicRaycaster raycaster = GetComponent<GraphicRaycaster>();
_raycaster = raycaster;
#endif
}
}
}
}

View File

@@ -2,18 +2,17 @@ using System;
using System.Collections.Generic;
using System.Linq;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Assets;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.Gameplay;
using BriarQueen.Framework.Events.Progression;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Assets;
using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Registries;
using BriarQueen.Framework.Services.Destruction;
using BriarQueen.Framework.Services.Puzzles;
using BriarQueen.Framework.Services.Puzzles.Base;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VContainer;
@@ -33,6 +32,7 @@ namespace BriarQueen.Framework.Managers.Levels
private readonly SaveManager _saveManager;
private UniTask<bool> _activeLoadTask = UniTask.FromResult(false);
private bool _isLoadInProgress;
private BaseLevel _currentLevel;
public bool Initialized { get; private set; }
@@ -57,7 +57,9 @@ namespace BriarQueen.Framework.Managers.Levels
public void Initialize()
{
if (Initialized)
{
return;
}
Debug.Log($"[{nameof(LevelManager)}] Initializing...");
_saveManager.OnSaveRequested += OnSaveGameRequested;
@@ -70,7 +72,9 @@ namespace BriarQueen.Framework.Managers.Levels
public void Dispose()
{
if (!Initialized)
{
return;
}
_saveManager.OnSaveRequested -= OnSaveGameRequested;
_eventCoordinator.Unsubscribe<UpdateHintProgressEvent>(OnHintStageUpdated);
@@ -81,10 +85,14 @@ namespace BriarQueen.Framework.Managers.Levels
private void OnHintStageUpdated(UpdateHintProgressEvent evt)
{
if (_currentLevel == null || evt == null)
{
return;
}
if (!string.Equals(evt.LevelID, _currentLevel.LevelID, StringComparison.Ordinal))
{
return;
}
var incoming = Mathf.Max(0, evt.Stage);
@@ -101,7 +109,9 @@ namespace BriarQueen.Framework.Managers.Levels
private void OnSaveGameRequested(SaveGame saveGame)
{
if (saveGame == null || _currentLevel == null)
{
return;
}
saveGame.CurrentLevelID = _currentLevel.LevelID;
saveGame.CurrentSceneID = _currentLevel.SceneID;
@@ -120,12 +130,73 @@ namespace BriarQueen.Framework.Managers.Levels
lock (_lock)
{
_activeLoadTask = LoadLevelInternal(levelAssetID);
if (_isLoadInProgress)
{
Debug.LogWarning(
$"[LevelManager] LoadLevel('{levelAssetID}') requested while another level load is already in progress. Returning the active load task.");
return _activeLoadTask;
}
_isLoadInProgress = true;
_activeLoadTask = LoadLevelTracked(levelAssetID);
return _activeLoadTask;
}
}
private async UniTask<bool> LoadLevelTracked(string levelAssetID)
{
try
{
return await LoadLevelInternal(levelAssetID);
}
finally
{
lock (_lock)
{
_isLoadInProgress = false;
_activeLoadTask = UniTask.FromResult(false);
}
}
}
private async UniTask<bool> LoadLevelInternal(string levelAssetID)
{
var previousLevelId = _currentLevel != null ? _currentLevel.LevelID : null;
_eventCoordinator.PublishImmediate(new FadeEvent(false, LEVEL_FADE_DURATION));
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
if (_currentLevel != null)
{
await UnloadLevelInternal();
}
if (await TryLoadLevelCore(levelAssetID))
{
return true;
}
if (!string.IsNullOrWhiteSpace(previousLevelId) &&
!string.Equals(previousLevelId, levelAssetID, StringComparison.Ordinal))
{
Debug.LogWarning(
$"[LevelManager] Failed to load '{levelAssetID}'. Attempting recovery by reloading previous level '{previousLevelId}'.");
if (await TryLoadLevelCore(previousLevelId))
{
Debug.LogWarning(
$"[LevelManager] Recovery succeeded by reloading '{previousLevelId}'.");
return false;
}
}
_eventCoordinator.PublishImmediate(new FadeEvent(true, LEVEL_FADE_DURATION));
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
return false;
}
private async UniTask<bool> TryLoadLevelCore(string levelAssetID)
{
try
{
@@ -141,12 +212,6 @@ namespace BriarQueen.Framework.Managers.Levels
return false;
}
_eventCoordinator.PublishImmediate(new FadeEvent(false, LEVEL_FADE_DURATION));
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
if (_currentLevel != null)
await UnloadLevelInternal();
var levelObj = await _addressableManager.InstantiateAsync(levelRef);
if (levelObj == null)
{
@@ -157,7 +222,8 @@ namespace BriarQueen.Framework.Managers.Levels
var level = levelObj.GetComponent<BaseLevel>();
if (level == null)
{
Debug.LogError($"[LevelManager] Instantiated level '{levelAssetID}' has no BaseLevel component. Destroying instance.");
Debug.LogError(
$"[LevelManager] Instantiated level '{levelAssetID}' has no BaseLevel component. Destroying instance.");
await _destructionService.Destroy(levelObj);
return false;
}
@@ -169,8 +235,7 @@ namespace BriarQueen.Framework.Managers.Levels
await _currentLevel.PostLoad();
if (_currentLevel is BasePuzzle puzzle)
await _puzzleService.LoadPuzzle(puzzle);
await _puzzleService.LoadPuzzles(_currentLevel.Puzzles);
_eventCoordinator.Publish(new LevelChangedEvent(_currentLevel));
@@ -178,7 +243,9 @@ namespace BriarQueen.Framework.Managers.Levels
await UniTask.Delay(TimeSpan.FromSeconds(LEVEL_FADE_DURATION));
if (_currentLevel != null)
{
await _currentLevel.PostActivate();
}
_eventCoordinator.Publish(new RequestGameSaveEvent());
return true;
@@ -186,29 +253,38 @@ namespace BriarQueen.Framework.Managers.Levels
catch (Exception ex)
{
Debug.LogError($"[LevelManager] Exception while loading '{levelAssetID}': {ex}");
if (_currentLevel != null)
{
try
{
await _destructionService.Destroy(_currentLevel.gameObject);
}
catch (Exception destroyEx)
{
Debug.LogWarning($"[LevelManager] Failed to destroy broken level instance: {destroyEx}");
}
_currentLevel = null;
}
await CleanupFailedCurrentLevel();
return false;
}
}
private async UniTask CleanupFailedCurrentLevel()
{
if (_currentLevel == null)
{
return;
}
try
{
await _destructionService.Destroy(_currentLevel.gameObject);
}
catch (Exception destroyEx)
{
Debug.LogWarning($"[LevelManager] Failed to destroy broken level instance: {destroyEx}");
}
finally
{
_currentLevel = null;
}
}
private void RestoreHintStageForCurrentLevel()
{
if (_currentLevel == null)
{
return;
}
var save = _saveManager.CurrentSave;
if (save?.LevelHintStages == null)
@@ -218,23 +294,33 @@ namespace BriarQueen.Framework.Managers.Levels
}
if (save.LevelHintStages.TryGetValue(_currentLevel.LevelID, out var stage))
{
_currentLevel.CurrentLevelHintStage = Mathf.Max(0, stage);
}
else
{
_currentLevel.CurrentLevelHintStage = 0;
}
}
private async UniTask RestoreItemStateForCurrentLevel()
{
if (_currentLevel == null)
{
return;
}
var save = _saveManager.CurrentSave;
if (save == null)
{
return;
}
var interactables = _currentLevel.Pickups;
if (interactables == null || interactables.Count == 0)
{
return;
}
foreach (var interactable in interactables)
{
@@ -245,10 +331,14 @@ namespace BriarQueen.Framework.Managers.Levels
}
if (save.CollectedItems.Any(x => x.UniqueIdentifier == interactable.ItemData.UniqueID))
{
await _destructionService.Destroy(interactable.gameObject);
}
if (save.RemovedItems.Any(x => x.UniqueIdentifier == interactable.ItemData.UniqueID))
{
await _destructionService.Destroy(interactable.gameObject);
}
}
var codexTriggers = _currentLevel.CodexTriggers;
@@ -258,7 +348,9 @@ namespace BriarQueen.Framework.Managers.Levels
if (save.DiscoveredCodexEntries.Any(x => x.UniqueIdentifier == trigger.Entry.UniqueID))
{
if (trigger.RemoveTrigger)
{
await _destructionService.Destroy(trigger.gameObject);
}
}
}
}
@@ -268,7 +360,9 @@ namespace BriarQueen.Framework.Managers.Levels
lock (_lock)
{
if (_activeLoadTask.Status == UniTaskStatus.Pending)
{
return _activeLoadTask.ContinueWith(_ => UnloadLevelInternal());
}
return UnloadLevelInternal();
}
@@ -277,15 +371,16 @@ namespace BriarQueen.Framework.Managers.Levels
private async UniTask UnloadLevelInternal()
{
if (_currentLevel == null)
{
return;
}
var level = _currentLevel;
_currentLevel = null;
try
{
if (level is BasePuzzle puzzle)
await _puzzleService.SavePuzzle(puzzle);
await _puzzleService.SavePuzzles(level.Puzzles);
_eventCoordinator.Publish(new RequestGameSaveEvent());
await level.PreUnload();
@@ -298,4 +393,4 @@ namespace BriarQueen.Framework.Managers.Levels
await _destructionService.Destroy(level.gameObject);
}
}
}
}

View File

@@ -7,10 +7,22 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
{
public class Codex
{
public Codex(bool unlocked = false)
{
CodexUnlocked = unlocked;
}
public bool CodexUnlocked { get; private set; }
private readonly List<CodexEntrySo> _entries = new();
public IReadOnlyList<CodexEntrySo> Entries => _entries;
public void UnlockCodex()
{
CodexUnlocked = true;
}
public void AddEntry(CodexEntrySo entry)
{
if (entry == null)
@@ -66,7 +78,7 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
public IEnumerable<CodexEntrySo> GetBookEntries()
{
return GetEntriesByType(CodexType.BookEntry);
return GetEntriesByType(CodexType.DocumentEntry);
}
public IEnumerable<CodexEntrySo> GetPuzzleClues()

View File

@@ -1,3 +1,8 @@
using System.Collections.Generic;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.UI;
using Cysharp.Threading.Tasks;
@@ -28,6 +33,12 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
{
if (!CheckEmptyHands())
return;
if (!PlayerManager.CodexUnlocked())
{
EventCoordinator.PublishImmediate(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CodexLocked)));
return;
}
PlayerManager.UnlockCodexEntry(_codexEntry);
@@ -36,5 +47,20 @@ namespace BriarQueen.Framework.Managers.Player.Data.Codex
await Remove();
}
}
protected override void UpdateSaveGameOnRemoval()
{
var save = SaveManager.CurrentSave;
Debug.Log($"[Base Item] Found save - {save.SaveFileName}");
save.RemovedItems ??= new List<ItemSaveData>();
save.RemovedItems.Add(new ItemSaveData
{
UniqueIdentifier = _codexEntry.UniqueID
});
EventCoordinator.PublishImmediate(new RequestGameSaveEvent());
}
}
}

View File

@@ -15,7 +15,7 @@ namespace BriarQueen.Framework.Managers.Player.Data
[Header("Codex ID")]
[SerializeField]
[ShowIf(nameof(IsBookEntry))]
private BookEntryID _bookEntryID;
private DocumentEntryID _documentEntryID;
[SerializeField]
[ShowIf(nameof(IsPuzzleClue))]
@@ -66,11 +66,11 @@ namespace BriarQueen.Framework.Managers.Player.Data
public CodexType EntryType => _codexType;
public Location Location => _location;
public bool IsBookEntry => _codexType == CodexType.BookEntry;
public bool IsBookEntry => _codexType == CodexType.DocumentEntry;
public bool IsPuzzleClue => _codexType == CodexType.PuzzleClue;
public bool IsPhoto => _codexType == CodexType.Photo;
public BookEntryID BookEntryID => _bookEntryID;
public DocumentEntryID DocumentEntryID => _documentEntryID;
public ClueEntryID ClueEntryID => _clueEntryID;
public PhotoEntryID PhotoEntryID => _photoEntryID;
@@ -92,8 +92,8 @@ namespace BriarQueen.Framework.Managers.Player.Data
{
return _codexType switch
{
CodexType.BookEntry when _bookEntryID != BookEntryID.None =>
CodexEntryIDs.Get(_bookEntryID),
CodexType.DocumentEntry when _documentEntryID != DocumentEntryID.None =>
CodexEntryIDs.Get(_documentEntryID),
CodexType.PuzzleClue when _clueEntryID != ClueEntryID.None =>
CodexEntryIDs.Get(_clueEntryID),

View File

@@ -7,7 +7,6 @@ using BriarQueen.Framework.Events.Gameplay;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.UI;
using BriarQueen.Framework.Services.Tutorials;
using NUnit.Framework;
using UnityEngine;
namespace BriarQueen.Framework.Managers.Player.Data.Tools

View File

@@ -179,7 +179,7 @@ namespace BriarQueen.Framework.Managers.Player
}
}
_audioManager.Play(AudioNameIdentifiers.Get(SFXKey.ItemCollected));
_audioManager.Play(AudioNameIdentifiers.Get(SFXKey.ItemPickup));
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
_eventCoordinator.Publish(new InventoryChangedEvent());
}
@@ -208,11 +208,21 @@ namespace BriarQueen.Framework.Managers.Player
#region Codex
public void UnlockCodex() => _codex.UnlockCodex();
public bool CodexUnlocked()
{
return _codex is { CodexUnlocked: true };
}
public void UnlockCodexEntry(string uniqueIdentifier)
{
var entry = _codexRegistry.FindEntryByID(uniqueIdentifier);
if (entry == null)
{
Debug.LogWarning($"[PlayerManager] Could not unlock codex entry '{uniqueIdentifier}'.");
return;
}
UnlockCodexEntry(entry);
}
@@ -243,7 +253,7 @@ namespace BriarQueen.Framework.Managers.Player
}
}
_tutorialService.DisplayTutorial(TutorialPopupID.Codex);
_tutorialService.DisplayTutorial(TutorialPopupID.CodexKeyboard);
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
_eventCoordinator.Publish(new CodexChangedEvent(entry.EntryType));
@@ -382,7 +392,7 @@ namespace BriarQueen.Framework.Managers.Player
private void LoadCodexFromSave(SaveGame save)
{
_codex = new Codex();
_codex = new Codex(save.CodexUnlocked);
if (save.DiscoveredCodexEntries != null)
{
@@ -458,4 +468,4 @@ namespace BriarQueen.Framework.Managers.Player
#endregion
}
}
}

View File

@@ -0,0 +1,7 @@
namespace BriarQueen.Framework.Managers.UI.Base
{
public interface IUIBackHandler
{
bool HandleBackRequest();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 18b01f6ab9d3468ba9a99360acbe4e5c
timeCreated: 1778300000

View File

@@ -57,7 +57,18 @@ namespace BriarQueen.Framework.Managers.UI
private bool _useVirtualCursor;
public CursorStyleEntry CurrentStyleEntry => _styleMap[_currentStyle];
public CursorStyleEntry CurrentStyleEntry
{
get
{
if (TryGetStyleEntry(GetEffectiveStyle(), out var entry))
{
return entry;
}
return default;
}
}
[Inject]
private void Construct(EventCoordinator eventCoordinator)
@@ -192,6 +203,22 @@ namespace BriarQueen.Framework.Managers.UI
return _isStyleOverridden ? _currentStyleOverride : _currentStyle;
}
private bool TryGetStyleEntry(CursorStyle style, out CursorStyleEntry entry)
{
if (_styleMap.TryGetValue(style, out entry))
{
return true;
}
if (_styleMap.TryGetValue(CursorStyle.Default, out entry))
{
return true;
}
entry = default;
return false;
}
private void ApplyVirtualCursorStyle(CursorStyle style)
{
if (_virtualCursorImage == null)
@@ -282,4 +309,4 @@ namespace BriarQueen.Framework.Managers.UI
public Vector2 TooltipOffset;
}
}
}
}

View File

@@ -6,6 +6,7 @@ using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Extensions;
using BriarQueen.Framework.Managers.Interaction;
using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Managers.Player;
using BriarQueen.Framework.Managers.UI.Base;
using BriarQueen.Framework.Managers.UI.Events;
using BriarQueen.Framework.Services.Settings;
@@ -28,6 +29,7 @@ namespace BriarQueen.Framework.Managers.UI
private readonly InteractManager _interactManager;
private readonly SaveManager _saveManager;
private readonly SettingsService _settingsService;
private readonly PlayerManager _playerManager;
private readonly Dictionary<WindowType, IUIWindow> _windows = new();
private readonly Stack<IUIWindow> _windowStack = new();
@@ -46,12 +48,14 @@ namespace BriarQueen.Framework.Managers.UI
EventCoordinator eventCoordinator,
InteractManager interactManager,
SettingsService settingsService,
SaveManager saveManager)
SaveManager saveManager,
PlayerManager playerManager)
{
_eventCoordinator = eventCoordinator;
_interactManager = interactManager;
_settingsService = settingsService;
_saveManager = saveManager;
_playerManager = playerManager;
}
private IUIWindow ActiveWindow => _windowStack.Count > 0 ? _windowStack.Peek() : null;
@@ -84,6 +88,7 @@ namespace BriarQueen.Framework.Managers.UI
private void SubscribeToEvents()
{
_eventCoordinator.Subscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
_eventCoordinator.Subscribe<UIBackRequestedEvent>(OnBackRequested);
_eventCoordinator.Subscribe<ToggleCodexEvent>(ToggleCodexWindow);
_eventCoordinator.Subscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
_eventCoordinator.Subscribe<FadeEvent>(OnFadeEvent);
@@ -97,6 +102,7 @@ namespace BriarQueen.Framework.Managers.UI
private void UnsubscribeFromEvents()
{
_eventCoordinator.Unsubscribe<PauseButtonClickedEvent>(OnPauseClickReceived);
_eventCoordinator.Unsubscribe<UIBackRequestedEvent>(OnBackRequested);
_eventCoordinator.Unsubscribe<ToggleCodexEvent>(ToggleCodexWindow);
_eventCoordinator.Unsubscribe<UIToggleSettingsWindow>(ToggleSettingsWindow);
_eventCoordinator.Unsubscribe<FadeEvent>(OnFadeEvent);
@@ -175,13 +181,18 @@ namespace BriarQueen.Framework.Managers.UI
{
if (_windowStack.Count > 0)
{
CloseTopWindow();
TryHandleBackRequest();
return;
}
OpenWindow(WindowType.PauseMenuWindow);
}
private void OnBackRequested(UIBackRequestedEvent _)
{
TryHandleBackRequest();
}
private void ToggleSettingsWindow(UIToggleSettingsWindow eventData)
{
if (eventData.Show)
@@ -192,6 +203,9 @@ namespace BriarQueen.Framework.Managers.UI
private void ToggleCodexWindow(ToggleCodexEvent eventData)
{
if(!_playerManager.CodexUnlocked())
return;
if (eventData.Shown)
OpenWindow(WindowType.CodexWindow);
else
@@ -231,7 +245,7 @@ namespace BriarQueen.Framework.Managers.UI
{
return codexType switch
{
CodexType.BookEntry => "You've acquired a new document.",
CodexType.DocumentEntry => "You've acquired a new document.",
CodexType.PuzzleClue => "You've acquired a new puzzle clue.",
CodexType.Photo => "You've acquired a new photo.",
_ => string.Empty
@@ -246,10 +260,14 @@ namespace BriarQueen.Framework.Managers.UI
if (!_settingsService.AreTutorialsEnabled())
return;
var duration = 3f;
var tutorialText = TutorialPopupTexts.AllPopups[eventData.TutorialID];
if (string.IsNullOrWhiteSpace(eventData.ResolvedText))
{
Debug.LogWarning($"[UIManager] Empty resolved text for tutorial '{eventData.TutorialID}'.");
return;
}
_tutorialPopup.Play(tutorialText, duration).Forget();
var duration = _settingsService?.Game?.PopupDisplayDuration ?? 3f;
_tutorialPopup.Play(eventData.ResolvedText, duration).Forget();
}
private void OnDisplayInteractText(DisplayInteractEvent eventData)
@@ -340,6 +358,21 @@ namespace BriarQueen.Framework.Managers.UI
CloseTopWindowInternal().Forget();
}
private void TryHandleBackRequest()
{
if (_disposed || _windowStack.Count == 0)
{
return;
}
if (ActiveWindow is IUIBackHandler backHandler && backHandler.HandleBackRequest())
{
return;
}
CloseTopWindow();
}
private async UniTask CloseTopWindowInternal()
{
if (_disposed || _windowStack.Count == 0)
@@ -431,4 +464,4 @@ namespace BriarQueen.Framework.Managers.UI
_eventCoordinator.Publish(new UIStackChangedEvent(_windowStack.Count > 0));
}
}
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Managers.Achievements.Data;
using UnityEngine;
@@ -19,8 +18,22 @@ namespace BriarQueen.Framework.Registries
if (_achievementDictionary != null)
return;
_achievementDictionary = _achievementSos.ToDictionary(achievement => achievement.Achievement,
achievement => achievement);
RebuildLookup();
}
private void RebuildLookup()
{
_achievementDictionary = new Dictionary<AchievementID, AchievementSo>();
RegistryLookupBuilder.AddEntries(
_achievementDictionary,
_achievementSos,
this,
nameof(AchievementRegistry),
"Achievements",
nameof(AchievementSo.Achievement),
entry => entry.Achievement,
entry => entry);
}
public bool TryGetAchievement(AchievementID identifier, out AchievementSo achievement)
@@ -35,4 +48,4 @@ namespace BriarQueen.Framework.Registries
return _achievementDictionary.Values;
}
}
}
}

View File

@@ -45,47 +45,57 @@ namespace BriarQueen.Framework.Registries
{
_assetDictionary = new Dictionary<string, AssetReference>();
AddEntries(_sceneReferences, "Scenes");
AddEntries(_levelReferences, "Levels");
AddEntries(_itemReferences, "Items");
AddEntries(_uiReferences, "UI");
}
RegistryLookupBuilder.AddEntries(
_assetDictionary,
_sceneReferences,
this,
nameof(AssetRegistry),
"Scenes",
"AssetKey",
entry => entry.AssetKey,
entry => entry.Asset,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.AssetKey),
entry => entry.Asset != null,
"AssetReference is null");
private void AddEntries(List<AssetEntry> entries, string category)
{
if (entries == null)
return;
RegistryLookupBuilder.AddEntries(
_assetDictionary,
_levelReferences,
this,
nameof(AssetRegistry),
"Levels",
"AssetKey",
entry => entry.AssetKey,
entry => entry.Asset,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.AssetKey),
entry => entry.Asset != null,
"AssetReference is null");
foreach (var entry in entries)
{
if (entry == null)
continue;
RegistryLookupBuilder.AddEntries(
_assetDictionary,
_itemReferences,
this,
nameof(AssetRegistry),
"Items",
"AssetKey",
entry => entry.AssetKey,
entry => entry.Asset,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.AssetKey),
entry => entry.Asset != null,
"AssetReference is null");
if (string.IsNullOrWhiteSpace(entry.AssetKey))
{
Debug.LogWarning(
$"[AssetRegistry] Skipping {category} entry '{entry.name}' because AssetKey is empty.", this);
continue;
}
if (entry.Asset == null)
{
Debug.LogWarning(
$"[AssetRegistry] Skipping {category} entry '{entry.name}' because AssetReference is null.",
this);
continue;
}
if (_assetDictionary.ContainsKey(entry.AssetKey))
{
Debug.LogError(
$"[AssetRegistry] Duplicate AssetKey detected: '{entry.AssetKey}' from entry '{entry.name}'.",
this);
continue;
}
_assetDictionary.Add(entry.AssetKey, entry.Asset);
}
RegistryLookupBuilder.AddEntries(
_assetDictionary,
_uiReferences,
this,
nameof(AssetRegistry),
"UI",
"AssetKey",
entry => entry.AssetKey,
entry => entry.Asset,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.AssetKey),
entry => entry.Asset != null,
"AssetReference is null");
}
public bool TryGetReference(string key, out AssetReference reference)
@@ -116,4 +126,4 @@ namespace BriarQueen.Framework.Registries
}
#endif
}
}
}

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using BriarQueen.Framework.Managers.Audio.Data;
using NaughtyAttributes;
using UnityEngine;
@@ -17,15 +16,32 @@ namespace BriarQueen.Framework.Registries
private void EnsureInitialized()
{
if (_audioFileDict == null)
_audioFileDict = _audioFiles.ToDictionary(entry => entry.UniqueID, entry => entry);
if (_audioFileDict != null)
return;
RebuildLookup();
}
private void RebuildLookup()
{
_audioFileDict = new Dictionary<string, AudioFileSo>();
RegistryLookupBuilder.AddEntries(
_audioFileDict,
_audioFiles,
this,
nameof(AudioRegistry),
"Audio Files",
"UniqueID",
entry => entry.UniqueID,
entry => entry,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
}
public bool TryGetAudio(string audioName, out AudioFileSo audioFile)
{
if (_audioFileDict == null) EnsureInitialized();
return _audioFileDict!.TryGetValue(audioName, out audioFile);
EnsureInitialized();
return _audioFileDict.TryGetValue(audioName, out audioFile);
}
}
}
}

View File

@@ -35,39 +35,38 @@ namespace BriarQueen.Framework.Registries
{
_entryLookup = new Dictionary<string, CodexEntrySo>();
AddEntries(_bookEntries, "Book Entries");
AddEntries(_puzzleClues, "Puzzle Clues");
AddEntries(_photoEntries, "Photo Entries");
}
RegistryLookupBuilder.AddEntries(
_entryLookup,
_bookEntries,
this,
nameof(CodexRegistry),
"Book Entries",
"UniqueID",
entry => entry.UniqueID,
entry => entry,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
private void AddEntries(List<CodexEntrySo> entries, string category)
{
if (entries == null)
return;
RegistryLookupBuilder.AddEntries(
_entryLookup,
_puzzleClues,
this,
nameof(CodexRegistry),
"Puzzle Clues",
"UniqueID",
entry => entry.UniqueID,
entry => entry,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
foreach (var entry in entries)
{
if (entry == null)
continue;
if (string.IsNullOrWhiteSpace(entry.UniqueID))
{
Debug.LogWarning(
$"[CodexRegistry] Skipping {category} entry '{entry.name}' because UniqueID is empty.",
this);
continue;
}
if (_entryLookup.ContainsKey(entry.UniqueID))
{
Debug.LogError(
$"[CodexRegistry] Duplicate UniqueID detected: '{entry.UniqueID}' from entry '{entry.name}'.",
this);
continue;
}
_entryLookup.Add(entry.UniqueID, entry);
}
RegistryLookupBuilder.AddEntries(
_entryLookup,
_photoEntries,
this,
nameof(CodexRegistry),
"Photo Entries",
"UniqueID",
entry => entry.UniqueID,
entry => entry,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
}
public bool TryGetEntry(string entryID, out CodexEntrySo entry)
@@ -130,4 +129,4 @@ namespace BriarQueen.Framework.Registries
}
#endif
}
}
}

View File

@@ -35,39 +35,38 @@ namespace BriarQueen.Framework.Registries
{
_entryLookup = new Dictionary<string, ItemDataSo>();
AddEntries(_puzzleSlots, "Puzzle Slots");
AddEntries(_pickupItems, "Pickup Items");
AddEntries(_environmentInteractables, "Environment Interactables");
}
RegistryLookupBuilder.AddEntries(
_entryLookup,
_puzzleSlots,
this,
nameof(ItemRegistry),
"Puzzle Slots",
"UniqueID",
entry => entry.UniqueID,
entry => entry,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
private void AddEntries(List<ItemDataSo> entries, string category)
{
if (entries == null)
return;
RegistryLookupBuilder.AddEntries(
_entryLookup,
_pickupItems,
this,
nameof(ItemRegistry),
"Pickup Items",
"UniqueID",
entry => entry.UniqueID,
entry => entry,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
foreach (var entry in entries)
{
if (entry == null)
continue;
if (string.IsNullOrWhiteSpace(entry.UniqueID))
{
Debug.LogWarning(
$"[ItemRegistry] Skipping {category} entry '{entry.name}' because UniqueID is empty.",
this);
continue;
}
if (_entryLookup.ContainsKey(entry.UniqueID))
{
Debug.LogError(
$"[ItemRegistry] Duplicate UniqueID detected: '{entry.UniqueID}' from entry '{entry.name}'.",
this);
continue;
}
_entryLookup.Add(entry.UniqueID, entry);
}
RegistryLookupBuilder.AddEntries(
_entryLookup,
_environmentInteractables,
this,
nameof(ItemRegistry),
"Environment Interactables",
"UniqueID",
entry => entry.UniqueID,
entry => entry,
entry => RegistryLookupBuilder.HasNonEmptyKey(entry.UniqueID));
}
public bool TryGetEntry(string itemID, out ItemDataSo entry)
@@ -130,4 +129,4 @@ namespace BriarQueen.Framework.Registries
}
#endif
}
}
}

View File

@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace BriarQueen.Framework.Registries
{
internal static class RegistryLookupBuilder
{
public static void AddEntries<TKey, TEntry, TValue>(
Dictionary<TKey, TValue> lookup,
IEnumerable<TEntry> entries,
UnityEngine.Object context,
string registryName,
string category,
string keyLabel,
Func<TEntry, TKey> keySelector,
Func<TEntry, TValue> valueSelector,
Func<TEntry, bool> isKeyValid = null,
Func<TEntry, bool> isEntryValid = null,
string invalidEntryReason = null)
where TEntry : UnityEngine.Object
{
if (lookup == null)
throw new ArgumentNullException(nameof(lookup));
if (entries == null)
return;
foreach (var entry in entries)
{
if (!entry)
continue;
if (isKeyValid != null && !isKeyValid(entry))
{
Debug.LogWarning(
$"[{registryName}] Skipping {category} entry '{entry.name}' because {keyLabel} is invalid.",
context);
continue;
}
if (isEntryValid != null && !isEntryValid(entry))
{
Debug.LogWarning(
$"[{registryName}] Skipping {category} entry '{entry.name}' because {invalidEntryReason}.",
context);
continue;
}
var key = keySelector(entry);
if (lookup.ContainsKey(key))
{
Debug.LogError(
$"[{registryName}] Duplicate {keyLabel} detected: '{key}' from entry '{entry.name}'.",
context);
continue;
}
lookup.Add(key, valueSelector(entry));
}
}
public static bool HasNonEmptyKey(string key)
{
return !string.IsNullOrWhiteSpace(key);
}
}
}

View File

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

View File

@@ -1,5 +1,5 @@
using System;
using BriarQueen.Framework.Assets;
using BriarQueen.Framework.Managers.Assets;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VContainer;

View File

@@ -1,19 +1,22 @@
using System;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Assets;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Assets;
using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Managers.Levels;
using BriarQueen.Framework.Registries;
using Cysharp.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
using UnityEngine.SceneManagement;
using VContainer;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace BriarQueen.Framework.Services.Game
{
public class GameService
@@ -75,7 +78,7 @@ namespace BriarQueen.Framework.Services.Game
_eventCoordinator.PublishImmediate(new FadeEvent(false, fadeDuration));
await UniTask.Delay(TimeSpan.FromSeconds(fadeDuration));
await UnloadGameSceneIfLoaded();
await PrepareToLeaveGameplayScene();
if (_assetRegistry == null ||
!_assetRegistry.TryGetReference(
@@ -193,6 +196,12 @@ namespace BriarQueen.Framework.Services.Game
}
}
private async UniTask PrepareToLeaveGameplayScene()
{
await _levelManager.UnloadLevel();
await UnloadGameSceneIfLoaded();
}
public async UniTask SwapGameSceneHandle(AsyncOperationHandle<SceneInstance> nextSceneHandle)
{
if (!nextSceneHandle.IsValid())
@@ -208,7 +217,10 @@ namespace BriarQueen.Framework.Services.Game
}
if (_gameSceneHandle.IsValid())
{
await _levelManager.UnloadLevel();
await _addressableManager.UnloadSceneAsync(_gameSceneHandle);
}
_gameSceneHandle = nextSceneHandle;
SceneManager.SetActiveScene(nextSceneHandle.Result.Scene);
@@ -223,4 +235,4 @@ namespace BriarQueen.Framework.Services.Game
#endif
}
}
}
}

View File

@@ -1,42 +1,44 @@
// ==============================
// PuzzleBase.cs (updated)
// ==============================
using System.Collections.Generic;
using BriarQueen.Framework.Assets;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Managers.Assets;
using BriarQueen.Framework.Managers.Audio;
using BriarQueen.Framework.Managers.Hints.Data;
using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Registries;
using BriarQueen.Framework.Services.Destruction;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VContainer;
namespace BriarQueen.Framework.Services.Puzzles.Base
{
public abstract class BasePuzzle : BaseLevel
public abstract class BasePuzzle : MonoBehaviour
{
protected AddressableManager AddressableManager;
protected AssetRegistry AssetRegistry;
protected AudioManager AudioManager;
protected DestructionService DestructionService;
protected EventCoordinator EventCoordinator;
protected ItemRegistry ItemRegistry;
protected PuzzleService PuzzleService;
protected SaveManager SaveManager;
public abstract string PuzzleID { get; }
public override bool IsPuzzleLevel => true;
// BaseLevel still requires these.
public abstract override string LevelName { get; }
public abstract override Dictionary<int, BaseHint> Hints { get; }
public abstract UniTask CompletePuzzle();
public virtual UniTask PostLoad()
{
return UniTask.CompletedTask;
}
public virtual UniTask PreUnload()
{
return UniTask.CompletedTask;
}
[Inject]
public void Construct(EventCoordinator eventCoordinator, AudioManager audioManager,
SaveManager saveManager, ItemRegistry itemRegistry, AddressableManager addressableManager,
AssetRegistry assetRegistry, PuzzleService puzzleService)
AssetRegistry assetRegistry, PuzzleService puzzleService, DestructionService destructionService)
{
EventCoordinator = eventCoordinator;
AudioManager = audioManager;
@@ -45,6 +47,7 @@ namespace BriarQueen.Framework.Services.Puzzles.Base
AddressableManager = addressableManager;
AssetRegistry = assetRegistry;
PuzzleService = puzzleService;
DestructionService = destructionService;
}
}
}
}

View File

@@ -14,7 +14,7 @@ namespace BriarQueen.Framework.Services.Puzzles
{
private readonly SaveManager _saveManager;
private BasePuzzle _currentBasePuzzle;
private readonly Dictionary<string, BasePuzzle> _activePuzzles = new();
private bool _isWritingState;
[Inject]
@@ -31,40 +31,98 @@ namespace BriarQueen.Framework.Services.Puzzles
public async UniTask LoadPuzzle(BasePuzzle basePuzzle)
{
_currentBasePuzzle = basePuzzle;
if (_currentBasePuzzle == null)
if (basePuzzle == null)
{
return;
}
await TryRestorePuzzleState(_currentBasePuzzle);
if (string.IsNullOrWhiteSpace(basePuzzle.PuzzleID))
{
Debug.LogWarning($"[PuzzleService] Cannot load puzzle '{basePuzzle.name}' with null/empty PuzzleID.");
return;
}
_activePuzzles[basePuzzle.PuzzleID] = basePuzzle;
await basePuzzle.PostLoad();
await TryRestorePuzzleState(basePuzzle);
}
public async UniTask LoadPuzzles(IEnumerable<BasePuzzle> basePuzzles)
{
_activePuzzles.Clear();
if (basePuzzles == null)
{
return;
}
foreach (var basePuzzle in basePuzzles)
{
await LoadPuzzle(basePuzzle);
}
}
public async UniTask SavePuzzle(BasePuzzle basePuzzle)
{
if (basePuzzle == null)
{
return;
}
if (_currentBasePuzzle != null && basePuzzle != _currentBasePuzzle)
if (!string.IsNullOrWhiteSpace(basePuzzle.PuzzleID) &&
_activePuzzles.TryGetValue(basePuzzle.PuzzleID, out var activePuzzle) &&
activePuzzle != basePuzzle)
{
return;
}
await SavePuzzleState(basePuzzle, flushToDisk: true);
_currentBasePuzzle = null;
await basePuzzle.PreUnload();
if (!string.IsNullOrWhiteSpace(basePuzzle.PuzzleID))
{
_activePuzzles.Remove(basePuzzle.PuzzleID);
}
}
public async UniTask SavePuzzles(IEnumerable<BasePuzzle> basePuzzles)
{
if (basePuzzles != null)
{
foreach (var basePuzzle in basePuzzles)
{
await SavePuzzle(basePuzzle);
}
}
_activePuzzles.Clear();
}
private async UniTask OnBeforeSaveRequestedAsync()
{
if (_currentBasePuzzle == null)
if (_activePuzzles.Count == 0)
{
return;
}
await SavePuzzleState(_currentBasePuzzle, flushToDisk: false);
foreach (var basePuzzle in _activePuzzles.Values.ToList())
{
await SavePuzzleState(basePuzzle, flushToDisk: false);
}
}
private async UniTask TryRestorePuzzleState(BasePuzzle basePuzzle)
{
if (basePuzzle == null || _saveManager.CurrentSave == null)
{
return;
}
if (basePuzzle is not IPuzzleStateful stateful)
{
return;
}
var save = _saveManager.CurrentSave;
var entry = save.PuzzleStates?.FirstOrDefault(x => x != null && x.PuzzleID == basePuzzle.PuzzleID);
@@ -83,20 +141,28 @@ namespace BriarQueen.Framework.Services.Puzzles
private async UniTask SavePuzzleState(BasePuzzle basePuzzle, bool flushToDisk)
{
if (basePuzzle == null || _saveManager.CurrentSave == null)
{
return;
}
if (basePuzzle is not IPuzzleStateful stateful)
{
return;
}
if (_isWritingState)
{
return;
}
_isWritingState = true;
try
{
if (basePuzzle is IPuzzleWorldStateSync worldStateSync)
{
worldStateSync.SyncWorldStateToSave();
}
var save = _saveManager.CurrentSave;
save.PuzzleStates ??= new List<PuzzleStateSaveData>();
@@ -123,7 +189,9 @@ namespace BriarQueen.Framework.Services.Puzzles
existing.Completed = stateful.IsCompleted;
if (flushToDisk)
{
await _saveManager.SaveGameDataLatest();
}
}
finally
{
@@ -131,4 +199,4 @@ namespace BriarQueen.Framework.Services.Puzzles
}
}
}
}
}

View File

@@ -9,9 +9,6 @@ namespace BriarQueen.Framework.Services.Settings.Data
public float MusicVolume;
public float SfxVolume;
public float VoiceVolume;
public float AmbienceVolume;
public float UIVolume;
public bool MuteWhenUnfocused;
public AudioSettings()
{
@@ -19,9 +16,6 @@ namespace BriarQueen.Framework.Services.Settings.Data
MusicVolume = 0.75f; // 75%
SfxVolume = 0.75f; // 75%
VoiceVolume = 1.0f; // 100%
AmbienceVolume = 0.75f; // 75%
UIVolume = 0.5f; // 50%
MuteWhenUnfocused = false;
}
}
}

View File

@@ -89,7 +89,6 @@ namespace BriarQueen.Framework.Services.Settings
AudioMixerGroups.MASTER_GROUP => Audio.MasterVolume,
AudioMixerGroups.MUSIC_GROUP => Audio.MusicVolume,
AudioMixerGroups.SFX_GROUP => Audio.SfxVolume,
AudioMixerGroups.UI_GROUP => Audio.UIVolume,
AudioMixerGroups.VOICE_GROUP => Audio.VoiceVolume,
_ => Audio.MasterVolume
};
@@ -111,10 +110,6 @@ namespace BriarQueen.Framework.Services.Settings
_audioManager.SetVolume(AudioMixerParameters.MUSIC_VOLUME, a.MusicVolume);
_audioManager.SetVolume(AudioMixerParameters.SFX_VOLUME, a.SfxVolume);
_audioManager.SetVolume(AudioMixerParameters.VOICE_VOLUME, a.VoiceVolume);
_audioManager.SetVolume(AudioMixerParameters.AMBIENCE_VOLUME, a.AmbienceVolume);
_audioManager.SetVolume(AudioMixerParameters.UI_VOLUME, a.UIVolume);
Application.runInBackground = !a.MuteWhenUnfocused;
}
private void ApplyVisual(VisualSettings v)

View File

@@ -2,8 +2,11 @@ using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Events.Save;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Input;
using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Services.Settings;
using System.Text.RegularExpressions;
using UnityEngine.InputSystem;
using VContainer;
namespace BriarQueen.Framework.Services.Tutorials
@@ -11,23 +14,26 @@ namespace BriarQueen.Framework.Services.Tutorials
public class TutorialService
{
private readonly EventCoordinator _eventCoordinator;
private readonly SettingsService _settingsService;
private readonly SaveManager _saveManager;
private readonly SettingsService _settingsService;
private readonly SaveManager _saveManager;
private readonly InputManager _inputManager;
[Inject]
public TutorialService(
EventCoordinator eventCoordinator,
SettingsService settingsService,
SaveManager saveManager)
SettingsService settingsService,
SaveManager saveManager,
InputManager inputManager)
{
_eventCoordinator = eventCoordinator;
_settingsService = settingsService;
_saveManager = saveManager;
_settingsService = settingsService;
_saveManager = saveManager;
_inputManager = inputManager;
}
public void DisplayTutorial(TutorialPopupID tutorialPopupID)
{
var save = _saveManager.CurrentSave;
var save = _saveManager.CurrentSave;
var tutorialVars = save?.PersistentVariables?.TutorialPopupVariables;
if (tutorialVars == null)
@@ -39,9 +45,60 @@ namespace BriarQueen.Framework.Services.Tutorials
tutorialVars.MarkDisplayed(tutorialPopupID);
if (_settingsService.AreTutorialsEnabled())
_eventCoordinator.Publish(new DisplayTutorialPopupEvent(tutorialPopupID));
{
var resolvedText = ResolveText(tutorialPopupID);
_eventCoordinator.Publish(new DisplayTutorialPopupEvent(tutorialPopupID, resolvedText));
}
_eventCoordinator.PublishImmediate(new RequestGameSaveEvent());
}
// ── Text resolution ───────────────────────────────────────────
/// <summary>
/// Resolves {ActionName} tokens in the tutorial text for the given ID
/// to the current binding display string for that action.
/// Hotswap-safe — reads the current control scheme at call time.
/// </summary>
public string ResolveText(TutorialPopupID id)
{
if (!TutorialPopupTexts.AllPopups.TryGetValue(id, out var template))
return string.Empty;
return ResolveText(template);
}
/// <summary>
/// Resolves {ActionName} tokens in an arbitrary string to the current
/// binding display string for that action.
/// </summary>
public string ResolveText(string template)
{
if (string.IsNullOrWhiteSpace(template))
return template;
return Regex.Replace(template, @"\{(\w+)\}", match =>
{
var actionName = match.Groups[1].Value;
var binding = GetBindingDisplayString(actionName);
return string.IsNullOrWhiteSpace(binding) ? match.Value : binding;
});
}
/// <summary>
/// Returns the display string for a named action's current binding,
/// matched to the active control scheme.
/// </summary>
public string GetBindingDisplayString(string actionName)
{
if (_inputManager == null) return string.Empty;
var action = _inputManager.GetAction(actionName);
if (action == null) return string.Empty;
var displayString = action.GetBindingDisplayString(group: _inputManager.CurrentControlScheme);
return string.IsNullOrWhiteSpace(displayString) ? string.Empty : displayString;
}
}
}

View File

@@ -148,6 +148,7 @@ namespace BriarQueen.Game.Cinematics
if (_cinematicCanvasGroup == null || _imageA == null || _imageB == null)
{
Debug.LogWarning("[BaseCinematic] Missing CanvasGroup or Images.");
await EndCinematicFlow();
return;
}

View File

@@ -1,7 +1,7 @@
using System;
using BriarQueen.Data.Identifiers;
using BriarQueen.Framework.Assets;
using BriarQueen.Framework.Coordinators.Events;
using BriarQueen.Framework.Managers.Assets;
using BriarQueen.Framework.Managers.Input;
using BriarQueen.Framework.Managers.IO;
using BriarQueen.Framework.Managers.Levels;
@@ -83,12 +83,12 @@ namespace BriarQueen.Game.Cinematics
_saveManager.CurrentSave.OpeningCinematicPlayed = true;
var levelLoaded = await _levelManager.LoadLevel(
AssetKeyIdentifiers.Get(LevelKey.ChapterOneVillageEdge));
AssetKeyIdentifiers.Get(LevelKey.ChapterOneArrivalRoad));
if (!levelLoaded)
{
Debug.LogError(
"[OpeningCinematic] Failed to load ChapterOneVillageEdge after cinematic. Returning to main menu.");
"[OpeningCinematic] Failed to load Chapter One Arrival Road after cinematic. Returning to main menu.");
await _gameService.LoadMainMenu();
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: be01b5741bc94861a9b41d8c8ce3b5ee
timeCreated: 1773590011

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: ae64d437e6594bf58b5512ee5ffca402
timeCreated: 1773590011

View File

@@ -1,45 +0,0 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
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;
namespace BriarQueen.Game.Items.Environment.ChapterOne.Pumphouse
{
public class PumpHouseWaterValve : BaseItem
{
public override string InteractableName => "Water Valve";
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Interact;
public override UniTask OnInteract(ItemDataSo item = null)
{
if (!CheckEmptyHands())
return UniTask.CompletedTask;
if (item != null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return UniTask.CompletedTask;
}
if (SaveManager.GetLevelFlag(LevelFlag.PumpWaterRestored))
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(LevelInteractKey.WaterValve)));
return UniTask.CompletedTask;
}
if (!SaveManager.GetLevelFlag(LevelFlag.FountainVinesCut))
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(LevelInteractKey.ClearVinesOutside)));
return UniTask.CompletedTask;
}
// TODO : Play Water SFX
SaveManager.SetLevelFlag(LevelFlag.PumpWaterRestored, true);
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 7ffe97b80fed45bbb7152752c4f10685
timeCreated: 1770991451

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 3825afbb1ee941b1af18781d8f1311fa
timeCreated: 1773590034

View File

@@ -1,44 +0,0 @@
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
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;
namespace BriarQueen.Game.Items.Environment.ChapterOne.Village
{
public class ChainLock : BaseItem
{
public override string InteractableName => "Locked Chain";
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Inspect;
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (!CheckEmptyHands())
return;
if (item == null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(LevelInteractKey.PumphouseChain)));
return;
}
if (item.UniqueID != ItemIDs.Pickups[ItemKey.PumphouseKey])
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return;
}
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(LevelInteractKey.UnlockedPumphouse)));
await Remove();
}
protected override UniTask OnRemoved()
{
SaveManager.SetLevelFlag(LevelFlag.PumpHouseOpened, true);
PlayerManager.RemoveItem(ItemIDs.Pickups[ItemKey.PumphouseKey]);
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: bf2e55b12a3040deb4e54465336fc989
timeCreated: 1770985414

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 45144a83888f401e8d282181328521fb
timeCreated: 1773953887

View File

@@ -1,76 +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 BriarQueen.Game.Levels.ChapterOne.VillageStreet;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace BriarQueen.Game.Items.Environment.ChapterOne.VillageStreet
{
public class StreetVines : BaseItem
{
[SerializeField]
private Street _owner;
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Inspect;
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (_owner == null)
{
Debug.LogWarning("StreetVines is missing its Street owner.", this);
return;
}
if (!CanUseKnife())
{
PublishFailureMessage();
return;
}
EventCoordinator.Publish(new DisplayInteractEvent(
InteractEventIDs.Get(EnvironmentInteractKey.UsingKnife)));
await _owner.CutVines();
}
private bool CanUseKnife()
{
if (SettingsService.Game.AutoUseTools)
return PlayerManager.HasAccessToTool(ToolID.Knife);
return PlayerManager.GetEquippedTool() == ToolID.Knife;
}
private void PublishFailureMessage()
{
var autoUseTools = SettingsService != null &&
SettingsService.Game != null &&
SettingsService.Game.AutoUseTools;
var equippedTool = PlayerManager.GetEquippedTool();
string message;
if (equippedTool == ToolID.None)
{
message = InteractEventIDs.Get(LevelInteractKey.CutVines);
}
else if (autoUseTools)
{
// In auto-use mode, reaching this point means the player does not have access
// to the required tool at all, so this should still read like a generic failure.
message = InteractEventIDs.Get(LevelInteractKey.CutVines);
}
else
{
// Auto-use is disabled, and the player has some tool equipped that is not valid.
message = InteractEventIDs.Get(ItemInteractKey.WrongTool);
}
EventCoordinator.Publish(new DisplayInteractEvent(message));
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 32a79fbd751c4216b83680bbc425cfa7
timeCreated: 1773954077

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: d5cf35e464ec407c81e5d29b6bf5c713
timeCreated: 1773590078

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 8c3efb5408364c23bc9f0256bb727fb5
timeCreated: 1773609093

View File

@@ -1,20 +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;
namespace BriarQueen.Game.Items.Environment.ChapterOne.Workshop.Downstairs
{
public class WorkshopBrokenLantern : BaseItem
{
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Inspect;
public override UniTask OnInteract(ItemDataSo item = null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(EnvironmentInteractKey.BrokenLantern)));
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1004a82a9b7f487180b0fcf4978a4446
timeCreated: 1773609258

View File

@@ -1,45 +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 BriarQueen.Game.Levels.ChapterOne.Workshop;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace BriarQueen.Game.Items.Environment.ChapterOne.Workshop.Downstairs
{
public class WorkshopDownstairsLight : BaseItem
{
[SerializeField]
private WorkshopDownstairs _workshopDownstairs;
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Interact;
public override string InteractableName
{
get
{
if (_workshopDownstairs.LightOn)
return "Turn off Light";
return "Turn on Light";
}
}
public override UniTask OnInteract(ItemDataSo item = null)
{
if(!CheckEmptyHands())
return UniTask.CompletedTask;
if (item != null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return UniTask.CompletedTask;
}
_workshopDownstairs.ToggleLightSwitch();
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 5c5889af77b0491bb18818491222520b
timeCreated: 1773609093

View File

@@ -1,20 +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;
namespace BriarQueen.Game.Items.Environment.ChapterOne.Workshop.Downstairs
{
public class WorkshopWriting : BaseItem
{
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Inspect;
public override UniTask OnInteract(ItemDataSo item = null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(EnvironmentInteractKey.WorkshopWriting)));
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f8ae950ec2f6412f9a05f9eb22a81004
timeCreated: 1773681035

View File

@@ -1,74 +0,0 @@
using System;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Events.Gameplay;
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.Items.Environment.ChapterOne.Workshop
{
public class GrindingStone : BaseItem
{
[SerializeField]
private Levels.ChapterOne.Workshop.Workshop _workshop;
public override string InteractableName => "Grinding Stone";
public override UICursorService.CursorStyle ApplicableCursorStyle => UICursorService.CursorStyle.Interact;
public override async UniTask OnInteract(ItemDataSo item = null)
{
if (!CheckEmptyHands())
return;
if (!SaveManager.GetLevelFlag(LevelFlag.WorkshopGrindstoneRepaired))
{
if (item == null)
{
EventCoordinator.Publish(
new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.SomethingMissing)));
return;
}
if (item.UniqueID != ItemIDs.Get(ItemKey.GrindstoneAxlePin))
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return;
}
await _workshop.SetWoodenPin();
return;
}
if (item == null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(EnvironmentInteractKey.UseGrindstone)));
return;
}
if (item.UniqueID != ItemIDs.Get(ItemKey.RustedKnife))
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return;
}
// TODO - Animations, SFX, etc
EventCoordinator.Publish(new FadeEvent(false, 0.6f));
await UniTask.Delay(TimeSpan.FromSeconds(1.2f));
AudioManager.Play(AudioNameIdentifiers.Get(SFXKey.SharpenKnife));
EventCoordinator.Publish(new FadeEvent(true, 0.6f));
await UniTask.Delay(TimeSpan.FromSeconds(0.8f));
PlayerManager.UnlockTool(ToolID.Knife);
TutorialService.DisplayTutorial(TutorialPopupID.Tools);
PlayerManager.RemoveItem(ItemIDs.Get(ItemKey.RustedKnife));
EventCoordinator.Publish(new SelectedItemChangedEvent(null));
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 7af3243d1b3240a5bb86ba7cd0b2e9b8
timeCreated: 1771003747

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 09d6091451f64822a55575f3ab2b6b68
timeCreated: 1773590204

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: b57d4daa04964b28b6c24a1e41fb4d6a
timeCreated: 1773590204

View File

@@ -1,40 +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.Game.Levels.ChapterOne.Workshop.Upstairs;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace BriarQueen.Game.Items.Environment.ChapterOne.Workshop.Upstairs.Bag
{
public class WorkshopBagBook : BaseItem
{
[Header("Internal")]
[SerializeField]
private WorkshopBag _bag;
public override string InteractableName => "Damaged Book";
public override async UniTask OnInteract(ItemDataSo item = null)
{
if(!CheckEmptyHands())
return;
if (item != null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return;
}
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(EnvironmentInteractKey.WorkshopBookDisintegrating)));
await Remove();
}
protected override UniTask OnRemoved()
{
_bag.RemoveBook();
return UniTask.CompletedTask;
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: a482fd71d48d4edaaceec7a7038a72a2
timeCreated: 1773496524

View File

@@ -1,48 +0,0 @@
using System;
using BriarQueen.Data.Identifiers;
using BriarQueen.Data.IO.Saves;
using BriarQueen.Framework.Events.UI;
using BriarQueen.Framework.Managers.Levels.Data;
using BriarQueen.Framework.Managers.Player.Data;
using BriarQueen.Game.Levels.ChapterOne.Workshop.Upstairs;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace BriarQueen.Game.Items.Environment.ChapterOne.Workshop.Upstairs.Bag
{
public class WorkshopBagHole : BaseItem
{
[Header("Workshop Bag")]
[SerializeField]
private WorkshopBag _workshopBag;
public override string InteractableName => "Dig";
public override async UniTask OnInteract(ItemDataSo item = null)
{
if(!CheckEmptyHands())
return;
if (SaveManager.GetLevelFlag(LevelFlag.WorkshopBagHoleDug))
{
EventCoordinator.Publish(new DisplayInteractEvent(
InteractEventIDs.Get(EnvironmentInteractKey.WorkshopBagNoItems)));
return;
}
if (item != null)
{
EventCoordinator.Publish(new DisplayInteractEvent(InteractEventIDs.Get(ItemInteractKey.CantUseItem)));
return;
}
EventCoordinator.Publish(new FadeEvent(false, 1f));
await UniTask.Delay(TimeSpan.FromSeconds(1));
await _workshopBag.DigHole();
await UniTask.Delay(TimeSpan.FromSeconds(1));
EventCoordinator.Publish(new FadeEvent(true, 1f));
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: d8e8a954a6d04904b15e4e1b50db8615
timeCreated: 1773494920

View File

@@ -19,7 +19,7 @@ namespace BriarQueen.Game.Items.Environment.General.Book
private AssetItemKey _bookAssetID;
[SerializeField]
private BookEntryID _bookEntryID;
private DocumentEntryID _documentEntryID;
[Header("Book Interface")]
[SerializeField]
@@ -58,7 +58,11 @@ namespace BriarQueen.Game.Items.Environment.General.Book
if (bookObj == null) return;
_bookInterface = bookObj.GetComponent<BookInterface>();
if (_bookInterface == null) return;
if (_bookInterface == null)
{
await DestructionService.Destroy(bookObj);
return;
}
_bookInterface.CanvasGroup.alpha = 0f;
_bookInterface.CanvasGroup.blocksRaycasts = false;
@@ -99,7 +103,7 @@ namespace BriarQueen.Game.Items.Environment.General.Book
private void UnlockCodexEntry()
{
PlayerManager.UnlockCodexEntry(CodexEntryIDs.Get(_bookEntryID));
PlayerManager.UnlockCodexEntry(CodexEntryIDs.Get(_documentEntryID));
}
public async UniTask CloseBookInterface()
@@ -144,4 +148,4 @@ namespace BriarQueen.Game.Items.Environment.General.Book
_displayCts = null;
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More