diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 41888d5..b44867f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -17,7 +17,10 @@ "Bash(grep -l \"bc14507c6a9500d4eac9e684e289d084\" /d/repos/trail-into-darkness/Assets/**/*.meta)", "mcp__plugin_context7_context7__resolve-library-id", "mcp__plugin_context7_context7__query-docs", - "Bash(find D:repostrail-into-darknessAssets -type f -name *View.cs)" + "Bash(find D:repostrail-into-darknessAssets -type f -name *View.cs)", + "Bash(grep -c '\\\\\\\\r')", + "Bash(xxd Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs)", + "Bash(find D:/repos/trail-into-darkness/Assets -name *.asmdef)" ] } } diff --git a/Assets/Code/Core/EntryPoint.cs b/Assets/Code/Core/EntryPoint.cs index 1b5de39..291ef09 100644 --- a/Assets/Code/Core/EntryPoint.cs +++ b/Assets/Code/Core/EntryPoint.cs @@ -5,6 +5,7 @@ using Nox.Game.UI; using Nox.Input; using Nox.Platform; using Jovian.SaveSystem; +using Jovian.InGameLogging; using Nox.EditorCode; using Unity.Profiling; using UnityEngine; @@ -24,6 +25,7 @@ namespace Nox.Core { private ProfilerMarker createApplicationStateMarker = new ProfilerMarker("createApplicationState"); private ISceneTransition sceneTransition; private ISaveSystem saveSystem; + private IGameLogStore gameLogStore; private InitializerSettingsFile initializerSettings; private BootstrapReferences bootstrapReferences; @@ -98,6 +100,8 @@ namespace Nox.Core { ISaveSlotManager saveSlotManager = new SaveSlotManager(saveStorage, saveSettings); saveSystem = new SaveSystem(saveSerializer, saveStorage, saveSlotManager, saveSettings); + gameLogStore = new GameLogStore(500); + var adventureData = new AdventureData(); var characterBaseSettings = Addressables.LoadAssetAsync("CharacterBaseSettings").WaitForCompletion(); @@ -111,8 +115,8 @@ namespace Nox.Core { applicationStates = new Dictionary { [GameState.BootState] = new SplashGameState(bootstrapReferences, gameDataState), - [GameState.MainMenu] = new MainMenuGameState(gameDataState, menuGameStateData, bootstrapReferences, saveSystem, partyCreatorModel, adventureData), - [GameState.GameMode] = new GameModeGameState(gameDataState, bootstrapReferences, platform.PlatformSettings, saveSystem, sceneTransition, adventureData), + [GameState.MainMenu] = new MainMenuGameState(gameDataState, menuGameStateData, bootstrapReferences, saveSystem, partyCreatorModel, adventureData, gameLogStore), + [GameState.GameMode] = new GameModeGameState(gameDataState, bootstrapReferences, platform.PlatformSettings, saveSystem, sceneTransition, adventureData, gameLogStore), }; createApplicationStateMarker.End(); } diff --git a/Assets/Code/Core/GameModeGameState.cs b/Assets/Code/Core/GameModeGameState.cs index c672c97..557622d 100644 --- a/Assets/Code/Core/GameModeGameState.cs +++ b/Assets/Code/Core/GameModeGameState.cs @@ -3,6 +3,7 @@ using Nox.Game; using Nox.Platform; using Nox.Game.UI; using Jovian.SaveSystem; +using Jovian.InGameLogging; using System.Collections.Generic; using ZLinq; using UnityEngine; @@ -21,6 +22,7 @@ namespace Nox.Core { private readonly ISaveSystem saveSystem; private readonly ISceneTransition sceneTransition; private readonly AdventureData adventuredata; + private readonly IGameLogStore gameLogStore; private readonly Dictionary playModeCache = new(); @@ -43,12 +45,14 @@ namespace Nox.Core { PlatformSettings platformSettings, ISaveSystem saveSystem, ISceneTransition sceneTransition, - AdventureData adventuredata) { + AdventureData adventuredata, + IGameLogStore gameLogStore) { this.gameDataState = gameDataState; this.platformSettings = platformSettings; this.saveSystem = saveSystem; this.sceneTransition = sceneTransition; this.adventuredata = adventuredata; + this.gameLogStore = gameLogStore; bootstrapSettings = Addressables.LoadAssetAsync(bootstrapReferences.playModeSettings).WaitForCompletion(); } @@ -75,7 +79,11 @@ namespace Nox.Core { private NoxSavedDataSet? CaptureNoxSaveData() { var adventure = FindAdventurePlayMode(); - return adventure?.CaptureNoxSaveData(); + var saveData = adventure?.CaptureNoxSaveData(); + if(saveData != null) { + saveData.gameLogData = gameLogStore.GetSaveData(); + } + return saveData; } private AdventurePlayMode? FindAdventurePlayMode() { diff --git a/Assets/Code/Core/MainMenuGameState.cs b/Assets/Code/Core/MainMenuGameState.cs index af6ddb8..c985a7f 100644 --- a/Assets/Code/Core/MainMenuGameState.cs +++ b/Assets/Code/Core/MainMenuGameState.cs @@ -1,5 +1,6 @@ using Nox.Game; using Jovian.SaveSystem; +using Jovian.InGameLogging; using Nox.UI; using System; using System.Threading.Tasks; @@ -21,6 +22,7 @@ namespace Nox.Core { private readonly BootstrapReferences bootstrapReferences; private readonly ISaveSystem saveSystem; private readonly PartyCreatorModel partyCreatorModel; + private readonly IGameLogStore gameLogStore; private AdventureData adventureData; private Action onStartGameRequested; private Action onContinueRequested; @@ -33,13 +35,15 @@ namespace Nox.Core { BootstrapReferences bootstrapReferences, ISaveSystem saveSystem, PartyCreatorModel partyCreatorModel, - AdventureData adventureData) { + AdventureData adventureData, + IGameLogStore gameLogStore) { this.gameDataState = gameDataState; this.menuGameStateData = menuGameStateData; this.bootstrapReferences = bootstrapReferences; this.saveSystem = saveSystem; this.partyCreatorModel = partyCreatorModel; this.adventureData = adventureData; + this.gameLogStore = gameLogStore; } public void EnterGameState() { @@ -56,7 +60,7 @@ namespace Nox.Core { gameDataState.ChangePlayMode(mode); }; onContinueRequested = () => { - var saveData = NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData); + var saveData = NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData, gameLogStore); if(saveData == null) { return; } diff --git a/Assets/Code/GameState/NoxSaveData.cs b/Assets/Code/GameState/NoxSaveData.cs index d08989e..95ce164 100644 --- a/Assets/Code/GameState/NoxSaveData.cs +++ b/Assets/Code/GameState/NoxSaveData.cs @@ -1,4 +1,5 @@ using Jovian.SaveSystem; +using Jovian.InGameLogging; using Nox.Core; using System; using ZLinq; @@ -11,7 +12,8 @@ namespace Nox.Game { public static NoxSavedDataSet RestoreSavedData( ISaveSystem saveSystem, GameDataState gameDataState, - ref AdventureData adventureData) { + ref AdventureData adventureData, + IGameLogStore gameLogStore = null) { var sessions = saveSystem.GetAllSessions().AsValueEnumerable().OrderByDescending(s => s.lastSaveDateUtc).ToList(); if(sessions.Count == 0) { return null; @@ -36,6 +38,10 @@ namespace Nox.Game { gameDataState.ActiveParty = saveData.partyDefinition; adventureData = saveData.adventureData; + if(gameLogStore != null && saveData.gameLogData != null) { + gameLogStore.RestoreFromSaveData(saveData.gameLogData); + } + return saveData; } } @@ -55,6 +61,9 @@ namespace Nox.Game { // Party public PartyDefinition partyDefinition; public SerializableVector3 partyPosition; + + // In-game log + public GameLogSaveData gameLogData; } /// diff --git a/Assets/Prefabs/UI/CharacterCreationReference.prefab b/Assets/Prefabs/UI/CharacterCreationReference.prefab index 935a1a7..7df6685 100644 --- a/Assets/Prefabs/UI/CharacterCreationReference.prefab +++ b/Assets/Prefabs/UI/CharacterCreationReference.prefab @@ -107,6 +107,7 @@ RectTransform: - {fileID: 7330333158031640233} - {fileID: 9065981712073220368} - {fileID: 1588673216748120674} + - {fileID: 8217010083354716901} m_Father: {fileID: 2577996418906990456} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0.33224753, y: 0} @@ -1513,6 +1514,144 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 4353505352457658128, guid: 4778a14b7457e644ea23e45b40fc3391, type: 3} m_PrefabInstance: {fileID: 4846874011051483482} m_PrefabAsset: {fileID: 0} +--- !u!1001 &6018359966710660021 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 8410804869288043777} + m_Modifications: + - target: {fileID: 374280998538979084, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 374280998538979084, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 1525412503934350410, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_Name + value: LogContainer + objectReference: {fileID: 0} + - target: {fileID: 2251566621526024109, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2251566621526024109, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2251566621526024109, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMax.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMax.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMin.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMin.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_SizeDelta.x + value: 737.42 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_SizeDelta.y + value: 504.25 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchoredPosition.x + value: 1.0002 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchoredPosition.y + value: -252.13 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7479392947244903680, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7479392947244903680, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7479392947244903680, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 7479392947244903680, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} +--- !u!224 &8217010083354716901 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 2417638750186197328, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3} + m_PrefabInstance: {fileID: 6018359966710660021} + m_PrefabAsset: {fileID: 0} --- !u!1001 &6727870666199461325 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/UI/LogContainer.prefab b/Assets/Prefabs/UI/LogContainer.prefab new file mode 100644 index 0000000..cbfb4c9 --- /dev/null +++ b/Assets/Prefabs/UI/LogContainer.prefab @@ -0,0 +1,893 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1447740470325782801 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6286131802514671469} + - component: {fileID: 7279458507301375298} + - component: {fileID: 7180659085512946289} + - component: {fileID: 1194623820596100263} + m_Layer: 0 + m_Name: Viewport + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6286131802514671469 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1447740470325782801} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 3059084814696477984} + m_Father: {fileID: 6248177754200331468} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 1} +--- !u!222 &7279458507301375298 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1447740470325782801} + m_CullTransparentMesh: 1 +--- !u!114 &7180659085512946289 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1447740470325782801} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10917, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &1194623820596100263 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1447740470325782801} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 31a19414c41e5ae4aae2af33fee712f6, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Mask + m_ShowMaskGraphic: 0 +--- !u!1 &1525412503934350410 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2417638750186197328} + - component: {fileID: 6899372841946195929} + m_Layer: 0 + m_Name: LogContainer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2417638750186197328 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1525412503934350410} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 6248177754200331468} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 1.0002, y: -252.13} + m_SizeDelta: {x: 737.42, y: 504.25} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &6899372841946195929 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1525412503934350410} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 786fb23f28122964cb30678ea785bd40, type: 3} + m_Name: + m_EditorClassIdentifier: Jovian.InGameLogging::Jovian.InGameLogging.UI.GameLogView + scrollRect: {fileID: 2554617498295089481} + content: {fileID: 3059084814696477984} + entryPrefab: {fileID: 0} + poolSize: 20 +--- !u!1 &2405629305058117087 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3059084814696477984} + - component: {fileID: 1744225011043606462} + m_Layer: 0 + m_Name: Content + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3059084814696477984 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2405629305058117087} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 7479392947244903680} + m_Father: {fileID: 6286131802514671469} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 487.26} + m_Pivot: {x: 0, y: 1} +--- !u!114 &1744225011043606462 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2405629305058117087} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup + m_Padding: + m_Left: 0 + m_Right: 0 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 0 + m_Spacing: 2 + m_ChildForceExpandWidth: 1 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 0 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!1 &3952660362247579954 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2688140319784364735} + - component: {fileID: 7390804912840202258} + - component: {fileID: 5246482949485489637} + - component: {fileID: 529710424251508929} + m_Layer: 0 + m_Name: Scrollbar Horizontal + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2688140319784364735 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3952660362247579954} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 3366448768354673732} + m_Father: {fileID: 6248177754200331468} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 20} + m_Pivot: {x: 0, y: 0} +--- !u!222 &7390804912840202258 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3952660362247579954} + m_CullTransparentMesh: 1 +--- !u!114 &5246482949485489637 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3952660362247579954} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &529710424251508929 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3952660362247579954} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Scrollbar + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1342770482644207694} + m_HandleRect: {fileID: 2251566621526024109} + m_Direction: 0 + m_Value: 1 + m_Size: 0.99999994 + m_NumberOfSteps: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &5969083621566914297 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 374280998538979084} + - component: {fileID: 5436844256530954601} + - component: {fileID: 6879256843609484833} + m_Layer: 0 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &374280998538979084 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5969083621566914297} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 8977826534581480953} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &5436844256530954601 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5969083621566914297} + m_CullTransparentMesh: 1 +--- !u!114 &6879256843609484833 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5969083621566914297} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &6050805973784497029 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3366448768354673732} + m_Layer: 0 + m_Name: Sliding Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3366448768354673732 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6050805973784497029} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2251566621526024109} + m_Father: {fileID: 2688140319784364735} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: -8.5, y: 0} + m_SizeDelta: {x: -20, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &6074956918412360435 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6248177754200331468} + - component: {fileID: 7678453280902357168} + - component: {fileID: 4027884099002409368} + - component: {fileID: 2554617498295089481} + m_Layer: 0 + m_Name: Scroll View + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6248177754200331468 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6074956918412360435} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 6286131802514671469} + - {fileID: 2688140319784364735} + - {fileID: 4070132176653801724} + m_Father: {fileID: 2417638750186197328} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0.0024719} + m_SizeDelta: {x: 737.42, y: 504.26} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &7678453280902357168 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6074956918412360435} + m_CullTransparentMesh: 1 +--- !u!114 &4027884099002409368 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6074956918412360435} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 0.392} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &2554617498295089481 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6074956918412360435} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ScrollRect + m_Content: {fileID: 3059084814696477984} + m_Horizontal: 0 + m_Vertical: 1 + m_MovementType: 1 + m_Elasticity: 0 + m_Inertia: 1 + m_DecelerationRate: 0.135 + m_ScrollSensitivity: 1 + m_Viewport: {fileID: 6286131802514671469} + m_HorizontalScrollbar: {fileID: 529710424251508929} + m_VerticalScrollbar: {fileID: 9136497547051495148} + m_HorizontalScrollbarVisibility: 2 + m_VerticalScrollbarVisibility: 2 + m_HorizontalScrollbarSpacing: -3 + m_VerticalScrollbarSpacing: -3 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &6994548556054523628 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4070132176653801724} + - component: {fileID: 8682590904891029538} + - component: {fileID: 5114829985009070717} + - component: {fileID: 9136497547051495148} + m_Layer: 0 + m_Name: Scrollbar Vertical + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &4070132176653801724 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6994548556054523628} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 8977826534581480953} + m_Father: {fileID: 6248177754200331468} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 0} + m_Pivot: {x: 1, y: 1} +--- !u!222 &8682590904891029538 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6994548556054523628} + m_CullTransparentMesh: 1 +--- !u!114 &5114829985009070717 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6994548556054523628} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &9136497547051495148 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6994548556054523628} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a4db7a114972834c8e4117be1d82ba3, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Scrollbar + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 6879256843609484833} + m_HandleRect: {fileID: 374280998538979084} + m_Direction: 2 + m_Value: 1 + m_Size: 1 + m_NumberOfSteps: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] +--- !u!1 &7187724186482890403 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8977826534581480953} + m_Layer: 0 + m_Name: Sliding Area + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8977826534581480953 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7187724186482890403} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 374280998538979084} + m_Father: {fileID: 4070132176653801724} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 8.5} + m_SizeDelta: {x: -20, y: -20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &7193882241213188023 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2251566621526024109} + - component: {fileID: 1372064744394430487} + - component: {fileID: 1342770482644207694} + m_Layer: 0 + m_Name: Handle + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2251566621526024109 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7193882241213188023} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 3366448768354673732} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 20, y: 20} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1372064744394430487 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7193882241213188023} + m_CullTransparentMesh: 1 +--- !u!114 &1342770482644207694 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7193882241213188023} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1001 &7098424197235870041 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 3059084814696477984} + m_Modifications: + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_Pivot.x + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_Pivot.y + value: 0.5 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_SizeDelta.x + value: 720.42 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_SizeDelta.y + value: 108.565 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 6501593483943143564, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + propertyPath: m_Name + value: LogEntry + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} +--- !u!224 &7479392947244903680 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 382400732949652569, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3} + m_PrefabInstance: {fileID: 7098424197235870041} + m_PrefabAsset: {fileID: 0} diff --git a/Assets/Prefabs/UI/LogContainer.prefab.meta b/Assets/Prefabs/UI/LogContainer.prefab.meta new file mode 100644 index 0000000..4bdf864 --- /dev/null +++ b/Assets/Prefabs/UI/LogContainer.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1b41f907ca960b644ae3af6e1942b9fb +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/UI/LogEntry.prefab b/Assets/Prefabs/UI/LogEntry.prefab new file mode 100644 index 0000000..f8fc8c1 --- /dev/null +++ b/Assets/Prefabs/UI/LogEntry.prefab @@ -0,0 +1,210 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6501593483943143564 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 382400732949652569} + - component: {fileID: 4832918257971952213} + - component: {fileID: 1727094465477629914} + m_Layer: 0 + m_Name: LogEntry + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &382400732949652569 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6501593483943143564} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 8597194705437786868} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 1} + m_AnchorMax: {x: 0, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 1093, y: 615} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &4832918257971952213 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6501593483943143564} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5526887cdf77a54439357be8b5754ffc, type: 3} + m_Name: + m_EditorClassIdentifier: Jovian.InGameLogging::Jovian.InGameLogging.UI.LogEntryView + messageText: {fileID: 4259574353180979179} +--- !u!114 &1727094465477629914 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6501593483943143564} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement + m_IgnoreLayout: 0 + m_MinWidth: -1 + m_MinHeight: -1 + m_PreferredWidth: -1 + m_PreferredHeight: 100 + m_FlexibleWidth: 1 + m_FlexibleHeight: -1 + m_LayoutPriority: 1 +--- !u!1 &8033193440249993941 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8597194705437786868} + - component: {fileID: 8331827018725656388} + - component: {fileID: 4259574353180979179} + m_Layer: 0 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &8597194705437786868 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8033193440249993941} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 382400732949652569} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: -0.0000000044237822} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8331827018725656388 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8033193440249993941} + m_CullTransparentMesh: 1 +--- !u!114 &4259574353180979179 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8033193440249993941} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: New Text + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 24 + m_fontSizeBase: 36 + m_fontWeight: 400 + m_enableAutoSizing: 1 + m_fontSizeMin: 5 + m_fontSizeMax: 24 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 3 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} diff --git a/Assets/Prefabs/UI/LogEntry.prefab.meta b/Assets/Prefabs/UI/LogEntry.prefab.meta new file mode 100644 index 0000000..8250348 --- /dev/null +++ b/Assets/Prefabs/UI/LogEntry.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9d1c7837b0b5a9f45baa84f326fc247c +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.jovian.ingame-logging/Editor.meta b/Packages/com.jovian.ingame-logging/Editor.meta new file mode 100644 index 0000000..ecb1f74 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1e182a45ed498c445b141e9ec6395805 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef b/Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef new file mode 100644 index 0000000..14c9f1a --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Jovian.InGameLogging.Editor", + "rootNamespace": "Jovian.InGameLogging.Editor", + "references": [ + "Jovian.InGameLogging" + ], + "includePlatforms": ["Editor"], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef.meta b/Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef.meta new file mode 100644 index 0000000..8a0a8b5 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 48c945ba5ea83b144b5bbf4eaf33fe29 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.jovian.ingame-logging/README.md b/Packages/com.jovian.ingame-logging/README.md new file mode 100644 index 0000000..acde50e --- /dev/null +++ b/Packages/com.jovian.ingame-logging/README.md @@ -0,0 +1,230 @@ +# Jovian In-Game Logging + +A low-allocation in-game logging system for Unity with channel-based filtering, pooled UI, and save system integration. This is a player-facing game log (combat feed, event history), not a debug logger. + +## Requirements + +- Unity 2022.3 or later +- Dependencies: + - `com.unity.textmeshpro` 3.0.6+ + - `com.unity.nuget.newtonsoft-json` 3.2.1+ + - `com.jovian.savesystem` 0.1.0+ + +## Quick Start + +### 1. Create the store + +`GameLogStore` is the central service that holds log entries in a ring buffer. Create it once and pass it via constructor injection. + +```csharp +// Default capacity of 500 entries +var logStore = new GameLogStore(); + +// Or specify a custom capacity +var logStore = new GameLogStore(capacity: 1000); +``` + +### 2. Create a logger + +`InGameLogger` is a lightweight facade bound to a specific channel. Create one per system or class that needs to write log entries. + +```csharp +var combatLogger = new InGameLogger(logStore, LogChannel.Combat); +var worldLogger = new InGameLogger(logStore, LogChannel.World); +``` + +### 3. Log messages + +```csharp +combatLogger.Log("Kael strikes the skeleton for 12 damage."); +combatLogger.Log("Critical hit!", "#FF4444"); +``` + +### 4. Set up the UI + +Add a `GameLogView` component to a GameObject with a `ScrollRect`. Assign the `ScrollRect`, a content `RectTransform`, and a `LogEntryView` prefab in the Inspector. Then initialize from code: + +```csharp +// Show all channels +gameLogView.Initialize(logStore); + +// Or filter to a single channel +gameLogView.Initialize(logStore, LogChannel.Combat); +``` + +The view uses object pooling internally. It auto-scrolls to the bottom as new entries arrive and pauses auto-scroll when the player scrolls up manually. + +## Prefab Setup + +The UI requires two prefabs: a **LogEntryView prefab** (the individual log row) and a **GameLogView prefab** (the scrollable container). + +### LogEntryView prefab + +1. Create a new GameObject, rename it `LogEntry` +2. Add a `LayoutElement` component. Set `Preferred Height` to your desired row height (e.g. 24). Enable `Flexible Width` so it stretches to the scroll content width +3. Add a child GameObject with a `TextMeshPro - Text (UI)` component + - Set `Overflow` to `Ellipsis` or `Truncate` (prevents text from spilling outside the row) + - Anchor and stretch it to fill the parent (`Left: 0, Right: 0, Top: 0, Bottom: 0`) + - Set font, font size, and color to match your game's UI style + - `Rich Text` should remain enabled (default) +4. Add the `LogEntryView` component to the root `LogEntry` GameObject +5. Drag the child `TMP_Text` into the `LogEntryView.messageText` field +6. Save as a prefab + +### GameLogView prefab + +1. Create a new UI GameObject with a `ScrollRect` component + - Disable `Horizontal` scrolling, keep `Vertical` enabled + - Set `Movement Type` to `Clamped` or `Elastic` as preferred + - Set `Scroll Sensitivity` to a comfortable value (e.g. 20) +2. Add a child `Content` GameObject as the scroll content area + - Add a `RectTransform` anchored top-left, pivot `(0, 1)`, with a `VerticalLayoutGroup`: + - `Child Alignment`: Upper Left + - `Child Force Expand Width`: true, `Height`: false + - `Spacing`: 2 (gap between log rows) + - `Padding`: set as needed + - Add a `ContentSizeFitter` with `Vertical Fit` set to `Preferred Size` (so it grows as entries are added) + - Assign this as the `ScrollRect.Content` +3. Optionally add a `Scrollbar` child for the vertical scrollbar, and assign it to `ScrollRect.Vertical Scrollbar` +4. Optionally add a `Mask` or `RectMask2D` on the scroll viewport to clip entries outside the visible area +5. Add the `GameLogView` component to the root GameObject +6. Assign the fields in the Inspector: + - `scrollRect`: the `ScrollRect` component + - `content`: the `Content` child's `RectTransform` + - `entryPrefab`: your `LogEntryView` prefab + - `poolSize`: number of pre-instantiated entries (default 20, increase for taller views) + +### Initialization from code + +After instantiating or finding the `GameLogView` in the scene: + +```csharp +var gameLogView = Object.FindFirstObjectByType(); +gameLogView.Initialize(logStore); + +// Or filtered to a single channel: +gameLogView.Initialize(logStore, LogChannel.Combat); +``` + +## Log Channels + +### Built-in channels + +| Channel | Usage | +|---------|-------| +| `LogChannel.Combat` | Combat events, damage, abilities | +| `LogChannel.CharacterCreation` | Character creation flow | +| `LogChannel.World` | World events, exploration | +| `LogChannel.General` | General-purpose messages | + +### Custom channels + +`LogChannel` is a readonly struct keyed by a string ID. Define custom channels as static fields: + +```csharp +public static class MyLogChannels { + public static readonly LogChannel Trading = new("Trading"); + public static readonly LogChannel Dialogue = new("Dialogue"); +} +``` + +Then use them like any built-in channel: + +```csharp +var tradeLogger = new InGameLogger(logStore, MyLogChannels.Trading); +tradeLogger.Log("Sold Iron Sword for 50 gold."); +``` + +## Rich Text + +Log messages support TextMeshPro rich text tags. The `InGameLogger.Log(message, hexColor)` overload wraps the message in a `` tag automatically: + +```csharp +logger.Log("Poisoned!", "#00FF00"); +// Stored as: Poisoned! +``` + +You can also embed TMP tags directly in the message string: + +```csharp +logger.Log("Gained +5 experience."); +``` + +## Save / Load Integration + +`GameLogStore` supports serializing its contents for use with a save system. + +### Save + +```csharp +GameLogSaveData saveData = logStore.GetSaveData(); +// Serialize saveData into your save file +``` + +### Load + +```csharp +// Deserialize saveData from your save file +logStore.RestoreFromSaveData(saveData); +``` + +`RestoreFromSaveData` replaces the current buffer contents. If the save data contains more entries than the store's capacity, only the most recent entries are kept. The `OnCleared` event fires after restoration so the UI can rebuild. + +### JSON serialization + +`LogChannel` is serialized as a plain string via the included `LogChannelJsonConverter` (Newtonsoft.Json). The converter is applied via attribute on `LogEntry.channel`, so no manual converter registration is needed. + +## API Reference + +### LogChannel (readonly struct) + +- `LogChannel(string id)` -- constructor +- `string Id` -- the channel identifier +- Static fields: `Combat`, `CharacterCreation`, `World`, `General` +- Implements `IEquatable`, ordinal string comparison + +### LogEntry (readonly struct) + +- `string message` -- the log message (may contain TMP rich text) +- `LogChannel channel` -- the channel this entry belongs to +- `float gameTime` -- `Time.time` when the entry was added + +### InGameLogger (readonly struct) + +- `InGameLogger(IGameLogStore store, LogChannel channel)` -- constructor +- `void Log(string message)` -- add an entry to the store +- `void Log(string message, string hexColor)` -- add a color-wrapped entry + +### IGameLogStore / GameLogStore + +- `GameLogStore(int capacity = 500)` -- constructor with ring buffer size +- `int Count` -- current number of entries +- `int Capacity` -- maximum entries before oldest are overwritten +- `void Add(LogChannel channel, string message)` -- add an entry +- `void Clear()` -- remove all entries +- `void Clear(LogChannel channel)` -- remove entries for a specific channel +- `ReadOnlySpan GetEntries()` -- all entries in chronological order +- `int GetEntries(LogChannel channel, List results)` -- filtered entries, returns count +- `event Action OnEntryAdded` -- fires after each new entry +- `event Action OnCleared` -- fires after `Clear()` or `RestoreFromSaveData()` +- `GameLogSaveData GetSaveData()` -- snapshot for serialization +- `void RestoreFromSaveData(GameLogSaveData data)` -- replace contents from save data + +### GameLogSaveData + +- `List entries` -- serializable entry list + +### GameLogView (MonoBehaviour) -- `Jovian.InGameLogging.UI` + +- `void Initialize(IGameLogStore store, LogChannel? channelFilter = null)` -- bind to a store, optionally filtering to one channel +- Requires: `ScrollRect`, content `RectTransform`, and `LogEntryView` prefab assigned in Inspector + +### LogEntryView (MonoBehaviour) -- `Jovian.InGameLogging.UI` + +- `void SetEntry(in LogEntry entry)` -- display an entry +- `void ClearEntry()` -- reset the text +- Requires: `TMP_Text` reference assigned in Inspector + +### LogChannelJsonConverter + +- Newtonsoft.Json `JsonConverter` -- serializes `LogChannel` as its string ID diff --git a/Packages/com.jovian.ingame-logging/README.md.meta b/Packages/com.jovian.ingame-logging/README.md.meta new file mode 100644 index 0000000..526c493 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8a8d67e42da5eea4a873d8f632bacbbe +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.jovian.ingame-logging/Runtime.meta b/Packages/com.jovian.ingame-logging/Runtime.meta new file mode 100644 index 0000000..a6075c4 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 51325b1e7a05b6740a458bdcae9f8998 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs b/Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs new file mode 100644 index 0000000..46259c1 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Jovian.InGameLogging { + [Serializable] + public sealed class GameLogSaveData { + public List entries; + + public GameLogSaveData() { + entries = new List(); + } + + public GameLogSaveData(List entries) { + this.entries = entries; + } + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs.meta new file mode 100644 index 0000000..caed639 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 965eab6edce9dbc49b93d0bda0ad6f6c \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs b/Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs new file mode 100644 index 0000000..0dacdbe --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Jovian.InGameLogging { + public sealed class GameLogStore : IGameLogStore { + readonly LogEntry[] buffer; + int head; + int count; + + public int Count => count; + public int Capacity => buffer.Length; + + public event Action OnEntryAdded; + public event Action OnCleared; + + public GameLogStore(int capacity = 500) { + buffer = new LogEntry[capacity]; + head = 0; + count = 0; + } + + public void Add(LogChannel channel, string message) { + var entry = new LogEntry(message, channel, Time.time); + buffer[head] = entry; + head = (head + 1) % buffer.Length; + if(count < buffer.Length) { + count++; + } + OnEntryAdded?.Invoke(entry); + } + + public void Clear() { + head = 0; + count = 0; + OnCleared?.Invoke(); + } + + public void Clear(LogChannel channel) { + var kept = new List(count); + var entries = GetEntries(); + for(int i = 0; i < entries.Length; i++) { + if(entries[i].channel != channel) { + kept.Add(entries[i]); + } + } + + head = 0; + count = 0; + for(int i = 0; i < kept.Count; i++) { + buffer[i] = kept[i]; + count++; + } + head = count % buffer.Length; + OnCleared?.Invoke(); + } + + public ReadOnlySpan GetEntries() { + if(count < buffer.Length) { + return new ReadOnlySpan(buffer, 0, count); + } + var result = new LogEntry[count]; + int start = head; + for(int i = 0; i < count; i++) { + result[i] = buffer[(start + i) % buffer.Length]; + } + return result; + } + + public int GetEntries(LogChannel channel, List results) { + results.Clear(); + var entries = GetEntries(); + for(int i = 0; i < entries.Length; i++) { + if(entries[i].channel == channel) { + results.Add(entries[i]); + } + } + return results.Count; + } + + public GameLogSaveData GetSaveData() { + var entries = GetEntries(); + var list = new List(entries.Length); + for(int i = 0; i < entries.Length; i++) { + list.Add(entries[i]); + } + return new GameLogSaveData(list); + } + + public void RestoreFromSaveData(GameLogSaveData data) { + head = 0; + count = 0; + if(data?.entries == null) { + OnCleared?.Invoke(); + return; + } + int startIndex = Math.Max(0, data.entries.Count - buffer.Length); + for(int i = startIndex; i < data.entries.Count; i++) { + buffer[count] = data.entries[i]; + count++; + } + head = count % buffer.Length; + OnCleared?.Invoke(); + } + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs.meta new file mode 100644 index 0000000..c13567a --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1eb9ff03ddd225e46852cb92ba213bc7 \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs b/Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs new file mode 100644 index 0000000..487bca0 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace Jovian.InGameLogging { + public interface IGameLogStore { + int Count { get; } + int Capacity { get; } + + void Add(LogChannel channel, string message); + void Clear(); + void Clear(LogChannel channel); + + ReadOnlySpan GetEntries(); + int GetEntries(LogChannel channel, List results); + + event Action OnEntryAdded; + event Action OnCleared; + + GameLogSaveData GetSaveData(); + void RestoreFromSaveData(GameLogSaveData data); + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs.meta new file mode 100644 index 0000000..ee500dd --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b5fac89eac17e874e888928c9618e812 \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs b/Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs new file mode 100644 index 0000000..ffff2b1 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs @@ -0,0 +1,23 @@ +using System.Runtime.CompilerServices; + +namespace Jovian.InGameLogging { + public readonly struct InGameLogger { + readonly IGameLogStore store; + readonly LogChannel channel; + + public InGameLogger(IGameLogStore store, LogChannel channel) { + this.store = store; + this.channel = channel; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log(string message) { + store.Add(channel, message); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log(string message, string hexColor) { + store.Add(channel, $"{message}"); + } + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs.meta new file mode 100644 index 0000000..276d3e9 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 79d11c151d20d2c41a4ad5e288a4f16f \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef b/Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef new file mode 100644 index 0000000..01b3346 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef @@ -0,0 +1,19 @@ +{ + "name": "Jovian.InGameLogging", + "rootNamespace": "Jovian.InGameLogging", + "references": [ + "Unity.TextMeshPro", + "Jovian.SaveSystem" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "Newtonsoft.Json.dll" + ], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef.meta b/Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef.meta new file mode 100644 index 0000000..307be22 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6053f37e557f955418ef96fdf46f7d6b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs b/Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs new file mode 100644 index 0000000..f17d7df --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs @@ -0,0 +1,44 @@ +using System; +using UnityEngine; + +namespace Jovian.InGameLogging { + [Serializable] + public readonly struct LogChannel : IEquatable { + [SerializeField] readonly string id; + + public string Id => id; + + public LogChannel(string id) { + this.id = id; + } + + public static readonly LogChannel Combat = new("Combat"); + public static readonly LogChannel CharacterCreation = new("CharacterCreation"); + public static readonly LogChannel World = new("World"); + public static readonly LogChannel General = new("General"); + + public bool Equals(LogChannel other) { + return string.Equals(id, other.id, StringComparison.Ordinal); + } + + public override bool Equals(object obj) { + return obj is LogChannel other && Equals(other); + } + + public override int GetHashCode() { + return id != null ? id.GetHashCode() : 0; + } + + public override string ToString() { + return id ?? string.Empty; + } + + public static bool operator ==(LogChannel left, LogChannel right) { + return left.Equals(right); + } + + public static bool operator !=(LogChannel left, LogChannel right) { + return !left.Equals(right); + } + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs.meta new file mode 100644 index 0000000..8bb320d --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 08b1132b10325ce4d922fa7b207db4e0 \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs b/Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs new file mode 100644 index 0000000..1a23c4b --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace Jovian.InGameLogging { + public sealed class LogChannelJsonConverter : JsonConverter { + public override void WriteJson(JsonWriter writer, LogChannel value, JsonSerializer serializer) { + writer.WriteValue(value.Id); + } + + public override LogChannel ReadJson(JsonReader reader, Type objectType, LogChannel existingValue, bool hasExistingValue, JsonSerializer serializer) { + var id = reader.Value as string; + return new LogChannel(id); + } + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs.meta new file mode 100644 index 0000000..9d8a172 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3c3eb6d23e195e345a3546749ca4e96f \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs b/Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs new file mode 100644 index 0000000..4099e56 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs @@ -0,0 +1,20 @@ +using System; +using Newtonsoft.Json; + +namespace Jovian.InGameLogging { + [Serializable] + public readonly struct LogEntry { + [JsonProperty] public readonly string message; + + [JsonProperty] [JsonConverter(typeof(LogChannelJsonConverter))] + public readonly LogChannel channel; + + [JsonProperty] public readonly float gameTime; + + public LogEntry(string message, LogChannel channel, float gameTime) { + this.message = message; + this.channel = channel; + this.gameTime = gameTime; + } + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs.meta new file mode 100644 index 0000000..5d98b52 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0f79543f908769543ac8fc14e138df62 \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Runtime/UI.meta b/Packages/com.jovian.ingame-logging/Runtime/UI.meta new file mode 100644 index 0000000..7c9d941 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b1125aedc37d43948aeef186bccbeac8 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs b/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs new file mode 100644 index 0000000..4aad9ad --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs @@ -0,0 +1,116 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace Jovian.InGameLogging.UI { + public class GameLogView : MonoBehaviour { + [SerializeField] ScrollRect scrollRect; + [SerializeField] RectTransform content; + [SerializeField] LogEntryView entryPrefab; + [SerializeField] int poolSize = 20; + + IGameLogStore store; + LogChannel? channelFilter; + bool autoScroll = true; + + readonly List activeEntries = new(); + readonly Stack pool = new(); + + public void Initialize(IGameLogStore store, LogChannel? channelFilter = null) { + this.store = store; + this.channelFilter = channelFilter; + + WarmPool(); + store.OnEntryAdded += HandleEntryAdded; + store.OnCleared += HandleCleared; + scrollRect.onValueChanged.AddListener(HandleScrollChanged); + + RebuildFromStore(); + } + + void OnDestroy() { + if(store != null) { + store.OnEntryAdded -= HandleEntryAdded; + store.OnCleared -= HandleCleared; + } + if(scrollRect != null) { + scrollRect.onValueChanged.RemoveListener(HandleScrollChanged); + } + } + + void WarmPool() { + for(int i = 0; i < poolSize; i++) { + var entry = Instantiate(entryPrefab, content); + entry.gameObject.SetActive(false); + pool.Push(entry); + } + } + + LogEntryView GetFromPool() { + LogEntryView entry; + if(pool.Count > 0) { + entry = pool.Pop(); + } + else { + entry = activeEntries[0]; + activeEntries.RemoveAt(0); + } + entry.gameObject.SetActive(true); + entry.transform.SetAsLastSibling(); + return entry; + } + + void ReturnToPool(LogEntryView entry) { + entry.ClearEntry(); + entry.gameObject.SetActive(false); + pool.Push(entry); + } + + void HandleEntryAdded(LogEntry entry) { + if(channelFilter.HasValue && entry.channel != channelFilter.Value) { + return; + } + + var view = GetFromPool(); + view.SetEntry(in entry); + activeEntries.Add(view); + + if(autoScroll) { + Canvas.ForceUpdateCanvases(); + scrollRect.verticalNormalizedPosition = 0f; + } + } + + void HandleCleared() { + for(int i = activeEntries.Count - 1; i >= 0; i--) { + ReturnToPool(activeEntries[i]); + } + activeEntries.Clear(); + + if(store.Count > 0) { + RebuildFromStore(); + } + } + + void RebuildFromStore() { + var entries = store.GetEntries(); + for(int i = 0; i < entries.Length; i++) { + if(channelFilter.HasValue && entries[i].channel != channelFilter.Value) { + continue; + } + var view = GetFromPool(); + view.SetEntry(in entries[i]); + activeEntries.Add(view); + } + + if(autoScroll) { + Canvas.ForceUpdateCanvases(); + scrollRect.verticalNormalizedPosition = 0f; + } + } + + void HandleScrollChanged(Vector2 position) { + autoScroll = position.y <= 0.01f; + } + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs.meta new file mode 100644 index 0000000..f62a959 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 786fb23f28122964cb30678ea785bd40 \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs b/Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs new file mode 100644 index 0000000..3f48b8a --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs @@ -0,0 +1,16 @@ +using TMPro; +using UnityEngine; + +namespace Jovian.InGameLogging.UI { + public class LogEntryView : MonoBehaviour { + [SerializeField] TMP_Text messageText; + + public void SetEntry(in LogEntry entry) { + messageText.text = entry.message; + } + + public void ClearEntry() { + messageText.text = string.Empty; + } + } +} diff --git a/Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs.meta b/Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs.meta new file mode 100644 index 0000000..484b3ce --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5526887cdf77a54439357be8b5754ffc \ No newline at end of file diff --git a/Packages/com.jovian.ingame-logging/Samples~/README.md b/Packages/com.jovian.ingame-logging/Samples~/README.md new file mode 100644 index 0000000..2803af2 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/Samples~/README.md @@ -0,0 +1,3 @@ +# Samples + +This folder is reserved for sample scenes and scripts demonstrating the In-Game Logging system. diff --git a/Packages/com.jovian.ingame-logging/package.json b/Packages/com.jovian.ingame-logging/package.json new file mode 100644 index 0000000..4a6e584 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/package.json @@ -0,0 +1,20 @@ +{ + "name": "com.jovian.ingame-logging", + "version": "0.1.0", + "displayName": "Jovian In-Game Logging", + "description": "An optimized, low-allocation in-game logging system with pooled UI, channel-based filtering, and save system integration.", + "unity": "2022.3", + "dependencies": { + "com.unity.textmeshpro": "3.0.6", + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.jovian.savesystem": "0.1.0" + }, + "keywords": [ + "logging", + "ui", + "ingame" + ], + "author": { + "name": "Jovian" + } +} diff --git a/Packages/com.jovian.ingame-logging/package.json.meta b/Packages/com.jovian.ingame-logging/package.json.meta new file mode 100644 index 0000000..b8a33a7 --- /dev/null +++ b/Packages/com.jovian.ingame-logging/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: acdee7574039a3a48980e2cc9c6fe31d +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 6dd3ac5..5dda8a1 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -18,6 +18,16 @@ "source": "embedded", "dependencies": {} }, + "com.jovian.ingame-logging": { + "version": "file:com.jovian.ingame-logging", + "depth": 0, + "source": "embedded", + "dependencies": { + "com.unity.textmeshpro": "3.0.6", + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.jovian.savesystem": "0.1.0" + } + }, "com.jovian.inspector-tools": { "version": "file:com.jovian.inspector-tools", "depth": 0, @@ -354,6 +364,14 @@ }, "url": "https://packages.unity.com" }, + "com.unity.textmeshpro": { + "version": "5.0.0", + "depth": 1, + "source": "builtin", + "dependencies": { + "com.unity.ugui": "2.0.0" + } + }, "com.unity.timeline": { "version": "1.8.11", "depth": 0, diff --git a/docs/plans/2026-04-05-ingame-logging-design.md b/docs/plans/2026-04-05-ingame-logging-design.md new file mode 100644 index 0000000..25915ae --- /dev/null +++ b/docs/plans/2026-04-05-ingame-logging-design.md @@ -0,0 +1,215 @@ +# In-Game Logging System Design + +**Package:** `com.jovian.ingame-logging` +**Date:** 2026-04-05 + +## Purpose + +An optimized, low-allocation in-game logging system for displaying gameplay messages (roll values, damage, events) to the player. Injectable via constructor DI, not static. Saves/loads with the Jovian save system. + +## Core Data Types + +### LogChannel (readonly struct) + +Zero-allocation value type with string-based identity for serialization. + +```csharp +[Serializable] +public readonly struct LogChannel : IEquatable { + [SerializeField] readonly string id; + public string Id => id; + public LogChannel(string id) => this.id = id; + + // Built-in channels + public static readonly LogChannel Combat = new("Combat"); + public static readonly LogChannel CharacterCreation = new("CharacterCreation"); + public static readonly LogChannel World = new("World"); + public static readonly LogChannel General = new("General"); + + // IEquatable, ==, !=, GetHashCode via id +} +``` + +Game code can define additional channels: `new LogChannel("Crafting")`. +Serialized via Newtonsoft `JsonConverter` that reads/writes the `id` string. + +### LogEntry (readonly struct) + +```csharp +[Serializable] +public readonly struct LogEntry { + public readonly string message; // supports TMP rich text tags + public readonly LogChannel channel; + public readonly float gameTime; // Time.time when logged +} +``` + +## Architecture + +``` +Game Code (CharacterCreationView, CombatSystem, etc.) + │ var log = new InGameLogger(store, LogChannel.Combat); + │ log.Log("Rolled 18 for Might"); + │ log.Log("Critical Hit! 24 damage"); + ▼ +InGameLogger (readonly struct, per-caller facade) + │ Holds: IGameLogStore reference + LogChannel + │ log.Log(msg) → store.Add(channel, msg) + │ log.Log(msg, color) → store.Add(channel, colored msg) + ▼ +IGameLogStore (singleton service, created in EntryPoint) + │ Ring buffer with configurable max capacity (default 500) + │ Add(channel, message), Clear(), Clear(channel) + │ GetEntries(), GetEntries(channel) + │ event Action OnEntryAdded + │ event Action OnCleared + │ SaveLog(ISaveSystem, sessionId), LoadLog(ISaveSystem, sessionId) + ▼ +GameLogView (MonoBehaviour on UI prefab) + ScrollRect + VerticalLayoutGroup + Object pool of LogEntryView instances + Subscribes to IGameLogStore.OnEntryAdded / OnCleared + Optional channel filter (show one or all channels) + Auto-scroll to bottom, pause on manual scroll-up +``` + +## InGameLogger (Facade Struct) + +```csharp +public readonly struct InGameLogger { + readonly IGameLogStore store; + readonly LogChannel channel; + + public InGameLogger(IGameLogStore store, LogChannel channel) { + this.store = store; + this.channel = channel; + } + + public void Log(string message) => store.Add(channel, message); + public void Log(string message, string hexColor) + => store.Add(channel, $"{message}"); +} +``` + +- No allocations beyond the message string itself +- Each caller (view, system) creates its own instance scoped to a channel +- Injected via `IGameLogStore` passed through constructor DI + +## IGameLogStore (Service Interface) + +```csharp +public interface IGameLogStore { + int Count { get; } + int Capacity { get; } + + void Add(LogChannel channel, string message); + void Clear(); + void Clear(LogChannel channel); + + ReadOnlySpan GetEntries(); + // Filtered access for UI + int GetEntries(LogChannel channel, List results); + + event Action OnEntryAdded; + event Action OnCleared; + + // Save integration + GameLogSaveData GetSaveData(); + void RestoreFromSaveData(GameLogSaveData data); +} +``` + +## GameLogStore (Implementation) + +- **Ring buffer** backed by `LogEntry[]` array +- Configurable capacity (default 500), oldest entries evicted when full +- `Add()` writes to next slot, increments head pointer, fires `OnEntryAdded` +- `Clear()` resets head/count, fires `OnCleared` +- `Clear(LogChannel)` removes entries matching channel, compacts buffer + +## Save Integration + +**Dependency direction:** `com.jovian.ingame-logging` depends on `com.jovian.savesystem`. + +```csharp +[Serializable] +public sealed class GameLogSaveData { + public List entries; // all channels, in order +} +``` + +- `GetSaveData()` snapshots the ring buffer into a `GameLogSaveData` +- `RestoreFromSaveData()` rebuilds the ring buffer and notifies UI +- All channels saved together in one payload, each entry tagged with its `LogChannel` +- Game code calls save/load alongside its own persistence: + - Save: `saveSystem.Save(logStore.GetSaveData())` in the same session + - Load: `logStore.RestoreFromSaveData(saveSystem.Load(slot))` + +## UI Design + +### GameLogView (MonoBehaviour) + +- Attached to a prefab with `ScrollRect` + `Content` (VerticalLayoutGroup) +- Holds reference to a `LogEntryView` prefab (for pooling) +- Subscribes to `IGameLogStore.OnEntryAdded` and `OnCleared` +- Object pool: pre-warms ~20 `LogEntryView` instances +- When pool is exhausted, recycles oldest visible entries +- Optional `LogChannel` filter field (null = show all) +- Auto-scrolls to bottom; pauses auto-scroll when user scrolls up manually + +### LogEntryView (MonoBehaviour) + +- Minimal: single `TMP_Text` serialized field reference +- `SetEntry(LogEntry entry)` sets text content +- TMP `richText = true` by default (supports ``, ``, ``, etc.) + +## Package Structure + +``` +Packages/com.jovian.ingame-logging/ +├── package.json # com.jovian.ingame-logging, deps: savesystem, TMP +├── Runtime/ +│ ├── Jovian.InGameLogging.asmdef +│ ├── LogChannel.cs +│ ├── LogEntry.cs +│ ├── InGameLogger.cs +│ ├── IGameLogStore.cs +│ ├── GameLogStore.cs +│ ├── GameLogSaveData.cs +│ ├── LogChannelJsonConverter.cs +│ └── UI/ +│ ├── GameLogView.cs +│ └── LogEntryView.cs +└── Editor/ + └── Jovian.InGameLogging.Editor.asmdef +``` + +### Dependencies + +- `com.jovian.savesystem` (for ISaveSystem) +- `com.unity.textmeshpro` (for TMP_Text) +- `com.unity.nuget.newtonsoft-json` (for LogChannel serialization) + +### Assembly References + +- `Jovian.InGameLogging` references: `Jovian.SaveSystem`, `Unity.TextMeshPro`, `Newtonsoft.Json` +- `Jovian.InGameLogging.Editor` references: `Jovian.InGameLogging` + +## Integration Points (Game Project) + +1. **EntryPoint.cs**: Create `GameLogStore` instance, pass to game states that need logging +2. **Game state constructors**: Receive `IGameLogStore`, create `InGameLogger` structs per channel +3. **UI prefab**: Instantiate `GameLogView` prefab, call `Initialize(IGameLogStore)` +4. **Save/Load**: Call `GetSaveData()` / `RestoreFromSaveData()` alongside existing save flow + +## Design Decisions + +| Decision | Rationale | +|----------|-----------| +| Readonly struct for LogChannel | Zero-alloc, value semantics, serializable via string id | +| Readonly struct for InGameLogger | Per-caller facade with no heap allocation | +| Ring buffer over List | Fixed capacity, no resizing, O(1) add, oldest eviction built-in | +| Object pool for UI | Eliminates instantiation after warmup, O(visible) rendering cost | +| Separate save file via save system | Logging depends on save system, not the other way around | +| TMP rich text passthrough | No parsing overhead; callers compose rich text directly | +| Simple Clear() API | Package stays decoupled from game-specific events | diff --git a/docs/plans/2026-04-05-ingame-logging-implementation.md b/docs/plans/2026-04-05-ingame-logging-implementation.md new file mode 100644 index 0000000..eb89215 --- /dev/null +++ b/docs/plans/2026-04-05-ingame-logging-implementation.md @@ -0,0 +1,757 @@ +# In-Game Logging System Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Build `com.jovian.ingame-logging`, a low-allocation in-game logging package with pooled UI, channel-based filtering, and save system integration. + +**Architecture:** Injectable `IGameLogStore` service with ring buffer storage. `InGameLogger` readonly struct facades scoped to a `LogChannel`. Pooled `GameLogView` MonoBehaviour for ScrollRect UI. Save data as `GameLogSaveData` serializable type, persisted as a field in the game's `NoxSavedDataSet`. + +**Tech Stack:** Unity 6 / C# 9, TextMeshPro, Newtonsoft.Json, Jovian SaveSystem + +--- + +### Task 1: Package scaffold + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/package.json` +- Create: `Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef` +- Create: `Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef` + +**Step 1: Create package.json** + +```json +{ + "name": "com.jovian.ingame-logging", + "version": "0.1.0", + "displayName": "Jovian In-Game Logging", + "description": "An optimized, low-allocation in-game logging system with pooled UI, channel-based filtering, and save system integration.", + "unity": "2022.3", + "dependencies": { + "com.unity.textmeshpro": "3.0.6", + "com.unity.nuget.newtonsoft-json": "3.2.1", + "com.jovian.savesystem": "0.1.0" + }, + "keywords": [ + "logging", + "ui", + "ingame" + ], + "author": { + "name": "Jovian" + } +} +``` + +**Step 2: Create Runtime asmdef** + +```json +{ + "name": "Jovian.InGameLogging", + "rootNamespace": "Jovian.InGameLogging", + "references": [ + "Unity.TextMeshPro", + "Jovian.SaveSystem" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "Newtonsoft.Json.dll" + ], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} +``` + +**Step 3: Create Editor asmdef** + +```json +{ + "name": "Jovian.InGameLogging.Editor", + "rootNamespace": "Jovian.InGameLogging.Editor", + "references": [ + "Jovian.InGameLogging" + ], + "includePlatforms": ["Editor"], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} +``` + +**Step 4: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/ +git commit -m "feat: scaffold com.jovian.ingame-logging package" +``` + +--- + +### Task 2: LogChannel readonly struct + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs` + +**Step 1: Implement LogChannel** + +```csharp +using System; +using UnityEngine; + +namespace Jovian.InGameLogging { + [Serializable] + public readonly struct LogChannel : IEquatable { + [SerializeField] readonly string id; + + public string Id => id; + + public LogChannel(string id) { + this.id = id; + } + + public static readonly LogChannel Combat = new("Combat"); + public static readonly LogChannel CharacterCreation = new("CharacterCreation"); + public static readonly LogChannel World = new("World"); + public static readonly LogChannel General = new("General"); + + public bool Equals(LogChannel other) => string.Equals(id, other.id, StringComparison.Ordinal); + public override bool Equals(object obj) => obj is LogChannel other && Equals(other); + public override int GetHashCode() => id != null ? id.GetHashCode() : 0; + public override string ToString() => id ?? string.Empty; + + public static bool operator ==(LogChannel left, LogChannel right) => left.Equals(right); + public static bool operator !=(LogChannel left, LogChannel right) => !left.Equals(right); + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs +git commit -m "feat: add LogChannel readonly struct with built-in channels" +``` + +--- + +### Task 3: LogChannelJsonConverter + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs` + +**Step 1: Implement Newtonsoft converter** + +```csharp +using System; +using Newtonsoft.Json; + +namespace Jovian.InGameLogging { + public sealed class LogChannelJsonConverter : JsonConverter { + public override void WriteJson(JsonWriter writer, LogChannel value, JsonSerializer serializer) { + writer.WriteValue(value.Id); + } + + public override LogChannel ReadJson(JsonReader reader, Type objectType, LogChannel existingValue, bool hasExistingValue, JsonSerializer serializer) { + var id = reader.Value as string; + return new LogChannel(id); + } + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs +git commit -m "feat: add Newtonsoft JSON converter for LogChannel" +``` + +--- + +### Task 4: LogEntry readonly struct + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs` + +**Step 1: Implement LogEntry** + +```csharp +using System; +using Newtonsoft.Json; + +namespace Jovian.InGameLogging { + [Serializable] + public readonly struct LogEntry { + public readonly string message; + + [JsonConverter(typeof(LogChannelJsonConverter))] + public readonly LogChannel channel; + + public readonly float gameTime; + + public LogEntry(string message, LogChannel channel, float gameTime) { + this.message = message; + this.channel = channel; + this.gameTime = gameTime; + } + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs +git commit -m "feat: add LogEntry readonly struct" +``` + +--- + +### Task 5: GameLogSaveData + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs` + +**Step 1: Implement serializable save container** + +```csharp +using System; +using System.Collections.Generic; + +namespace Jovian.InGameLogging { + [Serializable] + public sealed class GameLogSaveData { + public List entries; + + public GameLogSaveData() { + entries = new List(); + } + + public GameLogSaveData(List entries) { + this.entries = entries; + } + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs +git commit -m "feat: add GameLogSaveData serializable container" +``` + +--- + +### Task 6: IGameLogStore interface + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs` + +**Step 1: Define the store interface** + +```csharp +using System; +using System.Collections.Generic; + +namespace Jovian.InGameLogging { + public interface IGameLogStore { + int Count { get; } + int Capacity { get; } + + void Add(LogChannel channel, string message); + void Clear(); + void Clear(LogChannel channel); + + ReadOnlySpan GetEntries(); + int GetEntries(LogChannel channel, List results); + + event Action OnEntryAdded; + event Action OnCleared; + + GameLogSaveData GetSaveData(); + void RestoreFromSaveData(GameLogSaveData data); + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs +git commit -m "feat: add IGameLogStore interface" +``` + +--- + +### Task 7: GameLogStore ring buffer implementation + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs` + +**Step 1: Implement the ring buffer store** + +```csharp +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Jovian.InGameLogging { + public sealed class GameLogStore : IGameLogStore { + readonly LogEntry[] buffer; + int head; + int count; + + public int Count => count; + public int Capacity => buffer.Length; + + public event Action OnEntryAdded; + public event Action OnCleared; + + public GameLogStore(int capacity = 500) { + buffer = new LogEntry[capacity]; + head = 0; + count = 0; + } + + public void Add(LogChannel channel, string message) { + var entry = new LogEntry(message, channel, Time.time); + buffer[head] = entry; + head = (head + 1) % buffer.Length; + if(count < buffer.Length) { + count++; + } + OnEntryAdded?.Invoke(entry); + } + + public void Clear() { + head = 0; + count = 0; + OnCleared?.Invoke(); + } + + public void Clear(LogChannel channel) { + // Compact: copy non-matching entries into a temp span, then rebuild + var kept = new List(count); + var entries = GetEntries(); + for(int i = 0; i < entries.Length; i++) { + if(entries[i].channel != channel) { + kept.Add(entries[i]); + } + } + + head = 0; + count = 0; + for(int i = 0; i < kept.Count; i++) { + buffer[i] = kept[i]; + count++; + } + head = count % buffer.Length; + OnCleared?.Invoke(); + } + + public ReadOnlySpan GetEntries() { + if(count < buffer.Length) { + return new ReadOnlySpan(buffer, 0, count); + } + // Buffer is full and wrapping - need to return in chronological order + var result = new LogEntry[count]; + int start = head; // head points to the oldest entry when full + for(int i = 0; i < count; i++) { + result[i] = buffer[(start + i) % buffer.Length]; + } + return result; + } + + public int GetEntries(LogChannel channel, List results) { + results.Clear(); + var entries = GetEntries(); + for(int i = 0; i < entries.Length; i++) { + if(entries[i].channel == channel) { + results.Add(entries[i]); + } + } + return results.Count; + } + + public GameLogSaveData GetSaveData() { + var entries = GetEntries(); + var list = new List(entries.Length); + for(int i = 0; i < entries.Length; i++) { + list.Add(entries[i]); + } + return new GameLogSaveData(list); + } + + public void RestoreFromSaveData(GameLogSaveData data) { + head = 0; + count = 0; + if(data?.entries == null) { + OnCleared?.Invoke(); + return; + } + // Only restore up to capacity, taking the most recent entries + int startIndex = Math.Max(0, data.entries.Count - buffer.Length); + for(int i = startIndex; i < data.entries.Count; i++) { + buffer[count] = data.entries[i]; + count++; + } + head = count % buffer.Length; + OnCleared?.Invoke(); // Signal UI to rebuild from scratch + } + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs +git commit -m "feat: add GameLogStore ring buffer implementation" +``` + +--- + +### Task 8: InGameLogger facade struct + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs` + +**Step 1: Implement the per-caller facade** + +```csharp +using System.Runtime.CompilerServices; + +namespace Jovian.InGameLogging { + public readonly struct InGameLogger { + readonly IGameLogStore store; + readonly LogChannel channel; + + public InGameLogger(IGameLogStore store, LogChannel channel) { + this.store = store; + this.channel = channel; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log(string message) { + store.Add(channel, message); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Log(string message, string hexColor) { + store.Add(channel, $"{message}"); + } + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs +git commit -m "feat: add InGameLogger readonly struct facade" +``` + +--- + +### Task 9: LogEntryView MonoBehaviour + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs` + +**Step 1: Implement the minimal UI entry component** + +```csharp +using TMPro; +using UnityEngine; + +namespace Jovian.InGameLogging.UI { + public class LogEntryView : MonoBehaviour { + [SerializeField] TMP_Text messageText; + + public void SetEntry(in LogEntry entry) { + messageText.text = entry.message; + } + + public void ClearEntry() { + messageText.text = string.Empty; + } + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs +git commit -m "feat: add LogEntryView MonoBehaviour for pooled UI entries" +``` + +--- + +### Task 10: GameLogView MonoBehaviour with object pooling + +**Files:** +- Create: `Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs` + +**Step 1: Implement the scrollable log view with object pool** + +```csharp +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; + +namespace Jovian.InGameLogging.UI { + public class GameLogView : MonoBehaviour { + [SerializeField] ScrollRect scrollRect; + [SerializeField] RectTransform content; + [SerializeField] LogEntryView entryPrefab; + [SerializeField] int poolSize = 20; + + IGameLogStore store; + LogChannel? channelFilter; + bool autoScroll = true; + + readonly List activeEntries = new(); + readonly Stack pool = new(); + + public void Initialize(IGameLogStore store, LogChannel? channelFilter = null) { + this.store = store; + this.channelFilter = channelFilter; + + WarmPool(); + store.OnEntryAdded += HandleEntryAdded; + store.OnCleared += HandleCleared; + scrollRect.onValueChanged.AddListener(HandleScrollChanged); + + // Populate existing entries + RebuildFromStore(); + } + + void OnDestroy() { + if(store != null) { + store.OnEntryAdded -= HandleEntryAdded; + store.OnCleared -= HandleCleared; + } + } + + void WarmPool() { + for(int i = 0; i < poolSize; i++) { + var entry = Instantiate(entryPrefab, content); + entry.gameObject.SetActive(false); + pool.Push(entry); + } + } + + LogEntryView GetFromPool() { + LogEntryView entry; + if(pool.Count > 0) { + entry = pool.Pop(); + } + else { + // Recycle oldest active entry + entry = activeEntries[0]; + activeEntries.RemoveAt(0); + } + entry.gameObject.SetActive(true); + entry.transform.SetAsLastSibling(); + return entry; + } + + void ReturnToPool(LogEntryView entry) { + entry.ClearEntry(); + entry.gameObject.SetActive(false); + pool.Push(entry); + } + + void HandleEntryAdded(LogEntry entry) { + if(channelFilter.HasValue && entry.channel != channelFilter.Value) { + return; + } + + var view = GetFromPool(); + view.SetEntry(in entry); + activeEntries.Add(view); + + if(autoScroll) { + Canvas.ForceUpdateCanvases(); + scrollRect.verticalNormalizedPosition = 0f; + } + } + + void HandleCleared() { + for(int i = activeEntries.Count - 1; i >= 0; i--) { + ReturnToPool(activeEntries[i]); + } + activeEntries.Clear(); + + // If store still has entries (channel-specific clear), rebuild + if(store.Count > 0) { + RebuildFromStore(); + } + } + + void RebuildFromStore() { + var entries = store.GetEntries(); + for(int i = 0; i < entries.Length; i++) { + if(channelFilter.HasValue && entries[i].channel != channelFilter.Value) { + continue; + } + var view = GetFromPool(); + view.SetEntry(in entries[i]); + activeEntries.Add(view); + } + + if(autoScroll) { + Canvas.ForceUpdateCanvases(); + scrollRect.verticalNormalizedPosition = 0f; + } + } + + void HandleScrollChanged(Vector2 position) { + // Pause auto-scroll when user scrolls up, resume when at bottom + autoScroll = position.y <= 0.01f; + } + } +} +``` + +**Step 2: Commit** + +```bash +git add Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs +git commit -m "feat: add GameLogView with object pooling and scroll management" +``` + +--- + +### Task 11: Generate Unity .meta files + +Unity requires `.meta` files for every file and folder. These will be auto-generated by Unity when the editor imports the package. However, to make the package work without opening Unity, we need to ensure the directory structure is correct. + +**Step 1: Create placeholder to ensure Editor directory exists** + +Create an empty `Editor/.gitkeep` or let Unity generate metas on next editor open. Since this project builds through Unity Editor, the meta files will be generated automatically on next load. + +**Step 2: Commit all remaining files** + +```bash +git add Packages/com.jovian.ingame-logging/ +git commit -m "feat: complete com.jovian.ingame-logging package" +``` + +--- + +### Task 12: Game integration - Add GameLogSaveData to NoxSavedDataSet + +**Files:** +- Modify: `Assets/Code/GameState/NoxSaveData.cs` (add `GameLogSaveData` field to `NoxSavedDataSet`) + +**Step 1: Add the field** + +Add to `NoxSavedDataSet` class at `Assets/Code/GameState/NoxSaveData.cs:50`: + +```csharp +using Jovian.InGameLogging; // add to imports + +// Inside NoxSavedDataSet class, add: +public GameLogSaveData gameLogData; +``` + +**Step 2: Commit** + +```bash +git add Assets/Code/GameState/NoxSaveData.cs +git commit -m "feat: add GameLogSaveData field to NoxSavedDataSet" +``` + +--- + +### Task 13: Game integration - Wire GameLogStore in EntryPoint + +**Files:** +- Modify: `Assets/Code/Core/EntryPoint.cs` + +**Step 1: Create GameLogStore and pass it to game states** + +In `EntryPoint.CreateApplicationStates()`, after the character systems creation (~line 109), add: + +```csharp +using Jovian.InGameLogging; // add to imports + +// After characterSystems creation: +var gameLogStore = new GameLogStore(500); +``` + +Then pass `gameLogStore` as `IGameLogStore` to the game state constructors that need it. The exact constructors depend on which states need logging - at minimum `GameModeGameState` for combat/adventure logging. + +**Step 2: Commit** + +```bash +git add Assets/Code/Core/EntryPoint.cs +git commit -m "feat: wire GameLogStore in EntryPoint" +``` + +--- + +### Task 14: Game integration - Save/Load log data + +**Files:** +- Modify: wherever `NoxSavedDataSet` is populated before save (look for `saveSystem.Save` calls) +- Modify: `Assets/Code/GameState/NoxSaveData.cs` (`RestoreSavedData` method) + +**Step 1: Populate log data before save** + +Wherever the game creates `NoxSavedDataSet` before saving, add: + +```csharp +savedDataSet.gameLogData = gameLogStore.GetSaveData(); +``` + +**Step 2: Restore log data after load** + +In `NoxSaveData.RestoreSavedData()`, after loading save data (~line 35), add: + +```csharp +// Pass gameLogStore as parameter to RestoreSavedData, then: +if(saveData.gameLogData != null) { + gameLogStore.RestoreFromSaveData(saveData.gameLogData); +} +``` + +**Step 3: Commit** + +```bash +git add Assets/Code/GameState/NoxSaveData.cs +git commit -m "feat: integrate log save/load with game persistence" +``` + +--- + +## File Summary + +| File | Type | Purpose | +|------|------|---------| +| `Packages/com.jovian.ingame-logging/package.json` | Create | Package manifest | +| `Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef` | Create | Runtime assembly | +| `Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef` | Create | Editor assembly | +| `Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs` | Create | Channel type (readonly struct) | +| `Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs` | Create | Newtonsoft serialization | +| `Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs` | Create | Entry type (readonly struct) | +| `Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs` | Create | Serializable save container | +| `Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs` | Create | Store interface | +| `Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs` | Create | Ring buffer implementation | +| `Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs` | Create | Per-caller facade struct | +| `Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs` | Create | Entry UI component | +| `Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs` | Create | Scrollable log view with pool | +| `Assets/Code/GameState/NoxSaveData.cs` | Modify | Add GameLogSaveData field + restore | +| `Assets/Code/Core/EntryPoint.cs` | Modify | Create and wire GameLogStore |