diff --git a/AGENTS.md b/AGENTS.md index 5929d92..f2ed332 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ - `claude-explorer` следует предпочитать для изучения архитектуры, исходников, команд и внутренних инструментов Claude Code. - `claude-rag` следует предпочитать для семантического поиска по проекту, анализа больших объёмов кода и ситуаций, где локальный индекс даёт преимущество над обычным текстовым поиском. - Память о проекте, его договорённостях, архитектурных решениях, важных гайдах, кодовой базе и ранее выявленном контексте следует по возможности сохранять и повторно использовать через `claude-rag`. -- Кодовую базу проекта также следует по возможности индексировать и использовать через `claude-rag`, чтобы агент мог опираться на локальную семантическую память о структуре, модулях, связях и ранее изученных участках кода. +- Кодовую базу проекта также следует по возможности индексировать и использовать через `claude-rag`, чтобы агент мог опираться на локальную семантическую память о структуре, модулях, связях и ранее изученных участках кода. - При повторяющихся задачах, исследовании крупных подсистем, возвращении к старым решениям, поиске проектного контекста и анализе существующей кодовой базы агенту следует сначала опираться на `claude-rag`, если для этого подходит локальный индекс. - Если MCP-инструмент хуже подходит для конкретной задачи, недоступен или создаёт лишние ограничения, агент может использовать встроенные инструменты как запасной вариант. diff --git a/Assets/AddressableAssetsData/AddressableAssetSettings.asset b/Assets/AddressableAssetsData/AddressableAssetSettings.asset index a75fabb..c89bc3b 100644 --- a/Assets/AddressableAssetsData/AddressableAssetSettings.asset +++ b/Assets/AddressableAssetsData/AddressableAssetSettings.asset @@ -15,7 +15,7 @@ MonoBehaviour: m_DefaultGroup: 89fc1fb0b7336ca419f23062b0857137 m_currentHash: serializedVersion: 2 - Hash: f52a25da7976b11e0a29af72b2abf0c3 + Hash: 4f891804dbd068047c1c49dbfdb37a72 m_OptimizeCatalogSize: 0 m_BuildRemoteCatalog: 0 m_CatalogRequestsTimeout: 0 @@ -58,7 +58,7 @@ MonoBehaviour: m_ContentStateBuildPathProfileVariableName: m_CustomContentStateBuildPath: m_ContentStateBuildPath: - m_BuildAddressablesWithPlayerBuild: 0 + m_BuildAddressablesWithPlayerBuild: 1 m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]' m_GroupAssets: - {fileID: 11400000, guid: a538a44e0a6ac93488028f109a2d8913, type: 2} @@ -111,6 +111,8 @@ MonoBehaviour: - Locale - Locale-ru - Locale-en + - settings + - service m_SchemaTemplates: [] m_GroupTemplateObjects: - {fileID: 11400000, guid: 9c5c03b019376ac43b6730325a038195, type: 2} diff --git a/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset b/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset index 11104a2..367d497 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset +++ b/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset @@ -14,7 +14,14 @@ MonoBehaviour: m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.AddressableAssetGroup m_GroupName: Default Local Group m_GUID: 89fc1fb0b7336ca419f23062b0857137 - m_SerializeEntries: [] + m_SerializeEntries: + - m_GUID: 40905558cac5cdb4894f2f9b76c5f92e + m_Address: settings/service-settings-database + m_ReadOnly: 0 + m_SerializedLabels: + - service + - settings + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 0 m_Settings: {fileID: 11400000, guid: 2ab5dad59ce41a7468cdf522a3e8755d, type: 2} m_SchemaSet: diff --git a/Assets/AddressableAssetsData/ProfileDataSourceSettings.asset b/Assets/AddressableAssetsData/ProfileDataSourceSettings.asset new file mode 100644 index 0000000..b674867 --- /dev/null +++ b/Assets/AddressableAssetsData/ProfileDataSourceSettings.asset @@ -0,0 +1,28 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7e3976da977cb49238499ea3b4c237ae, type: 3} + m_Name: ProfileDataSourceSettings + m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.ProfileDataSourceSettings + profileGroupTypes: + - m_GroupTypePrefix: Built-In + m_Variables: + - m_Suffix: BuildPath + m_Value: '[UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]' + - m_Suffix: LoadPath + m_Value: '{UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]' + environments: [] + currentEnvironment: + id: + projectId: + projectGenesisId: + name: + isDefault: 0 diff --git a/Assets/AddressableAssetsData/ProfileDataSourceSettings.asset.meta b/Assets/AddressableAssetsData/ProfileDataSourceSettings.asset.meta new file mode 100644 index 0000000..6d92b9a --- /dev/null +++ b/Assets/AddressableAssetsData/ProfileDataSourceSettings.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4846fae8984a95b449132820bb1b4b63 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/AddressableAssetsData/Windows.meta b/Assets/AddressableAssetsData/Windows.meta new file mode 100644 index 0000000..04bb38a --- /dev/null +++ b/Assets/AddressableAssetsData/Windows.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 99a8d774c7497e442bc27dac4459593f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Animations/Bat.meta b/Assets/Animations/Bat.meta new file mode 100644 index 0000000..1cddbb3 --- /dev/null +++ b/Assets/Animations/Bat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 679786d94f230e14ead9f3595d08a656 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Animations/Bat/Bat.controller b/Assets/Animations/Bat/Bat.controller new file mode 100644 index 0000000..52636fc --- /dev/null +++ b/Assets/Animations/Bat/Bat.controller @@ -0,0 +1,101 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1102 &-8396895325890622289 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: New Animation_Bat + m_Speed: 2 + m_CycleOffset: 0 + m_Transitions: [] + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: 62ff08544e561a7418dded4606da6b55, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!91 &9100000 +AnimatorController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Bat + serializedVersion: 5 + m_AnimatorParameters: [] + m_AnimatorLayers: + - serializedVersion: 5 + m_Name: Base Layer + m_StateMachine: {fileID: 3455026678298623027} + m_Mask: {fileID: 0} + m_Motions: [] + m_Behaviours: [] + m_BlendingMode: 0 + m_SyncedLayerIndex: -1 + m_DefaultWeight: 0 + m_IKPass: 0 + m_SyncedLayerAffectsTiming: 0 + m_Controller: {fileID: 9100000} +--- !u!1107 &3455026678298623027 +AnimatorStateMachine: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Base Layer + m_ChildStates: + - serializedVersion: 1 + m_State: {fileID: -8396895325890622289} + m_Position: {x: 440, y: 220, z: 0} + - serializedVersion: 1 + m_State: {fileID: 5233408764562546203} + m_Position: {x: 235, y: 65, z: 0} + m_ChildStateMachines: [] + m_AnyStateTransitions: [] + m_EntryTransitions: [] + m_StateMachineTransitions: {} + m_StateMachineBehaviours: [] + m_AnyStatePosition: {x: 50, y: 20, z: 0} + m_EntryPosition: {x: 30, y: 140, z: 0} + m_ExitPosition: {x: 800, y: 120, z: 0} + m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} + m_DefaultState: {fileID: -8396895325890622289} +--- !u!1102 &5233408764562546203 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: New Animation_Bat 0 + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: [] + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: 62ff08544e561a7418dded4606da6b55, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: diff --git a/Assets/Animations/Bat/Bat.controller.meta b/Assets/Animations/Bat/Bat.controller.meta new file mode 100644 index 0000000..9a5097b --- /dev/null +++ b/Assets/Animations/Bat/Bat.controller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3f4c71c00bf3d624cb0e59d388599684 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Animations/Bat/New Animation_Bat.anim b/Assets/Animations/Bat/New Animation_Bat.anim new file mode 100644 index 0000000..0b94977 --- /dev/null +++ b/Assets/Animations/Bat/New Animation_Bat.anim @@ -0,0 +1,2209 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!74 &7400000 +AnimationClip: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: New Animation_Bat + serializedVersion: 7 + m_Legacy: 0 + m_Compressed: 0 + m_UseHighQualityCurve: 1 + m_RotationCurves: [] + m_CompressedRotationCurves: [] + m_EulerCurves: + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: 47.571} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: 81.441} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: 47.571} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_10 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: -57.824} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: -92.265} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: -57.824} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_6 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: -101.237} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: -87.56} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: -101.237} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_3/bone_4 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: -107.033} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: -115.837} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: -107.033} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_3/bone_5 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: -48.762} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: -55.458} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: -48.762} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_6/bone_7/bone_8 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: -24.06} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: -37.418} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: -24.06} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_6/bone_7/bone_8/bone_9 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: 36.384} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: 43.321} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: 36.384} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_10/bone_11/bone_12 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: 33.945} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: 51.368} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: 33.945} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_10/bone_11/bone_12/bone_13 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: -7.552} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: -11.413} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: -7.552} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: -36.686} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: -39.907} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: -36.686} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_10/bone_11 + - curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: {x: 0, y: 0, z: 53.311} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 0.5 + value: {x: 0, y: 0, z: 57.513} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + - serializedVersion: 3 + time: 1 + value: {x: 0, y: 0, z: 53.311} + inSlope: {x: 0, y: 0, z: 0} + outSlope: {x: 0, y: 0, z: 0} + tangentMode: 0 + weightedMode: 0 + inWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + outWeight: {x: 0.33333334, y: 0.33333334, z: 0.33333334} + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + path: bone_1/bone_2/bone_6/bone_7 + m_PositionCurves: [] + m_ScaleCurves: [] + m_FloatCurves: [] + m_PPtrCurves: [] + m_SampleRate: 60 + m_WrapMode: 0 + m_Bounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 0, y: 0, z: 0} + m_ClipBindingConstant: + genericBindings: + - serializedVersion: 2 + path: 3796966390 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 2020542332 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 3094415487 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 3480631529 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 755801333 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 3327049994 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 3965079158 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 3717257699 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 3980913656 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 3011343836 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + - serializedVersion: 2 + path: 1771591585 + attribute: 4 + script: {fileID: 0} + typeID: 4 + customType: 4 + isPPtrCurve: 0 + isIntCurve: 0 + isSerializeReferenceCurve: 0 + pptrCurveMapping: [] + m_AnimationClipSettings: + serializedVersion: 2 + m_AdditiveReferencePoseClip: {fileID: 0} + m_AdditiveReferencePoseTime: 0 + m_StartTime: 0 + m_StopTime: 1 + m_OrientationOffsetY: 0 + m_Level: 0 + m_CycleOffset: 0 + m_HasAdditiveReferencePose: 0 + m_LoopTime: 1 + m_LoopBlend: 0 + m_LoopBlendOrientation: 0 + m_LoopBlendPositionY: 0 + m_LoopBlendPositionXZ: 0 + m_KeepOriginalOrientation: 0 + m_KeepOriginalPositionY: 1 + m_KeepOriginalPositionXZ: 0 + m_HeightFromFeet: 0 + m_Mirror: 0 + m_EditorCurves: + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_10 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_10 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 47.571 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 81.441 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 47.571 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_10 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_6 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_6 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: -57.824 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: -92.265 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: -57.824 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_6 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_3/bone_4 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_3/bone_4 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: -101.237 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: -87.56 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: -101.237 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_3/bone_4 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_3/bone_5 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_3/bone_5 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: -107.033 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: -115.837 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: -107.033 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_3/bone_5 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_6/bone_7/bone_8 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_6/bone_7/bone_8 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: -48.762 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: -55.458 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: -48.762 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_6/bone_7/bone_8 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_6/bone_7/bone_8/bone_9 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_6/bone_7/bone_8/bone_9 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: -24.06 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: -37.418 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: -24.06 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_6/bone_7/bone_8/bone_9 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_10/bone_11/bone_12 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_10/bone_11/bone_12 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 36.384 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 43.321 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 36.384 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_10/bone_11/bone_12 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_10/bone_11/bone_12/bone_13 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_10/bone_11/bone_12/bone_13 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 33.945 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 51.368 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 33.945 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_10/bone_11/bone_12/bone_13 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: -7.552 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: -11.413 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: -7.552 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_10/bone_11 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_10/bone_11 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: -36.686 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: -39.907 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: -36.686 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_10/bone_11 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.x + path: bone_1/bone_2/bone_6/bone_7 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.y + path: bone_1/bone_2/bone_6/bone_7 + classID: 4 + script: {fileID: 0} + flags: 16 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 53.311 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 0.5 + value: 57.513 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 53.311 + inSlope: 0 + outSlope: 0 + tangentMode: 136 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: localEulerAnglesRaw.z + path: bone_1/bone_2/bone_6/bone_7 + classID: 4 + script: {fileID: 0} + flags: 16 + m_EulerEditorCurves: + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_10/bone_11 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_10/bone_11 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_10/bone_11 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_6/bone_7 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_6/bone_7 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_6/bone_7 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_10/bone_11/bone_12 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_10/bone_11/bone_12 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_10/bone_11/bone_12 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_10 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_10 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_10 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_10/bone_11/bone_12/bone_13 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_10/bone_11/bone_12/bone_13 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_10/bone_11/bone_12/bone_13 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_3/bone_4 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_3/bone_4 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_3/bone_4 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_3/bone_5 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_3/bone_5 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_3/bone_5 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_6/bone_7/bone_8/bone_9 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_6/bone_7/bone_8/bone_9 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_6/bone_7/bone_8/bone_9 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_6/bone_7/bone_8 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_6/bone_7/bone_8 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_6/bone_7/bone_8 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.x + path: bone_1/bone_2/bone_6 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.y + path: bone_1/bone_2/bone_6 + classID: 4 + script: {fileID: 0} + flags: 0 + - serializedVersion: 2 + curve: + serializedVersion: 2 + m_Curve: [] + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + attribute: m_LocalEulerAngles.z + path: bone_1/bone_2/bone_6 + classID: 4 + script: {fileID: 0} + flags: 0 + m_HasGenericRootTransform: 0 + m_HasMotionFloatCurves: 0 + m_Events: [] diff --git a/Assets/Animations/Bat/New Animation_Bat.anim.meta b/Assets/Animations/Bat/New Animation_Bat.anim.meta new file mode 100644 index 0000000..2baf3a8 --- /dev/null +++ b/Assets/Animations/Bat/New Animation_Bat.anim.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 62ff08544e561a7418dded4606da6b55 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DefaultVolumeProfile.asset b/Assets/DefaultVolumeProfile.asset index 0cd0e1a..8cea4d2 100644 --- a/Assets/DefaultVolumeProfile.asset +++ b/Assets/DefaultVolumeProfile.asset @@ -322,6 +322,9 @@ MonoBehaviour: highQualityFiltering: m_OverrideState: 1 m_Value: 0 + filter: + m_OverrideState: 1 + m_Value: 0 downscale: m_OverrideState: 1 m_Value: 0 diff --git a/Assets/InputSystem_Actions.inputactions b/Assets/InputSystem_Actions.inputactions index 1a12cb9..8408c2b 100644 --- a/Assets/InputSystem_Actions.inputactions +++ b/Assets/InputSystem_Actions.inputactions @@ -1,4 +1,5 @@ { + "version": 1, "name": "InputSystem_Actions", "maps": [ { @@ -501,7 +502,7 @@ "name": "Cancel", "type": "Button", "id": "15cef263-9014-4fd5-94d9-4e4a6234a6ef", - "expectedControlType": "Button", + "expectedControlType": "", "processors": "", "interactions": "", "initialStateCheck": false @@ -568,6 +569,15 @@ "processors": "", "interactions": "", "initialStateCheck": false + }, + { + "name": "Secret", + "type": "Button", + "id": "1f9d3187-9fdb-4228-a91b-26d8dc96e7d0", + "expectedControlType": "", + "processors": "", + "interactions": "", + "initialStateCheck": false } ], "bindings": [ @@ -988,6 +998,17 @@ "action": "TrackedDeviceOrientation", "isComposite": false, "isPartOfComposite": false + }, + { + "name": "", + "id": "4d76d233-1faa-47fe-8595-55c39fbe3552", + "path": "/f6", + "interactions": "", + "processors": "", + "groups": "", + "action": "Secret", + "isComposite": false, + "isPartOfComposite": false } ] } diff --git a/Assets/Prefabs/Gameplay/Collectibles.meta b/Assets/Prefabs/Gameplay/Collectibles.meta new file mode 100644 index 0000000..172cca0 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Collectibles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ca975eead98335848b513efde68a38c7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/Collectibles/BounceBoostCollectible.prefab b/Assets/Prefabs/Gameplay/Collectibles/BounceBoostCollectible.prefab new file mode 100644 index 0000000..0250f89 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Collectibles/BounceBoostCollectible.prefab @@ -0,0 +1,159 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1649926752591026283 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3454048493872327249} + - component: {fileID: 3591721972395430666} + - component: {fileID: 9139793318294144304} + - component: {fileID: 6495022491269397702} + - component: {fileID: 8526854025431675824} + m_Layer: 0 + m_Name: BounceBoostCollectible + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &3454048493872327249 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649926752591026283} + serializedVersion: 2 + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &3591721972395430666 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649926752591026283} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 10 + m_MaskInteraction: 0 + m_Sprite: {fileID: 21300000, guid: 341628d8840484648b84419110eeabfa, type: 3} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 0.59, y: 0.62} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!58 &9139793318294144304 +CircleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649926752591026283} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_Radius: 0.25 +--- !u!114 &6495022491269397702 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649926752591026283} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 45176cc3d2b0adf4592469063916cafe, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.BounceBoostCollectible + disableOnCollected: 1 + bounceVelocity: 40 + scoreValue: 5 +--- !u!114 &8526854025431675824 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1649926752591026283} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 87915a1531f287e40b455801243acc45, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnedContent diff --git a/Assets/Prefabs/Gameplay/Collectibles/BounceBoostCollectible.prefab.meta b/Assets/Prefabs/Gameplay/Collectibles/BounceBoostCollectible.prefab.meta new file mode 100644 index 0000000..3d7dc0d --- /dev/null +++ b/Assets/Prefabs/Gameplay/Collectibles/BounceBoostCollectible.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 42ac5c9fb765781449b1a8cdc5bd3d64 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/Collectibles/CrystalCollectible.prefab b/Assets/Prefabs/Gameplay/Collectibles/CrystalCollectible.prefab new file mode 100644 index 0000000..a5086ce --- /dev/null +++ b/Assets/Prefabs/Gameplay/Collectibles/CrystalCollectible.prefab @@ -0,0 +1,192 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &7341068655679281185 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4026968076215623752} + - component: {fileID: 9140651947484360701} + - component: {fileID: 6853623286791460233} + - component: {fileID: 8462963241259200514} + m_Layer: 0 + m_Name: CrystalCollectible + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4026968076215623752 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7341068655679281185} + serializedVersion: 2 + 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: 8349381518911893225} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!70 &9140651947484360701 +CapsuleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7341068655679281185} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_Size: {x: 0.5, y: 0.75} + m_Direction: 0 +--- !u!114 &6853623286791460233 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7341068655679281185} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: deb903e81ee443948b7397fcac22b117, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.CoinCollectible + disableOnCollected: 1 + coinValue: 1 + scoreValue: 1 +--- !u!114 &8462963241259200514 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7341068655679281185} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 87915a1531f287e40b455801243acc45, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnedContent +--- !u!1 &9134611908005895849 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8349381518911893225} + - component: {fileID: 8296298383556791415} + m_Layer: 0 + m_Name: Crystal + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8349381518911893225 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9134611908005895849} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0.2, y: 0.2, z: 0.2} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 4026968076215623752} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &8296298383556791415 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 9134611908005895849} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: 7479642271789056252, guid: d61b05f68855c484e824fc5acfb172a1, type: 3} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 diff --git a/Assets/Prefabs/Gameplay/Collectibles/CrystalCollectible.prefab.meta b/Assets/Prefabs/Gameplay/Collectibles/CrystalCollectible.prefab.meta new file mode 100644 index 0000000..8b407d0 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Collectibles/CrystalCollectible.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9987915360f9bf740bc0939e23db9349 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/Collectibles/HeightLiftCollectible.prefab b/Assets/Prefabs/Gameplay/Collectibles/HeightLiftCollectible.prefab new file mode 100644 index 0000000..616f89d --- /dev/null +++ b/Assets/Prefabs/Gameplay/Collectibles/HeightLiftCollectible.prefab @@ -0,0 +1,160 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &382672864354709237 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 384491279874040987} + - component: {fileID: 6170450325268674343} + - component: {fileID: 8596285276108703207} + - component: {fileID: 406616014793274496} + - component: {fileID: 7456204191848197440} + m_Layer: 0 + m_Name: HeightLiftCollectible + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &384491279874040987 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 382672864354709237} + serializedVersion: 2 + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &6170450325268674343 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 382672864354709237} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 10 + m_MaskInteraction: 0 + m_Sprite: {fileID: 21300000, guid: 83d3aff135b0eba41879575691eb5bb8, type: 3} + m_Color: {r: 0.9622642, g: 0.34950158, b: 0.38523003, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 0.62, y: 0.65} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!58 &8596285276108703207 +CircleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 382672864354709237} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_Radius: 0.27 +--- !u!114 &406616014793274496 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 382672864354709237} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d6608696d5f177540b0f06b9cbb46826, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.HeightLiftCollectible + disableOnCollected: 1 + liftSpeed: 40 + liftHeight: 100 + scoreValue: 10 +--- !u!114 &7456204191848197440 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 382672864354709237} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 87915a1531f287e40b455801243acc45, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnedContent diff --git a/Assets/Prefabs/Gameplay/Collectibles/HeightLiftCollectible.prefab.meta b/Assets/Prefabs/Gameplay/Collectibles/HeightLiftCollectible.prefab.meta new file mode 100644 index 0000000..c2f03c1 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Collectibles/HeightLiftCollectible.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 23063041ef8ecb449b37527f2279534f +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/EmptyPlatform.prefab b/Assets/Prefabs/Gameplay/EmptyPlatform.prefab index d3eaa05..455b9cf 100644 --- a/Assets/Prefabs/Gameplay/EmptyPlatform.prefab +++ b/Assets/Prefabs/Gameplay/EmptyPlatform.prefab @@ -159,7 +159,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.OneWayPlatformCollider player: {fileID: 0} - topBoundaryPadding: 0.02 + topBoundaryPadding: 0 + landingVelocityThreshold: 0.05 + crossingGraceDistance: 0.8 --- !u!114 &2235637253677343842 MonoBehaviour: m_ObjectHideFlags: 0 @@ -253,8 +255,8 @@ SpriteRenderer: m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 + m_SortingLayerID: 1658682819 + m_SortingLayer: -1 m_SortingOrder: 0 m_MaskInteraction: 0 m_Sprite: {fileID: -70544561768458129, guid: d440231d6f96217499ab62926952e2b3, type: 3} @@ -391,8 +393,8 @@ SpriteRenderer: m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 + m_SortingLayerID: 1658682819 + m_SortingLayer: -1 m_SortingOrder: 0 m_MaskInteraction: 0 m_Sprite: {fileID: 8449129219236236317, guid: d440231d6f96217499ab62926952e2b3, type: 3} diff --git a/Assets/Prefabs/Gameplay/Enemies.meta b/Assets/Prefabs/Gameplay/Enemies.meta new file mode 100644 index 0000000..b1da3c1 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Enemies.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1f0b72bfb64324440bbd24dd992124ed +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/Enemies/Bat.prefab b/Assets/Prefabs/Gameplay/Enemies/Bat.prefab new file mode 100644 index 0000000..cce10e4 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Enemies/Bat.prefab @@ -0,0 +1,339 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1001 &6060741554283226580 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: -8653719598885068355, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_Name + value: Bat + objectReference: {fileID: 0} + - target: {fileID: -7573160195067319704, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.w + value: 0.9657164 + objectReference: {fileID: 0} + - target: {fileID: -7573160195067319704, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -7573160195067319704, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -7573160195067319704, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.z + value: 0.25959963 + objectReference: {fileID: 0} + - target: {fileID: -7573160195067319704, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 30.093 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalScale.x + value: 0.1 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalScale.y + value: 0.1 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalScale.z + value: 0.1 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalPosition.x + value: -0.8778173 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalPosition.y + value: -6.702254 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_ConstrainProportionsScale + value: 1 + objectReference: {fileID: 0} + - target: {fileID: -7221020487405985688, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.w + value: 0.9978294 + objectReference: {fileID: 0} + - target: {fileID: -7221020487405985688, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -7221020487405985688, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -7221020487405985688, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.z + value: -0.06585223 + objectReference: {fileID: 0} + - target: {fileID: -7221020487405985688, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: -7.552 + objectReference: {fileID: 0} + - target: {fileID: -1914268060482745136, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.w + value: 0.9865411 + objectReference: {fileID: 0} + - target: {fileID: -1914268060482745136, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -1914268060482745136, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -1914268060482745136, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.z + value: -0.16351335 + objectReference: {fileID: 0} + - target: {fileID: -1914268060482745136, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: -18.822 + objectReference: {fileID: 0} + - target: {fileID: -103470509922460878, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.w + value: 0.93103963 + objectReference: {fileID: 0} + - target: {fileID: -103470509922460878, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -103470509922460878, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: -103470509922460878, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.z + value: -0.36491826 + objectReference: {fileID: 0} + - target: {fileID: -103470509922460878, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: -42.805 + objectReference: {fileID: 0} + - target: {fileID: 2471920254983572574, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.w + value: 0.90787965 + objectReference: {fileID: 0} + - target: {fileID: 2471920254983572574, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 2471920254983572574, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 2471920254983572574, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.z + value: 0.4192309 + objectReference: {fileID: 0} + - target: {fileID: 2471920254983572574, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 49.572 + objectReference: {fileID: 0} + - target: {fileID: 7905440446968883676, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.w + value: 0.85633266 + objectReference: {fileID: 0} + - target: {fileID: 7905440446968883676, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 7905440446968883676, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.y + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 7905440446968883676, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalRotation.z + value: -0.5164246 + objectReference: {fileID: 0} + - target: {fileID: 7905440446968883676, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: -62.185 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: -8653719598885068355, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + insertIndex: -1 + addedObject: {fileID: 6616969598510582301} + - targetCorrespondingSourceObject: {fileID: -8653719598885068355, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + insertIndex: -1 + addedObject: {fileID: 7030633707417827889} + - targetCorrespondingSourceObject: {fileID: -8653719598885068355, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + insertIndex: -1 + addedObject: {fileID: 9214245330110399167} + - targetCorrespondingSourceObject: {fileID: -8653719598885068355, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + insertIndex: -1 + addedObject: {fileID: 5029523047635980553} + m_SourcePrefab: {fileID: 4843985084834002234, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} +--- !u!4 &4842427733413059594 stripped +Transform: + m_CorrespondingSourceObject: {fileID: -7552582706839291426, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + m_PrefabInstance: {fileID: 6060741554283226580} + m_PrefabAsset: {fileID: 0} +--- !u!1 &6051671913546024041 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: -8653719598885068355, guid: 5e0dbc12e9f97e14cbad53fb747704c9, type: 3} + m_PrefabInstance: {fileID: 6060741554283226580} + m_PrefabAsset: {fileID: 0} +--- !u!95 &6616969598510582301 +Animator: + serializedVersion: 7 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6051671913546024041} + m_Enabled: 1 + m_Avatar: {fileID: 0} + m_Controller: {fileID: 9100000, guid: 3f4c71c00bf3d624cb0e59d388599684, type: 2} + m_CullingMode: 0 + m_UpdateMode: 0 + m_ApplyRootMotion: 0 + m_LinearVelocityBlending: 0 + m_StabilizeFeet: 0 + m_AnimatePhysics: 0 + m_WarningMessage: + m_HasTransformHierarchy: 1 + m_AllowConstantClipSamplingOptimization: 1 + m_KeepAnimatorStateOnDisable: 0 + m_WriteDefaultValuesOnDisable: 0 +--- !u!114 &7030633707417827889 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6051671913546024041} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c29cff538c195c249b69c6f2236de67b, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.2D.Animation.Runtime::UnityEngine.U2D.Animation.SpriteLibrary + m_Library: [] + m_SpriteLibraryAsset: {fileID: 7108339969198208228, guid: 303efce1e093b814d8980dc929aff8ed, type: 3} +--- !u!60 &9214245330110399167 +PolygonCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6051671913546024041} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0, y: 0} + oldSize: {x: 0, y: 0} + newSize: {x: 0, y: 0} + adaptiveTilingThreshold: 0 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Points: + m_Paths: + - - {x: 5.2754264, y: 15.056673} + - {x: 2.2463593, y: 10.904937} + - {x: 2.055397, y: 10.063164} + - {x: 2.8733213, y: 7.463923} + - {x: 0.6368122, y: 8.815968} + - {x: -1.6915423, y: 7.731138} + - {x: -1.9680666, y: 5.7833405} + - {x: -3.9252276, y: 8.813034} + - {x: -10.103769, y: 9.624693} + - {x: -11.543783, y: 8.284389} + - {x: -6.7071548, y: 3.448938} + - {x: -2.083034, y: 1.3314323} + - {x: 3.1557622, y: 0.10868433} + - {x: 6.223524, y: 4.1325207} + - {x: 8.0000925, y: 8.740171} + - {x: 8.46353, y: 15.634463} + m_UseDelaunayMesh: 1 +--- !u!114 &5029523047635980553 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6051671913546024041} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 605dc94842c31e540ae7a09b17007591, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PatrolEnemy + spawnKind: 1 + stompBounceVelocity: 17 + stompScore: 10 + stompTopTolerance: 0.2 + stompMinimumHeightFactor: 0.55 + localPatrolAxis: {x: 1, y: 0} + patrolDistance: 1.2 + patrolSpeed: 1.4 + flipWhenMovingBack: 1 + visualFacesPositiveDirection: 0 + visualRoot: {fileID: 4842427733413059594} diff --git a/Assets/Prefabs/Gameplay/Enemies/Bat.prefab.meta b/Assets/Prefabs/Gameplay/Enemies/Bat.prefab.meta new file mode 100644 index 0000000..319ab14 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Enemies/Bat.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: dc382d4fdc981aa439af1624e230f11f +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/Enemies/PatrolEnemy.prefab b/Assets/Prefabs/Gameplay/Enemies/PatrolEnemy.prefab new file mode 100644 index 0000000..3f9b9cf --- /dev/null +++ b/Assets/Prefabs/Gameplay/Enemies/PatrolEnemy.prefab @@ -0,0 +1,194 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1911682497373903437 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8262002995828152749} + - component: {fileID: 5577005654857578664} + - component: {fileID: 3404602057596076165} + - component: {fileID: 1556031391106333805} + m_Layer: 0 + m_Name: PatrolEnemy + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8262002995828152749 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1911682497373903437} + serializedVersion: 2 + 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: 4866556234619625411} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!58 &5577005654857578664 +CircleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1911682497373903437} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_Radius: 0.38 +--- !u!114 &3404602057596076165 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1911682497373903437} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 605dc94842c31e540ae7a09b17007591, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PatrolEnemy + localPatrolAxis: {x: 1, y: 0} + patrolDistance: 1.2 + patrolSpeed: 1.4 + stompBounceVelocity: 17 + stompScore: 10 + stompTopTolerance: 0.2 +--- !u!114 &1556031391106333805 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1911682497373903437} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 87915a1531f287e40b455801243acc45, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnedContent +--- !u!1 &6676621499933334337 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 4866556234619625411} + - component: {fileID: 8366382549807975629} + m_Layer: 0 + m_Name: Sprite + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &4866556234619625411 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6676621499933334337} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0.2, y: 0.2, z: 0.2} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 8262002995828152749} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &8366382549807975629 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6676621499933334337} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: -8781757214100938709, guid: dfda0581a0387f14fb9b79970fbe2478, type: 3} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 3.89, y: 3.05} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 diff --git a/Assets/Prefabs/Gameplay/Enemies/PatrolEnemy.prefab.meta b/Assets/Prefabs/Gameplay/Enemies/PatrolEnemy.prefab.meta new file mode 100644 index 0000000..443009d --- /dev/null +++ b/Assets/Prefabs/Gameplay/Enemies/PatrolEnemy.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: caa08bdad17cf83468a8416384277fe4 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/Equipment/HazardEquipment.prefab b/Assets/Prefabs/Gameplay/Equipment/HazardEquipment.prefab new file mode 100644 index 0000000..a406cf0 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Equipment/HazardEquipment.prefab @@ -0,0 +1,157 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6689162642216591743 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5085244689812310206} + - component: {fileID: 4464912994306914734} + - component: {fileID: 517635067233225658} + - component: {fileID: 7544208898612138932} + - component: {fileID: 5070194482802988398} + m_Layer: 0 + m_Name: HazardEquipment + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &5085244689812310206 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6689162642216591743} + serializedVersion: 2 + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &4464912994306914734 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6689162642216591743} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 10 + m_MaskInteraction: 0 + m_Sprite: {fileID: 21300000, guid: 83d3aff135b0eba41879575691eb5bb8, type: 3} + m_Color: {r: 1, g: 0.35, b: 0.35, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 0.62, y: 0.65} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!58 &517635067233225658 +CircleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6689162642216591743} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_Radius: 0.25 +--- !u!114 &7544208898612138932 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6689162642216591743} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cf34504aafe850849b76ddd3857f681b, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.HazardEquipment + activationCooldown: 0.2 +--- !u!114 &5070194482802988398 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6689162642216591743} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 87915a1531f287e40b455801243acc45, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnedContent diff --git a/Assets/Prefabs/Gameplay/Equipment/HazardEquipment.prefab.meta b/Assets/Prefabs/Gameplay/Equipment/HazardEquipment.prefab.meta new file mode 100644 index 0000000..cfdefcb --- /dev/null +++ b/Assets/Prefabs/Gameplay/Equipment/HazardEquipment.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c605966864b25ba42a9f33ff919a800c +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/Equipment/SpeedBoostEquipment.prefab b/Assets/Prefabs/Gameplay/Equipment/SpeedBoostEquipment.prefab new file mode 100644 index 0000000..a2526ab --- /dev/null +++ b/Assets/Prefabs/Gameplay/Equipment/SpeedBoostEquipment.prefab @@ -0,0 +1,160 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &6394508084590719524 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8225866807125319873} + - component: {fileID: 7400502600871487111} + - component: {fileID: 6910139583701154754} + - component: {fileID: 8861890666834832349} + - component: {fileID: 101100010298365801} + m_Layer: 0 + m_Name: SpeedBoostEquipment + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8225866807125319873 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6394508084590719524} + serializedVersion: 2 + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &7400502600871487111 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6394508084590719524} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 10 + m_MaskInteraction: 0 + m_Sprite: {fileID: 21300000, guid: 7414a1b979dd3124699a42c794a592d3, type: 3} + m_Color: {r: 0.5, g: 1, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 0.62, y: 0.64} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!58 &6910139583701154754 +CircleCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6394508084590719524} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 1 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_Radius: 0.25 +--- !u!114 &8861890666834832349 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6394508084590719524} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 747a5c8ca2dafe94babc0829532cf3b4, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.SpeedBoostEquipment + moveSpeedMultiplier: 1.45 + duration: 3 + bounceVelocity: 0 + activationCooldown: 0.25 +--- !u!114 &101100010298365801 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6394508084590719524} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 87915a1531f287e40b455801243acc45, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnedContent diff --git a/Assets/Prefabs/Gameplay/Equipment/SpeedBoostEquipment.prefab.meta b/Assets/Prefabs/Gameplay/Equipment/SpeedBoostEquipment.prefab.meta new file mode 100644 index 0000000..e5beb44 --- /dev/null +++ b/Assets/Prefabs/Gameplay/Equipment/SpeedBoostEquipment.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a8e076fdd882f2e4ab07a76984a0bb97 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/FragilePlatform.prefab b/Assets/Prefabs/Gameplay/FragilePlatform.prefab new file mode 100644 index 0000000..c81c3f3 --- /dev/null +++ b/Assets/Prefabs/Gameplay/FragilePlatform.prefab @@ -0,0 +1,694 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &964816656659499055 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1205885904080248378} + - component: {fileID: 3222511668002816921} + - component: {fileID: 390504419653235742} + - component: {fileID: 8258886993694919767} + - component: {fileID: 315965220548801040} + - component: {fileID: 2235637253677343842} + - component: {fileID: 310175300831842493} + m_Layer: 0 + m_Name: FragilePlatform + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1205885904080248378 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + serializedVersion: 2 + 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: 6710763819495474449} + - {fileID: 1053927436537949408} + - {fileID: 2175820264372441907} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!50 &3222511668002816921 +Rigidbody2D: + serializedVersion: 5 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_BodyType: 1 + m_Simulated: 1 + m_UseFullKinematicContacts: 0 + m_UseAutoMass: 0 + m_Mass: 1 + m_LinearDamping: 0 + m_AngularDamping: 0.05 + m_GravityScale: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_Interpolate: 0 + m_SleepingMode: 1 + m_CollisionDetection: 0 + m_Constraints: 0 +--- !u!66 &390504419653235742 +CompositeCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_GeometryType: 1 + m_GenerationType: 0 + m_EdgeRadius: 0 + m_ColliderPaths: + - m_Collider: {fileID: 3218710261601004859} + m_ColliderPaths: + - - X: 5000000 + Y: 5000000 + - X: -5000000 + Y: 5000000 + - X: -5000000 + Y: 0 + - X: 5000000 + Y: 0 + - m_Collider: {fileID: 5554127825943555543} + m_ColliderPaths: + - - X: 15000000 + Y: 5000000 + - X: 5000000 + Y: 5000000 + - X: 5000000 + Y: 0 + - X: 15000000 + Y: 0 + m_CompositePaths: + m_Paths: + - - {x: 1.4999707, y: 0} + - {x: 1.4999707, y: 0.5} + - {x: -0.5, y: 0.4999707} + - {x: -0.4999707, y: 0} + m_VertexDistance: 0.0005 + m_OffsetDistance: 0.00005 + m_UseDelaunayMesh: 0 + m_CompositeGameObject: {fileID: 964816656659499055} +--- !u!114 &8258886993694919767 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 63a76cd9bb67ade47b92da08d5312f7a, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.JumpPlatform +--- !u!114 &315965220548801040 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b7304af1773d2d841ac6233d76035ef1, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.OneWayPlatformCollider + player: {fileID: 0} + topBoundaryPadding: 0 + landingVelocityThreshold: 0.05 + crossingGraceDistance: 0.8 +--- !u!114 &2235637253677343842 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0a288dffeb6d69d4699ecd00e49b0f2c, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformController + collectibleSpawnPoints: + - {fileID: 8634086561103883683} + equipmentSpawnPoints: + - {fileID: 7063958560061733011} + enemySpawnPoints: [] +--- !u!114 &310175300831842493 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: de467f1f456b3c1409c4e51eca4911e2, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.FragilePlatformBehaviour + breakDelay: 0.08 + suppressAutoJump: 0 + disabledOnBreak: [] + colliders: [] +--- !u!1 &2372201055787281608 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2175820264372441907} + m_Layer: 0 + m_Name: SpawnPoints + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2175820264372441907 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2372201055787281608} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0.5, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 7296410561034137608} + - {fileID: 8232334407418952531} + m_Father: {fileID: 1205885904080248378} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &5077136950761168767 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2540267024819485677} + - component: {fileID: 1978967242000210638} + m_Layer: 0 + m_Name: Square + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!4 &2540267024819485677 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5077136950761168767} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0.6, y: 0.6, z: 0.6} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 8232334407418952531} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &1978967242000210638 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5077136950761168767} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: 21300000, guid: a7ea8ba2d9fa2eb42a0b8af96e11fc66, type: 3} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!1 &6543131293092626866 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1053927436537949408} + - component: {fileID: 1219762918678929400} + - component: {fileID: 5554127825943555543} + m_Layer: 0 + m_Name: PlatformTile_2 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1053927436537949408 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6543131293092626866} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1205885904080248378} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &1219762918678929400 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6543131293092626866} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 1658682819 + m_SortingLayer: -1 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: -70544561768458129, guid: d440231d6f96217499ab62926952e2b3, type: 3} + m_Color: {r: 0.8207547, g: 0.41424885, b: 0.41424885, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!61 &5554127825943555543 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6543131293092626866} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 1 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0.25} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0.5, y: 0.5} + oldSize: {x: 1, y: 1} + newSize: {x: 1, y: 1} + adaptiveTilingThreshold: 0.5 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Size: {x: 1, y: 0.5} + m_EdgeRadius: 0 +--- !u!1 &7236042952510876049 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8232334407418952531} + - component: {fileID: 7063958560061733011} + m_Layer: 0 + m_Name: EqiupmentSpawnPoint + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8232334407418952531 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7236042952510876049} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0.65, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2540267024819485677} + m_Father: {fileID: 2175820264372441907} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &7063958560061733011 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7236042952510876049} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4b8997e854b32fc45b20a35799d85c67, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnPoint + kind: 1 + optional: 0 + weight: 1 +--- !u!1 &7300961631766593696 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6710763819495474449} + - component: {fileID: 3849633677881264400} + - component: {fileID: 3218710261601004859} + m_Layer: 0 + m_Name: PlatformTile + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6710763819495474449 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7300961631766593696} + serializedVersion: 2 + 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: 1205885904080248378} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &3849633677881264400 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7300961631766593696} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 1658682819 + m_SortingLayer: -1 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: 8449129219236236317, guid: d440231d6f96217499ab62926952e2b3, type: 3} + m_Color: {r: 0.8207547, g: 0.41424885, b: 0.41424885, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!61 &3218710261601004859 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7300961631766593696} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 1 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0.25} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0.5, y: 0.5} + oldSize: {x: 1, y: 1} + newSize: {x: 1, y: 1} + adaptiveTilingThreshold: 0.5 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Size: {x: 1, y: 0.5} + m_EdgeRadius: 0 +--- !u!1 &7729436994615948155 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7296410561034137608} + - component: {fileID: 8634086561103883683} + m_Layer: 0 + m_Name: CollectibleSpawnPoint + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &7296410561034137608 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7729436994615948155} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2175820264372441907} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8634086561103883683 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7729436994615948155} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4b8997e854b32fc45b20a35799d85c67, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnPoint + kind: 0 + optional: 1 + weight: 1 diff --git a/Assets/Prefabs/Gameplay/FragilePlatform.prefab.meta b/Assets/Prefabs/Gameplay/FragilePlatform.prefab.meta new file mode 100644 index 0000000..4eddee7 --- /dev/null +++ b/Assets/Prefabs/Gameplay/FragilePlatform.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fa1b0ae89608c924e92657d291389138 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/MovingPlatform.prefab b/Assets/Prefabs/Gameplay/MovingPlatform.prefab new file mode 100644 index 0000000..079bebe --- /dev/null +++ b/Assets/Prefabs/Gameplay/MovingPlatform.prefab @@ -0,0 +1,743 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &964816656659499055 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1205885904080248378} + - component: {fileID: 3222511668002816921} + - component: {fileID: 390504419653235742} + - component: {fileID: 8258886993694919767} + - component: {fileID: 315965220548801040} + - component: {fileID: 2235637253677343842} + - component: {fileID: 2103314207307173484} + m_Layer: 0 + m_Name: MovingPlatform + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1205885904080248378 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + serializedVersion: 2 + 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: 6710763819495474449} + - {fileID: 1053927436537949408} + - {fileID: 2175820264372441907} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!50 &3222511668002816921 +Rigidbody2D: + serializedVersion: 5 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_BodyType: 1 + m_Simulated: 1 + m_UseFullKinematicContacts: 0 + m_UseAutoMass: 0 + m_Mass: 1 + m_LinearDamping: 0 + m_AngularDamping: 0.05 + m_GravityScale: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_Interpolate: 0 + m_SleepingMode: 1 + m_CollisionDetection: 0 + m_Constraints: 0 +--- !u!66 &390504419653235742 +CompositeCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 0 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0} + m_GeometryType: 1 + m_GenerationType: 0 + m_EdgeRadius: 0 + m_ColliderPaths: + - m_Collider: {fileID: 3218710261601004859} + m_ColliderPaths: + - - X: 5000000 + Y: 5000000 + - X: -5000000 + Y: 5000000 + - X: -5000000 + Y: 0 + - X: 5000000 + Y: 0 + - m_Collider: {fileID: 5554127825943555543} + m_ColliderPaths: + - - X: 15000000 + Y: 5000000 + - X: 5000000 + Y: 5000000 + - X: 5000000 + Y: 0 + - X: 15000000 + Y: 0 + m_CompositePaths: + m_Paths: + - - {x: 1.4999707, y: 0} + - {x: 1.4999707, y: 0.5} + - {x: -0.5, y: 0.4999707} + - {x: -0.4999707, y: 0} + m_VertexDistance: 0.0005 + m_OffsetDistance: 0.00005 + m_UseDelaunayMesh: 0 + m_CompositeGameObject: {fileID: 964816656659499055} +--- !u!114 &8258886993694919767 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 63a76cd9bb67ade47b92da08d5312f7a, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.JumpPlatform +--- !u!114 &315965220548801040 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: b7304af1773d2d841ac6233d76035ef1, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.OneWayPlatformCollider + player: {fileID: 0} + topBoundaryPadding: 0 + landingVelocityThreshold: 0.05 + crossingGraceDistance: 0.8 +--- !u!114 &2235637253677343842 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0a288dffeb6d69d4699ecd00e49b0f2c, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformController + collectibleSpawnPoints: + - {fileID: 8634086561103883683} + equipmentSpawnPoints: + - {fileID: 7063958560061733011} + enemySpawnPoints: + - {fileID: 5305667757614428695} +--- !u!114 &2103314207307173484 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 964816656659499055} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2263840105d5eb34fb85ba6f13d89c65, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.MovingPlatformBehaviour + localMoveAxis: {x: 1, y: 0} + amplitude: 1.25 + cycleDuration: 2.5 + useSpawnIndexOffset: 1 +--- !u!1 &2372201055787281608 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2175820264372441907} + m_Layer: 0 + m_Name: SpawnPoints + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2175820264372441907 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2372201055787281608} + serializedVersion: 2 + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0.5, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 7296410561034137608} + - {fileID: 8232334407418952531} + - {fileID: 6950890482960297564} + m_Father: {fileID: 1205885904080248378} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &5077136950761168767 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2540267024819485677} + - component: {fileID: 1978967242000210638} + m_Layer: 0 + m_Name: Square + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!4 &2540267024819485677 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5077136950761168767} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0.6, y: 0.6, z: 0.6} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 8232334407418952531} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &1978967242000210638 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5077136950761168767} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: 21300000, guid: a7ea8ba2d9fa2eb42a0b8af96e11fc66, type: 3} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!1 &6543131293092626866 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1053927436537949408} + - component: {fileID: 1219762918678929400} + - component: {fileID: 5554127825943555543} + m_Layer: 0 + m_Name: PlatformTile_2 + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1053927436537949408 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6543131293092626866} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 1, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1205885904080248378} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &1219762918678929400 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6543131293092626866} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: -70544561768458129, guid: d440231d6f96217499ab62926952e2b3, type: 3} + m_Color: {r: 0.20496617, g: 0.5488127, b: 0.9245283, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!61 &5554127825943555543 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6543131293092626866} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 1 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0.25} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0.5, y: 0.5} + oldSize: {x: 1, y: 1} + newSize: {x: 1, y: 1} + adaptiveTilingThreshold: 0.5 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Size: {x: 1, y: 0.5} + m_EdgeRadius: 0 +--- !u!1 &7236042952510876049 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8232334407418952531} + - component: {fileID: 7063958560061733011} + m_Layer: 0 + m_Name: EqiupmentSpawnPoint + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8232334407418952531 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7236042952510876049} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0.65, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2540267024819485677} + m_Father: {fileID: 2175820264372441907} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &7063958560061733011 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7236042952510876049} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4b8997e854b32fc45b20a35799d85c67, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnPoint + kind: 1 + optional: 0 + weight: 1 +--- !u!1 &7300961631766593696 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6710763819495474449} + - component: {fileID: 3849633677881264400} + - component: {fileID: 3218710261601004859} + m_Layer: 0 + m_Name: PlatformTile + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6710763819495474449 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7300961631766593696} + serializedVersion: 2 + 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: 1205885904080248378} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!212 &3849633677881264400 +SpriteRenderer: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7300961631766593696} + m_Enabled: 1 + m_CastShadows: 0 + m_ReceiveShadows: 0 + m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 + m_MotionVectors: 1 + m_LightProbeUsage: 1 + m_ReflectionProbeUsage: 1 + m_RayTracingMode: 0 + m_RayTraceProcedural: 0 + m_RayTracingAccelStructBuildFlagsOverride: 0 + m_RayTracingAccelStructBuildFlags: 1 + m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 + m_RenderingLayerMask: 1 + m_RendererPriority: 0 + m_Materials: + - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} + m_StaticBatchInfo: + firstSubMesh: 0 + subMeshCount: 0 + m_StaticBatchRoot: {fileID: 0} + m_ProbeAnchor: {fileID: 0} + m_LightProbeVolumeOverride: {fileID: 0} + m_ScaleInLightmap: 1 + m_ReceiveGI: 1 + m_PreserveUVs: 0 + m_IgnoreNormalsForChartDetection: 0 + m_ImportantGI: 0 + m_StitchLightmapSeams: 1 + m_SelectedEditorRenderState: 0 + m_MinimumChartSize: 4 + m_AutoUVMaxDistance: 0.5 + m_AutoUVMaxAngle: 89 + m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 + m_SortingLayerID: 0 + m_SortingLayer: 0 + m_SortingOrder: 0 + m_MaskInteraction: 0 + m_Sprite: {fileID: 8449129219236236317, guid: d440231d6f96217499ab62926952e2b3, type: 3} + m_Color: {r: 0.20496617, g: 0.5488127, b: 0.9245283, a: 1} + m_FlipX: 0 + m_FlipY: 0 + m_DrawMode: 0 + m_Size: {x: 1, y: 1} + m_AdaptiveModeThreshold: 0.5 + m_SpriteTileMode: 0 + m_WasSpriteAssigned: 1 + m_SpriteSortPoint: 0 +--- !u!61 &3218710261601004859 +BoxCollider2D: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7300961631766593696} + m_Enabled: 1 + serializedVersion: 3 + m_Density: 1 + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_ForceSendLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ForceReceiveLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_ContactCaptureLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_CallbackLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_IsTrigger: 0 + m_UsedByEffector: 0 + m_CompositeOperation: 1 + m_CompositeOrder: 0 + m_Offset: {x: 0, y: 0.25} + m_SpriteTilingProperty: + border: {x: 0, y: 0, z: 0, w: 0} + pivot: {x: 0.5, y: 0.5} + oldSize: {x: 1, y: 1} + newSize: {x: 1, y: 1} + adaptiveTilingThreshold: 0.5 + drawMode: 0 + adaptiveTiling: 0 + m_AutoTiling: 0 + m_Size: {x: 1, y: 0.5} + m_EdgeRadius: 0 +--- !u!1 &7729436994615948155 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7296410561034137608} + - component: {fileID: 8634086561103883683} + m_Layer: 0 + m_Name: CollectibleSpawnPoint + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &7296410561034137608 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7729436994615948155} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1.5, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2175820264372441907} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &8634086561103883683 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7729436994615948155} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4b8997e854b32fc45b20a35799d85c67, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnPoint + kind: 0 + optional: 1 + weight: 1 +--- !u!1 &8846698754903096030 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6950890482960297564} + - component: {fileID: 5305667757614428695} + m_Layer: 0 + m_Name: EnemySpawnPoint + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6950890482960297564 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8846698754903096030} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 1.25, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 2175820264372441907} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &5305667757614428695 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8846698754903096030} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4b8997e854b32fc45b20a35799d85c67, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.PlatformSpawnPoint + kind: 2 + optional: 0 + weight: 1 diff --git a/Assets/Prefabs/Gameplay/MovingPlatform.prefab.meta b/Assets/Prefabs/Gameplay/MovingPlatform.prefab.meta new file mode 100644 index 0000000..6ad3052 --- /dev/null +++ b/Assets/Prefabs/Gameplay/MovingPlatform.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 15b31b8e5d8677545b1ce2c8f700f901 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Prefabs/Gameplay/Player.prefab b/Assets/Prefabs/Gameplay/Player.prefab index c108d1b..90b717c 100644 --- a/Assets/Prefabs/Gameplay/Player.prefab +++ b/Assets/Prefabs/Gameplay/Player.prefab @@ -1837,7 +1837,7 @@ MonoBehaviour: groundProbeDistance: 0.34 equipmentProbeHalfExtents: {x: 0.5, y: 0.35} horizontalWrapPadding: 0.65 - fallResetDistance: 5 + fallResetDistance: 0.5 fallResetFromCameraBottom: 1 respawnViewportY: 0.28 centerRespawnX: 1 diff --git a/Assets/Prefabs/Gameplay/SimplePlatform.prefab b/Assets/Prefabs/Gameplay/SimplePlatform.prefab index 0cff095..6a01ed1 100644 --- a/Assets/Prefabs/Gameplay/SimplePlatform.prefab +++ b/Assets/Prefabs/Gameplay/SimplePlatform.prefab @@ -160,7 +160,9 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.OneWayPlatformCollider player: {fileID: 0} - topBoundaryPadding: 0.02 + topBoundaryPadding: 0 + landingVelocityThreshold: 0.05 + crossingGraceDistance: 0.8 --- !u!114 &2235637253677343842 MonoBehaviour: m_ObjectHideFlags: 0 @@ -204,7 +206,7 @@ Transform: m_GameObject: {fileID: 2372201055787281608} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0.8688, y: 0.24718, z: 0} + m_LocalPosition: {x: 0.5, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: @@ -382,8 +384,8 @@ SpriteRenderer: m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 + m_SortingLayerID: 1658682819 + m_SortingLayer: -1 m_SortingOrder: 0 m_MaskInteraction: 0 m_Sprite: {fileID: -70544561768458129, guid: d440231d6f96217499ab62926952e2b3, type: 3} @@ -468,7 +470,7 @@ Transform: m_GameObject: {fileID: 7236042952510876049} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -0.35, y: 0.43, z: 0} + m_LocalPosition: {x: 0, y: 0.65, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: @@ -568,8 +570,8 @@ SpriteRenderer: m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 + m_SortingLayerID: 1658682819 + m_SortingLayer: -1 m_SortingOrder: 0 m_MaskInteraction: 0 m_Sprite: {fileID: 8449129219236236317, guid: d440231d6f96217499ab62926952e2b3, type: 3} @@ -639,7 +641,7 @@ GameObject: - component: {fileID: 7296410561034137608} - component: {fileID: 8634086561103883683} m_Layer: 0 - m_Name: CollectibleSpanPoint + m_Name: CollectibleSpawnPoint m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 @@ -654,7 +656,7 @@ Transform: m_GameObject: {fileID: 7729436994615948155} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0.5, z: 0} + m_LocalPosition: {x: 0, y: 1.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -701,7 +703,7 @@ Transform: m_GameObject: {fileID: 8846698754903096030} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -0.4, y: 0.43, z: 0} + m_LocalPosition: {x: 0, y: 1.25, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] diff --git a/Assets/Resources/ProjectContext.prefab b/Assets/Resources/ProjectContext.prefab new file mode 100644 index 0000000..8ba95d9 --- /dev/null +++ b/Assets/Resources/ProjectContext.prefab @@ -0,0 +1,83 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1890352100173040858 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6987677732352224487} + - component: {fileID: 6104063899786295062} + - component: {fileID: 1793366000134005455} + m_Layer: 0 + m_Name: ProjectContext + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &6987677732352224487 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1890352100173040858} + serializedVersion: 2 + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &6104063899786295062 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1890352100173040858} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a4e6589720da476459dc6dd71624b071, type: 3} + m_Name: + m_EditorClassIdentifier: Zenject::Zenject.ProjectContext + _scriptableObjectInstallers: [] + _monoInstallers: + - {fileID: 1793366000134005455} + _installerPrefabs: [] + _findSiblingMonoInstallers: 0 + _parentNewObjectsUnderContext: 0 + _editorReflectionBakingCoverageMode: 0 + _buildsReflectionBakingCoverageMode: 0 + _settings: + _ensureDeterministicDestructionOrderOnApplicationQuit: 0 + _displayWarningWhenResolvingDuringInstall: 1 + _validationRootResolveMethod: 0 + _validationErrorResponse: 0 + _constructorChoiceStrategy: 0 + _signalSettings: + _defaultSyncMode: 0 + _missingHandlerDefaultResponse: 2 + _requireStrictUnsubscribe: 0 + _defaultAsyncTickPriority: 1 +--- !u!114 &1793366000134005455 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1890352100173040858} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9aa54c60d21f3754a9bbead2e5c6875b, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Settings.ServiceSettingsInstaller + databaseAddress: settings/service-settings-database + fallbackDatabase: {fileID: 0} + fileName: service-settings.json + loadImmediately: 1 + bindActionsInScene: 1 diff --git a/Assets/Resources/ProjectContext.prefab.meta b/Assets/Resources/ProjectContext.prefab.meta new file mode 100644 index 0000000..1d4a510 --- /dev/null +++ b/Assets/Resources/ProjectContext.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a93150cb651c3c84fbf3f1c2b2f056d7 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/UI.meta b/Assets/Resources/UI.meta new file mode 100644 index 0000000..df88c84 --- /dev/null +++ b/Assets/Resources/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ede0e1d8b348f8448934a56ee10e36fb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/UI/MainMenu.meta b/Assets/Resources/UI/MainMenu.meta new file mode 100644 index 0000000..30550b5 --- /dev/null +++ b/Assets/Resources/UI/MainMenu.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4919a0f6834fe42428715c9cab547950 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/UI/MainMenu/Templates.meta b/Assets/Resources/UI/MainMenu/Templates.meta new file mode 100644 index 0000000..4ae6006 --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3e035b8e4dab00246b4f7a466afe5a28 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsBoolControl.uxml b/Assets/Resources/UI/MainMenu/Templates/SettingsBoolControl.uxml new file mode 100644 index 0000000..bcb6aba --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsBoolControl.uxml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsBoolControl.uxml.meta b/Assets/Resources/UI/MainMenu/Templates/SettingsBoolControl.uxml.meta new file mode 100644 index 0000000..b5d8475 --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsBoolControl.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 2ac8a4d8759da50468703edffcdb63fb +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsButtonControl.uxml b/Assets/Resources/UI/MainMenu/Templates/SettingsButtonControl.uxml new file mode 100644 index 0000000..e42851b --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsButtonControl.uxml @@ -0,0 +1,7 @@ + + + + + + diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsButtonControl.uxml.meta b/Assets/Resources/UI/MainMenu/Templates/SettingsButtonControl.uxml.meta new file mode 100644 index 0000000..5c3a213 --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsButtonControl.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 3dc41f4dd71944f4a84b73b3617d0187 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsGroupHeader.uxml b/Assets/Resources/UI/MainMenu/Templates/SettingsGroupHeader.uxml new file mode 100644 index 0000000..febf05b --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsGroupHeader.uxml @@ -0,0 +1,6 @@ + + + + + diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsGroupHeader.uxml.meta b/Assets/Resources/UI/MainMenu/Templates/SettingsGroupHeader.uxml.meta new file mode 100644 index 0000000..e5361b9 --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsGroupHeader.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 8bdc16de0a8def84884149599b00c65f +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsStepperControl.uxml b/Assets/Resources/UI/MainMenu/Templates/SettingsStepperControl.uxml new file mode 100644 index 0000000..98b1867 --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsStepperControl.uxml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsStepperControl.uxml.meta b/Assets/Resources/UI/MainMenu/Templates/SettingsStepperControl.uxml.meta new file mode 100644 index 0000000..3ff3858 --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsStepperControl.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: c12ecb9297b1a4a45a32d3913894fb87 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsTextControl.uxml b/Assets/Resources/UI/MainMenu/Templates/SettingsTextControl.uxml new file mode 100644 index 0000000..71cb7ce --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsTextControl.uxml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Assets/Resources/UI/MainMenu/Templates/SettingsTextControl.uxml.meta b/Assets/Resources/UI/MainMenu/Templates/SettingsTextControl.uxml.meta new file mode 100644 index 0000000..384dcec --- /dev/null +++ b/Assets/Resources/UI/MainMenu/Templates/SettingsTextControl.uxml.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 94d66c77f966ace46af48ae1bfdc3b67 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0} diff --git a/Assets/Scenes/Game.unity b/Assets/Scenes/Game.unity index ac333f2..2fdcd8c 100644 --- a/Assets/Scenes/Game.unity +++ b/Assets/Scenes/Game.unity @@ -161,6 +161,7 @@ GameObject: m_Component: - component: {fileID: 344459309} - component: {fileID: 344459308} + - component: {fileID: 344459310} m_Layer: 0 m_Name: Platform Spawner m_TagString: Untagged @@ -184,7 +185,8 @@ MonoBehaviour: horizontalPadding: 0.85 recycleDistanceBelowCameraBottom: 12 useHardwareRandomSeed: 1 - randomSeed: 1207 + randomSeed: 1 + proceduralEnemySpawner: {fileID: 344459310} --- !u!4 &344459309 Transform: m_ObjectHideFlags: 0 @@ -200,6 +202,83 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &344459310 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 344459307} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 47ed0f3875f8ad348bdfdce600932c12, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.ProceduralEnemySpawner + initialSpawnY: 8 + verticalStep: 12 + maxAliveEnemies: 8 + spawnAttempts: 12 + horizontalPadding: 0.85 + verticalJitter: 1.5 + fallbackEnemyHalfExtents: {x: 0.55, y: 0.55} + platformClearance: {x: 0.15, y: 0.15} + blockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!1 &624463940 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 624463943} + - component: {fileID: 624463941} + m_Layer: 0 + m_Name: Gameplay HUD + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &624463941 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 624463940} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f64c7f029e315df4e80ec13942c0a85e, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::DanroJump.UI.Gameplay.GameplayHudController + visualTreeAsset: {fileID: 9197481963319205126, guid: e5019473f4474704f8a8e8c93608eafc, type: 3} + panelSettings: {fileID: 11400000, guid: d0960c9338bdb86469da533ffbbbc1fb, type: 2} + sortingOrder: 10 + scoreLabelName: ScoreValue + heightLabelName: HeightValue + bestHeightLabelName: BestHeightValue + livesLabelName: LivesValue + coinsLabelName: CoinsValue + deathsLabelName: DeathsValue + stateLabelName: StateValue +--- !u!4 &624463943 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 624463940} + serializedVersion: 2 + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &973362138 GameObject: m_ObjectHideFlags: 0 @@ -310,6 +389,85 @@ PrefabInstance: m_AddedGameObjects: [] m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: d705af614a982e74bba9e2a3b697f565, type: 3} +--- !u!1 &1693654372 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1693654375} + - component: {fileID: 1693654374} + - component: {fileID: 1693654373} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1693654373 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1693654372} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.UI.InputSystemUIInputModule + m_SendPointerHoverToParent: 1 + m_MoveRepeatDelay: 0.5 + m_MoveRepeatRate: 0.1 + m_XRTrackingOrigin: {fileID: 0} + m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_DeselectOnBackgroundClick: 1 + m_PointerBehavior: 0 + m_CursorLockBehavior: 0 + m_ScrollDeltaPerTick: 6 +--- !u!114 &1693654374 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1693654372} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.EventSystems.EventSystem + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &1693654375 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1693654372} + serializedVersion: 2 + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!4 &1740411098 stripped Transform: m_CorrespondingSourceObject: {fileID: 7172431620267875091, guid: d705af614a982e74bba9e2a3b697f565, type: 3} @@ -557,8 +715,8 @@ SpriteRenderer: m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} m_GlobalIlluminationMeshLod: 0 - m_SortingLayerID: 0 - m_SortingLayer: 0 + m_SortingLayerID: -876065631 + m_SortingLayer: -2 m_SortingOrder: -200 m_MaskInteraction: 0 m_Sprite: {fileID: 21300000, guid: cf6b76e72a39f144aab3928a6d6f71ad, type: 3} @@ -709,11 +867,15 @@ MonoBehaviour: groundProbeStartOffset: 0.08 groundProbeDistance: 0.34 equipmentProbeHalfExtents: {x: 0.5, y: 0.35} + sweptLandingProbeHalfExtents: {x: 0.48, y: 0.08} + sweptLandingExtraDistance: 0.18 + collisionDetectionMode: 1 horizontalWrapPadding: 0.65 - fallResetDistance: 5 + fallResetDistance: 0.5 fallResetFromCameraBottom: 1 respawnViewportY: 0.28 centerRespawnX: 1 + deathRespawnDelay: 0.35 animator: {fileID: 0} visualRoot: {fileID: 0} visualRootName: Body @@ -854,3 +1016,5 @@ SceneRoots: - {fileID: 344459309} - {fileID: 1998332553} - {fileID: 973362140} + - {fileID: 624463943} + - {fileID: 1693654375} diff --git a/Assets/Scenes/MainMenu.unity b/Assets/Scenes/MainMenu.unity index e283f64..a2758c1 100644 --- a/Assets/Scenes/MainMenu.unity +++ b/Assets/Scenes/MainMenu.unity @@ -222,7 +222,7 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} - m_Name: + m_Name: m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData m_RenderShadows: 1 m_RequiresDepthTextureOption: 2 @@ -256,6 +256,127 @@ MonoBehaviour: m_VarianceClampScale: 0.9 m_ContrastAdaptiveSharpening: 0 m_Version: 2 +--- !u!1 &8222466 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8222469} + - component: {fileID: 8222468} + - component: {fileID: 8222467} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &8222467 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8222466} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.UI.InputSystemUIInputModule + m_SendPointerHoverToParent: 1 + m_MoveRepeatDelay: 0.5 + m_MoveRepeatRate: 0.1 + m_XRTrackingOrigin: {fileID: 0} + m_ActionsAsset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_PointAction: {fileID: 1932080467} + m_MoveAction: {fileID: 2130237725} + m_SubmitAction: {fileID: 1037422209} + m_CancelAction: {fileID: 491707281} + m_LeftClickAction: {fileID: 589680430} + m_MiddleClickAction: {fileID: 647806552} + m_RightClickAction: {fileID: 953355540} + m_ScrollWheelAction: {fileID: 1409131790} + m_TrackedDevicePositionAction: {fileID: 1014446460} + m_TrackedDeviceOrientationAction: {fileID: 2034036131} + m_DeselectOnBackgroundClick: 1 + m_PointerBehavior: 0 + m_CursorLockBehavior: 0 + m_ScrollDeltaPerTick: 6 +--- !u!114 &8222468 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8222466} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.EventSystems.EventSystem + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &8222469 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8222466} + serializedVersion: 2 + 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: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &491707281 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/Cancel + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: 15cef263-9014-4fd5-94d9-4e4a6234a6ef +--- !u!114 &589680430 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/Click + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: 3c7022bf-7922-4f7c-a998-c437916075ad +--- !u!114 &647806552 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/MiddleClick + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: dad70c86-b58c-4b17-88ad-f5e53adf419e --- !u!1 &653892269 GameObject: m_ObjectHideFlags: 0 @@ -340,9 +461,9 @@ Transform: m_GameObject: {fileID: 653892269} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 9.5, y: 0, z: 0} - m_LocalScale: {x: 5.4, y: 9.6, z: 1} - m_ConstrainProportionsScale: 0 + m_LocalPosition: {x: 12, y: 0, z: 0} + m_LocalScale: {x: 10.8, y: 19.2, z: 2} + m_ConstrainProportionsScale: 1 m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} @@ -378,6 +499,11 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::DanroJump.UI.MainMenu.MainMenuController gameplaySceneName: Game settingsSceneName: + defaultFocusedButtonName: RulesButton + normalButtonImageTint: {r: 1, g: 1, b: 1, a: 1} + focusedButtonImageTint: {r: 0.80784315, g: 1, b: 0.101960786, a: 1} + secretActionReference: {fileID: 7552203824317127043, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + legacyInputActionsAsset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} --- !u!114 &916321582 MonoBehaviour: m_ObjectHideFlags: 0 @@ -416,6 +542,104 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &953355540 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/RightClick + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: 44b200b1-1557-4083-816c-b22cbdf77ddf +--- !u!114 &1014446460 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/TrackedDevicePosition + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: 24908448-c609-4bc3-a128-ea258674378a +--- !u!114 &1037422209 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/Submit + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: 7607c7b6-cd76-4816-beef-bd0341cfe950 +--- !u!114 &1409131790 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/ScrollWheel + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: 0489e84a-4833-4c40-bfae-cea84b696689 +--- !u!114 &1932080467 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/Point + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: 32b35790-4ed0-4e9a-aa41-69ac6d629449 +--- !u!114 &2034036131 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/TrackedDeviceOrientation + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: 9caa3d8a-6b2f-4e8e-8bad-6ede561bd9be +--- !u!114 &2130237725 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3} + m_Name: UI/Navigate + m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference + m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3} + m_ActionId: c95b2375-e6d9-4b88-9c4c-c5e76515df4b --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 @@ -423,3 +647,4 @@ SceneRoots: - {fileID: 200001} - {fileID: 916321583} - {fileID: 653892272} + - {fileID: 8222469} diff --git a/Assets/Screenshots.meta b/Assets/Screenshots.meta new file mode 100644 index 0000000..31085cf --- /dev/null +++ b/Assets/Screenshots.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 20656d1321e888c418d9cdcc452dce18 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/ScriptableObjects/Gameplay/DifficultyStages/ExtremeStage.asset b/Assets/ScriptableObjects/Gameplay/DifficultyStages/ExtremeStage.asset index e2a417a..e95778a 100644 --- a/Assets/ScriptableObjects/Gameplay/DifficultyStages/ExtremeStage.asset +++ b/Assets/ScriptableObjects/Gameplay/DifficultyStages/ExtremeStage.asset @@ -14,13 +14,17 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.GameplayDifficultyStage stageName: "\u042D\u043A\u0441\u0442\u0440\u0435\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u044D\u0442\u0430\u043F" - startY: 800 + startY: 650 verticalLength: 200 horizontalDistribution: 0.65 platforms: platforms: - prefab: {fileID: 964816656659499055, guid: b314e5b810bdbd841824f4b5d47bdd92, type: 3} weight: 1 + - prefab: {fileID: 964816656659499055, guid: 15b31b8e5d8677545b1ce2c8f700f901, type: 3} + weight: 0.55 + - prefab: {fileID: 964816656659499055, guid: fa1b0ae89608c924e92657d291389138, type: 3} + weight: 0.35 verticalSpacingCurve: serializedVersion: 2 m_Curve: @@ -76,9 +80,11 @@ MonoBehaviour: m_PostInfinity: 2 m_RotationOrder: 4 initialSpawnChance: 0.5 - maximumSpawnChance: 0.75 + maximumSpawnChance: 0.78 enemies: - prefabs: [] + prefabs: + - prefab: {fileID: 6051671913546024041, guid: dc382d4fdc981aa439af1624e230f11f, type: 3} + weight: 1 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -103,10 +109,14 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.13 + maximumSpawnChance: 0.3 collectibles: - prefabs: [] + prefabs: + - prefab: {fileID: 7341068655679281185, guid: 9987915360f9bf740bc0939e23db9349, type: 3} + weight: 1 + - prefab: {fileID: 1649926752591026283, guid: 42ac5c9fb765781449b1a8cdc5bd3d64, type: 3} + weight: 0.35 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -131,5 +141,5 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.3 + maximumSpawnChance: 0.62 diff --git a/Assets/ScriptableObjects/Gameplay/DifficultyStages/HardStage.asset b/Assets/ScriptableObjects/Gameplay/DifficultyStages/HardStage.asset index c2f7345..3411c5a 100644 --- a/Assets/ScriptableObjects/Gameplay/DifficultyStages/HardStage.asset +++ b/Assets/ScriptableObjects/Gameplay/DifficultyStages/HardStage.asset @@ -13,13 +13,17 @@ MonoBehaviour: m_Name: HardStage m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.GameplayDifficultyStage stageName: "\u0421\u043B\u043E\u0436\u043D\u044B\u0439 \u044D\u0442\u0430\u043F" - startY: 600 + startY: 450 verticalLength: 200 horizontalDistribution: 0.6 platforms: platforms: - prefab: {fileID: 964816656659499055, guid: b314e5b810bdbd841824f4b5d47bdd92, type: 3} weight: 1 + - prefab: {fileID: 964816656659499055, guid: 15b31b8e5d8677545b1ce2c8f700f901, type: 3} + weight: 0.55 + - prefab: {fileID: 964816656659499055, guid: fa1b0ae89608c924e92657d291389138, type: 3} + weight: 0.35 verticalSpacingCurve: serializedVersion: 2 m_Curve: @@ -75,9 +79,11 @@ MonoBehaviour: m_PostInfinity: 2 m_RotationOrder: 4 initialSpawnChance: 0.5 - maximumSpawnChance: 0.5 + maximumSpawnChance: 0.68 enemies: - prefabs: [] + prefabs: + - prefab: {fileID: 6051671913546024041, guid: dc382d4fdc981aa439af1624e230f11f, type: 3} + weight: 1 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -102,10 +108,14 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.09 + maximumSpawnChance: 0.22 collectibles: - prefabs: [] + prefabs: + - prefab: {fileID: 7341068655679281185, guid: 9987915360f9bf740bc0939e23db9349, type: 3} + weight: 1 + - prefab: {fileID: 1649926752591026283, guid: 42ac5c9fb765781449b1a8cdc5bd3d64, type: 3} + weight: 0.35 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -130,5 +140,5 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.25 + maximumSpawnChance: 0.55 diff --git a/Assets/ScriptableObjects/Gameplay/DifficultyStages/ImpossibleStage.asset b/Assets/ScriptableObjects/Gameplay/DifficultyStages/ImpossibleStage.asset index e6ee505..e8bf327 100644 --- a/Assets/ScriptableObjects/Gameplay/DifficultyStages/ImpossibleStage.asset +++ b/Assets/ScriptableObjects/Gameplay/DifficultyStages/ImpossibleStage.asset @@ -14,13 +14,15 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.GameplayDifficultyStage stageName: "\u041D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u044B\u0439 \u044D\u0442\u0430\u043F" - startY: 800 + startY: 850 verticalLength: 200 horizontalDistribution: 0.8 platforms: platforms: - prefab: {fileID: 964816656659499055, guid: b314e5b810bdbd841824f4b5d47bdd92, type: 3} weight: 1 + - prefab: {fileID: 964816656659499055, guid: 15b31b8e5d8677545b1ce2c8f700f901, type: 3} + weight: 0.55 verticalSpacingCurve: serializedVersion: 2 m_Curve: @@ -45,7 +47,7 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialVerticalSpacing: 18 + initialVerticalSpacing: 16 maximumVerticalSpacing: 18 equipment: prefabs: @@ -78,7 +80,9 @@ MonoBehaviour: initialSpawnChance: 1 maximumSpawnChance: 1 enemies: - prefabs: [] + prefabs: + - prefab: {fileID: 6051671913546024041, guid: dc382d4fdc981aa439af1624e230f11f, type: 3} + weight: 1 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -103,10 +107,14 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.18 + maximumSpawnChance: 0.38 collectibles: - prefabs: [] + prefabs: + - prefab: {fileID: 7341068655679281185, guid: 9987915360f9bf740bc0939e23db9349, type: 3} + weight: 1 + - prefab: {fileID: 1649926752591026283, guid: 42ac5c9fb765781449b1a8cdc5bd3d64, type: 3} + weight: 0.35 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -131,5 +139,5 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.35 + maximumSpawnChance: 0.68 diff --git a/Assets/ScriptableObjects/Gameplay/DifficultyStages/InitialStage.asset b/Assets/ScriptableObjects/Gameplay/DifficultyStages/InitialStage.asset index c040358..81dbc31 100644 --- a/Assets/ScriptableObjects/Gameplay/DifficultyStages/InitialStage.asset +++ b/Assets/ScriptableObjects/Gameplay/DifficultyStages/InitialStage.asset @@ -14,7 +14,7 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.GameplayDifficultyStage stageName: "\u041D\u0430\u0447\u0430\u043B\u044C\u043D\u044B\u0439 \u044D\u0442\u0430\u043F" startY: -3.25 - verticalLength: 203.25 + verticalLength: 53.25 horizontalDistribution: 0 platforms: platforms: @@ -57,9 +57,9 @@ MonoBehaviour: m_Curve: - serializedVersion: 3 time: 0 - value: 0 + value: 1 inSlope: 0 - outSlope: 1 + outSlope: 0 tangentMode: 0 weightedMode: 0 inWeight: 0 @@ -67,17 +67,17 @@ MonoBehaviour: - serializedVersion: 3 time: 1 value: 1 - inSlope: 1 - outSlope: 0 + inSlope: -0 + outSlope: -0 tangentMode: 0 weightedMode: 0 - inWeight: 0 + inWeight: 0.2916667 outWeight: 0 m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0.2 - maximumSpawnChance: 0.75 + initialSpawnChance: 1 + maximumSpawnChance: 1 enemies: prefabs: [] spawnChanceCurve: diff --git a/Assets/ScriptableObjects/Gameplay/DifficultyStages/NormalStage.asset b/Assets/ScriptableObjects/Gameplay/DifficultyStages/NormalStage.asset index db2f82e..3c38fa6 100644 --- a/Assets/ScriptableObjects/Gameplay/DifficultyStages/NormalStage.asset +++ b/Assets/ScriptableObjects/Gameplay/DifficultyStages/NormalStage.asset @@ -13,13 +13,17 @@ MonoBehaviour: m_Name: NormalStage m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.GameplayDifficultyStage stageName: "\u041D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u044D\u0442\u0430\u043F" - startY: 400 + startY: 250 verticalLength: 200 horizontalDistribution: 0.5 platforms: platforms: - prefab: {fileID: 964816656659499055, guid: b314e5b810bdbd841824f4b5d47bdd92, type: 3} weight: 1 + - prefab: {fileID: 964816656659499055, guid: 15b31b8e5d8677545b1ce2c8f700f901, type: 3} + weight: 0.55 + - prefab: {fileID: 964816656659499055, guid: fa1b0ae89608c924e92657d291389138, type: 3} + weight: 0.35 verticalSpacingCurve: serializedVersion: 2 m_Curve: @@ -77,7 +81,9 @@ MonoBehaviour: initialSpawnChance: 0.5 maximumSpawnChance: 0.85 enemies: - prefabs: [] + prefabs: + - prefab: {fileID: 6051671913546024041, guid: dc382d4fdc981aa439af1624e230f11f, type: 3} + weight: 1 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -102,10 +108,14 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.05 + maximumSpawnChance: 0.14 collectibles: - prefabs: [] + prefabs: + - prefab: {fileID: 7341068655679281185, guid: 9987915360f9bf740bc0939e23db9349, type: 3} + weight: 1 + - prefab: {fileID: 1649926752591026283, guid: 42ac5c9fb765781449b1a8cdc5bd3d64, type: 3} + weight: 0.35 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -130,5 +140,5 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.22 + maximumSpawnChance: 0.45 diff --git a/Assets/ScriptableObjects/Gameplay/DifficultyStages/SimpleStage.asset b/Assets/ScriptableObjects/Gameplay/DifficultyStages/SimpleStage.asset index 3336b58..8b182fa 100644 --- a/Assets/ScriptableObjects/Gameplay/DifficultyStages/SimpleStage.asset +++ b/Assets/ScriptableObjects/Gameplay/DifficultyStages/SimpleStage.asset @@ -13,13 +13,15 @@ MonoBehaviour: m_Name: SimpleStage m_EditorClassIdentifier: Assembly-CSharp::DanroJump.Gameplay.GameplayDifficultyStage stageName: "\u041F\u0440\u043E\u0441\u0442\u043E\u0439 \u044D\u0442\u0430\u043F" - startY: 200 + startY: 50 verticalLength: 200 horizontalDistribution: 0 platforms: platforms: - prefab: {fileID: 964816656659499055, guid: b314e5b810bdbd841824f4b5d47bdd92, type: 3} weight: 1 + - prefab: {fileID: 964816656659499055, guid: fa1b0ae89608c924e92657d291389138, type: 3} + weight: 0.35 verticalSpacingCurve: serializedVersion: 2 m_Curve: @@ -77,7 +79,9 @@ MonoBehaviour: initialSpawnChance: 0.3 maximumSpawnChance: 0.75 enemies: - prefabs: [] + prefabs: + - prefab: {fileID: 0} + weight: 1 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -102,10 +106,16 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.02 + maximumSpawnChance: 0.08 collectibles: - prefabs: [] + prefabs: + - prefab: {fileID: 7341068655679281185, guid: 9987915360f9bf740bc0939e23db9349, type: 3} + weight: 1 + - prefab: {fileID: 1649926752591026283, guid: 42ac5c9fb765781449b1a8cdc5bd3d64, type: 3} + weight: 0.35 + - prefab: {fileID: 382672864354709237, guid: 23063041ef8ecb449b37527f2279534f, type: 3} + weight: 0.35 spawnChanceCurve: serializedVersion: 2 m_Curve: @@ -130,5 +140,5 @@ MonoBehaviour: m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 - initialSpawnChance: 0 - maximumSpawnChance: 0 + initialSpawnChance: 0.18 + maximumSpawnChance: 0.35 diff --git a/Assets/Scripts/Editor/Gameplay/GameplayProgressionOverlayWindow.cs b/Assets/Scripts/Editor/Gameplay/GameplayProgressionOverlayWindow.cs index e0b75ac..30faf6e 100644 --- a/Assets/Scripts/Editor/Gameplay/GameplayProgressionOverlayWindow.cs +++ b/Assets/Scripts/Editor/Gameplay/GameplayProgressionOverlayWindow.cs @@ -4,8 +4,14 @@ using UnityEngine; namespace DanroJump.Editor.Gameplay { + /// + /// Editor-окно для быстрой проверки текущего этапа сложности и прогресса генерации. + /// public sealed class GameplayProgressionOverlayWindow : EditorWindow { + /// + /// Открывает overlay-окно прогрессии из меню Unity. + /// [MenuItem("Window/Danro Jump/Gameplay Progression Overlay")] public static void Open() { @@ -14,16 +20,25 @@ namespace DanroJump.Editor.Gameplay window.Show(); } + /// + /// Подписывается на editor update, чтобы overlay обновлялся без ручного Repaint. + /// private void OnEnable() { EditorApplication.update += Repaint; } + /// + /// Снимает подписку на editor update при закрытии окна. + /// private void OnDisable() { EditorApplication.update -= Repaint; } + /// + /// Рисует актуальные данные сцены без ручной привязки к конкретному объекту. + /// private void OnGUI() { var progression = FindFirstObjectByType(); @@ -32,6 +47,9 @@ namespace DanroJump.Editor.Gameplay } } + /// + /// Добавляет компактный preview прогрессии прямо в инспектор GameplayDifficultyProgression. + /// [CustomEditor(typeof(GameplayDifficultyProgression))] public sealed class GameplayDifficultyProgressionEditor : UnityEditor.Editor { @@ -46,8 +64,14 @@ namespace DanroJump.Editor.Gameplay } } + /// + /// Общий IMGUI-рендерер для отдельного окна и встроенного блока в инспекторе. + /// internal static class GameplayProgressionOverlayGui { + /// + /// Рисует состояние этапа игрока и этапа генерации платформ. + /// public static void Draw(GameplayDifficultyProgression progression, PlatformSpawner spawner, bool showHeader) { if (showHeader) @@ -83,6 +107,9 @@ namespace DanroJump.Editor.Gameplay $"Generation Y: {spawner.CurrentGenerationY:0.##} | Distribution: {spawner.CurrentPlatformDistribution:0.##}"); } + /// + /// Рисует одну карточку этапа с числовым прогрессом и progress bar. + /// private static void DrawStageBlock(string title, string stageName, float progress, string subtitle) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); @@ -101,6 +128,9 @@ namespace DanroJump.Editor.Gameplay EditorGUILayout.EndVertical(); } + /// + /// Возвращает подсказку о том, из какого режима редактора сейчас читаются данные. + /// private static string GetPlayModeHint() { return EditorApplication.isPlaying ? "Play Mode: данные обновляются runtime-логикой" : "Edit Mode: данные рассчитаны по текущим ссылкам сцены"; diff --git a/Assets/Scripts/Runtime/Bootstrap/BootstrapEntryPoint.cs b/Assets/Scripts/Runtime/Bootstrap/BootstrapEntryPoint.cs index 250076a..5ac6aff 100644 --- a/Assets/Scripts/Runtime/Bootstrap/BootstrapEntryPoint.cs +++ b/Assets/Scripts/Runtime/Bootstrap/BootstrapEntryPoint.cs @@ -5,11 +5,70 @@ using UnityEngine.SceneManagement; namespace DanroJump.Bootstrap { + /// + /// Первая точка входа приложения: фиксирует портретное соотношение сторон и загружает главное меню. + /// [DefaultExecutionOrder(-10000)] public sealed class BootstrapEntryPoint : MonoBehaviour { - [SerializeField] private string mainMenuSceneName = "MainMenu"; + private const float AspectTolerance = 0.0025f; + private static BootstrapEntryPoint instance; + [SerializeField] private string mainMenuSceneName = "MainMenu"; + [SerializeField] private bool forceBuildAspectRatio = true; + [SerializeField] [Min(1)] private int targetAspectWidth = 9; + [SerializeField] [Min(1)] private int targetAspectHeight = 16; + [SerializeField] [Min(1)] private int preferredPortraitHeight = 1920; + [SerializeField] private bool clampToDisplaySize = true; + [SerializeField] private FullScreenMode buildFullScreenMode = FullScreenMode.Windowed; + [SerializeField] [Min(1)] private int startupEnforcementFrames = 8; + [SerializeField] [Min(0.1f)] private float aspectCheckInterval = 0.5f; + + private float nextAspectCheckTime; + + /// + /// Создает singleton bootstrap-объект и применяет разрешение билда до загрузки меню. + /// + private void Awake() + { + if (instance != null && instance != this) + { + Destroy(gameObject); + return; + } + + instance = this; + DontDestroyOnLoad(gameObject); + + // В редакторе не вмешиваемся в Game View, а в билде принудительно держим 9:16. + if (!Application.isEditor && forceBuildAspectRatio) + { + ApplyBuildAspectResolution(); + StartCoroutine(EnforceBuildAspectOnStartup()); + } + } + + /// + /// Периодически возвращает разрешение к целевому aspect ratio, если окно было изменено. + /// + private void Update() + { + if (Application.isEditor || !forceBuildAspectRatio || Time.unscaledTime < nextAspectCheckTime) + { + return; + } + + nextAspectCheckTime = Time.unscaledTime + aspectCheckInterval; + + if (!HasTargetAspectRatio(Screen.width, Screen.height)) + { + ApplyBuildAspectResolution(); + } + } + + /// + /// Загружает сцену главного меню после bootstrap-настройки. + /// private IEnumerator Start() { if (string.IsNullOrWhiteSpace(mainMenuSceneName)) @@ -35,6 +94,95 @@ namespace DanroJump.Bootstrap yield return null; } + /// + /// Очищает singleton-ссылку при выходе из Play Mode или уничтожении bootstrap-объекта. + /// + private void OnDestroy() + { + if (instance == this) + { + instance = null; + } + } + + /// + /// Несколько кадров подряд повторяет установку разрешения, пока Unity завершает старт окна. + /// + private IEnumerator EnforceBuildAspectOnStartup() + { + for (int i = 0; i < startupEnforcementFrames; i++) + { + if (!HasTargetAspectRatio(Screen.width, Screen.height)) + { + ApplyBuildAspectResolution(); + } + + yield return null; + } + } + + /// + /// Применяет рассчитанное разрешение к окну игры. + /// + private void ApplyBuildAspectResolution() + { + Vector2Int resolution = CalculateTargetResolution(); + Screen.SetResolution(resolution.x, resolution.y, buildFullScreenMode); + } + + /// + /// Рассчитывает целевое портретное разрешение с ограничением по размеру основного дисплея. + /// + private Vector2Int CalculateTargetResolution() + { + int height = Mathf.Max(1, preferredPortraitHeight); + int width = Mathf.Max(1, Mathf.RoundToInt(height * GetTargetAspect())); + + if (clampToDisplaySize && Display.main != null) + { + int displayWidth = Display.main.systemWidth; + int displayHeight = Display.main.systemHeight; + + if (displayHeight > 0 && height > displayHeight) + { + height = displayHeight; + width = Mathf.Max(1, Mathf.RoundToInt(height * GetTargetAspect())); + } + + if (displayWidth > 0 && width > displayWidth) + { + width = displayWidth; + height = Mathf.Max(1, Mathf.RoundToInt(width / GetTargetAspect())); + } + } + + return new Vector2Int(width, height); + } + + /// + /// Проверяет, совпадает ли текущее разрешение с целевым соотношением сторон. + /// + private bool HasTargetAspectRatio(int width, int height) + { + if (width <= 0 || height <= 0) + { + return false; + } + + return Mathf.Abs(width / (float)height - GetTargetAspect()) <= AspectTolerance; + } + + /// + /// Возвращает целевой aspect ratio из настроек ширины и высоты. + /// + private float GetTargetAspect() + { + return targetAspectWidth / (float)targetAspectHeight; + } + + /// + /// Проверяет, добавлена ли сцена в Build Settings. + /// private static bool SceneExistsInBuildSettings(string sceneName) { for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++) diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/BounceBoostCollectible.cs b/Assets/Scripts/Runtime/Gameplay/Collectibles/BounceBoostCollectible.cs new file mode 100644 index 0000000..3481c58 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/BounceBoostCollectible.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Collectible, который дает игроку усиленный вертикальный отскок и очки. + /// + public sealed class BounceBoostCollectible : CollectibleBase + { + [SerializeField] [Min(0f)] private float bounceVelocity = 24f; + [SerializeField] [Min(0)] private int scoreValue = 5; + + private GameplaySession session; + + /// + /// Получает игровую сессию для начисления очков. + /// + [Zenject.Inject] + public void Construct(GameplaySession injectedSession) + { + session = injectedSession; + } + + /// + /// Применяет усиленный прыжок и награду за подбор. + /// + protected override void OnCollected(JumpPlayerController player) + { + player.BounceWithEnemyPassThrough(bounceVelocity); + session?.AddScore(scoreValue); + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/BounceBoostCollectible.cs.meta b/Assets/Scripts/Runtime/Gameplay/Collectibles/BounceBoostCollectible.cs.meta new file mode 100644 index 0000000..5da905a --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/BounceBoostCollectible.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 45176cc3d2b0adf4592469063916cafe \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs b/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs new file mode 100644 index 0000000..4585a15 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Collectible монеты, который увеличивает счетчик монет и очки сессии. + /// + public sealed class CoinCollectible : CollectibleBase + { + [SerializeField] [Min(1)] private int coinValue = 1; + [SerializeField] [Min(0)] private int scoreValue = 1; + + private GameplaySession session; + + /// + /// Получает игровую сессию для записи награды. + /// + [Zenject.Inject] + public void Construct(GameplaySession injectedSession) + { + session = injectedSession; + } + + /// + /// Начисляет монеты и очки после подбора. + /// + protected override void OnCollected(JumpPlayerController player) + { + session?.AddCoins(coinValue); + session?.AddScore(scoreValue); + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs.meta b/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs.meta new file mode 100644 index 0000000..16649ef --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/CoinCollectible.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: deb903e81ee443948b7397fcac22b117 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/CollectibleBase.cs b/Assets/Scripts/Runtime/Gameplay/Collectibles/CollectibleBase.cs index e6bf171..28f1107 100644 --- a/Assets/Scripts/Runtime/Gameplay/Collectibles/CollectibleBase.cs +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/CollectibleBase.cs @@ -2,13 +2,22 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Базовый класс collectible-предметов с trigger-контактом и защитой от повторного подбора. + /// [RequireComponent(typeof(Collider2D))] public abstract class CollectibleBase : MonoBehaviour, ICollectible, IPlayerContactHandler { [SerializeField] private bool disableOnCollected = true; + /// + /// Показывает, был ли предмет уже подобран в текущем цикле жизни объекта. + /// public bool IsCollected { get; private set; } + /// + /// В редакторе автоматически переводит коллайдер предмета в trigger. + /// protected virtual void Reset() { if (TryGetComponent(out Collider2D collectibleCollider)) @@ -17,27 +26,39 @@ namespace DanroJump.Gameplay } } + /// + /// При переиспользовании prefab сбрасывает состояние подбора. + /// protected virtual void OnEnable() { IsCollected = false; } + /// + /// Реагирует на вход игрока в trigger-зону предмета. + /// private void OnTriggerEnter2D(Collider2D other) { - if (other.TryGetComponent(out JumpPlayerController player)) + if (PlayerContactUtility.TryGetAlivePlayer(other, out var player)) { HandlePlayerContact(player, this); } } + /// + /// Обрабатывает контакт через общий интерфейс игровых объектов. + /// public void HandlePlayerContact(JumpPlayerController player, Component source) { Collect(player); } + /// + /// Применяет эффект предмета и при необходимости выключает объект. + /// public void Collect(JumpPlayerController player) { - if (IsCollected || player == null) + if (IsCollected || player == null || player.IsDead) { return; } @@ -51,6 +72,9 @@ namespace DanroJump.Gameplay } } + /// + /// Точка расширения для конкретного эффекта collectible. + /// protected virtual void OnCollected(JumpPlayerController player) { } diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/HeightLiftCollectible.cs b/Assets/Scripts/Runtime/Gameplay/Collectibles/HeightLiftCollectible.cs new file mode 100644 index 0000000..226e2dd --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/HeightLiftCollectible.cs @@ -0,0 +1,34 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Collectible, который временно поднимает игрока вверх с заданной скоростью. + /// + public sealed class HeightLiftCollectible : CollectibleBase + { + [SerializeField] [Min(0f)] private float liftSpeed = 18f; + [SerializeField] [Min(0f)] private float liftHeight = 12f; + [SerializeField] [Min(0)] private int scoreValue = 8; + + private GameplaySession session; + + /// + /// Получает игровую сессию для начисления очков. + /// + [Zenject.Inject] + public void Construct(GameplaySession injectedSession) + { + session = injectedSession; + } + + /// + /// Запускает плавный подъем игрока и начисляет награду. + /// + protected override void OnCollected(JumpPlayerController player) + { + player.ApplyHeightLift(liftSpeed, liftHeight); + session?.AddScore(scoreValue); + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/HeightLiftCollectible.cs.meta b/Assets/Scripts/Runtime/Gameplay/Collectibles/HeightLiftCollectible.cs.meta new file mode 100644 index 0000000..fa5bb28 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/HeightLiftCollectible.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d6608696d5f177540b0f06b9cbb46826 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/ICollectible.cs b/Assets/Scripts/Runtime/Gameplay/Collectibles/ICollectible.cs index 10b27ec..34a2f17 100644 --- a/Assets/Scripts/Runtime/Gameplay/Collectibles/ICollectible.cs +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/ICollectible.cs @@ -1,9 +1,18 @@ namespace DanroJump.Gameplay { + /// + /// Общий контракт подбираемого предмета. + /// public interface ICollectible { + /// + /// True, если предмет уже был подобран текущим игроком. + /// bool IsCollected { get; } + /// + /// Применяет эффект предмета к игроку. + /// void Collect(JumpPlayerController player); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/IPlayerContactHandler.cs b/Assets/Scripts/Runtime/Gameplay/Collectibles/IPlayerContactHandler.cs index b3d0163..ba9b56e 100644 --- a/Assets/Scripts/Runtime/Gameplay/Collectibles/IPlayerContactHandler.cs +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/IPlayerContactHandler.cs @@ -2,8 +2,14 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Контракт объектов, которые реагируют на контакт с игроком. + /// public interface IPlayerContactHandler { + /// + /// Обрабатывает контакт игрока с источником события. + /// void HandlePlayerContact(JumpPlayerController player, Component source); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Collectibles/ScoreCollectible.cs b/Assets/Scripts/Runtime/Gameplay/Collectibles/ScoreCollectible.cs index d1729cd..2b52b64 100644 --- a/Assets/Scripts/Runtime/Gameplay/Collectibles/ScoreCollectible.cs +++ b/Assets/Scripts/Runtime/Gameplay/Collectibles/ScoreCollectible.cs @@ -2,15 +2,35 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Collectible, который начисляет только очки. + /// public sealed class ScoreCollectible : CollectibleBase { [SerializeField] private int scoreValue = 1; + private GameplaySession session; + + /// + /// Количество очков, начисляемое при подборе. + /// public int Value => scoreValue; + /// + /// Получает игровую сессию для начисления очков. + /// + [Zenject.Inject] + public void Construct(GameplaySession injectedSession) + { + session = injectedSession; + } + + /// + /// Добавляет очки в текущую сессию после подбора. + /// protected override void OnCollected(JumpPlayerController player) { - Debug.Log($"{nameof(ScoreCollectible)} collected for {scoreValue} points.", this); + session?.AddScore(scoreValue); } } } diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyBase.cs b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyBase.cs index 8dd9c7d..8c14849 100644 --- a/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyBase.cs +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyBase.cs @@ -2,52 +2,167 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Базовый класс противника с обработкой trigger- и collision-контакта с игроком. + /// [RequireComponent(typeof(Collider2D))] public abstract class EnemyBase : MonoBehaviour, IEnemy, IPlayerContactHandler { + [SerializeField] private EnemySpawnKind spawnKind = EnemySpawnKind.GroundedOnly; + [SerializeField] [Min(0)] private int stompScore = 10; + [SerializeField] private float stompTopTolerance = 0.2f; + [SerializeField] [Range(0f, 1f)] private float stompMinimumHeightFactor = 0.55f; + + private GameplaySession session; + private Collider2D enemyCollider; + + /// + /// Тип мест, где этот prefab врага разрешено создавать. + /// + public EnemySpawnKind SpawnKind => spawnKind; + + /// + /// True, если врага можно создавать на точках платформ. + /// + public bool CanSpawnOnPlatform => spawnKind == EnemySpawnKind.GroundedOnly || spawnKind == EnemySpawnKind.Universal; + + /// + /// True, если врага можно создавать процедурно в поле, отдельно от платформ. + /// + public bool CanSpawnProcedurally => spawnKind == EnemySpawnKind.FlyingOnly || spawnKind == EnemySpawnKind.Universal; + + /// + /// True, если противник сейчас должен реагировать на игрока. + /// public bool IsActive { get; private set; } + /// + /// Получает игровую сессию для начисления очков за stomp. + /// + [Zenject.Inject] + public void Construct(GameplaySession injectedSession) + { + session = injectedSession; + } + + /// + /// Активирует противника при включении объекта. + /// protected virtual void OnEnable() { ActivateEnemy(); } + /// + /// Деактивирует противника при выключении объекта. + /// protected virtual void OnDisable() { DeactivateEnemy(); } + /// + /// Передает trigger-контакт с игроком в конкретную реализацию. + /// private void OnTriggerEnter2D(Collider2D other) { - if (other.TryGetComponent(out JumpPlayerController player)) + if (PlayerContactUtility.TryGetAlivePlayer(other, out var player)) { HandlePlayerContact(player, this); } } + /// + /// Передает физическое столкновение с игроком в конкретную реализацию. + /// private void OnCollisionEnter2D(Collision2D collision) { - if (collision.collider.TryGetComponent(out JumpPlayerController player)) + if (PlayerContactUtility.TryGetAlivePlayer(collision.collider, out var player)) { HandlePlayerContact(player, this); } } + /// + /// Включает противника. + /// public virtual void ActivateEnemy() { IsActive = true; } + /// + /// Выключает противника. + /// public virtual void DeactivateEnemy() { IsActive = false; } + /// + /// Сбрасывает противника к активному состоянию. + /// public virtual void ResetEnemy() { ActivateEnemy(); } - public abstract void HandlePlayerContact(JumpPlayerController player, Component source); + /// + /// Обрабатывает stomp сверху или убивает игрока при обычном контакте. + /// + public virtual void HandlePlayerContact(JumpPlayerController player, Component source) + { + if (!IsActive || player == null) + { + return; + } + + if (player.TryIgnoreEnemy(this)) + { + return; + } + + if (IsStomp(player)) + { + HandleStomp(player); + return; + } + + player.Kill(); + } + + /// + /// Реакция врага на успешное уничтожение прыжком сверху. + /// + protected virtual void HandleStomp(JumpPlayerController player) + { + session?.AddScore(stompScore); + player.Bounce(player.AutoJumpVelocity); + DeactivateEnemy(); + gameObject.SetActive(false); + } + + /// + /// Проверяет, что игрок падает и его GroundCheck находится выше верхней части противника. + /// + protected virtual bool IsStomp(JumpPlayerController player) + { + enemyCollider ??= GetComponent(); + var bounds = enemyCollider != null + ? enemyCollider.bounds + : new Bounds(transform.position, Vector3.one); + var groundCheck = player.GroundCheckPosition; + var isAboveEnemyHorizontally = + groundCheck.x >= bounds.min.x - stompTopTolerance && + groundCheck.x <= bounds.max.x + stompTopTolerance; + var stompLineY = Mathf.Lerp(bounds.min.y, bounds.max.y, stompMinimumHeightFactor); + var isInUpperZone = + groundCheck.y >= stompLineY - stompTopTolerance || + player.PreviousGroundCheckY >= bounds.max.y - stompTopTolerance; + + return player.VerticalVelocity <= 0.05f && + isAboveEnemyHorizontally && + isInUpperZone; + } } } diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyPrefabRules.cs b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyPrefabRules.cs new file mode 100644 index 0000000..d7f7ceb --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyPrefabRules.cs @@ -0,0 +1,35 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Проверяет, в каких контекстах разрешено создавать enemy-prefab. + /// + public static class EnemyPrefabRules + { + /// + /// True, если prefab можно создать на платформенной точке. + /// + public static bool CanSpawnOnPlatform(GameObject prefab) + { + return TryGetEnemy(prefab, out var enemy) && enemy.CanSpawnOnPlatform; + } + + /// + /// True, если prefab можно создать процедурно в свободном поле. + /// + public static bool CanSpawnProcedurally(GameObject prefab) + { + return TryGetEnemy(prefab, out var enemy) && enemy.CanSpawnProcedurally; + } + + /// + /// Получает базовый enemy-компонент из prefab. + /// + public static bool TryGetEnemy(GameObject prefab, out EnemyBase enemy) + { + enemy = prefab != null ? prefab.GetComponentInChildren(true) : null; + return enemy != null; + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyPrefabRules.cs.meta b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyPrefabRules.cs.meta new file mode 100644 index 0000000..2fe8559 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemyPrefabRules.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d0181c1c29bf84f4dba77f0ca874181e \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/EnemySpawnKind.cs b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemySpawnKind.cs new file mode 100644 index 0000000..1e01422 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemySpawnKind.cs @@ -0,0 +1,23 @@ +namespace DanroJump.Gameplay +{ + /// + /// Описывает, в каких местах допустимо создавать врага. + /// + public enum EnemySpawnKind + { + /// + /// Враг живет на платформе и не должен появляться в воздухе сам по себе. + /// + GroundedOnly = 0, + + /// + /// Враг появляется только в свободном поле, отдельно от платформ. + /// + FlyingOnly = 1, + + /// + /// Враг подходит и для платформенного, и для процедурного полевого спавна. + /// + Universal = 2 + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/EnemySpawnKind.cs.meta b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemySpawnKind.cs.meta new file mode 100644 index 0000000..a567a26 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/EnemySpawnKind.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 99f6706e719352342843eda151910111 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/IEnemy.cs b/Assets/Scripts/Runtime/Gameplay/Enemies/IEnemy.cs index 2c7f0ca..74f61b7 100644 --- a/Assets/Scripts/Runtime/Gameplay/Enemies/IEnemy.cs +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/IEnemy.cs @@ -1,11 +1,23 @@ namespace DanroJump.Gameplay { + /// + /// Контракт противника, которого можно активировать, деактивировать и сбрасывать при рецикле платформы. + /// public interface IEnemy { + /// + /// Переводит противника в активное игровое состояние. + /// void ActivateEnemy(); + /// + /// Выключает игровую логику противника. + /// void DeactivateEnemy(); + /// + /// Возвращает противника к начальному состоянию. + /// void ResetEnemy(); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/PatrolEnemy.cs b/Assets/Scripts/Runtime/Gameplay/Enemies/PatrolEnemy.cs new file mode 100644 index 0000000..97457ad --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/PatrolEnemy.cs @@ -0,0 +1,122 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Противник, который патрулирует локальный отрезок и может быть побежден прыжком сверху. + /// + public sealed class PatrolEnemy : EnemyBase + { + [SerializeField] private Vector2 localPatrolAxis = Vector2.right; + [SerializeField] [Min(0f)] private float patrolDistance = 1.2f; + [SerializeField] [Min(0f)] private float patrolSpeed = 1.4f; + [SerializeField] private bool flipWhenMovingBack = true; + [SerializeField] private bool visualFacesPositiveDirection = true; + [SerializeField] private Transform visualRoot; + + private Transform cachedTransform; + private Vector3 visualRootInitialScale = Vector3.one; + private Vector3 basePosition; + private Vector2 normalizedPatrolAxis = Vector2.right; + private Vector3 patrolAxis = Vector3.right; + private int direction = 1; + + /// + /// Запоминает локальную стартовую позицию патруля. + /// + private void Awake() + { + cachedTransform = transform; + ResolveVisualRoot(); + RefreshPatrolAxis(); + basePosition = cachedTransform.localPosition; + ApplyFacing(); + } + +#if UNITY_EDITOR + private void OnValidate() + { + RefreshPatrolAxis(); + } +#endif + + /// + /// Двигает противника туда-обратно по локальной оси. + /// + private void Update() + { + if (!IsActive || patrolDistance <= 0f || patrolSpeed <= 0f) + { + return; + } + + var nextPosition = cachedTransform.localPosition + patrolAxis * (direction * patrolSpeed * Time.deltaTime); + var offset = Vector2.Dot((Vector2)(nextPosition - basePosition), normalizedPatrolAxis); + + if (Mathf.Abs(offset) >= patrolDistance) + { + direction *= -1; + nextPosition = basePosition + patrolAxis * Mathf.Clamp(offset, -patrolDistance, patrolDistance); + ApplyFacing(); + } + + cachedTransform.localPosition = nextPosition; + } + + /// + /// Возвращает противника в начало патруля при рецикле платформы. + /// + public override void ResetEnemy() + { + base.ResetEnemy(); + (cachedTransform != null ? cachedTransform : transform).localPosition = basePosition; + direction = 1; + ApplyFacing(); + } + + private void RefreshPatrolAxis() + { + normalizedPatrolAxis = localPatrolAxis.sqrMagnitude > 0f ? localPatrolAxis.normalized : Vector2.right; + patrolAxis = normalizedPatrolAxis; + } + + /// + /// Находит визуальный корень, который можно отражать без изменения коллайдера. + /// + private void ResolveVisualRoot() + { + if (visualRoot == null) + { + var spriteRenderer = GetComponentInChildren(true); + visualRoot = spriteRenderer != null ? spriteRenderer.transform : transform; + } + + visualRootInitialScale = visualRoot != null ? visualRoot.localScale : Vector3.one; + } + + /// + /// Разворачивает визуал при движении назад, если это включено в инспекторе. + /// + private void ApplyFacing() + { + if (visualRoot == null) + { + return; + } + + var scale = visualRootInitialScale; + if (flipWhenMovingBack) + { + var facingSign = direction < 0 ? -1f : 1f; + if (!visualFacesPositiveDirection) + { + facingSign *= -1f; + } + + scale.x = Mathf.Abs(scale.x) * facingSign; + } + + visualRoot.localScale = scale; + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/PatrolEnemy.cs.meta b/Assets/Scripts/Runtime/Gameplay/Enemies/PatrolEnemy.cs.meta new file mode 100644 index 0000000..9548540 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/PatrolEnemy.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 605dc94842c31e540ae7a09b17007591 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs b/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs new file mode 100644 index 0000000..a332777 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs @@ -0,0 +1,207 @@ +using UnityEngine; +using Zenject; + +namespace DanroJump.Gameplay +{ + /// + /// Создает летающих и универсальных врагов в диапазонах высоты, которые подготовил PlatformSpawner. + /// Перед созданием проверяет, что область врага не пересекает платформенные коллайдеры. + /// + public sealed class ProceduralEnemySpawner : MonoBehaviour + { + [Header("Spawn cadence")] + [SerializeField] [Min(0f)] private float initialSpawnY = 8f; + [SerializeField] [Min(0.1f)] private float verticalStep = 12f; + [SerializeField] [Min(0)] private int maxAliveEnemies = 8; + [SerializeField] [Min(1)] private int spawnAttempts = 12; + + [Header("Spawn area")] + [SerializeField] [Min(0f)] private float horizontalPadding = 0.85f; + [SerializeField] [Min(0f)] private float verticalJitter = 1.5f; + [SerializeField] private Vector2 fallbackEnemyHalfExtents = new(0.55f, 0.55f); + [SerializeField] private Vector2 platformClearance = new(0.15f, 0.15f); + [SerializeField] private LayerMask blockingMask = ~0; + + private readonly Collider2D[] overlapBuffer = new Collider2D[16]; + private readonly System.Collections.Generic.List aliveEnemies = new(); + private DiContainer container; + private Camera gameplayCamera; + private IGameplayDifficultyProgression difficultyProgression; + private System.Random random; + private float nextSpawnY; + private bool isInitialized; + + /// + /// Инициализирует спавнер из PlatformSpawner, чтобы seed и генерация были синхронизированы. + /// + public void Initialize( + DiContainer injectedContainer, + Camera injectedGameplayCamera, + IGameplayDifficultyProgression injectedDifficultyProgression, + int seed) + { + container = injectedContainer; + gameplayCamera = injectedGameplayCamera; + difficultyProgression = injectedDifficultyProgression; + random = new System.Random(seed); + nextSpawnY = initialSpawnY; + isInitialized = true; + } + + /// + /// Создает врагов в новом вертикальном диапазоне, который только что сгенерировал PlatformSpawner. + /// + public void SpawnForGeneratedRange(float fromY, float toY) + { + CleanupDestroyedEnemies(); + if (!CanTrySpawn()) + { + return; + } + + var minY = Mathf.Min(fromY, toY); + var maxY = Mathf.Max(fromY, toY); + while (nextSpawnY < minY) + { + nextSpawnY += verticalStep; + } + + while (nextSpawnY <= maxY && CanTrySpawn()) + { + TrySpawnEnemyAtNextStep(minY, maxY); + nextSpawnY += verticalStep; + } + } + + /// + /// Удаляет процедурных врагов ниже линии рецикла платформ. + /// + public void CleanupBelow(float worldY) + { + for (var index = aliveEnemies.Count - 1; index >= 0; index--) + { + var enemy = aliveEnemies[index]; + if (enemy == null) + { + aliveEnemies.RemoveAt(index); + continue; + } + + if (enemy.transform.position.y < worldY) + { + enemy.SetActive(false); + Destroy(enemy); + aliveEnemies.RemoveAt(index); + } + } + } + + private bool CanTrySpawn() + { + return isInitialized && + gameplayCamera != null && + container != null && + difficultyProgression != null && + (maxAliveEnemies <= 0 || aliveEnemies.Count < maxAliveEnemies); + } + + private void TrySpawnEnemyAtNextStep(float minGeneratedY, float maxGeneratedY) + { + if (!difficultyProgression.TryGetProceduralEnemyPrefab(nextSpawnY, random, out var prefab)) + { + return; + } + + var halfExtents = GetEnemyHalfExtents(prefab); + for (var attempt = 0; attempt < spawnAttempts; attempt++) + { + var position = CreateSpawnPosition(minGeneratedY, maxGeneratedY, halfExtents); + if (IntersectsBlockingCollider(position, halfExtents + platformClearance)) + { + continue; + } + + Spawn(prefab, position); + return; + } + } + + private Vector3 CreateSpawnPosition(float minGeneratedY, float maxGeneratedY, Vector2 halfExtents) + { + var cameraHalfWidth = gameplayCamera.orthographicSize * gameplayCamera.aspect; + var minX = gameplayCamera.transform.position.x - cameraHalfWidth + horizontalPadding + halfExtents.x; + var maxX = gameplayCamera.transform.position.x + cameraHalfWidth - horizontalPadding - halfExtents.x; + var minY = minGeneratedY + halfExtents.y; + var maxY = Mathf.Max(minY, maxGeneratedY - halfExtents.y); + var jitteredY = nextSpawnY + RandomRange(-verticalJitter, verticalJitter); + + return new Vector3( + RandomRange(minX, maxX), + Mathf.Clamp(jitteredY, minY, maxY), + 0f); + } + + private void Spawn(GameObject prefab, Vector3 position) + { + var instance = container.InstantiatePrefab(prefab, position, Quaternion.identity, transform); + instance.SetActive(true); + aliveEnemies.Add(instance); + } + + private bool IntersectsBlockingCollider(Vector3 position, Vector2 halfExtents) + { + var contactFilter = new ContactFilter2D + { + useLayerMask = true, + layerMask = blockingMask, + useTriggers = false + }; + + var count = Physics2D.OverlapBox(position, halfExtents * 2f, 0f, contactFilter, overlapBuffer); + for (var index = 0; index < count; index++) + { + var hit = overlapBuffer[index]; + if (hit != null && !hit.GetComponentInParent()) + { + return true; + } + } + + return false; + } + + private Vector2 GetEnemyHalfExtents(GameObject prefab) + { + var collider = prefab != null ? prefab.GetComponentInChildren(true) : null; + if (collider == null) + { + return fallbackEnemyHalfExtents; + } + + return new Vector2( + Mathf.Max(fallbackEnemyHalfExtents.x, collider.bounds.extents.x), + Mathf.Max(fallbackEnemyHalfExtents.y, collider.bounds.extents.y)); + } + + private void CleanupDestroyedEnemies() + { + for (var index = aliveEnemies.Count - 1; index >= 0; index--) + { + if (aliveEnemies[index] == null || !aliveEnemies[index].activeInHierarchy) + { + aliveEnemies.RemoveAt(index); + } + } + } + + private float RandomRange(float min, float max) + { + if (max <= min) + { + return min; + } + + return Mathf.Lerp(min, max, (float)(random?.NextDouble() ?? 0d)); + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs.meta b/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs.meta new file mode 100644 index 0000000..1cc1a04 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Enemies/ProceduralEnemySpawner.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 47ed0f3875f8ad348bdfdce600932c12 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/HazardEquipment.cs b/Assets/Scripts/Runtime/Gameplay/Equipment/HazardEquipment.cs new file mode 100644 index 0000000..328c8a1 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/HazardEquipment.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Опасное оборудование платформы, которое убивает игрока при контакте. + /// + public sealed class HazardEquipment : PlatformEquipmentBase + { + [SerializeField] [Min(0f)] private float activationCooldown = 0.2f; + + private float lastActivationTime = -999f; + + /// + /// Сбрасывает cooldown при переиспользовании оборудования. + /// + public override void ResetEquipment() + { + lastActivationTime = -999f; + } + + /// + /// Убивает игрока, если контакт не находится внутри cooldown. + /// + public override void HandlePlayerContact(JumpPlayerController player, Component source) + { + if (player == null || Time.time - lastActivationTime < activationCooldown) + { + return; + } + + lastActivationTime = Time.time; + player.Kill(); + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/HazardEquipment.cs.meta b/Assets/Scripts/Runtime/Gameplay/Equipment/HazardEquipment.cs.meta new file mode 100644 index 0000000..be11425 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/HazardEquipment.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cf34504aafe850849b76ddd3857f681b \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/ILandingEquipment.cs b/Assets/Scripts/Runtime/Gameplay/Equipment/ILandingEquipment.cs new file mode 100644 index 0000000..1794005 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/ILandingEquipment.cs @@ -0,0 +1,13 @@ +namespace DanroJump.Gameplay +{ + /// + /// Контракт оборудования, которое должно срабатывать от посадки игрока сверху. + /// + public interface ILandingEquipment + { + /// + /// Пробует активировать оборудование из общего landing flow игрока. + /// + bool TryActivateFromLanding(JumpPlayerController player); + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/ILandingEquipment.cs.meta b/Assets/Scripts/Runtime/Gameplay/Equipment/ILandingEquipment.cs.meta new file mode 100644 index 0000000..15664f0 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/ILandingEquipment.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ed809fd23ca2e954694b7d5bbf0b20ff \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/IPlatformEquipment.cs b/Assets/Scripts/Runtime/Gameplay/Equipment/IPlatformEquipment.cs index 4181563..f65e4b5 100644 --- a/Assets/Scripts/Runtime/Gameplay/Equipment/IPlatformEquipment.cs +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/IPlatformEquipment.cs @@ -1,9 +1,18 @@ namespace DanroJump.Gameplay { + /// + /// Контракт оборудования, которое устанавливается на платформу. + /// public interface IPlatformEquipment { + /// + /// Привязывает оборудование к runtime-платформе. + /// void Install(IPlatformRuntime platform); + /// + /// Сбрасывает состояние оборудования при спавне или переиспользовании. + /// void ResetEquipment(); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/JumpPad.cs b/Assets/Scripts/Runtime/Gameplay/Equipment/JumpPad.cs index 1c38517..c1cbd74 100644 --- a/Assets/Scripts/Runtime/Gameplay/Equipment/JumpPad.cs +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/JumpPad.cs @@ -1,11 +1,16 @@ -using System.Collections; +using System; +using System.Threading; +using Cysharp.Threading.Tasks; using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Пружинная площадка, которая усиливает прыжок игрока и проигрывает squash/stretch-анимацию. + /// [DisallowMultipleComponent] [RequireComponent(typeof(Collider2D))] - public sealed class JumpPad : PlatformEquipmentBase + public sealed class JumpPad : PlatformEquipmentBase, ILandingEquipment { [SerializeField] [Min(1f)] private float jumpHeightMultiplier = 3f; [SerializeField] private float landingVelocityThreshold = 0.05f; @@ -18,13 +23,19 @@ namespace DanroJump.Gameplay [SerializeField] [Min(0f)] private float recoverDuration = 0.14f; private Collider2D jumpPadCollider; - private Coroutine stretchRoutine; + private CancellationTokenSource stretchCancellation; private Vector3 visualBaseScale = Vector3.one; private float lastActivationTime = -1f; private bool hasVisualBaseScale; + /// + /// Во сколько раз пружина усиливает стандартный прыжок игрока. + /// public float JumpHeightMultiplier => jumpHeightMultiplier; + /// + /// Подготавливает trigger-коллайдер и визуальную часть пружины. + /// private void Awake() { EnsureTriggerCollider(); @@ -32,16 +43,25 @@ namespace DanroJump.Gameplay CacheVisualBaseScale(); } + /// + /// Проверяет контакт при первом входе игрока в trigger. + /// private void OnTriggerEnter2D(Collider2D other) { TryHandlePlayerContact(other); } + /// + /// Повторно проверяет контакт, если игрок уже находится внутри trigger. + /// private void OnTriggerStay2D(Collider2D other) { TryHandlePlayerContact(other); } + /// + /// Преобразует Collider2D в игрока и передает контакт в общий handler. + /// private void TryHandlePlayerContact(Collider2D other) { if (other.TryGetComponent(out JumpPlayerController player)) @@ -50,6 +70,9 @@ namespace DanroJump.Gameplay } } + /// + /// Сбрасывает cooldown, коллайдер и визуальный scale при переиспользовании. + /// public override void ResetEquipment() { lastActivationTime = -1f; @@ -59,11 +82,18 @@ namespace DanroJump.Gameplay ResetVisualScale(); } + /// + /// Пробует активировать пружину от контакта игрока. + /// public override void HandlePlayerContact(JumpPlayerController player, Component source) { TryActivate(player); } + /// + /// Активирует усиленный прыжок, если игрок действительно приземляется сверху. + /// + /// True, если пружина сработала. public bool TryActivate(JumpPlayerController player) { if (!CanActivate(player)) @@ -71,12 +101,20 @@ namespace DanroJump.Gameplay return false; } - player.Bounce(CalculateBoostVelocity(player)); + player.BounceWithEnemyPassThrough(CalculateBoostVelocity(player)); lastActivationTime = Time.time; PlayStretchAnimation(); return true; } + public bool TryActivateFromLanding(JumpPlayerController player) + { + return TryActivate(player); + } + + /// + /// Проверяет направление движения, cooldown и высоту GroundCheck относительно верха пружины. + /// private bool CanActivate(JumpPlayerController player) { if (player == null || Time.time - lastActivationTime < activationCooldown) @@ -84,7 +122,9 @@ namespace DanroJump.Gameplay return false; } - if (player.VerticalVelocity > landingVelocityThreshold) + var isLanding = player.VerticalVelocity <= landingVelocityThreshold || + player.PreviousVerticalVelocity <= landingVelocityThreshold; + if (!isLanding) { return false; } @@ -94,14 +134,26 @@ namespace DanroJump.Gameplay EnsureTriggerCollider(); } - return jumpPadCollider != null && player.GroundCheckY >= jumpPadCollider.bounds.min.y - landingTopTolerance; + if (jumpPadCollider == null) + { + return false; + } + + var landingLineY = jumpPadCollider.bounds.max.y - landingTopTolerance; + return player.GroundCheckY >= landingLineY || player.PreviousGroundCheckY >= landingLineY; } + /// + /// Переводит множитель высоты в скорость прыжка через квадратный корень. + /// private float CalculateBoostVelocity(JumpPlayerController player) { return player.AutoJumpVelocity * Mathf.Sqrt(jumpHeightMultiplier); } + /// + /// Гарантирует, что коллайдер пружины работает как trigger. + /// private void EnsureTriggerCollider() { jumpPadCollider = GetComponent(); @@ -111,6 +163,9 @@ namespace DanroJump.Gameplay } } + /// + /// Находит визуальный корень пружины для stretch-анимации. + /// private void ResolveVisualRoot() { if (visualRoot != null) @@ -125,6 +180,9 @@ namespace DanroJump.Gameplay } } + /// + /// Запоминает исходный scale визуала, чтобы корректно восстановить его после анимации. + /// private void CacheVisualBaseScale() { if (visualRoot == null || hasVisualBaseScale) @@ -136,6 +194,9 @@ namespace DanroJump.Gameplay hasVisualBaseScale = true; } + /// + /// Запускает короткую анимацию растяжения пружины. + /// private void PlayStretchAnimation() { ResolveVisualRoot(); @@ -146,21 +207,17 @@ namespace DanroJump.Gameplay return; } - if (stretchRoutine != null) - { - StopCoroutine(stretchRoutine); - } - - stretchRoutine = StartCoroutine(StretchVisualRoutine()); + CancelStretchAnimation(); + stretchCancellation = new CancellationTokenSource(); + StretchVisualAsync(stretchCancellation).Forget(); } + /// + /// Останавливает активную stretch-анимацию и возвращает базовый scale. + /// private void ResetVisualScale() { - if (stretchRoutine != null) - { - StopCoroutine(stretchRoutine); - stretchRoutine = null; - } + CancelStretchAnimation(); if (visualRoot != null && hasVisualBaseScale) { @@ -168,39 +225,102 @@ namespace DanroJump.Gameplay } } - private IEnumerator StretchVisualRoutine() + private void OnDestroy() + { + CancelStretchAnimation(); + } + + private void CancelStretchAnimation() + { + if (stretchCancellation == null) + { + return; + } + + try + { + stretchCancellation.Cancel(); + } + catch + { + // ignored + } + + stretchCancellation.Dispose(); + stretchCancellation = null; + } + + /// + /// Последовательно растягивает визуал и возвращает его к исходному размеру. + /// + private async UniTaskVoid StretchVisualAsync(CancellationTokenSource cancellationSource) { var stretchedScale = new Vector3( visualBaseScale.x * stretchScale.x, visualBaseScale.y * stretchScale.y, visualBaseScale.z); - yield return AnimateVisualScale(visualBaseScale, stretchedScale, stretchOutDuration); - yield return AnimateVisualScale(stretchedScale, visualBaseScale, recoverDuration); + var token = cancellationSource.Token; + try + { + await AnimateVisualScaleAsync(visualBaseScale, stretchedScale, stretchOutDuration, token); + await AnimateVisualScaleAsync(stretchedScale, visualBaseScale, recoverDuration, token); - visualRoot.localScale = visualBaseScale; - stretchRoutine = null; + if (!token.IsCancellationRequested && visualRoot != null) + { + visualRoot.localScale = visualBaseScale; + } + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + // ignored + } + finally + { + if (stretchCancellation == cancellationSource) + { + stretchCancellation = null; + cancellationSource.Dispose(); + } + } } - private IEnumerator AnimateVisualScale(Vector3 from, Vector3 to, float duration) + /// + /// Интерполирует scale визуального корня за указанное время. + /// + private async UniTask AnimateVisualScaleAsync(Vector3 from, Vector3 to, float duration, CancellationToken cancellationToken) { if (duration <= 0f) { - visualRoot.localScale = to; - yield break; + if (visualRoot != null) + { + visualRoot.localScale = to; + } + + return; } var elapsed = 0f; while (elapsed < duration) { + cancellationToken.ThrowIfCancellationRequested(); + + if (visualRoot == null) + { + return; + } + elapsed += Time.deltaTime; var t = Mathf.Clamp01(elapsed / duration); var easedT = 1f - (1f - t) * (1f - t); visualRoot.localScale = Vector3.LerpUnclamped(from, to, easedT); - yield return null; + await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken); } - visualRoot.localScale = to; + if (visualRoot != null) + { + visualRoot.localScale = to; + } } } } diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/PlatformEquipmentBase.cs b/Assets/Scripts/Runtime/Gameplay/Equipment/PlatformEquipmentBase.cs index 5d093c3..2061086 100644 --- a/Assets/Scripts/Runtime/Gameplay/Equipment/PlatformEquipmentBase.cs +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/PlatformEquipmentBase.cs @@ -2,39 +2,63 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Базовый класс оборудования платформы с trigger-контактом игрока. + /// [RequireComponent(typeof(Collider2D))] public abstract class PlatformEquipmentBase : MonoBehaviour, IPlatformEquipment, IPlayerContactHandler { + /// + /// Платформа, на которую установлено оборудование. + /// protected IPlatformRuntime Platform { get; private set; } + /// + /// В редакторе переводит коллайдер оборудования в trigger. + /// protected virtual void Reset() { var equipmentCollider = GetComponent(); equipmentCollider.isTrigger = true; } + /// + /// Сбрасывает состояние при включении, чтобы объект можно было переиспользовать. + /// private void OnEnable() { ResetEquipment(); } + /// + /// Передает контакт с игроком в конкретную реализацию оборудования. + /// private void OnTriggerEnter2D(Collider2D other) { - if (other.TryGetComponent(out JumpPlayerController player)) + if (PlayerContactUtility.TryGetAlivePlayer(other, out var player)) { HandlePlayerContact(player, this); } } + /// + /// Привязывает оборудование к платформе. + /// public virtual void Install(IPlatformRuntime platform) { Platform = platform; } + /// + /// Точка расширения для сброса внутреннего состояния оборудования. + /// public virtual void ResetEquipment() { } + /// + /// Обрабатывает контакт игрока с оборудованием. + /// public abstract void HandlePlayerContact(JumpPlayerController player, Component source); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/SpeedBoostEquipment.cs b/Assets/Scripts/Runtime/Gameplay/Equipment/SpeedBoostEquipment.cs new file mode 100644 index 0000000..a829041 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/SpeedBoostEquipment.cs @@ -0,0 +1,46 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Оборудование платформы, которое временно ускоряет горизонтальное движение игрока. + /// + public sealed class SpeedBoostEquipment : PlatformEquipmentBase + { + [SerializeField] [Min(0.01f)] private float moveSpeedMultiplier = 1.45f; + [SerializeField] [Min(0.01f)] private float duration = 3f; + [SerializeField] [Min(0f)] private float bounceVelocity; + [SerializeField] [Min(0f)] private float activationCooldown = 0.25f; + + private float lastActivationTime = -999f; + + /// + /// Сбрасывает cooldown при переиспользовании оборудования. + /// + public override void ResetEquipment() + { + lastActivationTime = -999f; + } + + /// + /// Применяет ускорение и опциональный отскок при контакте с игроком. + /// + public override void HandlePlayerContact(JumpPlayerController player, Component source) + { + if (player == null || Time.time - lastActivationTime < activationCooldown) + { + return; + } + + player.ApplyMoveSpeedMultiplier(moveSpeedMultiplier, duration); + + // Bounce опционален: при нуле оборудование влияет только на скорость. + if (bounceVelocity > 0f) + { + player.Bounce(bounceVelocity); + } + + lastActivationTime = Time.time; + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/SpeedBoostEquipment.cs.meta b/Assets/Scripts/Runtime/Gameplay/Equipment/SpeedBoostEquipment.cs.meta new file mode 100644 index 0000000..ba59b2e --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/SpeedBoostEquipment.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 747a5c8ca2dafe94babc0829532cf3b4 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Equipment/SpringEquipment.cs b/Assets/Scripts/Runtime/Gameplay/Equipment/SpringEquipment.cs index 75ca7df..2075696 100644 --- a/Assets/Scripts/Runtime/Gameplay/Equipment/SpringEquipment.cs +++ b/Assets/Scripts/Runtime/Gameplay/Equipment/SpringEquipment.cs @@ -2,20 +2,107 @@ using UnityEngine; namespace DanroJump.Gameplay { - public sealed class SpringEquipment : PlatformEquipmentBase + /// + /// Простая пружина платформы, которая задает игроку фиксированный вертикальный отскок. + /// + public sealed class SpringEquipment : PlatformEquipmentBase, ILandingEquipment { [SerializeField] private float bounceVelocity = 22f; + [SerializeField] private float landingVelocityThreshold = 0.05f; + [SerializeField] private float landingTopTolerance = 0.2f; + [SerializeField] private float activationCooldown = 0.08f; + private Collider2D springCollider; + private float lastActivationTime = -1f; + + /// + /// Вертикальная скорость, которую пружина задает игроку. + /// public float BounceVelocity => bounceVelocity; + /// + /// Сбрасывает cooldown и кэширует trigger-коллайдер. + /// + public override void ResetEquipment() + { + lastActivationTime = -1f; + EnsureTriggerCollider(); + } + + /// + /// Отталкивает игрока вверх только при падении сверху. + /// public override void HandlePlayerContact(JumpPlayerController player, Component source) + { + TryActivate(player); + } + + /// + /// Пробует активировать пружину от контакта игрока. + /// + public bool TryActivate(JumpPlayerController player) { if (player == null) { - return; + return false; } - player.Bounce(bounceVelocity); + if (!CanActivate(player)) + { + return false; + } + + player.BounceWithEnemyPassThrough(bounceVelocity); + lastActivationTime = Time.time; + return true; + } + + public bool TryActivateFromLanding(JumpPlayerController player) + { + return TryActivate(player); + } + + /// + /// Проверяет направление движения, cooldown и высоту GroundCheck относительно верха пружины. + /// + private bool CanActivate(JumpPlayerController player) + { + if (Time.time - lastActivationTime < activationCooldown) + { + return false; + } + + var isLanding = player.VerticalVelocity <= landingVelocityThreshold || + player.PreviousVerticalVelocity <= landingVelocityThreshold; + if (!isLanding) + { + return false; + } + + if (springCollider == null) + { + EnsureTriggerCollider(); + } + + if (springCollider == null) + { + return false; + } + + var landingLineY = springCollider.bounds.max.y - landingTopTolerance; + return player.GroundCheckY >= landingLineY || player.PreviousGroundCheckY >= landingLineY; + } + + /// + /// Гарантирует, что коллайдер пружины работает как trigger. + /// + private void EnsureTriggerCollider() + { + springCollider = GetComponent(); + if (springCollider != null) + { + springCollider.isTrigger = true; + } } } } diff --git a/Assets/Scripts/Runtime/Gameplay/GameplayDifficultyProgression.cs b/Assets/Scripts/Runtime/Gameplay/GameplayDifficultyProgression.cs index fe5a965..3108d9c 100644 --- a/Assets/Scripts/Runtime/Gameplay/GameplayDifficultyProgression.cs +++ b/Assets/Scripts/Runtime/Gameplay/GameplayDifficultyProgression.cs @@ -4,29 +4,56 @@ using Zenject; namespace DanroJump.Gameplay { + /// + /// Выбирает активный этап сложности по максимальной высоте игрока или Y-координате генерации. + /// public sealed class GameplayDifficultyProgression : MonoBehaviour, IGameplayDifficultyProgression { [SerializeField] private JumpPlayerController player; [SerializeField] private bool progressionEnabled = true; [SerializeField] private List stages = new(); - public bool HasStages => stages != null && stages.Exists(stage => stage != null); + /// + /// Показывает, есть ли хотя бы один настроенный этап сложности. + /// + public bool HasStages => progressionEnabled && stages != null && stages.Exists(stage => stage != null); + + /// + /// Начальная Y-координата генерации по первому этапу. + /// public float InitialSpawnY => GetInitialSpawnY(); + + /// + /// Прогресс игрока внутри текущего этапа по максимальной высоте забега. + /// public float CurrentProgress => CalculateProgress(player != null ? player.HighestY : 0f); + + /// + /// Имя текущего этапа игрока для HUD и editor overlay. + /// public string CurrentStageName => GetStage(player != null ? player.HighestY : 0f)?.StageName ?? string.Empty; + /// + /// Получает игрока из SceneContext, чтобы UI и диагностические окна видели текущий этап забега. + /// [Inject] public void Construct(JumpPlayerController injectedPlayer) { player = injectedPlayer; } + /// + /// Выбирает prefab платформы для указанной высоты по настройкам активного этапа. + /// public GameObject GetPlatformPrefab(float worldY, System.Random random) { var stage = GetStage(worldY); return stage != null ? stage.GetPlatformPrefab(random) : null; } + /// + /// Возвращает расстояние между платформами для указанной высоты. + /// public float GetVerticalSpacing(float worldY) { var stage = GetStage(worldY); @@ -38,12 +65,18 @@ namespace DanroJump.Gameplay return stage.GetVerticalSpacing(worldY); } + /// + /// Возвращает распределение платформ по ширине уровня для указанной высоты. + /// public float GetHorizontalDistribution(float worldY) { var stage = GetStage(worldY); return stage != null ? stage.GetHorizontalDistribution() : 0f; } + /// + /// Создает профиль контента активного этапа: collectibles, equipment и enemies. + /// public IPlatformContentSource CreateContentSource(float worldY) { var stage = GetStage(worldY); @@ -55,11 +88,32 @@ namespace DanroJump.Gameplay return stage.CreateContentSource(worldY); } + /// + /// Пробует выбрать процедурного врага для указанной высоты из активного ScriptableObject-этапа. + /// + public bool TryGetProceduralEnemyPrefab(float worldY, System.Random random, out GameObject prefab) + { + var stage = GetStage(worldY); + if (stage == null) + { + prefab = null; + return false; + } + + return stage.TryGetProceduralEnemyPrefab(worldY, random, out prefab); + } + + /// + /// Возвращает имя этапа для диагностики и overlay-окна. + /// public string GetStageName(float worldY) { return GetStage(worldY)?.StageName ?? string.Empty; } + /// + /// Возвращает прогресс внутри текущего этапа в диапазоне 0..1. + /// public float GetStageProgress(float worldY) { var stage = GetStage(worldY); @@ -72,12 +126,18 @@ namespace DanroJump.Gameplay return stage != null ? stage.CalculateProgress(worldY) : 0f; } + /// + /// Возвращает стартовую высоту генерации из первого доступного этапа. + /// private float GetInitialSpawnY() { var firstStage = GetFirstStage(); return firstStage != null ? firstStage.StartY : 0f; } + /// + /// Находит первый этап по минимальной стартовой высоте. + /// private GameplayDifficultyStage GetFirstStage() { if (stages == null || stages.Count == 0) @@ -102,6 +162,9 @@ namespace DanroJump.Gameplay return firstStage; } + /// + /// Находит последний этап, стартовая высота которого уже пройдена. + /// private GameplayDifficultyStage GetStage(float worldY) { if (!progressionEnabled || stages == null || stages.Count == 0) @@ -123,12 +186,15 @@ namespace DanroJump.Gameplay } } - // The last reached stage intentionally keeps running forever after its length ends. - // verticalLength controls curve progress only; it does not disable the stage. + // Последний достигнутый этап намеренно продолжается бесконечно. + // verticalLength управляет только прогрессом кривых, а не выключением этапа. return currentStage; } } + /// + /// Интерфейс чтения прогрессии сложности для gameplay-систем и диагностических окон. + /// public interface IGameplayDifficultyProgression { float CurrentProgress { get; } @@ -139,6 +205,7 @@ namespace DanroJump.Gameplay float GetVerticalSpacing(float worldY); float GetHorizontalDistribution(float worldY); IPlatformContentSource CreateContentSource(float worldY); + bool TryGetProceduralEnemyPrefab(float worldY, System.Random random, out GameObject prefab); string GetStageName(float worldY); float GetStageProgress(float worldY); } diff --git a/Assets/Scripts/Runtime/Gameplay/GameplayDifficultyStage.cs b/Assets/Scripts/Runtime/Gameplay/GameplayDifficultyStage.cs index 00b0a01..414413d 100644 --- a/Assets/Scripts/Runtime/Gameplay/GameplayDifficultyStage.cs +++ b/Assets/Scripts/Runtime/Gameplay/GameplayDifficultyStage.cs @@ -4,12 +4,19 @@ using UnityEngine.Serialization; namespace DanroJump.Gameplay { + /// + /// ScriptableObject-описание одного вертикального этапа сложности. + /// Хранит веса платформ, кривые расстояний и вероятности спавна контента. + /// [CreateAssetMenu( fileName = "GameplayDifficultyStage", menuName = "Danro Jump/Gameplay/Difficulty Stage", order = 10)] public sealed class GameplayDifficultyStage : ScriptableObject { + /// + /// Настройки платформ этапа: какие prefab доступны и как меняется вертикальный шаг. + /// [System.Serializable] private sealed class StagePlatformSettings { @@ -18,11 +25,17 @@ namespace DanroJump.Gameplay [SerializeField] [Min(0.1f)] private float initialVerticalSpacing = 1.15f; [SerializeField] [Min(0.1f)] private float maximumVerticalSpacing = 1.95f; + /// + /// Выбирает prefab платформы по весам. + /// public GameObject GetPlatformPrefab(System.Random random) { return WeightedPrefabOption.TrySelect(platforms, random, out var prefab) ? prefab : null; } + /// + /// Вычисляет расстояние до следующей платформы по прогрессу этапа. + /// public float EvaluateVerticalSpacing(float progress) { var spacingProgress = EvaluateCurve01(verticalSpacingCurve, progress); @@ -32,6 +45,9 @@ namespace DanroJump.Gameplay } + /// + /// Настройки одной категории спавнимого контента этапа. + /// [System.Serializable] private sealed class StageContentSettings { @@ -40,14 +56,54 @@ namespace DanroJump.Gameplay [SerializeField] [Range(0f, 1f)] private float initialSpawnChance; [SerializeField] [Range(0f, 1f)] private float maximumSpawnChance; + /// + /// Взвешенный список prefab этой категории контента. + /// public IReadOnlyList Prefabs => prefabs; + /// + /// Вычисляет шанс появления контента по прогрессу этапа. + /// public float EvaluateSpawnChance(float progress) { + if (!HasValidPrefab()) + { + return 0f; + } + var chanceProgress = EvaluateCurve01(spawnChanceCurve, progress); var maxChance = Mathf.Max(initialSpawnChance, maximumSpawnChance); return Mathf.Lerp(initialSpawnChance, maxChance, chanceProgress); } + + /// + /// Выбирает prefab из этой категории с учетом дополнительного фильтра. + /// + public bool TryGetPrefab( + System.Random random, + System.Predicate prefabFilter, + out GameObject prefab) + { + return WeightedPrefabOption.TrySelect(prefabs, random, prefabFilter, out prefab); + } + + private bool HasValidPrefab() + { + if (prefabs == null) + { + return false; + } + + foreach (var option in prefabs) + { + if (option?.Prefab != null && option.Weight > 0f) + { + return true; + } + } + + return false; + } } [Header("Общие настройки этапа")] @@ -69,24 +125,43 @@ namespace DanroJump.Gameplay [Header("Подбираемые предметы")] [SerializeField] private StageContentSettings collectibles = new(); + /// + /// Отображаемое имя этапа для инспектора, overlay и диагностики. + /// public string StageName => stageName; + + /// + /// Y-координата, с которой этап начинает действовать. + /// public float StartY => startY; + /// + /// Выбирает платформу этапа по весам. + /// public GameObject GetPlatformPrefab(System.Random random) { return platforms.GetPlatformPrefab(random); } + /// + /// Возвращает вертикальное расстояние до следующей платформы на указанной высоте. + /// public float GetVerticalSpacing(float worldY) { return platforms.EvaluateVerticalSpacing(CalculateProgress(worldY)); } + /// + /// Возвращает распределение платформ по горизонтали: -1 центр, 0 вся ширина, 1 края. + /// public float GetHorizontalDistribution() { return Mathf.Clamp(horizontalDistribution, -1f, 1f); } + /// + /// Создает источник контента с вероятностями, актуальными для указанной высоты. + /// public IPlatformContentSource CreateContentSource(float worldY) { var progress = CalculateProgress(worldY); @@ -99,6 +174,31 @@ namespace DanroJump.Gameplay enemies.EvaluateSpawnChance(progress)); } + /// + /// Пробует выбрать процедурного врага из настроек противников текущего этапа. + /// + public bool TryGetProceduralEnemyPrefab(float worldY, System.Random random, out GameObject prefab) + { + var progress = CalculateProgress(worldY); + var spawnChance = enemies.EvaluateSpawnChance(progress); + if (spawnChance <= 0f) + { + prefab = null; + return false; + } + + if (spawnChance < 1f && (random != null ? random.NextDouble() : 0d) > spawnChance) + { + prefab = null; + return false; + } + + return enemies.TryGetPrefab(random, EnemyPrefabRules.CanSpawnProcedurally, out prefab); + } + + /// + /// Считает прогресс внутри этапа в диапазоне 0..1. + /// public float CalculateProgress(float worldY) { if (verticalLength <= 0f) @@ -109,6 +209,9 @@ namespace DanroJump.Gameplay return Mathf.Clamp01((worldY - startY) / verticalLength); } + /// + /// Безопасно вычисляет значение кривой и ограничивает результат диапазоном 0..1. + /// private static float EvaluateCurve01(AnimationCurve curve, float progress) { if (curve == null || curve.length == 0) diff --git a/Assets/Scripts/Runtime/Gameplay/GameplayInstaller.cs b/Assets/Scripts/Runtime/Gameplay/GameplayInstaller.cs index 45e6330..ad6acce 100644 --- a/Assets/Scripts/Runtime/Gameplay/GameplayInstaller.cs +++ b/Assets/Scripts/Runtime/Gameplay/GameplayInstaller.cs @@ -3,6 +3,10 @@ using Zenject; namespace DanroJump.Gameplay { + /// + /// Регистрирует зависимости игровой сцены в SceneContext Extenject. + /// Является единой точкой связывания gameplay-сервисов и сценовых объектов. + /// public sealed class GameplayInstaller : MonoInstaller { [SerializeField] private JumpPlayerController player; @@ -10,19 +14,25 @@ namespace DanroJump.Gameplay [SerializeField] private PlatformSpawner platformSpawner; [SerializeField] private GameplayDifficultyProgression difficultyProgression; + /// + /// Привязывает игрока, камеру, спавнер, прогрессию сложности и runtime-сервисы. + /// public override void InstallBindings() { if (!ValidateReferences()) { - return; + throw new ZenjectException($"{nameof(GameplayInstaller)} has missing required scene references."); } Container.BindInstance(player).AsSingle(); Container.BindInstance(gameplayCamera).AsSingle(); Container.BindInstance(platformSpawner).AsSingle(); Container.BindInstance(difficultyProgression).AsSingle(); + Container.Bind().FromInstance(difficultyProgression).AsSingle(); + Container.Bind().AsSingle().NonLazy(); Container.Bind().AsSingle(); + // Сценовые MonoBehaviour уже существуют, поэтому явно ставим их в очередь на injection. Container.QueueForInject(player); Container.QueueForInject(difficultyProgression); Container.QueueForInject(platformSpawner); @@ -33,6 +43,10 @@ namespace DanroJump.Gameplay } } + /// + /// Проверяет обязательные ссылки сцены до регистрации зависимостей. + /// + /// True, если SceneContext настроен достаточно для запуска gameplay. private bool ValidateReferences() { var isValid = true; @@ -45,6 +59,9 @@ namespace DanroJump.Gameplay return isValid; } + /// + /// Валидирует одну сценовую ссылку и пишет понятную ошибку в Unity Console. + /// private bool ValidateReference(Object reference, string fieldName) { if (reference != null) @@ -55,5 +72,6 @@ namespace DanroJump.Gameplay Debug.LogError($"{nameof(GameplayInstaller)} is missing '{fieldName}' reference.", this); return false; } + } } diff --git a/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs b/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs new file mode 100644 index 0000000..b05c143 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs @@ -0,0 +1,246 @@ +using System; +using DanroJump.SceneFlow; +using DanroJump.Settings; +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Хранит состояние текущего забега: счет, монеты, жизни, высоту и рекорд. + /// + public sealed class GameplaySession : IDisposable + { + private const string RecordHeightPrefsKey = "DanroJump.Gameplay.RecordHeight"; + private const int DefaultStartingLives = 3; + private static readonly TimeSpan GameOverReturnDelay = TimeSpan.FromSeconds(0.35f); + + private readonly JumpPlayerController player; + private readonly IReadOnlyServiceSettings serviceSettings; + private readonly ISceneFlowService sceneFlow; + private readonly int startingLives; + private float savedRecordHeight; + + /// + /// Очки, набранные в текущем забеге. + /// + public int Score { get; private set; } + + /// + /// Монеты, собранные в текущем забеге. + /// + public int Coins { get; private set; } + + /// + /// Количество смертей в текущем забеге. + /// + public int DeathCount { get; private set; } + + /// + /// Оставшиеся жизни до завершения забега. + /// + public int RemainingLives { get; private set; } + + /// + /// Текущее состояние gameplay-сессии. + /// + public GameplaySessionState State { get; private set; } + + /// + /// Фактическая текущая высота игрока над стартом. + /// + public float CurrentHeight => player != null ? Mathf.Max(0f, player.transform.position.y) : 0f; + + /// + /// Максимальная высота, достигнутая в текущем забеге. + /// + public float MaxRunHeight => player != null ? Mathf.Max(0f, player.HighestY) : 0f; + + /// + /// Лучшая высота текущего забега. + /// + public float BestHeight => MaxRunHeight; + + /// + /// Абсолютный рекорд с учетом сохраненного значения и текущего забега. + /// + public float RecordHeight => Mathf.Max(savedRecordHeight, MaxRunHeight); + + /// + /// Показывает, находится ли игрок в состоянии смерти или завершенного забега. + /// + public bool IsPlayerDead => State == GameplaySessionState.Respawning || State == GameplaySessionState.GameOver; + + public event Action Changed; + + /// + /// Создает сессию и подписывается на события смерти/respawn игрока. + /// + public GameplaySession( + JumpPlayerController player, + [Zenject.InjectOptional] IReadOnlyServiceSettings serviceSettings = null, + [Zenject.InjectOptional] ISceneFlowService sceneFlow = null) + { + this.player = player; + this.serviceSettings = serviceSettings; + this.sceneFlow = sceneFlow; + startingLives = ResolveStartingLives(); + RemainingLives = startingLives; + State = GameplaySessionState.Running; + savedRecordHeight = PlayerPrefs.GetFloat(RecordHeightPrefsKey, 0f); + + if (this.player != null) + { + this.player.Died += HandlePlayerDied; + this.player.Respawned += HandlePlayerRespawned; + } + } + + /// + /// Добавляет очки в текущий забег. + /// + public void AddScore(int amount) + { + if (amount <= 0) + { + return; + } + + Score += amount; + Changed?.Invoke(); + } + + /// + /// Добавляет монеты в текущий забег. + /// + public void AddCoins(int amount) + { + if (amount <= 0) + { + return; + } + + Coins += amount; + Changed?.Invoke(); + } + + /// + /// Полностью сбрасывает забег, сохранив рекорд при необходимости. + /// + public void ResetSession() + { + SaveRecordIfNeeded(); + Score = 0; + Coins = 0; + DeathCount = 0; + RemainingLives = startingLives; + State = GameplaySessionState.Running; + player?.ResetRun(); + Changed?.Invoke(); + } + + /// + /// Сохраняет рекорд и отписывается от событий игрока. + /// + public void Dispose() + { + SaveRecordIfNeeded(); + + if (player == null) + { + return; + } + + player.Died -= HandlePlayerDied; + player.Respawned -= HandlePlayerRespawned; + } + + /// + /// Обновляет состояние сессии после смерти игрока. + /// + private void HandlePlayerDied(JumpPlayerController _) + { + DeathCount++; + RemainingLives = Mathf.Max(0, RemainingLives - 1); + State = RemainingLives > 0 ? GameplaySessionState.Respawning : GameplaySessionState.GameOver; + if (State == GameplaySessionState.GameOver) + { + player?.BlockPendingRespawn(); + StartGameOverReturn(); + } + else + { + player?.ScheduleRespawn(player.DeathRespawnDelay); + } + + SaveRecordIfNeeded(); + Changed?.Invoke(); + } + + /// + /// Возвращает сессию в Running после respawn, если игра еще не закончена. + /// + private void HandlePlayerRespawned(JumpPlayerController _) + { + if (State != GameplaySessionState.GameOver) + { + State = GameplaySessionState.Running; + } + + Changed?.Invoke(); + } + + /// + /// Читает стартовое количество жизней из сервисных настроек. + /// + private int ResolveStartingLives() + { + if (serviceSettings == null) + { + return DefaultStartingLives; + } + + var configuredLives = serviceSettings.GetInt(ServiceSettingsKeys.StartingLives); + return Mathf.Max(1, configuredLives); + } + + /// + /// Сохраняет лучший результат по высоте в PlayerPrefs. + /// + private void SaveRecordIfNeeded() + { + if (MaxRunHeight <= savedRecordHeight) + { + return; + } + + savedRecordHeight = MaxRunHeight; + PlayerPrefs.SetFloat(RecordHeightPrefsKey, savedRecordHeight); + PlayerPrefs.Save(); + } + + /// + /// Запускает возврат в главное меню после финальной смерти. + /// + private void StartGameOverReturn() + { + if (sceneFlow == null) + { + Debug.LogWarning($"{nameof(GameplaySession)} cannot return to main menu because {nameof(ISceneFlowService)} is not bound."); + return; + } + + sceneFlow.ReturnToMainMenuAfterGameOver(GameOverReturnDelay); + } + } + + /// + /// Состояние игрового забега. + /// + public enum GameplaySessionState + { + Waiting = 0, + Running = 1, + Respawning = 2, + GameOver = 3, + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs.meta b/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs.meta new file mode 100644 index 0000000..0abb8a8 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 993f37ab6fd94234b9fc09213874df63 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/JumpPlatform.cs b/Assets/Scripts/Runtime/Gameplay/JumpPlatform.cs index f60b95f..16a2408 100644 --- a/Assets/Scripts/Runtime/Gameplay/JumpPlatform.cs +++ b/Assets/Scripts/Runtime/Gameplay/JumpPlatform.cs @@ -2,6 +2,9 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Маркер обычной платформы, которую игрок может использовать для автопрыжка. + /// [RequireComponent(typeof(Collider2D))] public sealed class JumpPlatform : MonoBehaviour { diff --git a/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs b/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs index 4b30d19..7e74765 100644 --- a/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs +++ b/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs @@ -1,3 +1,4 @@ +using System.Collections; using System.Collections.Generic; using UnityEngine; using Zenject; @@ -7,6 +8,9 @@ using UnityEngine.InputSystem; namespace DanroJump.Gameplay { + /// + /// Управляет игроком вертикального платформера: вводом, автопрыжками, wrap-переходом, смертью и анимациями. + /// [RequireComponent(typeof(Rigidbody2D))] [RequireComponent(typeof(Collider2D))] public sealed class JumpPlayerController : MonoBehaviour @@ -20,11 +24,15 @@ namespace DanroJump.Gameplay [SerializeField] private float groundProbeStartOffset = 0.08f; [SerializeField] private float groundProbeDistance = 0.34f; [SerializeField] private Vector2 equipmentProbeHalfExtents = new(0.5f, 0.35f); + [SerializeField] private Vector2 sweptLandingProbeHalfExtents = new(0.48f, 0.08f); + [SerializeField] private float sweptLandingExtraDistance = 0.18f; + [SerializeField] private CollisionDetectionMode2D collisionDetectionMode = CollisionDetectionMode2D.Continuous; [SerializeField] private float horizontalWrapPadding = 0.65f; - [SerializeField] private float fallResetDistance = 5f; + [SerializeField] private float fallResetDistance = 0.5f; [SerializeField] private bool fallResetFromCameraBottom = true; [SerializeField] [Range(0f, 1f)] private float respawnViewportY = 0.28f; [SerializeField] private bool centerRespawnX = true; + [SerializeField] private float deathRespawnDelay = 0.35f; [SerializeField] private Animator animator; [SerializeField] private Transform visualRoot; [SerializeField] private string visualRootName = "Body"; @@ -40,55 +48,152 @@ namespace DanroJump.Gameplay private Vector3 visualRootInitialScale; private Quaternion visualRootInitialRotation; private float horizontalInput; + private float moveSpeedMultiplier = 1f; private float jumpAnimationPulseUntil; + private float respawnAtTime = -1f; private float lastAutoJumpTime = -1f; + private Coroutine moveSpeedBoostRoutine; + private Coroutine heightLiftRoutine; + private bool respawnBlocked; + private bool enemyPassThroughWhileRising; private bool hasSpeedParameter; private bool hasVerticalSpeedParameter; private bool hasGroundedParameter; private bool hasJumpParameter; private bool hasDeathParameter; + private AnimatorControllerParameterType deathParameterType; private Transform wrapVisualCloneRoot; private readonly List sourceRenderers = new(); private readonly List wrapRendererClones = new(); + private readonly List landingHandlerBuffer = new(); + private readonly List landingEquipmentBuffer = new(); + private readonly List ignoredEnemyColliders = new(); private readonly RaycastHit2D[] groundProbeHits = new RaycastHit2D[8]; + private readonly RaycastHit2D[] sweptGroundProbeHits = new RaycastHit2D[12]; + private readonly RaycastHit2D[] sweptEquipmentProbeHits = new RaycastHit2D[12]; private readonly Collider2D[] equipmentProbeHits = new Collider2D[8]; + private readonly ContactFilter2D solidProbeFilter = new() + { + useTriggers = false + }; + private readonly ContactFilter2D triggerProbeFilter = new() + { + useTriggers = true + }; + private Bounds cachedVisualBounds; + private int cachedVisualBoundsFrame = -1; private static readonly int SpeedHash = Animator.StringToHash("Speed"); private static readonly int VerticalSpeedHash = Animator.StringToHash("VSpeed"); private static readonly int GroundedHash = Animator.StringToHash("Grounded"); private static readonly int JumpHash = Animator.StringToHash("Jump"); private static readonly int DeathHash = Animator.StringToHash("Death"); + private static readonly WaitForFixedUpdate WaitForFixedUpdateYield = new(); + /// + /// Максимальная высота текущего забега. + /// public float HighestY { get; private set; } + + /// + /// Текущая вертикальная скорость Rigidbody2D. + /// public float VerticalVelocity => body != null ? body.linearVelocity.y : 0f; + + /// + /// Позиция GroundCheck на предыдущем физическом шаге. + /// + public Vector2 PreviousGroundCheckPosition { get; private set; } + + /// + /// Y-координата GroundCheck на предыдущем физическом шаге. + /// + public float PreviousGroundCheckY { get; private set; } + + /// + /// Вертикальная скорость игрока на предыдущем физическом шаге. + /// + public float PreviousVerticalVelocity { get; private set; } + + /// + /// Текущая мировая позиция GroundCheck. + /// + public Vector2 GroundCheckPosition => groundCheck != null ? groundCheck.position : transform.position; + + /// + /// Текущая Y-координата GroundCheck. + /// public float GroundCheckY => groundCheck != null ? groundCheck.position.y : transform.position.y; + + /// + /// Базовая скорость автоматического прыжка игрока. + /// public float AutoJumpVelocity => autoJumpVelocity; + /// + /// Задержка перед respawn после смерти игрока. + /// + public float DeathRespawnDelay => deathRespawnDelay; + + /// + /// True, если игрок сейчас должен проходить сквозь врагов после усиленного подъема. + /// + public bool IgnoresEnemyContacts => enemyPassThroughWhileRising && VerticalVelocity > 0f && !IsDead; + + /// + /// Показывает, находится ли игрок в состоянии смерти/respawn. + /// + public bool IsDead { get; private set; } + + public event System.Action Died; + public event System.Action Respawned; + + /// + /// Получает gameplay-камеру через SceneContext. + /// [Inject] public void Construct(Camera injectedGameplayCamera) { gameplayCamera = injectedGameplayCamera; } + /// + /// Кэширует физику, визуальные ссылки и параметры Animator. + /// private void Awake() { body = GetComponent(); playerCollider = GetComponent(); startPosition = transform.position; HighestY = transform.position.y; + body.collisionDetectionMode = collisionDetectionMode; ResolveVisualReferences(); RebuildWrapVisuals(); CacheAnimatorParameters(); + CachePhysicsStateForNextStep(); } + /// + /// Запускает первый прыжок сразу после старта сцены. + /// private void Start() { Bounce(initialJumpVelocity); } + /// + /// Обрабатывает ввод, wrap, смерть от падения и обновление Animator. + /// private void Update() { + if (IsDead) + { + TryFinishDeathRespawn(); + UpdateAnimatorParameters(); + return; + } + horizontalInput = ReadHorizontalInput(); HighestY = Mathf.Max(HighestY, transform.position.y); @@ -98,72 +203,319 @@ namespace DanroJump.Gameplay UpdateAnimatorParameters(); } + /// + /// Двигает Rigidbody2D и проверяет автопрыжок в физическом шаге. + /// private void FixedUpdate() { - body.linearVelocity = new Vector2(horizontalInput * moveSpeed, body.linearVelocity.y); + if (IsDead) + { + body.linearVelocity = Vector2.zero; + EndEnemyPassThrough(); + CachePhysicsStateForNextStep(); + return; + } + + body.linearVelocity = new Vector2(horizontalInput * moveSpeed * moveSpeedMultiplier, body.linearVelocity.y); TryAutoJumpFromGroundProbe(); + UpdateEnemyPassThroughState(); + CachePhysicsStateForNextStep(); } + /// + /// Обновляет визуальный клон игрока на противоположной стороне экрана. + /// private void LateUpdate() { UpdateWrappedVisualPreview(); } + /// + /// Уничтожает служебный визуальный клон при удалении игрока. + /// private void OnDestroy() { + EndEnemyPassThrough(); + if (wrapVisualCloneRoot != null) { Destroy(wrapVisualCloneRoot.gameObject); } } + /// + /// Временно изменяет горизонтальную скорость игрока. + /// + public void ApplyMoveSpeedMultiplier(float multiplier, float duration) + { + if (multiplier <= 0f || duration <= 0f) + { + return; + } + + if (moveSpeedBoostRoutine != null) + { + StopCoroutine(moveSpeedBoostRoutine); + } + + moveSpeedBoostRoutine = StartCoroutine(ApplyMoveSpeedMultiplierRoutine(multiplier, duration)); + } + + /// + /// Поднимает игрока вверх с заданной скоростью на заданную высоту. + /// + public void ApplyHeightLift(float liftSpeed, float height) + { + if (liftSpeed <= 0f || height <= 0f) + { + return; + } + + if (heightLiftRoutine != null) + { + StopCoroutine(heightLiftRoutine); + } + + heightLiftRoutine = StartCoroutine(ApplyHeightLiftRoutine(liftSpeed, height)); + BeginEnemyPassThroughWhileRising(); + } + + /// + /// Проверяет автопрыжок при первом контакте с платформой. + /// private void OnCollisionEnter2D(Collision2D collision) { + if (IsDead) + { + return; + } + + if (TryIgnoreEnemyCollision(collision)) + { + return; + } + TryAutoJump(collision); } + /// + /// Повторно проверяет автопрыжок, если игрок уже находится на платформе. + /// private void OnCollisionStay2D(Collision2D collision) { + if (IsDead) + { + return; + } + + if (TryIgnoreEnemyCollision(collision)) + { + return; + } + TryAutoJump(collision); } + /// + /// Задает вертикальный импульс прыжка, не уменьшая уже существующую скорость вверх. + /// public void Bounce(float velocity) { body.linearVelocity = new Vector2(body.linearVelocity.x, Mathf.Max(body.linearVelocity.y, velocity)); jumpAnimationPulseUntil = Time.time + jumpAnimationPulseDuration; } - public void ResetRun() + /// + /// Задает усиленный вертикальный импульс и временно отключает столкновения с врагами на подъеме. + /// + public void BounceWithEnemyPassThrough(float velocity) { - transform.position = GetRespawnPosition(); - body.linearVelocity = Vector2.zero; - HighestY = Mathf.Max(HighestY, transform.position.y); - lastAutoJumpTime = -1f; - Bounce(initialJumpVelocity); + Bounce(velocity); + BeginEnemyPassThroughWhileRising(); } + /// + /// Отключает столкновение с указанным enemy-коллайдером, пока игрок поднимается после усиления. + /// + public bool TryIgnoreEnemyCollider(Collider2D enemyCollider) + { + if (!IgnoresEnemyContacts || enemyCollider == null || playerCollider == null) + { + return false; + } + + var enemyRoot = enemyCollider.GetComponentInParent(); + if (enemyRoot != null) + { + return TryIgnoreEnemy(enemyRoot); + } + + IgnoreEnemyCollider(enemyCollider); + return true; + } + + /// + /// Отключает столкновения со всеми коллайдерами врага, пока игрок поднимается после усиления. + /// + public bool TryIgnoreEnemy(EnemyBase enemy) + { + if (!IgnoresEnemyContacts || enemy == null || playerCollider == null) + { + return false; + } + + var enemyColliders = enemy.GetComponentsInChildren(true); + for (var index = 0; index < enemyColliders.Length; index++) + { + IgnoreEnemyCollider(enemyColliders[index]); + } + + return true; + } + + private void IgnoreEnemyCollider(Collider2D enemyCollider) + { + if (enemyCollider == null || enemyCollider == playerCollider) + { + return; + } + + Physics2D.IgnoreCollision(playerCollider, enemyCollider, true); + if (!ignoredEnemyColliders.Contains(enemyCollider)) + { + ignoredEnemyColliders.Add(enemyCollider); + } + } + + /// + /// Отключает столкновение с врагом, если игрок поднимается после усиленного прыжка. + /// + private bool TryIgnoreEnemyCollision(Collision2D collision) + { + if (!IgnoresEnemyContacts || collision.collider == null) + { + return false; + } + + var enemy = collision.collider.GetComponentInParent(); + return enemy != null && TryIgnoreEnemy(enemy); + } + + /// + /// Убивает игрока внешним игровым объектом или системой. + /// + public void Kill() + { + Die(); + } + + /// + /// Полностью начинает новый забег со стартовой позиции. + /// + public void ResetRun() + { + ResetPlayerState(startPosition, true); + } + + /// + /// Блокирует отложенный respawn после окончательного GameOver. + /// + public void BlockPendingRespawn() + { + respawnBlocked = true; + respawnAtTime = -1f; + } + + /// + /// Разрешает отложенный respawn после того, как сессия подтвердила наличие жизней. + /// + public void ScheduleRespawn(float delay) + { + if (!IsDead || respawnBlocked) + { + return; + } + + respawnAtTime = Time.time + Mathf.Max(0f, delay); + } + + /// + /// Возвращает игрока в поле зрения камеры без сброса прогресса текущей сессии. + /// + public void Respawn() + { + if (respawnBlocked) + { + return; + } + + ResetPlayerState(GetRespawnPosition(), false); + } + + /// + /// Возвращает игрока к заданной позиции и сбрасывает временные эффекты. + /// + private void ResetPlayerState(Vector3 position, bool resetRunProgress) + { + IsDead = false; + respawnBlocked = false; + respawnAtTime = -1f; + ClearDeathAnimationFlag(); + transform.position = position; + body.linearVelocity = Vector2.zero; + HighestY = resetRunProgress ? transform.position.y : Mathf.Max(HighestY, transform.position.y); + lastAutoJumpTime = -1f; + ResetMoveSpeedMultiplier(); + ResetHeightLift(); + EndEnemyPassThrough(); + CachePhysicsStateForNextStep(); + Bounce(initialJumpVelocity); + Respawned?.Invoke(this); + } + + /// + /// Animation Event для клипа прыжка. + /// public void OnJump() { } + /// + /// Animation Event для шага или вторичных звуков движения. + /// public void OnStep() { } + /// + /// Animation Event начала смерти. + /// public void OnDeathBegin() { } + /// + /// Animation Event завершения смерти. Если игрок еще мертв, запускает respawn. + /// public void OnDeathEnd() { - ResetRun(); + if (IsDead && !respawnBlocked && respawnAtTime >= 0f && Time.time >= respawnAtTime) + { + Respawn(); + } } + /// + /// Совместимый Animation Event для старых клипов, которые вызывают Jump. + /// public void Jump() { OnJump(); } + /// + /// Совместимый Animation Event для старых клипов, которые вызывают Step. + /// public void Step() { OnStep(); @@ -181,7 +533,20 @@ namespace DanroJump.Gameplay return; } - if (!TryActivateJumpPadAtGround()) + if (!IsValidLandingCollider(collision.collider)) + { + return; + } + + if (TryHandlePlatformLanding(collision)) + { + // Специальная платформа или оборудование уже обработали отскок. + lastAutoJumpTime = Time.time; + return; + } + + // Оборудование платформы имеет приоритет над обычным автопрыжком. + if (!TryActivateLandingEquipmentAtGround(Vector2.zero) && !TryActivateLandingEquipmentFromSweptProbe(Vector2.zero)) { Bounce(autoJumpVelocity); } @@ -189,6 +554,9 @@ namespace DanroJump.Gameplay lastAutoJumpTime = Time.time; } + /// + /// Проверяет, что collision действительно похож на приземление ногами, а не на удар боком или головой. + /// private bool HasGroundContact(Collision2D collision) { for (var index = 0; index < collision.contactCount; index++) @@ -199,6 +567,7 @@ namespace DanroJump.Gameplay continue; } + // GroundCheck защищает от прыжка при контакте головой или боком. if (contact.point.y <= GroundCheckY + groundContactTolerance) { return true; @@ -208,6 +577,109 @@ namespace DanroJump.Gameplay return false; } + /// + /// Передает landing-событие платформенным обработчикам, если они есть на объекте столкновения. + /// + private bool TryHandlePlatformLanding(Collision2D collision) + { + var contactPoint = GroundCheckPosition; + var contactNormal = Vector2.up; + if (collision.contactCount > 0) + { + var contact = collision.GetContact(0); + contactPoint = contact.point; + contactNormal = contact.normal; + } + + return TryHandlePlatformLanding( + new PlatformLandingContext(collision.collider, collision, contactPoint, contactNormal, false)); + } + + /// + /// Передает landing-событие платформенным обработчикам, найденным по collider. + /// + private bool TryHandlePlatformLanding(PlatformLandingContext context) + { + var platformCollider = context.PlatformCollider; + if (platformCollider == null) + { + return false; + } + + var handled = false; + platformCollider.GetComponentsInParent(false, landingHandlerBuffer); + foreach (var landingHandler in landingHandlerBuffer) + { + handled |= landingHandler.TryHandleLanding(this, context); + } + + landingHandlerBuffer.Clear(); + return handled; + } + + /// + /// Применяет временный множитель скорости, затем возвращает нормальное значение. + /// + private IEnumerator ApplyMoveSpeedMultiplierRoutine(float multiplier, float duration) + { + moveSpeedMultiplier = multiplier; + yield return new WaitForSeconds(duration); + moveSpeedBoostRoutine = null; + moveSpeedMultiplier = 1f; + } + + /// + /// Управляет подъемом игрока от lift-collectible без телепортации. + /// + private IEnumerator ApplyHeightLiftRoutine(float liftSpeed, float height) + { + var targetY = transform.position.y + height; + lastAutoJumpTime = Time.time; + + while (!IsDead && transform.position.y < targetY) + { + body.linearVelocity = new Vector2(body.linearVelocity.x, liftSpeed); + HighestY = Mathf.Max(HighestY, transform.position.y); + yield return WaitForFixedUpdateYield; + } + + if (!IsDead) + { + body.linearVelocity = new Vector2(body.linearVelocity.x, Mathf.Max(body.linearVelocity.y, 0f)); + } + + heightLiftRoutine = null; + } + + /// + /// Сбрасывает временный множитель горизонтальной скорости. + /// + private void ResetMoveSpeedMultiplier() + { + if (moveSpeedBoostRoutine != null) + { + StopCoroutine(moveSpeedBoostRoutine); + moveSpeedBoostRoutine = null; + } + + moveSpeedMultiplier = 1f; + } + + /// + /// Останавливает активный принудительный подъем игрока. + /// + private void ResetHeightLift() + { + if (heightLiftRoutine != null) + { + StopCoroutine(heightLiftRoutine); + heightLiftRoutine = null; + } + } + + /// + /// Проверяет приземление через GroundCheck-пробы, включая ситуацию на другой стороне экрана при wrap. + /// private void TryAutoJumpFromGroundProbe() { if (!CanAutoJumpNow()) @@ -215,37 +687,77 @@ namespace DanroJump.Gameplay return; } - if (!HasGroundProbeHit()) + if (TryResolveAutoJumpAtOffset(Vector2.zero, false)) { return; } - if (!TryActivateJumpPadAtGround()) + if (TryGetWrapProbeOffset(out var wrapOffset)) { - Bounce(autoJumpVelocity); + // Если игрок физически еще не перенесен, проверяем платформу на другой стороне экрана. + TryResolveAutoJumpAtOffset(wrapOffset, true); + } + } + + /// + /// Проверяет cooldown и направление движения перед автопрыжком. + /// + private bool CanAutoJumpNow() + { + return !IsDead && body.linearVelocity.y <= 0.05f && Time.time - lastAutoJumpTime >= autoJumpCooldown; + } + + /// + /// Проверяет посадку обычным и swept-пробником, затем активирует JumpPad или автопрыжок. + /// + private bool TryResolveAutoJumpAtOffset(Vector2 probeOffset, bool commitWrapOnHit) + { + if (!CanAutoJumpNow()) + { + return false; + } + + if (!TryGetGroundProbeHit(probeOffset, out var groundCollider) && + !TryGetSweptGroundProbeHit(probeOffset, out groundCollider)) + { + return false; + } + + if (commitWrapOnHit) + { + // Физическое тело переносится только когда найден реальный контакт после wrap. + CommitWrapOffset(probeOffset); + probeOffset = Vector2.zero; + } + + var suppressAutoJump = TryHandlePlatformLanding( + new PlatformLandingContext(groundCollider, null, GroundCheckPosition + probeOffset, Vector2.up, true)); + if (!TryActivateLandingEquipmentAtGround(probeOffset) && !TryActivateLandingEquipmentFromSweptProbe(probeOffset)) + { + if (!suppressAutoJump) + { + Bounce(autoJumpVelocity); + } } lastAutoJumpTime = Time.time; + return true; } - private bool CanAutoJumpNow() + /// + /// Выполняет короткий raycast вниз от GroundCheck и ищет валидную платформу под игроком. + /// + private bool TryGetGroundProbeHit(Vector2 probeOffset, out Collider2D hitCollider) { - return body.linearVelocity.y <= 0.05f && Time.time - lastAutoJumpTime >= autoJumpCooldown; - } - - private bool HasGroundProbeHit() - { - var origin = (Vector2)(groundCheck != null ? groundCheck.position : transform.position) + Vector2.up * groundProbeStartOffset; - var contactFilter = new ContactFilter2D - { - useTriggers = false - }; - var hitCount = Physics2D.Raycast(origin, Vector2.down, contactFilter, groundProbeHits, groundProbeStartOffset + groundProbeDistance); + hitCollider = null; + var origin = GroundCheckPosition + probeOffset + Vector2.up * groundProbeStartOffset; + var hitCount = Physics2D.Raycast(origin, Vector2.down, solidProbeFilter, groundProbeHits, groundProbeStartOffset + groundProbeDistance); for (var index = 0; index < hitCount; index++) { - if (IsValidGroundProbeHit(groundProbeHits[index])) + if (IsValidGroundProbeHit(groundProbeHits[index], false)) { + hitCollider = groundProbeHits[index].collider; return true; } } @@ -253,7 +765,42 @@ namespace DanroJump.Gameplay return false; } - private bool IsValidGroundProbeHit(RaycastHit2D hit) + /// + /// Выполняет swept-проверку между предыдущей и текущей позицией GroundCheck, чтобы не пролетать платформы. + /// + private bool TryGetSweptGroundProbeHit(Vector2 probeOffset, out Collider2D hitCollider) + { + hitCollider = null; + if (!TryGetSweptLandingProbe(probeOffset, out var origin, out var direction, out var distance)) + { + return false; + } + + var hitCount = Physics2D.BoxCast( + origin, + sweptLandingProbeHalfExtents * 2f, + 0f, + direction, + solidProbeFilter, + sweptGroundProbeHits, + distance + sweptLandingExtraDistance); + + for (var index = 0; index < hitCount; index++) + { + if (IsValidGroundProbeHit(sweptGroundProbeHits[index], true)) + { + hitCollider = sweptGroundProbeHits[index].collider; + return true; + } + } + + return false; + } + + /// + /// Отбрасывает неподходящие probe-попадания: самого игрока, trigger, верхние препятствия и закрытые one-way платформы. + /// + private bool IsValidGroundProbeHit(RaycastHit2D hit, bool isSweptProbe) { var hitCollider = hit.collider; if (hitCollider == null || hitCollider == playerCollider || hitCollider.isTrigger) @@ -261,7 +808,10 @@ namespace DanroJump.Gameplay return false; } - if (hit.point.y > GroundCheckY + groundContactTolerance) + var allowedTopY = isSweptProbe + ? Mathf.Max(PreviousGroundCheckY, GroundCheckY) + groundContactTolerance + : GroundCheckY + groundContactTolerance; + if (hit.point.y > allowedTopY) { return false; } @@ -271,21 +821,43 @@ namespace DanroJump.Gameplay return oneWayPlatform.ShouldCollideWith(this); } + return IsValidLandingCollider(hitCollider); + } + + /// + /// Разрешает автопрыжок только от платформенных коллайдеров, а не от произвольных solid-объектов. + /// + private static bool IsValidLandingCollider(Collider2D hitCollider) + { + if (hitCollider == null) + { + return false; + } + return hitCollider.TryGetComponent(out _) || hitCollider.TryGetComponent(out _); } - private bool TryActivateJumpPadAtGround() + /// + /// Запоминает состояние GroundCheck и вертикальной скорости для следующего физического шага. + /// + private void CachePhysicsStateForNextStep() { - var origin = (Vector2)(groundCheck != null ? groundCheck.position : transform.position); - var contactFilter = new ContactFilter2D - { - useTriggers = true - }; + PreviousGroundCheckPosition = GroundCheckPosition; + PreviousGroundCheckY = GroundCheckY; + PreviousVerticalVelocity = VerticalVelocity; + } + + /// + /// Ищет landing-оборудование рядом с GroundCheck и активирует его вместо обычного прыжка. + /// + private bool TryActivateLandingEquipmentAtGround(Vector2 probeOffset) + { + var origin = GroundCheckPosition + probeOffset; var hitCount = Physics2D.OverlapBox( origin, equipmentProbeHalfExtents * 2f, 0f, - contactFilter, + triggerProbeFilter, equipmentProbeHits); for (var index = 0; index < hitCount; index++) @@ -296,7 +868,7 @@ namespace DanroJump.Gameplay continue; } - if (hitCollider.TryGetComponent(out JumpPad jumpPad) && jumpPad.TryActivate(this)) + if (TryActivateLandingEquipment(hitCollider)) { return true; } @@ -305,6 +877,77 @@ namespace DanroJump.Gameplay return false; } + /// + /// Ищет landing-оборудование в swept-зоне между предыдущей и текущей позицией GroundCheck. + /// + private bool TryActivateLandingEquipmentFromSweptProbe(Vector2 probeOffset) + { + if (!TryGetSweptLandingProbe(probeOffset, out var origin, out var direction, out var distance)) + { + return false; + } + + var hitCount = Physics2D.BoxCast( + origin, + equipmentProbeHalfExtents * 2f, + 0f, + direction, + triggerProbeFilter, + sweptEquipmentProbeHits, + distance + sweptLandingExtraDistance); + + for (var index = 0; index < hitCount; index++) + { + var hitCollider = sweptEquipmentProbeHits[index].collider; + if (hitCollider == null || hitCollider == playerCollider || !hitCollider.isTrigger) + { + continue; + } + + if (TryActivateLandingEquipment(hitCollider)) + { + return true; + } + } + + return false; + } + + private bool TryActivateLandingEquipment(Collider2D hitCollider) + { + hitCollider.GetComponentsInParent(false, landingEquipmentBuffer); + foreach (var landingEquipment in landingEquipmentBuffer) + { + if (landingEquipment.TryActivateFromLanding(this)) + { + landingEquipmentBuffer.Clear(); + return true; + } + } + + landingEquipmentBuffer.Clear(); + return false; + } + + /// + /// Вычисляет направление и дистанцию swept-пробы посадки между физическими шагами. + /// + private bool TryGetSweptLandingProbe(Vector2 probeOffset, out Vector2 origin, out Vector2 direction, out float distance) + { + origin = PreviousGroundCheckPosition + probeOffset; + var currentPosition = GroundCheckPosition + probeOffset; + var delta = currentPosition - origin; + distance = delta.magnitude; + direction = distance > 0f ? delta / distance : Vector2.down; + + return PreviousVerticalVelocity <= 0.05f && + currentPosition.y < PreviousGroundCheckPosition.y && + distance > 0.001f; + } + + /// + /// Переносит игрока на противоположную сторону камеры, когда весь визуал ушел за край. + /// private void WrapAroundCameraBounds() { if (gameplayCamera == null) @@ -317,20 +960,77 @@ namespace DanroJump.Gameplay var rightEdge = gameplayCamera.transform.position.x + cameraHalfWidth; var screenWidth = rightEdge - leftEdge; var visualBounds = GetPlayerVisualBounds(); - var position = transform.position; + var wrapOffset = Vector2.zero; if (visualBounds.max.x < leftEdge - horizontalWrapPadding) { - position.x += screenWidth; + wrapOffset.x = screenWidth; } else if (visualBounds.min.x > rightEdge + horizontalWrapPadding) { - position.x -= screenWidth; + wrapOffset.x = -screenWidth; } - transform.position = position; + CommitWrapOffset(wrapOffset); } + /// + /// Возвращает временный offset для проверки посадки на противоположной стороне экрана. + /// + private bool TryGetWrapProbeOffset(out Vector2 wrapOffset) + { + wrapOffset = Vector2.zero; + if (gameplayCamera == null) + { + return false; + } + + var cameraHalfWidth = gameplayCamera.orthographicSize * gameplayCamera.aspect; + var leftEdge = gameplayCamera.transform.position.x - cameraHalfWidth; + var rightEdge = gameplayCamera.transform.position.x + cameraHalfWidth; + var screenWidth = rightEdge - leftEdge; + var visualBounds = GetPlayerVisualBounds(); + + if (visualBounds.min.x < leftEdge) + { + wrapOffset.x = screenWidth; + } + else if (visualBounds.max.x > rightEdge) + { + wrapOffset.x = -screenWidth; + } + + return !Mathf.Approximately(wrapOffset.x, 0f); + } + + /// + /// Физически применяет wrap-offset к Rigidbody2D и синхронизирует Transform. + /// + private void CommitWrapOffset(Vector2 wrapOffset) + { + if (wrapOffset.sqrMagnitude <= 0f) + { + return; + } + + var position = transform.position + (Vector3)wrapOffset; + if (body != null) + { + body.position = new Vector2(position.x, position.y); + transform.position = new Vector3(body.position.x, body.position.y, position.z); + } + else + { + transform.position = position; + } + + Physics2D.SyncTransforms(); + PreviousGroundCheckPosition += wrapOffset; + } + + /// + /// Запускает смерть, если игрок упал ниже допустимой линии камеры. + /// private void ResetWhenTooLow() { if (gameplayCamera == null) @@ -339,24 +1039,130 @@ namespace DanroJump.Gameplay } var resetLineY = GetFallResetLineY(); - if (transform.position.y < resetLineY) + if (GroundCheckY < resetLineY) { - ResetRun(); + Die(); } } + /// + /// Переводит игрока в состояние смерти и запускает delayed respawn. + /// + private void Die() + { + if (IsDead) + { + return; + } + + IsDead = true; + respawnBlocked = false; + respawnAtTime = -1f; + body.linearVelocity = Vector2.zero; + EndEnemyPassThrough(); + PlayDeathAnimation(); + Died?.Invoke(this); + } + + /// + /// Запускает режим прохождения сквозь врагов до начала падения. + /// + private void BeginEnemyPassThroughWhileRising() + { + enemyPassThroughWhileRising = true; + } + + /// + /// Выключает режим прохождения сквозь врагов, когда усиленный подъем закончился. + /// + private void UpdateEnemyPassThroughState() + { + if (enemyPassThroughWhileRising && (IsDead || VerticalVelocity <= 0f)) + { + EndEnemyPassThrough(); + } + } + + /// + /// Возвращает физические столкновения игрока с ранее проигнорированными врагами. + /// + private void EndEnemyPassThrough() + { + enemyPassThroughWhileRising = false; + + if (playerCollider != null) + { + for (var index = ignoredEnemyColliders.Count - 1; index >= 0; index--) + { + var enemyCollider = ignoredEnemyColliders[index]; + if (enemyCollider != null) + { + Physics2D.IgnoreCollision(playerCollider, enemyCollider, false); + } + } + } + + ignoredEnemyColliders.Clear(); + } + + /// + /// Завершает задержку смерти и перезапускает забег. + /// + private void TryFinishDeathRespawn() + { + if (!respawnBlocked && respawnAtTime >= 0f && Time.time >= respawnAtTime) + { + Respawn(); + } + } + + /// + /// Устанавливает параметр смерти в Animator, если он есть в текущем controller. + /// + private void PlayDeathAnimation() + { + if (!hasDeathParameter || animator == null) + { + return; + } + + if (deathParameterType == AnimatorControllerParameterType.Trigger) + { + animator.SetTrigger(DeathHash); + } + else if (deathParameterType == AnimatorControllerParameterType.Bool) + { + animator.SetBool(DeathHash, true); + } + } + + /// + /// Сбрасывает bool-параметр смерти после respawn. + /// + private void ClearDeathAnimationFlag() + { + if (hasDeathParameter && animator != null && deathParameterType == AnimatorControllerParameterType.Bool) + { + animator.SetBool(DeathHash, false); + } + } + + /// + /// Возвращает Y-линию, ниже которой игрок считается упавшим. + /// private float GetFallResetLineY() { - var cameraReferenceY = gameplayCamera.transform.position.y; - if (fallResetFromCameraBottom) { - cameraReferenceY -= gameplayCamera.orthographicSize; + return gameplayCamera.ViewportToWorldPoint(new Vector3(0.5f, 0f, 0f)).y - fallResetDistance; } - return cameraReferenceY - fallResetDistance; + return gameplayCamera.transform.position.y - fallResetDistance; } + /// + /// Вычисляет позицию respawn внутри текущего поля зрения камеры. + /// private Vector3 GetRespawnPosition() { if (gameplayCamera == null) @@ -375,6 +1181,9 @@ namespace DanroJump.Gameplay return position; } + /// + /// Поворачивает визуальную часть игрока без изменения физического объекта. + /// private void UpdateFacing() { if (Mathf.Abs(horizontalInput) < 0.01f) @@ -393,6 +1202,9 @@ namespace DanroJump.Gameplay visualRoot.localScale = scale; } + /// + /// Находит Animator, visualRoot и GroundCheck, если они не назначены в инспекторе. + /// private void ResolveVisualReferences() { if (animator == null) @@ -422,6 +1234,9 @@ namespace DanroJump.Gameplay } } + /// + /// Создает набор SpriteRenderer-клонов для визуального предпросмотра wrap-перехода. + /// private void RebuildWrapVisuals() { sourceRenderers.Clear(); @@ -455,6 +1270,9 @@ namespace DanroJump.Gameplay } } + /// + /// Показывает копию визуала игрока на противоположном краю экрана перед фактическим wrap. + /// private void UpdateWrappedVisualPreview() { if (!showWrappedVisualPreview || gameplayCamera == null || sourceRenderers.Count == 0) @@ -489,6 +1307,9 @@ namespace DanroJump.Gameplay CopyWrappedVisuals(new Vector3(wrapOffsetX, 0f, 0f)); } + /// + /// Копирует состояние исходных SpriteRenderer в wrap-клоны. + /// private void CopyWrappedVisuals(Vector3 offset) { for (var index = 0; index < sourceRenderers.Count; index++) @@ -514,6 +1335,9 @@ namespace DanroJump.Gameplay } } + /// + /// Включает или выключает корень визуальных wrap-клонов. + /// private void SetWrappedVisualsActive(bool isActive) { if (wrapVisualCloneRoot != null && wrapVisualCloneRoot.gameObject.activeSelf != isActive) @@ -522,8 +1346,16 @@ namespace DanroJump.Gameplay } } + /// + /// Возвращает суммарные визуальные bounds игрока для wrap-логики. + /// private Bounds GetPlayerVisualBounds() { + if (cachedVisualBoundsFrame == Time.frameCount) + { + return cachedVisualBounds; + } + var hasBounds = false; var bounds = default(Bounds); @@ -547,12 +1379,19 @@ namespace DanroJump.Gameplay if (hasBounds) { - return bounds; + cachedVisualBounds = bounds; + cachedVisualBoundsFrame = Time.frameCount; + return cachedVisualBounds; } - return playerCollider != null ? playerCollider.bounds : new Bounds(transform.position, Vector3.zero); + cachedVisualBounds = playerCollider != null ? playerCollider.bounds : new Bounds(transform.position, Vector3.zero); + cachedVisualBoundsFrame = Time.frameCount; + return cachedVisualBounds; } + /// + /// Проверяет, какие параметры действительно есть в Animator Controller игрока. + /// private void CacheAnimatorParameters() { if (animator == null) @@ -581,10 +1420,14 @@ namespace DanroJump.Gameplay else if (parameter.nameHash == DeathHash) { hasDeathParameter = true; + deathParameterType = parameter.type; } } } + /// + /// Передает скорость и одноразовый импульс прыжка в Animator. + /// private void UpdateAnimatorParameters() { if (animator == null) @@ -592,14 +1435,16 @@ namespace DanroJump.Gameplay return; } + var velocity = body.linearVelocity; + if (hasSpeedParameter) { - animator.SetFloat(SpeedHash, Mathf.Abs(body.linearVelocity.x)); + animator.SetFloat(SpeedHash, Mathf.Abs(velocity.x)); } if (hasVerticalSpeedParameter) { - animator.SetFloat(VerticalSpeedHash, body.linearVelocity.y); + animator.SetFloat(VerticalSpeedHash, velocity.y); } if (hasGroundedParameter) @@ -613,6 +1458,9 @@ namespace DanroJump.Gameplay } } + /// + /// Запускает death-триггер Animator, если он есть в текущем контроллере. + /// private void TriggerDeathAnimation() { if (animator != null && hasDeathParameter) @@ -621,6 +1469,9 @@ namespace DanroJump.Gameplay } } + /// + /// Считывает горизонтальный ввод из Input System или legacy Input Manager. + /// private static float ReadHorizontalInput() { #if ENABLE_INPUT_SYSTEM diff --git a/Assets/Scripts/Runtime/Gameplay/OneWayPlatformCollider.cs b/Assets/Scripts/Runtime/Gameplay/OneWayPlatformCollider.cs index 09922dc..db72659 100644 --- a/Assets/Scripts/Runtime/Gameplay/OneWayPlatformCollider.cs +++ b/Assets/Scripts/Runtime/Gameplay/OneWayPlatformCollider.cs @@ -3,31 +3,44 @@ using Zenject; namespace DanroJump.Gameplay { + /// + /// Кастомная one-way логика платформы. + /// Игнорирует столкновение, если GroundCheck игрока ниже верхней границы платформы. + /// [DisallowMultipleComponent] [RequireComponent(typeof(Collider2D))] public sealed class OneWayPlatformCollider : MonoBehaviour { [SerializeField] private JumpPlayerController player; [SerializeField] private float topBoundaryPadding; + [SerializeField] private float landingVelocityThreshold = 0.05f; + [SerializeField] private float crossingGraceDistance = 0.8f; private Collider2D platformCollider; + private BoxCollider2D boxPlatformCollider; private Collider2D playerCollider; private bool isIgnoringPlayerCollision; + /// + /// Y-координата верхней границы коллайдера платформы. + /// public float TopY { get { - if (platformCollider is BoxCollider2D boxCollider) + if (boxPlatformCollider != null) { - var localTop = boxCollider.offset + Vector2.up * (boxCollider.size.y * 0.5f); - return boxCollider.transform.TransformPoint(localTop).y; + var localTop = boxPlatformCollider.offset + Vector2.up * (boxPlatformCollider.size.y * 0.5f); + return boxPlatformCollider.transform.TransformPoint(localTop).y; } return platformCollider != null ? platformCollider.bounds.max.y : transform.position.y; } } + /// + /// Получает игрока через DI, чтобы не искать его по сцене на каждой платформе. + /// [Inject] public void Construct(JumpPlayerController injectedPlayer) { @@ -35,9 +48,13 @@ namespace DanroJump.Gameplay ResolvePlayerCollider(); } + /// + /// Подготавливает Collider2D и отключает стандартный PlatformEffector2D, если он остался на prefab. + /// private void Awake() { platformCollider = GetComponent(); + boxPlatformCollider = platformCollider as BoxCollider2D; platformCollider.usedByEffector = false; if (TryGetComponent(out PlatformEffector2D platformEffector)) @@ -48,16 +65,33 @@ namespace DanroJump.Gameplay RefreshColliderState(); } + /// + /// После повторного включения сразу синхронизирует IgnoreCollision, не ожидая следующего FixedUpdate. + /// + private void OnEnable() + { + RefreshColliderState(); + } + + /// + /// При выключении платформы обязательно возвращает столкновение с игроком. + /// private void OnDisable() { SetPlayerCollisionIgnored(false); } + /// + /// Синхронизирует состояние IgnoreCollision с текущей позицией GroundCheck игрока. + /// private void FixedUpdate() { RefreshColliderState(); } + /// + /// Обновляет режим столкновения между платформой и игроком. + /// private void RefreshColliderState() { if (player == null && !ResolvePlayer()) @@ -73,16 +107,48 @@ namespace DanroJump.Gameplay SetPlayerCollisionIgnored(!ShouldCollideWith(player)); } + /// + /// Проверяет, должен ли игрок столкнуться с платформой в текущем физическом шаге. + /// + /// Игрок, для которого проверяется столкновение. + /// True, если игрок падает сверху или только что пересек верх платформы сверху вниз. public bool ShouldCollideWith(JumpPlayerController targetPlayer) { - return targetPlayer != null && targetPlayer.GroundCheckY >= TopY + topBoundaryPadding; + if (targetPlayer == null) + { + return false; + } + + var topY = TopY + topBoundaryPadding; + var isFalling = targetPlayer.VerticalVelocity <= landingVelocityThreshold || + targetPlayer.PreviousVerticalVelocity <= landingVelocityThreshold; + if (!isFalling) + { + return false; + } + + if (targetPlayer.GroundCheckY >= topY) + { + return true; + } + + // Grace-зона закрывает быстрые кадры, когда GroundCheck успел проскочить немного ниже верхней границы. + var crossedFromAbove = targetPlayer.PreviousGroundCheckY >= topY && + targetPlayer.GroundCheckY >= topY - crossingGraceDistance; + return crossedFromAbove; } + /// + /// Возвращает true, если ссылка на игрока уже получена. + /// private bool ResolvePlayer() { return player != null; } + /// + /// Кэширует коллайдер игрока для Physics2D.IgnoreCollision. + /// private bool ResolvePlayerCollider() { if (player == null) @@ -93,6 +159,9 @@ namespace DanroJump.Gameplay return player.TryGetComponent(out playerCollider); } + /// + /// Включает или выключает физическое столкновение пары игрок-платформа. + /// private void SetPlayerCollisionIgnored(bool shouldIgnore) { if (platformCollider == null || playerCollider == null || isIgnoringPlayerCollision == shouldIgnore) diff --git a/Assets/Scripts/Runtime/Gameplay/PlatformSpawner.cs b/Assets/Scripts/Runtime/Gameplay/PlatformSpawner.cs index 94e96dd..8aadc82 100644 --- a/Assets/Scripts/Runtime/Gameplay/PlatformSpawner.cs +++ b/Assets/Scripts/Runtime/Gameplay/PlatformSpawner.cs @@ -5,6 +5,9 @@ using Zenject; namespace DanroJump.Gameplay { + /// + /// Генерирует и переиспользует платформы выше камеры с учетом текущего этапа сложности. + /// public sealed class PlatformSpawner : MonoBehaviour { [Min(1)] @@ -18,12 +21,13 @@ namespace DanroJump.Gameplay private float recycleDistanceBelowCameraBottom = 12f; [SerializeField] private bool useHardwareRandomSeed = true; [SerializeField] private int randomSeed = 1207; + [SerializeField] private ProceduralEnemySpawner proceduralEnemySpawner; private readonly List platforms = new(); private readonly Dictionary prefabByPlatform = new(); private DiContainer container; private Camera gameplayCamera; - private GameplayDifficultyProgression difficultyProgression; + private IGameplayDifficultyProgression difficultyProgression; private PlatformContentSpawner contentSpawner; private JumpPlayerController playerController; private System.Random random; @@ -31,22 +35,44 @@ namespace DanroJump.Gameplay private float highestPlatformY; private int nextSpawnIndex; + /// + /// Seed, который реально использует генератор платформ в текущем запуске. + /// public int CurrentRandomSeed => currentRandomSeed; + + /// + /// Самая высокая Y-координата, до которой уже сгенерированы платформы. + /// public float CurrentGenerationY => highestPlatformY; + + /// + /// Имя этапа, по которому сейчас генерируются платформы. + /// public string CurrentGenerationStage => GetStageName(highestPlatformY); + + /// + /// Прогресс генерации внутри текущего этапа. + /// public float CurrentGenerationStageProgress => ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetStageProgress(highestPlatformY) : 0f; + + /// + /// Текущее распределение платформ по ширине уровня. + /// public float CurrentPlatformDistribution => ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetHorizontalDistribution(highestPlatformY) : 0f; - private GameplayDifficultyProgression ProgressionForDiagnostics => - difficultyProgression != null ? difficultyProgression : FindFirstObjectByType(); + private IGameplayDifficultyProgression ProgressionForDiagnostics => + difficultyProgression ?? FindFirstObjectByType(); + /// + /// Получает зависимости через SceneContext: контейнер, игрока, камеру, контент-спавнер и прогрессию. + /// [Inject] public void Construct( DiContainer injectedContainer, JumpPlayerController playerController, Camera injectedGameplayCamera, PlatformContentSpawner injectedContentSpawner, - GameplayDifficultyProgression injectedDifficultyProgression) + IGameplayDifficultyProgression injectedDifficultyProgression) { container = injectedContainer; this.playerController = playerController; @@ -55,6 +81,9 @@ namespace DanroJump.Gameplay difficultyProgression = injectedDifficultyProgression; } + /// + /// Инициализирует генератор случайных чисел и выводит выбранный seed в консоль. + /// private void Awake() { currentRandomSeed = useHardwareRandomSeed ? CreateHardwareRandomSeed() : randomSeed; @@ -62,6 +91,9 @@ namespace DanroJump.Gameplay Debug.Log($"{nameof(PlatformSpawner)} random seed: {currentRandomSeed} ({(useHardwareRandomSeed ? "hardware" : "manual")}).", this); } + /// + /// Проверяет runtime-зависимости и создает стартовый набор платформ. + /// private void Start() { if (!ValidateRuntimeState()) @@ -69,9 +101,14 @@ namespace DanroJump.Gameplay return; } + ResolveProceduralEnemySpawner(); + proceduralEnemySpawner?.Initialize(container, gameplayCamera, difficultyProgression, currentRandomSeed); GenerateInitialPlatforms(); } + /// + /// Каждый кадр переносит платформы, которые ушли ниже камеры, наверх уровня. + /// private void Update() { RecyclePlatformsBelowCamera(); @@ -79,19 +116,22 @@ namespace DanroJump.Gameplay private void OnDestroy() { - if (contentSpawner == null) + if (contentSpawner != null) { - return; - } - - foreach (var platform in platforms) - { - contentSpawner.ClearSpawnedContent(platform); + // Перед уничтожением сцены очищаем дочерний контент платформ, чтобы не оставлять связи на prefab. + foreach (var platform in platforms) + { + contentSpawner.ClearSpawnedContent(platform); + } } prefabByPlatform.Clear(); + proceduralEnemySpawner?.CleanupBelow(float.PositiveInfinity); } + /// + /// Создает стартовую площадку и начальную вертикальную цепочку платформ. + /// private void GenerateInitialPlatforms() { highestPlatformY = difficultyProgression.InitialSpawnY; @@ -100,11 +140,16 @@ namespace DanroJump.Gameplay for (var index = 1; index < initialPlatformCount; index++) { + var previousGenerationY = highestPlatformY; highestPlatformY += RandomVerticalSpacing(); SpawnPlatform(new Vector2(RandomHorizontalPosition(highestPlatformY), highestPlatformY), false); + proceduralEnemySpawner?.SpawnForGeneratedRange(previousGenerationY, highestPlatformY); } } + /// + /// Находит платформы ниже камеры и переносит их выше текущей генерации. + /// private void RecyclePlatformsBelowCamera() { if (gameplayCamera == null || platforms.Count == 0) @@ -113,7 +158,9 @@ namespace DanroJump.Gameplay } var bottomLimit = gameplayCamera.transform.position.y - recycleDistanceBelowCameraBottom; + proceduralEnemySpawner?.CleanupBelow(bottomLimit); + // Вместо постоянного Instantiate/Destroy переиспользуем платформы ниже видимой зоны. for (var index = 0; index < platforms.Count; index++) { var platform = platforms[index]; @@ -122,11 +169,16 @@ namespace DanroJump.Gameplay continue; } + var previousGenerationY = highestPlatformY; highestPlatformY += RandomVerticalSpacing(); platforms[index] = RecyclePlatform(platform, new Vector2(RandomHorizontalPosition(highestPlatformY), highestPlatformY)); + proceduralEnemySpawner?.SpawnForGeneratedRange(previousGenerationY, highestPlatformY); } } + /// + /// Создает новую платформу и инициализирует ее контекстом спавна. + /// private void SpawnPlatform(Vector2 position, bool isStartPlatform) { var selectedPlatformPrefab = GetPlatformPrefab(position.y); @@ -140,11 +192,15 @@ namespace DanroJump.Gameplay InitializePlatform(platform, CreateContext(platform, isStartPlatform)); } + /// + /// Переиспользует платформу или заменяет ее, если активный этап выбрал другой prefab. + /// private PlatformController RecyclePlatform(PlatformController platform, Vector2 position) { var selectedPlatformPrefab = GetPlatformPrefab(position.y); if (ShouldReplacePlatform(platform, selectedPlatformPrefab)) { + // Если новый этап требует другой prefab, безопаснее заменить объект, чем мутировать старый. return ReplacePlatform(platform, selectedPlatformPrefab, position); } @@ -155,6 +211,9 @@ namespace DanroJump.Gameplay return platform; } + /// + /// Создает экземпляр платформы через DI-контейнер, чтобы дочерние компоненты получили injection. + /// private PlatformController CreatePlatform(GameObject prefab, Vector2 position, bool isStartPlatform) { if (prefab == null) @@ -178,6 +237,9 @@ namespace DanroJump.Gameplay return platform; } + /// + /// Удаляет старый экземпляр платформы и создает новый через DI-контейнер. + /// private PlatformController ReplacePlatform(PlatformController platform, GameObject selectedPlatformPrefab, Vector2 position) { contentSpawner.ClearSpawnedContent(platform); @@ -195,6 +257,9 @@ namespace DanroJump.Gameplay return replacement; } + /// + /// Проверяет, нужно ли заменить платформу из-за смены prefab активного этапа. + /// private bool ShouldReplacePlatform(PlatformController platform, GameObject selectedPlatformPrefab) { if (platform == null || selectedPlatformPrefab == null) @@ -205,23 +270,35 @@ namespace DanroJump.Gameplay return prefabByPlatform.TryGetValue(platform, out var currentPrefab) && currentPrefab != selectedPlatformPrefab; } + /// + /// Выполняет первичную инициализацию платформы и спавнит ее содержимое. + /// private void InitializePlatform(PlatformController platform, PlatformSpawnContext context) { platform.Initialize(context); contentSpawner.SpawnContent(platform, GetContentSource(context.WorldY), random); } + /// + /// Передает платформе новый контекст recycle и пересоздает контент. + /// private void RecyclePlatform(PlatformController platform, PlatformSpawnContext context) { platform.Recycle(context); contentSpawner.SpawnContent(platform, GetContentSource(context.WorldY), random); } + /// + /// Создает контекст, который платформа и ее содержимое используют при инициализации. + /// private PlatformSpawnContext CreateContext(PlatformController platform, bool isStartPlatform) { return new PlatformSpawnContext(container, playerController, gameplayCamera, nextSpawnIndex++, isStartPlatform, platform.transform.position.y); } + /// + /// Проверяет, что SceneContext передал все зависимости, необходимые для генерации. + /// private bool ValidateRuntimeState() { var isValid = true; @@ -264,6 +341,20 @@ namespace DanroJump.Gameplay return isValid; } + /// + /// Находит procedural enemy spawner рядом с генератором платформ, если ссылка не задана в инспекторе. + /// + private void ResolveProceduralEnemySpawner() + { + if (proceduralEnemySpawner == null) + { + proceduralEnemySpawner = GetComponentInChildren(true); + } + } + + /// + /// Выбирает X-позицию платформы с учетом ширины камеры и распределения текущего этапа. + /// private float RandomHorizontalPosition(float worldY) { var halfWidth = gameplayCamera != null ? gameplayCamera.orthographicSize * gameplayCamera.aspect : 3.75f; @@ -281,6 +372,7 @@ namespace DanroJump.Gameplay var sign = signed < 0f ? -1f : 1f; var magnitude = Mathf.Abs(signed); + // distribution -1 сжимает позиции к центру, +1 вытягивает их к краям. var normalizedMagnitude = distribution < 0f ? magnitude * (distribution + 1f) : Mathf.Lerp(magnitude, 1f, distribution); @@ -293,6 +385,9 @@ namespace DanroJump.Gameplay return Mathf.Lerp(min, max, (float)random.NextDouble()); } + /// + /// Генерирует seed из времени и характеристик текущего устройства. + /// private static int CreateHardwareRandomSeed() { var unixTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -308,6 +403,9 @@ namespace DanroJump.Gameplay return StableHash(seedSource); } + /// + /// Стабильно превращает строку источников entropy в положительный int seed. + /// private static int StableHash(string value) { unchecked @@ -326,6 +424,9 @@ namespace DanroJump.Gameplay } } + /// + /// Возвращает вертикальный шаг до следующей платформы из текущего этапа. + /// private float RandomVerticalSpacing() { if (difficultyProgression != null) @@ -336,6 +437,9 @@ namespace DanroJump.Gameplay return 0f; } + /// + /// Выбирает prefab платформы для указанной высоты. + /// private GameObject GetPlatformPrefab(float worldY) { return difficultyProgression != null @@ -343,6 +447,9 @@ namespace DanroJump.Gameplay : null; } + /// + /// Создает источник контента для указанной высоты генерации. + /// private IPlatformContentSource GetContentSource(float worldY) { return difficultyProgression != null @@ -350,6 +457,9 @@ namespace DanroJump.Gameplay : null; } + /// + /// Возвращает имя этапа для diagnostics overlay. + /// private string GetStageName(float worldY) { return ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetStageName(worldY) : string.Empty; diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformLandingHandler.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformLandingHandler.cs index 43da21e..5af60e5 100644 --- a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformLandingHandler.cs +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformLandingHandler.cs @@ -2,8 +2,15 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Контракт платформы или дочернего компонента, который может обработать приземление игрока. + /// public interface IPlatformLandingHandler { - bool TryHandleLanding(JumpPlayerController player, Collision2D collision); + /// + /// Пробует обработать посадку игрока. + /// + /// True, если стандартный автопрыжок игрока нужно подавить. + bool TryHandleLanding(JumpPlayerController player, PlatformLandingContext context); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformLifecycle.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformLifecycle.cs index eb1904f..729fa8c 100644 --- a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformLifecycle.cs +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformLifecycle.cs @@ -1,9 +1,23 @@ namespace DanroJump.Gameplay { + /// + /// Контракт компонентов платформы, которым нужно знать о спавне, рецикле и сбросе платформы. + /// public interface IPlatformLifecycle { + /// + /// Вызывается при первом создании платформы. + /// void Initialize(PlatformSpawnContext context); + + /// + /// Вызывается при переиспользовании платформы выше камеры. + /// void Recycle(PlatformSpawnContext context); + + /// + /// Возвращает компонент платформы к начальному состоянию. + /// void ResetPlatform(); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformRuntime.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformRuntime.cs index a61f7bb..877e6e1 100644 --- a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformRuntime.cs +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/IPlatformRuntime.cs @@ -3,11 +3,29 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Runtime-представление платформы для спавна оборудования, врагов и collectibles. + /// public interface IPlatformRuntime { + /// + /// Transform корня платформы. + /// Transform Transform { get; } + + /// + /// Точки спавна подбираемых предметов. + /// IReadOnlyList CollectibleSpawnPoints { get; } + + /// + /// Точки спавна оборудования платформы. + /// IReadOnlyList EquipmentSpawnPoints { get; } + + /// + /// Точки спавна противников. + /// IReadOnlyList EnemySpawnPoints { get; } } } diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformContentKind.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformContentKind.cs index 2eb70c3..520f0c7 100644 --- a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformContentKind.cs +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformContentKind.cs @@ -1,9 +1,23 @@ namespace DanroJump.Gameplay { + /// + /// Категория контента, который может быть установлен на точку спавна платформы. + /// public enum PlatformContentKind { + /// + /// Подбираемый предмет. + /// Collectible, + + /// + /// Оборудование платформы. + /// Equipment, + + /// + /// Противник. + /// Enemy } } diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformLandingContext.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformLandingContext.cs new file mode 100644 index 0000000..9403901 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformLandingContext.cs @@ -0,0 +1,34 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Описывает посадку игрока на платформу независимо от источника события: collision или probe. + /// + public readonly struct PlatformLandingContext + { + public PlatformLandingContext( + Collider2D platformCollider, + Collision2D collision, + Vector2 point, + Vector2 normal, + bool isSweptProbe) + { + PlatformCollider = platformCollider; + Collision = collision; + Point = point; + Normal = normal; + IsSweptProbe = isSweptProbe; + } + + public Collider2D PlatformCollider { get; } + + public Collision2D Collision { get; } + + public Vector2 Point { get; } + + public Vector2 Normal { get; } + + public bool IsSweptProbe { get; } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformLandingContext.cs.meta b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformLandingContext.cs.meta new file mode 100644 index 0000000..2266b2d --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformLandingContext.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8cf028878536c55408ad9ce6bb90e955 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformSpawnContext.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformSpawnContext.cs index 62c8731..ef3d273 100644 --- a/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformSpawnContext.cs +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/Abstractions/PlatformSpawnContext.cs @@ -3,8 +3,15 @@ using Zenject; namespace DanroJump.Gameplay { + /// + /// Контекст создания или рецикла платформы. + /// Передается платформе и дочерним lifecycle-компонентам. + /// public readonly struct PlatformSpawnContext { + /// + /// Создает контекст спавна платформы. + /// public PlatformSpawnContext( DiContainer container, JumpPlayerController player, @@ -21,11 +28,34 @@ namespace DanroJump.Gameplay WorldY = worldY; } + /// + /// DI-контейнер сцены для создания дочернего контента. + /// public DiContainer Container { get; } + + /// + /// Игрок текущей gameplay-сцены. + /// public JumpPlayerController Player { get; } + + /// + /// Камера gameplay-сцены. + /// public Camera GameplayCamera { get; } + + /// + /// Порядковый номер платформы в генерации. + /// public int SpawnIndex { get; } + + /// + /// True для стартовой платформы забега. + /// public bool IsStartPlatform { get; } + + /// + /// Y-координата платформы в мире. + /// public float WorldY { get; } } } diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/FragilePlatformBehaviour.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/FragilePlatformBehaviour.cs new file mode 100644 index 0000000..4609e48 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/FragilePlatformBehaviour.cs @@ -0,0 +1,358 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Поведение ломкой платформы: после приземления выключает коллайдеры и визуально разбивает платформу. + /// + public sealed class FragilePlatformBehaviour : MonoBehaviour, IPlatformLandingHandler, IPlatformLifecycle + { + [SerializeField] [Min(0f)] private float breakDelay = 0.08f; + [SerializeField] [Min(0f)] private float breakAnimationDuration = 0.22f; + [SerializeField] [Min(0f)] private float breakHorizontalOffset = 0.45f; + [SerializeField] [Min(0f)] private float breakFallDistance = 0.45f; + [SerializeField] [Min(0f)] private float breakRotation = 18f; + [SerializeField] private bool suppressAutoJump; + [SerializeField] private Behaviour[] disabledOnBreak; + [SerializeField] private Collider2D[] colliders; + [SerializeField] private Transform[] breakParts; + [SerializeField] private SpriteRenderer[] hiddenOnBreak; + + private Coroutine breakRoutine; + private readonly List rendererBuffer = new(); + private readonly List stalePoseKeys = new(); + private readonly Dictionary initialBreakPoses = new(); + + /// + /// Кэширует цели выключения при загрузке платформы. + /// + private void Awake() + { + CacheTargetsIfNeeded(); + } + + /// + /// Запускает разрушение платформы после посадки игрока. + /// + /// True, если нужно подавить стандартный автопрыжок игрока. + public bool TryHandleLanding(JumpPlayerController player, PlatformLandingContext context) + { + if (breakRoutine == null) + { + breakRoutine = StartCoroutine(BreakAfterDelay()); + } + + return suppressAutoJump; + } + + /// + /// Восстанавливает платформу при первом создании. + /// + public void Initialize(PlatformSpawnContext context) + { + Restore(); + } + + /// + /// Восстанавливает платформу при рецикле. + /// + public void Recycle(PlatformSpawnContext context) + { + Restore(); + } + + /// + /// Восстанавливает платформу при ручном сбросе. + /// + public void ResetPlatform() + { + Restore(); + } + + /// + /// Ждет задержку и выключает выбранные части платформы. + /// + private IEnumerator BreakAfterDelay() + { + if (breakDelay > 0f) + { + yield return new WaitForSeconds(breakDelay); + } + + RefreshBreakVisualTargets(); + SetTargetsEnabled(false); + yield return AnimateBreakVisuals(); + breakRoutine = null; + } + + /// + /// Останавливает разрушение и возвращает цели в активное состояние. + /// + private void Restore() + { + if (breakRoutine != null) + { + StopCoroutine(breakRoutine); + breakRoutine = null; + } + + CacheTargetsIfNeeded(); + RefreshBreakVisualTargets(); + RestoreBreakVisuals(); + SetTargetsEnabled(true); + } + + /// + /// Автоматически собирает коллайдеры и визуальные части, если они не назначены в инспекторе. + /// + private void CacheTargetsIfNeeded() + { + if (colliders == null || colliders.Length == 0) + { + colliders = GetComponentsInChildren(true); + } + + RefreshBreakVisualTargets(); + } + + /// + /// Обновляет список текущих визуальных объектов, включая runtime-контент на платформе. + /// + private void RefreshBreakVisualTargets() + { + rendererBuffer.Clear(); + GetComponentsInChildren(true, rendererBuffer); + hiddenOnBreak = rendererBuffer.ToArray(); + breakParts = BuildBreakPartsFromRenderers(hiddenOnBreak); + RemoveStaleInitialPoses(); + + for (var index = 0; index < breakParts.Length; index++) + { + var part = breakParts[index]; + if (part == null || initialBreakPoses.ContainsKey(part)) + { + continue; + } + + initialBreakPoses.Add(part, LocalPose.From(part)); + } + } + + /// + /// Включает или выключает коллайдеры и дополнительные Behaviour-компоненты платформы. + /// + private void SetTargetsEnabled(bool enabled) + { + if (colliders != null) + { + foreach (var targetCollider in colliders) + { + if (targetCollider != null) + { + targetCollider.enabled = enabled; + } + } + } + + if (disabledOnBreak == null) + { + return; + } + + foreach (var behaviour in disabledOnBreak) + { + if (behaviour != null) + { + behaviour.enabled = enabled; + } + } + } + + /// + /// Разводит части платформы в стороны; если частей нет, просто скрывает renderers. + /// + private IEnumerator AnimateBreakVisuals() + { + if (breakParts == null || breakParts.Length == 0 || breakAnimationDuration <= 0f) + { + SetRenderersEnabled(false); + yield break; + } + + var elapsed = 0f; + while (elapsed < breakAnimationDuration) + { + elapsed += Time.deltaTime; + ApplyBreakVisualProgress(Mathf.Clamp01(elapsed / breakAnimationDuration)); + yield return null; + } + + ApplyBreakVisualProgress(1f); + SetRenderersEnabled(false); + } + + /// + /// Возвращает визуальные части платформы в исходное состояние. + /// + private void RestoreBreakVisuals() + { + if (breakParts != null) + { + for (var index = 0; index < breakParts.Length; index++) + { + var part = breakParts[index]; + if (part == null) + { + continue; + } + + if (!initialBreakPoses.TryGetValue(part, out var initialPose)) + { + initialPose = LocalPose.From(part); + initialBreakPoses.Add(part, initialPose); + } + + initialPose.ApplyTo(part); + } + } + + SetRenderersEnabled(true); + } + + /// + /// Применяет текущий прогресс разрушения к каждой визуальной части. + /// + private void ApplyBreakVisualProgress(float progress) + { + if (breakParts == null) + { + return; + } + + for (var index = 0; index < breakParts.Length; index++) + { + var part = breakParts[index]; + if (part == null) + { + continue; + } + + if (!initialBreakPoses.TryGetValue(part, out var initialPose)) + { + initialPose = LocalPose.From(part); + initialBreakPoses.Add(part, initialPose); + } + + var side = GetBreakSide(index, breakParts.Length); + var offset = new Vector3(side * breakHorizontalOffset, -breakFallDistance, 0f) * progress; + part.localPosition = initialPose.LocalPosition + offset; + part.localRotation = initialPose.LocalRotation * Quaternion.Euler(0f, 0f, -side * breakRotation * progress); + } + } + + /// + /// Убирает из кэша позы уничтоженных runtime-объектов. + /// + private void RemoveStaleInitialPoses() + { + stalePoseKeys.Clear(); + foreach (var pair in initialBreakPoses) + { + if (pair.Key == null) + { + stalePoseKeys.Add(pair.Key); + } + } + + foreach (var staleKey in stalePoseKeys) + { + initialBreakPoses.Remove(staleKey); + } + + stalePoseKeys.Clear(); + } + + /// + /// Включает или выключает визуальные renderers платформы. + /// + private void SetRenderersEnabled(bool enabled) + { + if (hiddenOnBreak == null) + { + return; + } + + foreach (var spriteRenderer in hiddenOnBreak) + { + if (spriteRenderer != null) + { + spriteRenderer.enabled = enabled; + } + } + } + + /// + /// Строит список уникальных Transform по SpriteRenderer дочерних частей платформы. + /// + private static Transform[] BuildBreakPartsFromRenderers(SpriteRenderer[] renderers) + { + if (renderers == null || renderers.Length == 0) + { + return System.Array.Empty(); + } + + var parts = new System.Collections.Generic.List(renderers.Length); + foreach (var spriteRenderer in renderers) + { + if (spriteRenderer == null || spriteRenderer.transform == null || parts.Contains(spriteRenderer.transform)) + { + continue; + } + + parts.Add(spriteRenderer.transform); + } + + parts.Sort((left, right) => left.localPosition.x.CompareTo(right.localPosition.x)); + return parts.ToArray(); + } + + /// + /// Возвращает направление разлета части: левая часть идет влево, правая - вправо. + /// + private static float GetBreakSide(int index, int count) + { + if (count <= 1) + { + return 0f; + } + + return index < count * 0.5f ? -1f : 1f; + } + + private readonly struct LocalPose + { + public LocalPose(Vector3 localPosition, Quaternion localRotation) + { + LocalPosition = localPosition; + LocalRotation = localRotation; + } + + public Vector3 LocalPosition { get; } + + public Quaternion LocalRotation { get; } + + public static LocalPose From(Transform transform) + { + return new LocalPose(transform.localPosition, transform.localRotation); + } + + public void ApplyTo(Transform transform) + { + transform.localPosition = LocalPosition; + transform.localRotation = LocalRotation; + } + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/FragilePlatformBehaviour.cs.meta b/Assets/Scripts/Runtime/Gameplay/Platforms/FragilePlatformBehaviour.cs.meta new file mode 100644 index 0000000..fea3d64 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/FragilePlatformBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: de467f1f456b3c1409c4e51eca4911e2 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/MovingPlatformBehaviour.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/MovingPlatformBehaviour.cs new file mode 100644 index 0000000..090d017 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/MovingPlatformBehaviour.cs @@ -0,0 +1,134 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Двигает платформу по синусоиде вокруг позиции спавна. + /// + public sealed class MovingPlatformBehaviour : MonoBehaviour, IPlatformLifecycle + { + [SerializeField] private Vector2 localMoveAxis = Vector2.right; + [SerializeField] [Min(0f)] private float amplitude = 1.25f; + [SerializeField] [Min(0.01f)] private float cycleDuration = 2.5f; + [SerializeField] private bool useSpawnIndexOffset = true; + + private const float TwoPi = Mathf.PI * 2f; + + private Transform cachedTransform; + private Rigidbody2D body; + private Vector3 basePosition; + private Vector2 normalizedMoveAxis = Vector2.right; + private float angularSpeed; + private float phaseOffset; + + private void Awake() + { + cachedTransform = transform; + body = GetComponent(); + RefreshMoveAxis(); + } + +#if UNITY_EDITOR + private void OnValidate() + { + RefreshMoveAxis(); + } +#endif + + /// + /// Обновляет позицию платформы вдоль локальной оси движения. + /// + private void Update() + { + if (body != null) + { + return; + } + + MoveVisual(Time.time); + } + + private void FixedUpdate() + { + if (body == null) + { + return; + } + + body.MovePosition((Vector2)CalculatePosition(Time.fixedTime)); + } + + /// + /// Двигает visual-only платформу через Transform. + /// + private void MoveVisual(float time) + { + if (amplitude <= 0f || cycleDuration <= 0f) + { + return; + } + + cachedTransform.position = CalculatePosition(time); + } + + /// + /// Рассчитывает позицию платформы на указанном времени симуляции. + /// + private Vector3 CalculatePosition(float time) + { + if (amplitude <= 0f || cycleDuration <= 0f) + { + return basePosition; + } + + var phase = time * angularSpeed + phaseOffset * TwoPi; + return basePosition + (Vector3)(normalizedMoveAxis * (Mathf.Sin(phase) * amplitude)); + } + + /// + /// Запоминает позицию спавна при первом создании. + /// + public void Initialize(PlatformSpawnContext context) + { + ApplyContext(context); + } + + /// + /// Запоминает новую позицию спавна при рецикле платформы. + /// + public void Recycle(PlatformSpawnContext context) + { + ApplyContext(context); + } + + /// + /// Возвращает платформу в базовую позицию. + /// + public void ResetPlatform() + { + if (body != null) + { + body.position = (Vector2)basePosition; + return; + } + + (cachedTransform != null ? cachedTransform : transform).position = basePosition; + } + + /// + /// Применяет контекст спавна и рассчитывает фазу движения. + /// + private void ApplyContext(PlatformSpawnContext context) + { + RefreshMoveAxis(); + basePosition = cachedTransform != null ? cachedTransform.position : transform.position; + phaseOffset = useSpawnIndexOffset ? context.SpawnIndex * 0.173f : 0f; + } + + private void RefreshMoveAxis() + { + normalizedMoveAxis = localMoveAxis.sqrMagnitude > 0f ? localMoveAxis.normalized : Vector2.right; + angularSpeed = cycleDuration > 0f ? TwoPi / cycleDuration : 0f; + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/MovingPlatformBehaviour.cs.meta b/Assets/Scripts/Runtime/Gameplay/Platforms/MovingPlatformBehaviour.cs.meta new file mode 100644 index 0000000..fa5db80 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/MovingPlatformBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2263840105d5eb34fb85ba6f13d89c65 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/PlatformController.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/PlatformController.cs index 9a5f7bc..3713958 100644 --- a/Assets/Scripts/Runtime/Gameplay/Platforms/PlatformController.cs +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/PlatformController.cs @@ -3,47 +3,95 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Runtime-контроллер платформы: хранит точки спавна контента и рассылает lifecycle-события. + /// public sealed class PlatformController : MonoBehaviour, IPlatformRuntime, IPlatformLifecycle { [SerializeField] private List collectibleSpawnPoints = new(); [SerializeField] private List equipmentSpawnPoints = new(); [SerializeField] private List enemySpawnPoints = new(); + /// + /// Transform платформы для систем генерации и контента. + /// public Transform Transform => transform; + + /// + /// Точки, где на платформе можно размещать collectible-предметы. + /// public IReadOnlyList CollectibleSpawnPoints => collectibleSpawnPoints; + + /// + /// Точки, где на платформе можно размещать специальное оборудование. + /// public IReadOnlyList EquipmentSpawnPoints => equipmentSpawnPoints; + + /// + /// Точки, где на платформе можно размещать противников. + /// public IReadOnlyList EnemySpawnPoints => enemySpawnPoints; + + /// + /// Порядковый номер спавна платформы в текущей генерации. + /// public int SpawnIndex { get; private set; } + + /// + /// Показывает, является ли платформа стартовой. + /// public bool IsStartPlatform { get; private set; } + + /// + /// Y-координата платформы в момент последней генерации или recycle. + /// public float WorldY { get; private set; } + /// + /// Собирает точки спавна при загрузке платформы. + /// private void Awake() { CollectSpawnPoints(); } + /// + /// Обновляет списки точек спавна при изменениях prefab в редакторе. + /// private void OnValidate() { CollectSpawnPoints(); } + /// + /// Инициализирует платформу после первого создания. + /// public void Initialize(PlatformSpawnContext context) { ApplyContext(context); NotifyChildrenInitialize(context); } + /// + /// Переиспользует платформу с новым контекстом генерации. + /// public void Recycle(PlatformSpawnContext context) { ApplyContext(context); NotifyChildrenRecycle(context); } + /// + /// Сбрасывает дочерние lifecycle-компоненты платформы. + /// public void ResetPlatform() { NotifyChildrenReset(); } + /// + /// Запоминает данные текущего спавна платформы. + /// private void ApplyContext(PlatformSpawnContext context) { SpawnIndex = context.SpawnIndex; @@ -51,6 +99,9 @@ namespace DanroJump.Gameplay WorldY = context.WorldY; } + /// + /// Собирает дочерние PlatformSpawnPoint по категориям контента. + /// private void CollectSpawnPoints() { collectibleSpawnPoints.Clear(); @@ -72,6 +123,9 @@ namespace DanroJump.Gameplay collectibleBuffer.Clear(); } + /// + /// Возвращает список точек спавна нужной категории. + /// private List GetSpawnPointList(PlatformContentKind kind) { switch (kind) @@ -87,6 +141,9 @@ namespace DanroJump.Gameplay } } + /// + /// Сообщает дочерним компонентам о первом создании платформы. + /// private void NotifyChildrenInitialize(PlatformSpawnContext context) { GetComponentsInChildren(true, lifecycleBuffer); @@ -102,6 +159,9 @@ namespace DanroJump.Gameplay lifecycleBuffer.Clear(); } + /// + /// Сообщает дочерним компонентам о рецикле платформы. + /// private void NotifyChildrenRecycle(PlatformSpawnContext context) { GetComponentsInChildren(true, lifecycleBuffer); @@ -117,6 +177,9 @@ namespace DanroJump.Gameplay lifecycleBuffer.Clear(); } + /// + /// Сообщает дочерним компонентам о сбросе платформы. + /// private void NotifyChildrenReset() { GetComponentsInChildren(true, lifecycleBuffer); diff --git a/Assets/Scripts/Runtime/Gameplay/Platforms/PlatformSpawnPoint.cs b/Assets/Scripts/Runtime/Gameplay/Platforms/PlatformSpawnPoint.cs index 081e688..4227a29 100644 --- a/Assets/Scripts/Runtime/Gameplay/Platforms/PlatformSpawnPoint.cs +++ b/Assets/Scripts/Runtime/Gameplay/Platforms/PlatformSpawnPoint.cs @@ -2,17 +2,38 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Маркер точки спавна контента на платформе. + /// public sealed class PlatformSpawnPoint : MonoBehaviour { [SerializeField] private PlatformContentKind kind; [SerializeField] private bool optional; [SerializeField] [Min(0f)] private float weight = 1f; + /// + /// Категория контента, который может появиться в этой точке. + /// public PlatformContentKind Kind => kind; + + /// + /// Можно ли пропустить эту точку, если контент этапа не выбран. + /// public bool Optional => optional; + + /// + /// Вес точки при выборе между несколькими подходящими позициями. + /// public float Weight => weight; + + /// + /// Transform, в позиции которого создается контент. + /// public Transform Anchor => transform; + /// + /// Рисует цветную gizmo-точку в редакторе для удобной настройки prefab платформ. + /// private void OnDrawGizmos() { Gizmos.color = KindColor(kind); @@ -20,6 +41,9 @@ namespace DanroJump.Gameplay Gizmos.DrawLine(transform.position, transform.position + transform.up * 0.25f); } + /// + /// Возвращает цвет gizmo по типу контента. + /// private static Color KindColor(PlatformContentKind contentKind) { switch (contentKind) diff --git a/Assets/Scripts/Runtime/Gameplay/PlayerContactUtility.cs b/Assets/Scripts/Runtime/Gameplay/PlayerContactUtility.cs new file mode 100644 index 0000000..7219527 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/PlayerContactUtility.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +namespace DanroJump.Gameplay +{ + /// + /// Общие правила поиска игрока и допуска player-contact событий. + /// + public static class PlayerContactUtility + { + public static bool TryGetAlivePlayer(Component source, out JumpPlayerController player) + { + player = source != null ? source.GetComponentInParent() : null; + return player != null && !player.IsDead; + } + } +} diff --git a/Assets/Scripts/Runtime/Gameplay/PlayerContactUtility.cs.meta b/Assets/Scripts/Runtime/Gameplay/PlayerContactUtility.cs.meta new file mode 100644 index 0000000..04b8847 --- /dev/null +++ b/Assets/Scripts/Runtime/Gameplay/PlayerContactUtility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7edc5967d0cc39d48ac301a9a3fa44fd \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Gameplay/Spawning/IPlatformContentSource.cs b/Assets/Scripts/Runtime/Gameplay/Spawning/IPlatformContentSource.cs index 10d3e8e..f375c1b 100644 --- a/Assets/Scripts/Runtime/Gameplay/Spawning/IPlatformContentSource.cs +++ b/Assets/Scripts/Runtime/Gameplay/Spawning/IPlatformContentSource.cs @@ -2,9 +2,19 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Источник правил спавна контента для платформы. + /// public interface IPlatformContentSource { + /// + /// Проверяет, нужно ли спавнить контент указанного типа. + /// bool ShouldSpawn(PlatformContentKind kind, System.Random random); + + /// + /// Выбирает prefab указанной категории по весам. + /// bool TryGetPrefab(PlatformContentKind kind, System.Random random, out GameObject prefab); } } diff --git a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentProfile.cs b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentProfile.cs index 538425f..614ec73 100644 --- a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentProfile.cs +++ b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentProfile.cs @@ -3,6 +3,9 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Immutable-профиль контента платформы, рассчитанный из текущего этапа сложности. + /// public sealed class PlatformContentProfile : IPlatformContentSource { private readonly IReadOnlyList collectiblePrefabs; @@ -12,6 +15,9 @@ namespace DanroJump.Gameplay private readonly float equipmentSpawnChance; private readonly float enemySpawnChance; + /// + /// Создает профиль вероятностей и весов для трех категорий контента. + /// public PlatformContentProfile( IReadOnlyList collectiblePrefabs, float collectibleSpawnChance, @@ -28,6 +34,9 @@ namespace DanroJump.Gameplay this.enemySpawnChance = Mathf.Clamp01(enemySpawnChance); } + /// + /// Делает бросок вероятности для выбранной категории контента. + /// public bool ShouldSpawn(PlatformContentKind kind, System.Random random) { var chance = GetSpawnChance(kind); @@ -44,9 +53,15 @@ namespace DanroJump.Gameplay return (random != null ? random.NextDouble() : 0d) <= chance; } + /// + /// Выбирает prefab категории по весам. + /// public bool TryGetPrefab(PlatformContentKind kind, System.Random random, out GameObject prefab) { - if (WeightedPrefabOption.TrySelect(GetPrefabs(kind), random, out prefab)) + System.Predicate prefabFilter = kind == PlatformContentKind.Enemy + ? EnemyPrefabRules.CanSpawnOnPlatform + : null; + if (WeightedPrefabOption.TrySelect(GetPrefabs(kind), random, prefabFilter, out prefab)) { return true; } @@ -55,6 +70,9 @@ namespace DanroJump.Gameplay return false; } + /// + /// Возвращает шанс спавна для категории контента. + /// private float GetSpawnChance(PlatformContentKind kind) { switch (kind) @@ -70,6 +88,9 @@ namespace DanroJump.Gameplay } } + /// + /// Возвращает список prefab-вариантов для категории контента. + /// private IReadOnlyList GetPrefabs(PlatformContentKind kind) { switch (kind) diff --git a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs index 49e345a..d4ab659 100644 --- a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs +++ b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs @@ -4,16 +4,27 @@ using Zenject; namespace DanroJump.Gameplay { + /// + /// Создает collectibles, equipment и enemies на точках спавна платформы. + /// public sealed class PlatformContentSpawner { private readonly DiContainer container; private readonly Dictionary> spawnedContentByPlatform = new(); + private readonly List orphanMarkerBuffer = new(); + private readonly List spawnPointCandidates = new(); + /// + /// Получает DI-контейнер для создания prefab с injection. + /// public PlatformContentSpawner(DiContainer container) { this.container = container; } + /// + /// Очищает старый контент платформы и спавнит новый по правилам источника. + /// public void SpawnContent( PlatformController platform, IPlatformContentSource catalog, @@ -31,6 +42,9 @@ namespace DanroJump.Gameplay SpawnIntoPoints(platform, catalog, PlatformContentKind.Enemy, platform.EnemySpawnPoints, random); } + /// + /// Удаляет весь контент, созданный для указанной платформы. + /// public void ClearSpawnedContent(PlatformController platform) { if (platform == null) @@ -44,14 +58,22 @@ namespace DanroJump.Gameplay } } + /// + /// Очищает контент всех отслеживаемых платформ. + /// public void ClearAll() { - foreach (var platform in new List(spawnedContentByPlatform.Keys)) + foreach (var pair in spawnedContentByPlatform) { - ClearSpawnedContent(platform); + ClearContentList(pair.Value); } + + spawnedContentByPlatform.Clear(); } + /// + /// Спавнит одну категорию контента в одну выбранную точку платформы. + /// private void SpawnIntoPoints( PlatformController platform, IPlatformContentSource catalog, @@ -64,22 +86,22 @@ namespace DanroJump.Gameplay return; } - foreach (var spawnPoint in spawnPoints) + var selectedSpawnPoint = SelectSpawnPoint(spawnPoints, random); + if (selectedSpawnPoint != null) { - if (spawnPoint == null || ShouldSkipOptionalPoint(spawnPoint, random)) - { - continue; - } - - Spawn(prefab, spawnPoint.Anchor, platform); + Spawn(prefab, selectedSpawnPoint.Anchor, platform); } } + /// + /// Создает конкретный prefab в точке спавна и привязывает оборудование к платформе. + /// private void Spawn(GameObject prefab, Transform parent, PlatformController platform) { var targetParent = parent != null ? parent : platform.transform; var instance = container.InstantiatePrefab(prefab, targetParent.position, targetParent.rotation, targetParent); + // Маркер нужен, чтобы при аварийной очистке найти даже неотслеженный дочерний контент. if (!instance.TryGetComponent(out PlatformSpawnedContent _)) { instance.AddComponent(); @@ -99,6 +121,9 @@ namespace DanroJump.Gameplay spawnedContent.Add(instance); } + /// + /// Удаляет контент, который был записан в словарь отслеживания. + /// private bool ClearTrackedContent(PlatformController platform) { if (!spawnedContentByPlatform.TryGetValue(platform, out var spawnedContent)) @@ -106,6 +131,17 @@ namespace DanroJump.Gameplay return false; } + ClearContentList(spawnedContent); + spawnedContent.Clear(); + spawnedContentByPlatform.Remove(platform); + return true; + } + + /// + /// Уничтожает объекты из уже выделенного списка без освобождения самого списка. + /// + private static void ClearContentList(List spawnedContent) + { for (var index = spawnedContent.Count - 1; index >= 0; index--) { var content = spawnedContent[index]; @@ -115,25 +151,31 @@ namespace DanroJump.Gameplay Object.Destroy(content); } } - - spawnedContent.Clear(); - spawnedContentByPlatform.Remove(platform); - return true; } - private static void ClearOrphanSpawnedContent(PlatformController platform) + /// + /// Fallback-очистка дочерних объектов, если словарь не содержит платформу. + /// + private void ClearOrphanSpawnedContent(PlatformController platform) { - var markers = platform.GetComponentsInChildren(true); - foreach (var marker in markers) + platform.GetComponentsInChildren(true, orphanMarkerBuffer); + + for (var index = orphanMarkerBuffer.Count - 1; index >= 0; index--) { + var marker = orphanMarkerBuffer[index]; if (marker != null) { marker.gameObject.SetActive(false); Object.Destroy(marker.gameObject); } } + + orphanMarkerBuffer.Clear(); } + /// + /// Проверяет, нужно ли пропустить optional-точку спавна по ее весу. + /// private static bool ShouldSkipOptionalPoint(PlatformSpawnPoint spawnPoint, System.Random random) { if (!spawnPoint.Optional) @@ -154,5 +196,48 @@ namespace DanroJump.Gameplay var roll = random != null ? random.NextDouble() : 0d; return roll > spawnPoint.Weight; } + + /// + /// Выбирает одну подходящую точку по весам, чтобы плотность контента не зависела от числа маркеров на prefab. + /// + private PlatformSpawnPoint SelectSpawnPoint( + IReadOnlyList spawnPoints, + System.Random random) + { + spawnPointCandidates.Clear(); + var totalWeight = 0f; + for (var index = 0; index < spawnPoints.Count; index++) + { + var spawnPoint = spawnPoints[index]; + if (spawnPoint == null || ShouldSkipOptionalPoint(spawnPoint, random)) + { + continue; + } + + spawnPointCandidates.Add(spawnPoint); + totalWeight += Mathf.Max(0f, spawnPoint.Weight); + } + + if (totalWeight <= 0f) + { + spawnPointCandidates.Clear(); + return null; + } + + var roll = (float)((random != null ? random.NextDouble() : 0d) * totalWeight); + for (var index = 0; index < spawnPointCandidates.Count; index++) + { + var spawnPoint = spawnPointCandidates[index]; + roll -= Mathf.Max(0f, spawnPoint.Weight); + if (roll <= 0f) + { + spawnPointCandidates.Clear(); + return spawnPoint; + } + } + + spawnPointCandidates.Clear(); + return null; + } } } diff --git a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformSpawnedContent.cs b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformSpawnedContent.cs index e16845a..2fb0edc 100644 --- a/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformSpawnedContent.cs +++ b/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformSpawnedContent.cs @@ -2,6 +2,9 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Маркер объекта, который был создан спавнером контента платформы. + /// public sealed class PlatformSpawnedContent : MonoBehaviour { } diff --git a/Assets/Scripts/Runtime/Gameplay/Spawning/WeightedPrefabOption.cs b/Assets/Scripts/Runtime/Gameplay/Spawning/WeightedPrefabOption.cs index 94b4e9d..25dc7c0 100644 --- a/Assets/Scripts/Runtime/Gameplay/Spawning/WeightedPrefabOption.cs +++ b/Assets/Scripts/Runtime/Gameplay/Spawning/WeightedPrefabOption.cs @@ -3,16 +3,42 @@ using UnityEngine; namespace DanroJump.Gameplay { + /// + /// Один prefab-вариант с весом для случайного выбора. + /// [System.Serializable] public sealed class WeightedPrefabOption { [SerializeField] private GameObject prefab; [SerializeField] [Min(0f)] private float weight = 1f; + /// + /// Prefab, который может быть выбран генератором. + /// public GameObject Prefab => prefab; + + /// + /// Относительный вес prefab в случайном выборе. + /// public float Weight => weight; + /// + /// Выбирает prefab из списка пропорционально весам. + /// + /// True, если найден валидный prefab с положительным весом. public static bool TrySelect(IReadOnlyList options, System.Random random, out GameObject prefab) + { + return TrySelect(options, random, null, out prefab); + } + + /// + /// Выбирает prefab среди вариантов, которые проходят дополнительный фильтр. + /// + public static bool TrySelect( + IReadOnlyList options, + System.Random random, + System.Predicate prefabFilter, + out GameObject prefab) { prefab = null; @@ -24,7 +50,7 @@ namespace DanroJump.Gameplay foreach (var option in options) { - if (option?.Prefab != null && option.Weight > 0f) + if (IsSelectable(option, prefabFilter)) { totalWeight += option.Weight; } @@ -38,7 +64,7 @@ namespace DanroJump.Gameplay var roll = (float)((random != null ? random.NextDouble() : 0d) * totalWeight); foreach (var option in options) { - if (option?.Prefab == null || option.Weight <= 0f) + if (!IsSelectable(option, prefabFilter)) { continue; } @@ -53,5 +79,12 @@ namespace DanroJump.Gameplay return false; } + + private static bool IsSelectable(WeightedPrefabOption option, System.Predicate prefabFilter) + { + return option?.Prefab != null && + option.Weight > 0f && + (prefabFilter == null || prefabFilter(option.Prefab)); + } } } diff --git a/Assets/Scripts/Runtime/Gameplay/VerticalCameraFollow.cs b/Assets/Scripts/Runtime/Gameplay/VerticalCameraFollow.cs index 994071c..fa2e995 100644 --- a/Assets/Scripts/Runtime/Gameplay/VerticalCameraFollow.cs +++ b/Assets/Scripts/Runtime/Gameplay/VerticalCameraFollow.cs @@ -3,6 +3,9 @@ using Zenject; namespace DanroJump.Gameplay { + /// + /// Камера, которая следует за игроком только вверх и не опускается при падении. + /// public sealed class VerticalCameraFollow : MonoBehaviour { [SerializeField] private Transform target; @@ -11,17 +14,26 @@ namespace DanroJump.Gameplay private float highestCameraY; + /// + /// Получает игрока через SceneContext. + /// [Inject] public void Construct(JumpPlayerController playerController) { target = playerController.transform; } + /// + /// Запоминает стартовую высоту камеры. + /// private void Awake() { highestCameraY = transform.position.y; } + /// + /// Плавно поднимает камеру за игроком, не позволяя ей двигаться вниз. + /// private void LateUpdate() { if (target == null) diff --git a/Assets/Scripts/Runtime/Hardware/Com/Abstractions/ComAbstractions.cs b/Assets/Scripts/Runtime/Hardware/Com/Abstractions/ComAbstractions.cs index 745c248..b723b05 100644 --- a/Assets/Scripts/Runtime/Hardware/Com/Abstractions/ComAbstractions.cs +++ b/Assets/Scripts/Runtime/Hardware/Com/Abstractions/ComAbstractions.cs @@ -6,6 +6,9 @@ using UnityEngine.Events; namespace DanroJump.Hardware.Com { + /// + /// Единая точка доступа к COM-подсистеме автомата: кредиты, ввод, витрина призов и подсветка. + /// public interface IComSystem : IComCreditWallet, IComInputSource, @@ -20,8 +23,16 @@ namespace DanroJump.Hardware.Com UnityEvent DeviceAuthorized { get; } UnityEvent DeviceDisconnected { get; } + /// + /// Запускает поиск, авторизацию и фоновые циклы обмена данными с COM-устройствами. + /// void StartSystem(); + + /// + /// Останавливает COM-подсистему и при необходимости отправляет устройствам команды выключения. + /// void StopSystem(bool sendShutdownCommands); + bool SendToMain(string command); bool SendToDevice(string deviceId, string command); void SendToAll(string command); @@ -29,6 +40,9 @@ namespace DanroJump.Hardware.Com string GetDeviceMac(string deviceId); } + /// + /// Хранилище кредитов, полученных от монетоприемника или другой платежной периферии. + /// public interface IComCreditWallet { int Credits { get; } @@ -36,24 +50,47 @@ namespace DanroJump.Hardware.Com UnityEvent CreditRemoved { get; } UnityEvent CreditsChanged { get; } + /// + /// Добавляет кредиты и уведомляет подписчиков об изменении баланса. + /// void AddCredit(int amount = 1); + + /// + /// Пытается списать кредиты без ухода в отрицательный баланс. + /// bool TrySpendCredits(int amount = 1); + + /// + /// Принудительно выставляет текущий баланс кредитов. + /// void SetCredits(int value); } + /// + /// Источник аппаратного ввода с основной платы: кнопки, сенсоры и последние биты состояния. + /// public interface IComInputSource { string LastMainInputBits { get; } UnityEvent MainInputReceived { get; } UnityEvent HopperSensorTriggered { get; } + /// + /// Возвращает последнее известное состояние входов конкретного устройства. + /// string GetLastInputBits(string deviceId); } + /// + /// Шлюз для работы с призовой витриной и выдачей призов через аппаратные ячейки. + /// public interface IPrizeShowcaseGateway { UnityEvent PrizeBoxOpened { get; } + /// + /// Запрашивает текущие статусы всех ячеек призовой витрины. + /// Task> GetPrizeBoxStatusesAsync( CancellationToken cancellationToken = default); @@ -64,6 +101,9 @@ namespace DanroJump.Hardware.Com bool HighlightPrizeBox(int boxNumber); } + /// + /// Контроллер аппаратной подсветки платы, шляпы и связанных световых сценариев. + /// public interface IComLightingController { void PlayBoardLight(LightMode mode, float cycles = -1f); @@ -78,16 +118,25 @@ namespace DanroJump.Hardware.Com void ResumeLighting(); } + /// + /// Поставщик имен доступных COM-портов, вынесенный для тестов и подмены транспорта. + /// public interface IComPortProvider { string[] GetPortNames(); } + /// + /// Фабрика транспорта поверх конкретных serial-настроек. + /// public interface IComTransportFactory { IComTransport Create(SerialSettings settings); } + /// + /// Минимальный serial-транспорт, который скрывает реализацию System.IO.Ports от остального кода. + /// public interface IComTransport : IDisposable { bool IsOpen { get; } diff --git a/Assets/Scripts/Runtime/Hardware/Com/ComSystem.cs b/Assets/Scripts/Runtime/Hardware/Com/ComSystem.cs index 5878211..010e380 100644 --- a/Assets/Scripts/Runtime/Hardware/Com/ComSystem.cs +++ b/Assets/Scripts/Runtime/Hardware/Com/ComSystem.cs @@ -1,20 +1,22 @@ using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; +using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.Events; using Random = UnityEngine.Random; namespace DanroJump.Hardware.Com { + /// + /// Управляет COM-устройствами автомата: авторизацией плат, кредитами, вводом, подсветкой и призовой витриной. + /// [DefaultExecutionOrder(-9000)] [DisallowMultipleComponent] [AddComponentMenu("Hardware/COM System")] @@ -108,8 +110,8 @@ namespace DanroJump.Hardware.Com private IComTransportFactory transportFactory; private CancellationTokenSource systemCts; - private Coroutine hatBlinkCoroutine; - private Coroutine lightSequenceCoroutine; + private CancellationTokenSource hatBlinkCancellation; + private CancellationTokenSource lightSequenceCancellation; private LightSequenceState pausedLightSequence; private bool hatIsOn; private bool hatBlinkWasPaused; @@ -117,38 +119,96 @@ namespace DanroJump.Hardware.Com private string prizeStandDeviceId = ""; private int credits; + /// + /// Последний активный экземпляр COM-системы для сцен, где DI еще не подключен. + /// public static ComSystem Instance { get; private set; } + /// + /// Событие начисления кредитов. + /// public UnityEvent CreditAdded => creditAdded; + + /// + /// Событие списания кредитов. + /// public UnityEvent CreditRemoved => creditRemoved; + + /// + /// Событие любого изменения баланса кредитов. + /// public UnityEvent CreditsChanged => creditsChanged; + + /// + /// Событие получения входных битов от основной платы. + /// public UnityEvent MainInputReceived => mainInputReceived; + + /// + /// Событие успешной авторизации COM-устройства. + /// public UnityEvent DeviceAuthorized => deviceAuthorized; + + /// + /// Событие отключения COM-устройства. + /// public UnityEvent DeviceDisconnected => deviceDisconnected; + + /// + /// Событие подтвержденного открытия призовой ячейки. + /// public UnityEvent PrizeBoxOpened => prizeBoxOpened; + + /// + /// Событие срабатывания датчика хоппера на основной плате. + /// public UnityEvent HopperSensorTriggered => hopperSensorTriggered; + /// + /// Показывает, запущена ли COM-подсистема. + /// public bool IsRunning => systemCts != null && !systemCts.IsCancellationRequested; + + /// + /// Текущий баланс аппаратных кредитов. + /// public int Credits => credits; - public string MainDeviceId => mainDeviceId; - public string PrizeStandDeviceId => prizeStandDeviceId; - public string LastMainInputBits => GetLastInputBits(mainDeviceId); + + /// + /// Идентификатор устройства, выбранного как основная плата. + /// + public string MainDeviceId => CurrentMainDeviceId; + + /// + /// Идентификатор устройства, выбранного как контроллер призовой витрины. + /// + public string PrizeStandDeviceId => CurrentPrizeStandDeviceId; + + /// + /// Последние входные биты от основной платы. + /// + public string LastMainInputBits => GetLastInputBits(CurrentMainDeviceId); public IReadOnlyCollection ConnectedDeviceIds { get { lock (devicesLock) { - return devices.Keys.ToArray(); + return new List(devices.Keys); } } } + /// + /// Инициализирует singleton-ссылку, инфраструктуру транспорта и сохраненный баланс кредитов. + /// private void Awake() { if (Instance != null && Instance != this) { - UnityEngine.Debug.LogWarning("[ComSystem] Another instance already exists. Keeping the newest scene instance active.", this); + UnityEngine.Debug.LogWarning("[ComSystem] Another instance already exists. Destroying duplicate scene instance.", this); + Destroy(gameObject); + return; } Instance = this; @@ -168,6 +228,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Подменяет поставщика портов и фабрику транспорта до запуска системы. + /// public void ConfigureInfrastructure(IComPortProvider customPortProvider, IComTransportFactory customTransportFactory) { if (IsRunning) @@ -180,6 +243,9 @@ namespace DanroJump.Hardware.Com transportFactory = customTransportFactory ?? new ReflectionSerialComTransportFactory(); } + /// + /// Автоматически запускает COM-подсистему при включении объекта, если это разрешено в инспекторе. + /// private void OnEnable() { if (startOnEnable) @@ -188,6 +254,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Переносит события из фоновых потоков COM-подключений в главный поток Unity. + /// private void Update() { while (mainThreadQueue.TryDequeue(out Action action)) @@ -202,13 +271,16 @@ namespace DanroJump.Hardware.Com while (inputQueue.TryDequeue(out ComInputMessage message)) { - if (message.DeviceId == mainDeviceId) + if (message.DeviceId == CurrentMainDeviceId) { mainInputReceived.Invoke(message.InputBits); } } } + /// + /// Останавливает порты при выключении компонента, если включен соответствующий режим. + /// private void OnDisable() { if (stopPortsOnDisable) @@ -217,11 +289,17 @@ namespace DanroJump.Hardware.Com } } + /// + /// Закрывает устройства при выходе из приложения и отправляет shutdown-команды. + /// private void OnApplicationQuit() { StopSystem(sendShutdownCommandsOnQuit); } + /// + /// Освобождает singleton-ссылку, фоновые подключения и semaphore призовых операций. + /// private void OnDestroy() { if (Instance == this) @@ -233,6 +311,9 @@ namespace DanroJump.Hardware.Com prizeOperationLock.Dispose(); } + /// + /// Запускает сканирование портов и авторизацию найденных устройств. + /// public void StartSystem() { if (IsRunning) @@ -241,23 +322,36 @@ namespace DanroJump.Hardware.Com } systemCts = new CancellationTokenSource(); - mainDeviceId = ""; - prizeStandDeviceId = ""; + ResetDeviceSelections(); ClearQueues(); string[] ports = SelectPorts(portProvider.GetPortNames()); if (ports.Length == 0) { UnityEngine.Debug.LogWarning("[ComSystem] COM ports were not found."); + ResetSystemToken(); return; } + int startedDevices = 0; foreach (string portName in ports) { - TryStartDevice(portName); + if (TryStartDevice(portName)) + { + startedDevices++; + } + } + + if (startedDevices == 0) + { + UnityEngine.Debug.LogWarning("[ComSystem] COM system did not start because all selected ports failed to open."); + ResetSystemToken(); } } + /// + /// Останавливает все фоновые COM-операции и закрывает открытые порты. + /// public void StopSystem(bool sendShutdownCommands) { if (systemCts == null) @@ -277,7 +371,7 @@ namespace DanroJump.Hardware.Com ComDeviceConnection[] deviceSnapshot; lock (devicesLock) { - deviceSnapshot = devices.Values.ToArray(); + deviceSnapshot = CopyDeviceValues(); devices.Clear(); } @@ -289,23 +383,31 @@ namespace DanroJump.Hardware.Com systemCts.Dispose(); systemCts = null; - mainDeviceId = ""; - prizeStandDeviceId = ""; + ResetDeviceSelections(); StopBoardLightSequence(); StopHatBlink(false); ClearQueues(); } + /// + /// Отправляет команду на основную плату управления. + /// public bool SendToMain(string command) { - return SendToDevice(mainDeviceId, command); + return SendToDevice(CurrentMainDeviceId, command); } + /// + /// Отправляет команду на контроллер призовой витрины. + /// public bool SendToPrizeStand(string command) { - return SendToDevice(prizeStandDeviceId, command); + return SendToDevice(CurrentPrizeStandDeviceId, command); } + /// + /// Отправляет команду конкретному авторизованному устройству. + /// public bool SendToDevice(string deviceId, string command) { if (string.IsNullOrWhiteSpace(deviceId) || string.IsNullOrWhiteSpace(command)) @@ -319,6 +421,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Рассылает команду всем подключенным устройствам. + /// public void SendToAll(string command) { if (string.IsNullOrWhiteSpace(command)) @@ -329,7 +434,7 @@ namespace DanroJump.Hardware.Com ComDeviceConnection[] deviceSnapshot; lock (devicesLock) { - deviceSnapshot = devices.Values.ToArray(); + deviceSnapshot = CopyDeviceValues(); } foreach (ComDeviceConnection device in deviceSnapshot) @@ -338,6 +443,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Возвращает последние входные биты устройства или безопасное значение по умолчанию. + /// public string GetLastInputBits(string deviceId) { lock (devicesLock) @@ -348,6 +456,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Возвращает роль авторизованного устройства. + /// public string GetDeviceRole(string deviceId) { lock (devicesLock) @@ -358,6 +469,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Возвращает MAC-адрес авторизованного устройства. + /// public string GetDeviceMac(string deviceId) { lock (devicesLock) @@ -368,6 +482,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Увеличивает баланс кредитов, пришедших от аппаратной периферии. + /// public void AddCredit(int amount = 1) { if (amount <= 0) @@ -381,6 +498,9 @@ namespace DanroJump.Hardware.Com creditsChanged.Invoke(credits); } + /// + /// Пытается списать кредиты, не позволяя балансу уйти ниже нуля. + /// public bool TrySpendCredits(int amount = 1) { if (amount <= 0) @@ -400,6 +520,9 @@ namespace DanroJump.Hardware.Com return true; } + /// + /// Принудительно задает баланс кредитов и рассылает события изменения. + /// public void SetCredits(int value) { int oldCredits = credits; @@ -418,9 +541,12 @@ namespace DanroJump.Hardware.Com creditsChanged.Invoke(credits); } + /// + /// Асинхронно запрашивает статусы ячеек призовой витрины. + /// public async Task> GetPrizeBoxStatusesAsync(CancellationToken cancellationToken = default) { - if (prizeStandBoxesCount <= 0 || string.IsNullOrWhiteSpace(prizeStandDeviceId)) + if (prizeStandBoxesCount <= 0 || string.IsNullOrWhiteSpace(CurrentPrizeStandDeviceId)) { return new Dictionary(); } @@ -454,43 +580,70 @@ namespace DanroJump.Hardware.Com return statuses; } + /// + /// Возвращает номера ячеек, из которых можно безопасно выдать приз. + /// public async Task GetReadyPrizeBoxesAsync(CancellationToken cancellationToken = default) { IReadOnlyDictionary statuses = await GetPrizeBoxStatusesAsync(cancellationToken); - return statuses - .Where(pair => IsReadyToOpen(pair.Value)) - .Select(pair => pair.Key) - .OrderBy(boxNumber => boxNumber) - .ToArray(); + List readyBoxes = new(); + foreach (var pair in statuses) + { + if (IsReadyToOpen(pair.Value)) + { + readyBoxes.Add(pair.Key); + } + } + + readyBoxes.Sort(); + return readyBoxes.ToArray(); } + /// + /// Открывает призовую ячейку только если контроллер сообщает, что она готова. + /// public async Task OpenPrizeBoxAsync(int boxNumber, CancellationToken cancellationToken = default) { return await OpenPrizeBoxInternalAsync(boxNumber, false, cancellationToken); } + /// + /// Открывает призовую ячейку без предварительной проверки статуса. + /// public async Task ForceOpenPrizeBoxAsync(int boxNumber, CancellationToken cancellationToken = default) { return await OpenPrizeBoxInternalAsync(boxNumber, true, cancellationToken); } + /// + /// Переводит подсветку призовой витрины в режим ожидания. + /// public bool SetPrizeStandIdleLighting() { return SendToPrizeStand(PrizeStandIdleLightingCommand); } + /// + /// Подсвечивает конкретную призовую ячейку на витрине. + /// public bool HighlightPrizeBox(int boxNumber) { return boxNumber > 0 && SendToPrizeStand($"{PrizeStandHighlightBoxCommandPrefix}{boxNumber:D2}"); } + /// + /// Запускает один световой эффект на основной плате. + /// public void PlayBoardLight(LightMode mode, float cycles = -1f) { StopBoardLightSequence(); float resolvedCycles = cycles > 0f ? cycles : defaultLightCycles; - lightSequenceCoroutine = StartCoroutine(BoardLightSequenceCoroutine(new[] { new LightStep(mode, resolvedCycles) }, false)); + StartBoardLightSequenceTask(new[] { new LightStep(mode, resolvedCycles) }, false); } + /// + /// Запускает последовательность световых эффектов на основной плате. + /// public void PlayBoardLightSequence(IReadOnlyList steps, bool loop) { if (steps == null || steps.Count == 0) @@ -499,76 +652,89 @@ namespace DanroJump.Hardware.Com } StopBoardLightSequence(); - lightSequenceCoroutine = StartCoroutine(BoardLightSequenceCoroutine(steps, loop)); + StartBoardLightSequenceTask(steps, loop); } + /// + /// Запускает световой эффект платы, выбранный как эффект по умолчанию. + /// public void PlayDefaultBoardLight() { PlayBoardLight(defaultLightMode, defaultLightCycles); } + /// + /// Останавливает текущую последовательность световых эффектов платы. + /// public void StopBoardLightSequence() { - if (lightSequenceCoroutine != null) - { - StopCoroutine(lightSequenceCoroutine); - lightSequenceCoroutine = null; - } - + CancelAndDispose(ref lightSequenceCancellation); pausedLightSequence = default; } + /// + /// Останавливает все эффекты платы и отправляет аппаратную команду выключения. + /// public void StopAllBoardEffects() { StopBoardLightSequence(); SendToMain(BoardEffectsOffCommand); } + /// + /// Запускает мигание шляпы с интервалом из инспектора. + /// public void BlinkHat() { BlinkHat(hatBlinkIntervalSeconds); } + /// + /// Запускает мигание шляпы с указанным интервалом. + /// public void BlinkHat(float intervalSeconds) { StopHatBlink(false); hatIsOn = true; - hatBlinkCoroutine = StartCoroutine(HatBlinkCoroutine(Mathf.Max(0.05f, intervalSeconds))); + hatBlinkCancellation = new CancellationTokenSource(); + HatBlinkAsync(Mathf.Max(0.05f, intervalSeconds), hatBlinkCancellation).Forget(); } + /// + /// Останавливает мигание шляпы и оставляет ее включенной или выключенной. + /// public void StopHatBlink(bool leaveHatEnabled = true) { - if (hatBlinkCoroutine != null) - { - StopCoroutine(hatBlinkCoroutine); - hatBlinkCoroutine = null; - } - + CancelAndDispose(ref hatBlinkCancellation); hatIsOn = leaveHatEnabled; SendToMain(leaveHatEnabled ? hatOnCommand : hatOffCommand); } + /// + /// Приостанавливает активные световые сценарии без сброса игрового состояния. + /// public void PauseLighting() { - if (hatBlinkCoroutine != null) + if (hatBlinkCancellation != null) { hatBlinkWasPaused = true; - StopCoroutine(hatBlinkCoroutine); - hatBlinkCoroutine = null; + CancelAndDispose(ref hatBlinkCancellation); } else { hatBlinkWasPaused = false; } - if (lightSequenceCoroutine != null) + if (lightSequenceCancellation != null) { - StopCoroutine(lightSequenceCoroutine); - lightSequenceCoroutine = null; + CancelAndDispose(ref lightSequenceCancellation); pausedLightSequence = new LightSequenceState(true); } } + /// + /// Возобновляет световые сценарии после паузы. + /// public void ResumeLighting() { if (hatBlinkWasPaused) @@ -587,6 +753,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Общий сценарий выдачи приза с сериализацией операций через semaphore. + /// private async Task OpenPrizeBoxInternalAsync( int boxNumber, bool force, @@ -609,7 +778,7 @@ namespace DanroJump.Hardware.Com return PrizeOpenResult.Fail(boxNumber, $"Номер ячейки должен быть РѕС‚ 1 РґРѕ {prizeStandBoxesCount}."); } - if (string.IsNullOrWhiteSpace(prizeStandDeviceId)) + if (string.IsNullOrWhiteSpace(CurrentPrizeStandDeviceId)) { return PrizeOpenResult.Fail(boxNumber, "Призовая витрина РЅРµ подключена."); } @@ -650,6 +819,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Запрашивает статусы витрины и возвращает статус одной ячейки. + /// private async Task ReadPrizeBoxStatusAsync( int boxNumber, float timeoutSeconds, @@ -664,6 +836,9 @@ namespace DanroJump.Hardware.Com return await WaitPrizeBoxStatusAsync(boxNumber, null, timeoutSeconds, cancellationToken); } + /// + /// Ожидает статус нужной ячейки до таймаута, опционально до конкретного ожидаемого состояния. + /// private async Task WaitPrizeBoxStatusAsync( int boxNumber, PrizeBoxStatus? expectedStatus, @@ -697,7 +872,10 @@ namespace DanroJump.Hardware.Com return lastStatus; } - private void TryStartDevice(string portName) + /// + /// Создает подключение к одному порту и добавляет устройство после успешного старта. + /// + private bool TryStartDevice(string portName) { var device = new ComDeviceConnection( transportFactory, @@ -723,15 +901,20 @@ namespace DanroJump.Hardware.Com { Unsubscribe(device); device.Dispose(false); - return; + return false; } lock (devicesLock) { devices[device.DeviceId] = device; } + + return true; } + /// + /// Подписывает COM-систему на события одного подключения. + /// private void Subscribe(ComDeviceConnection device) { device.Authorized += OnDeviceAuthorized; @@ -742,6 +925,9 @@ namespace DanroJump.Hardware.Com device.HopperSensorTriggered += OnHopperSensorTriggered; } + /// + /// Снимает подписки с одного подключения. + /// private void Unsubscribe(ComDeviceConnection device) { device.Authorized -= OnDeviceAuthorized; @@ -752,23 +938,32 @@ namespace DanroJump.Hardware.Com device.HopperSensorTriggered -= OnHopperSensorTriggered; } + /// + /// Назначает роли авторизованному устройству и отправляет событие в главный поток. + /// private void OnDeviceAuthorized(ComDeviceConnection device) { - if (string.IsNullOrWhiteSpace(mainDeviceId) || - device.PortName == preferredMainPort || - IsRole(device.Role, mainControllerRole)) + lock (devicesLock) { - mainDeviceId = device.DeviceId; - } + if (string.IsNullOrWhiteSpace(mainDeviceId) || + device.PortName == preferredMainPort || + IsRole(device.Role, mainControllerRole)) + { + mainDeviceId = device.DeviceId; + } - if (IsPrizeStandRole(device.Role)) - { - prizeStandDeviceId = device.DeviceId; + if (IsPrizeStandRole(device.Role)) + { + prizeStandDeviceId = device.DeviceId; + } } mainThreadQueue.Enqueue(() => deviceAuthorized.Invoke(device.DeviceId)); } + /// + /// Удаляет отключенное устройство из реестра и освобождает его ресурсы. + /// private void OnDeviceDisconnected(ComDeviceConnection device) { Unsubscribe(device); @@ -779,13 +974,13 @@ namespace DanroJump.Hardware.Com if (device.DeviceId == mainDeviceId) { - mainDeviceId = devices.Keys.FirstOrDefault() ?? ""; + mainDeviceId = GetFirstDeviceId(); } - } - if (device.DeviceId == prizeStandDeviceId) - { - prizeStandDeviceId = ""; + if (device.DeviceId == prizeStandDeviceId) + { + prizeStandDeviceId = ""; + } } device.Dispose(false); @@ -793,9 +988,12 @@ namespace DanroJump.Hardware.Com mainThreadQueue.Enqueue(() => deviceDisconnected.Invoke(device.DeviceId)); } + /// + /// Переносит событие принятой монеты в очередь главного потока. + /// private void OnCoinAccepted(string deviceId) { - if (deviceId == prizeStandDeviceId) + if (deviceId == CurrentPrizeStandDeviceId) { return; } @@ -803,9 +1001,12 @@ namespace DanroJump.Hardware.Com coinQueue.Enqueue(deviceId); } + /// + /// Переносит входные биты основной платы в очередь главного потока. + /// private void OnInputReceived(ComInputMessage message) { - if (message.DeviceId == prizeStandDeviceId) + if (message.DeviceId == CurrentPrizeStandDeviceId) { return; } @@ -813,22 +1014,31 @@ namespace DanroJump.Hardware.Com inputQueue.Enqueue(message); } + /// + /// Разбирает сырые строки призовой витрины в статусы ячеек. + /// private void OnRawLineReceived(string deviceId, string line) { - if (deviceId == prizeStandDeviceId && TryParsePrizeBoxStatus(line, out PrizeBoxStatusEntry status)) + if (deviceId == CurrentPrizeStandDeviceId && TryParsePrizeBoxStatus(line, out PrizeBoxStatusEntry status)) { prizeStatusQueue.Enqueue(status); } } + /// + /// Публикует срабатывание датчика хоппера только от основной платы. + /// private void OnHopperSensorTriggered(string deviceId) { - if (deviceId == mainDeviceId) + if (deviceId == CurrentMainDeviceId) { mainThreadQueue.Enqueue(() => hopperSensorTriggered.Invoke()); } } + /// + /// Выбирает COM-порты для подключения с учетом режима auto-scan или явного списка. + /// private string[] SelectPorts(string[] availablePorts) { if (availablePorts == null || availablePorts.Length == 0) @@ -838,16 +1048,35 @@ namespace DanroJump.Hardware.Com if (useAllDetectedPorts) { - return availablePorts.OrderBy(port => port, StringComparer.OrdinalIgnoreCase).ToArray(); + var sortedPorts = (string[])availablePorts.Clone(); + Array.Sort(sortedPorts, StringComparer.OrdinalIgnoreCase); + return sortedPorts; } - HashSet explicitSet = new( - explicitPorts.Where(port => !string.IsNullOrWhiteSpace(port)), - StringComparer.OrdinalIgnoreCase); + HashSet explicitSet = new(StringComparer.OrdinalIgnoreCase); + foreach (var explicitPort in explicitPorts ?? Array.Empty()) + { + if (!string.IsNullOrWhiteSpace(explicitPort)) + { + explicitSet.Add(explicitPort); + } + } - return availablePorts.Where(explicitSet.Contains).ToArray(); + List selectedPorts = new(); + foreach (var availablePort in availablePorts) + { + if (explicitSet.Contains(availablePort)) + { + selectedPorts.Add(availablePort); + } + } + + return selectedPorts.ToArray(); } + /// + /// Проверяет, относится ли роль устройства к призовой витрине. + /// private bool IsPrizeStandRole(string role) { if (string.IsNullOrWhiteSpace(role)) @@ -856,9 +1085,92 @@ namespace DanroJump.Hardware.Com } string[] roles = prizeStandRoles == null || prizeStandRoles.Length == 0 ? DefaultPrizeStandRoles : prizeStandRoles; - return roles.Any(candidate => IsRole(role, candidate)); + foreach (var candidate in roles) + { + if (IsRole(role, candidate)) + { + return true; + } + } + + return false; } + private ComDeviceConnection[] CopyDeviceValues() + { + var snapshot = new ComDeviceConnection[devices.Count]; + var index = 0; + foreach (var device in devices.Values) + { + snapshot[index++] = device; + } + + return snapshot; + } + + private string CurrentMainDeviceId + { + get + { + lock (devicesLock) + { + return mainDeviceId; + } + } + } + + private string CurrentPrizeStandDeviceId + { + get + { + lock (devicesLock) + { + return prizeStandDeviceId; + } + } + } + + private void ResetDeviceSelections() + { + lock (devicesLock) + { + mainDeviceId = ""; + prizeStandDeviceId = ""; + } + } + + private string GetFirstDeviceId() + { + foreach (var deviceId in devices.Keys) + { + return deviceId; + } + + return string.Empty; + } + + private static Task RunLoggedTask(Func taskFactory, CancellationToken token, string operationName) + { + var task = Task.Run(taskFactory, token); + _ = task.ContinueWith( + completedTask => + { + var exception = completedTask.Exception?.GetBaseException(); + if (exception != null) + { + UnityEngine.Debug.LogWarning($"[ComSystem] Background task '{operationName}' failed: {exception.Message}"); + } + }, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); + + return task; + } + + /// + /// Сравнивает роли устройств без учета регистра и лишних пробелов. + /// private static bool IsRole(string role, string expected) { return !string.IsNullOrWhiteSpace(role) && @@ -866,11 +1178,17 @@ namespace DanroJump.Hardware.Com string.Equals(role.Trim(), expected.Trim(), StringComparison.OrdinalIgnoreCase); } + /// + /// Проверяет, можно ли выдавать приз из ячейки с таким статусом. + /// private static bool IsReadyToOpen(PrizeBoxStatus status) { return status == PrizeBoxStatus.Ok || status == PrizeBoxStatus.OkWithCard; } + /// + /// Пытается разобрать строку вида BOX{номер}_{статус} от призовой витрины. + /// private static bool TryParsePrizeBoxStatus(string line, out PrizeBoxStatusEntry status) { status = default; @@ -890,6 +1208,9 @@ namespace DanroJump.Hardware.Com return true; } + /// + /// Преобразует строковый статус прошивки в enum проекта. + /// private static PrizeBoxStatus MapPrizeStatus(string status) { return status switch @@ -905,6 +1226,9 @@ namespace DanroJump.Hardware.Com }; } + /// + /// Сохраняет баланс кредитов, если включено сохранение между сессиями. + /// private void SaveCredits() { if (!saveCreditsBetweenSessions) @@ -913,8 +1237,22 @@ namespace DanroJump.Hardware.Com } PlayerPrefs.SetInt(creditsPlayerPrefsKey, credits); + PlayerPrefs.Save(); } + /// + /// Освобождает токен запуска, если COM-система не смогла перейти в рабочее состояние. + /// + private void ResetSystemToken() + { + systemCts?.Cancel(); + systemCts?.Dispose(); + systemCts = null; + } + + /// + /// Очищает очереди событий, накопленные между потоками. + /// private void ClearQueues() { while (mainThreadQueue.TryDequeue(out _)) { } @@ -923,59 +1261,146 @@ namespace DanroJump.Hardware.Com ClearPrizeStatusQueue(); } + /// + /// Очищает очередь статусов призовой витрины перед новым запросом. + /// private void ClearPrizeStatusQueue() { while (prizeStatusQueue.TryDequeue(out _)) { } } - private IEnumerator HatBlinkCoroutine(float intervalSeconds) + /// + /// Выполняет случайное мигание шляпы с заданным интервалом. + /// + private async UniTaskVoid HatBlinkAsync(float intervalSeconds, CancellationTokenSource cancellationSource) { - while (true) + var token = cancellationSource.Token; + try { - SendToMain(Random.Range(0f, 1f) < 0.5f ? hatOnCommand : hatOffCommand); + while (!token.IsCancellationRequested) + { + SendToMain(Random.Range(0f, 1f) < 0.5f ? hatOnCommand : hatOffCommand); - float wait = hatBlinkVarianceSeconds > 0f - ? Mathf.Max(0.05f, intervalSeconds + Random.Range(-hatBlinkVarianceSeconds, hatBlinkVarianceSeconds)) - : intervalSeconds; + float wait = hatBlinkVarianceSeconds > 0f + ? Mathf.Max(0.05f, intervalSeconds + Random.Range(-hatBlinkVarianceSeconds, hatBlinkVarianceSeconds)) + : intervalSeconds; - yield return new WaitForSecondsRealtime(wait); + await UniTask.Delay( + TimeSpan.FromSeconds(wait), + DelayType.UnscaledDeltaTime, + PlayerLoopTiming.Update, + token); + } + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + // ignored + } + finally + { + ClearCancellationIfCurrent(ref hatBlinkCancellation, cancellationSource); } } - private IEnumerator BoardLightSequenceCoroutine(IReadOnlyList steps, bool loop) + /// + /// Поддерживает длинные световые эффекты, периодически продлевая команду на плате. + /// + private void StartBoardLightSequenceTask(IReadOnlyList steps, bool loop) { - do + lightSequenceCancellation = new CancellationTokenSource(); + BoardLightSequenceAsync(steps, loop, lightSequenceCancellation).Forget(); + } + + private async UniTaskVoid BoardLightSequenceAsync( + IReadOnlyList steps, + bool loop, + CancellationTokenSource cancellationSource) + { + var token = cancellationSource.Token; + try { - foreach (LightStep step in steps) + do { - SendToMain(BuildBoardLightCommand(step.Mode, step.Cycles)); - - float waitSeconds = LightTimings.GetCycleSeconds(step.Mode) * step.Cycles; - if (step.Mode == LightMode.Off || waitSeconds <= 0f) + foreach (LightStep step in steps) { - continue; - } + token.ThrowIfCancellationRequested(); + SendToMain(BuildBoardLightCommand(step.Mode, step.Cycles)); - float elapsed = 0f; - while (elapsed < waitSeconds) - { - float chunk = Mathf.Min(2f, waitSeconds - elapsed); - yield return new WaitForSecondsRealtime(chunk); - elapsed += chunk; - - if (elapsed < waitSeconds) + float waitSeconds = LightTimings.GetCycleSeconds(step.Mode) * step.Cycles; + if (step.Mode == LightMode.Off || waitSeconds <= 0f) { - int remainSeconds = Mathf.CeilToInt(waitSeconds - elapsed); - SendToMain(BuildBoardLightCommand(step.Mode, remainSeconds)); + continue; + } + + float elapsed = 0f; + while (elapsed < waitSeconds) + { + float chunk = Mathf.Min(2f, waitSeconds - elapsed); + await UniTask.Delay( + TimeSpan.FromSeconds(chunk), + DelayType.UnscaledDeltaTime, + PlayerLoopTiming.Update, + token); + elapsed += chunk; + + if (elapsed < waitSeconds) + { + int remainSeconds = Mathf.CeilToInt(waitSeconds - elapsed); + SendToMain(BuildBoardLightCommand(step.Mode, remainSeconds)); + } } } } + while (loop && !token.IsCancellationRequested); + } + catch (OperationCanceledException) when (token.IsCancellationRequested) + { + // ignored + } + finally + { + ClearCancellationIfCurrent(ref lightSequenceCancellation, cancellationSource); } - while (loop); - - lightSequenceCoroutine = null; } + private static void CancelAndDispose(ref CancellationTokenSource cancellationSource) + { + if (cancellationSource == null) + { + return; + } + + var source = cancellationSource; + cancellationSource = null; + + try + { + source.Cancel(); + } + catch + { + // ignored + } + + source.Dispose(); + } + + private static void ClearCancellationIfCurrent( + ref CancellationTokenSource currentSource, + CancellationTokenSource completedSource) + { + if (currentSource != completedSource) + { + return; + } + + currentSource = null; + completedSource.Dispose(); + } + + /// + /// Собирает команду платы из светового режима и количества циклов. + /// private static string BuildBoardLightCommand(LightMode mode, float cycles) { if (mode == LightMode.Off) @@ -987,6 +1412,9 @@ namespace DanroJump.Hardware.Com return BuildBoardLightCommand(mode, seconds); } + /// + /// Собирает команду платы из светового режима и длительности в секундах. + /// private static string BuildBoardLightCommand(LightMode mode, int durationSeconds) { if (mode == LightMode.Off) @@ -997,6 +1425,9 @@ namespace DanroJump.Hardware.Com return $"{BoardEffectsCommandPrefix}{(int)mode:D2}{Mathf.Clamp(durationSeconds, 0, 99):D2}"; } + /// + /// Инкапсулирует одно физическое COM-подключение, авторизацию и фоновый обмен с платой. + /// private sealed class ComDeviceConnection { private const long CoinDebounceMs = 250; @@ -1045,6 +1476,9 @@ namespace DanroJump.Hardware.Com private bool isOperational; private bool disposed; + /// + /// Создает подключение к одному физическому порту с настройками авторизации и keep-alive. + /// public ComDeviceConnection( IComTransportFactory transportFactory, string portName, @@ -1083,19 +1517,64 @@ namespace DanroJump.Hardware.Com aesKey = BuildAesKey(); } + /// + /// Внутренний идентификатор устройства, сейчас совпадает с именем порта. + /// public string DeviceId { get; } + + /// + /// Имя COM-порта, через который подключено устройство. + /// public string PortName { get; } + + /// + /// Роль устройства, полученная после авторизации. + /// public string Role { get; private set; } = ""; + + /// + /// MAC-адрес устройства, если прошивка его возвращает. + /// public string MacAddress { get; private set; } = ""; + + /// + /// Последние входные биты, полученные от устройства. + /// public string LastInputBits { get; private set; } = DefaultInputBits; + /// + /// Событие успешной авторизации подключения. + /// public event Action Authorized; + + /// + /// Событие потери подключения. + /// public event Action Disconnected; + + /// + /// Событие принятой монеты. + /// public event Action CoinAccepted; + + /// + /// Событие получения аппаратного ввода. + /// public event Action InputReceived; + + /// + /// Событие сырой строки, которую не обработали стандартные парсеры. + /// public event Action RawLineReceived; + + /// + /// Событие срабатывания датчика хоппера. + /// public event Action HopperSensorTriggered; + /// + /// Открывает порт, запускает чтение и при необходимости проходит защищенное рукопожатие. + /// public bool Start(CancellationToken token) { parentToken = token; @@ -1106,11 +1585,11 @@ namespace DanroJump.Hardware.Com return false; } - readTask = Task.Run(() => ReadLoopAsync(localCts.Token), localCts.Token); + readTask = RunLoggedTask(() => ReadLoopAsync(localCts.Token), localCts.Token, $"{PortName} read loop"); if (useEncryptedHandshake) { - _ = Task.Run(() => RunHandshakeAsync(localCts.Token), localCts.Token); + _ = RunLoggedTask(() => RunHandshakeAsync(localCts.Token), localCts.Token, $"{PortName} handshake"); } else { @@ -1120,11 +1599,17 @@ namespace DanroJump.Hardware.Com return true; } + /// + /// Отправляет команду только после перехода устройства в рабочее состояние. + /// public bool Send(string command) { return Send(command, false); } + /// + /// Закрывает подключение и опционально отправляет команды безопасного выключения. + /// public void Dispose(bool sendShutdownCommands) { if (disposed) @@ -1154,6 +1639,9 @@ namespace DanroJump.Hardware.Com localCts = null; } + /// + /// Создает serial-транспорт, открывает порт и очищает его буферы. + /// private bool OpenPort() { try @@ -1184,6 +1672,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Фоновый цикл чтения строк из serial-порта. + /// private async Task ReadLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) @@ -1219,6 +1710,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Маршрутизирует входящую строку в handshake, идентификацию, ввод или сырые события. + /// private void ProcessLine(string line) { line = (line ?? "").Trim(); @@ -1258,6 +1752,9 @@ namespace DanroJump.Hardware.Com RawLineReceived?.Invoke(DeviceId, line); } + /// + /// Выполняет CHAL/RAES-авторизацию, совместимую с текущей прошивкой устройства. + /// private async Task RunHandshakeAsync(CancellationToken token) { handshakeStage = HandshakeStage.WaitingChal; @@ -1285,6 +1782,9 @@ namespace DanroJump.Hardware.Com FailHandshake($"Handshake timeout. Last response: {lastHandshakeResponse}"); } + /// + /// Обрабатывает строку текущего этапа защищенного рукопожатия. + /// private void ProcessHandshakeLine(string line) { lastHandshakeResponse = line; @@ -1301,6 +1801,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Обрабатывает CHAL-вызов устройства и отправляет зашифрованный ответ. + /// private void HandleChal(string line) { if (line.Length < 36) @@ -1321,6 +1824,9 @@ namespace DanroJump.Hardware.Com handshakeStage = HandshakeStage.WaitingRaes; } + /// + /// Проверяет RAES-ответ устройства на соответствие исходному запросу. + /// private void HandleRaes(string line) { if (line.Length < 36) @@ -1347,6 +1853,9 @@ namespace DanroJump.Hardware.Com handshakeCompletion?.TrySetResult(true); } + /// + /// Завершает авторизацию ошибкой и помечает устройство отключенным. + /// private void FailHandshake(string message) { UnityEngine.Debug.LogWarning($"[ComSystem] {PortName} authorization failed: {message}"); @@ -1355,6 +1864,9 @@ namespace DanroJump.Hardware.Com NotifyDisconnected(); } + /// + /// Переводит устройство в рабочее состояние и запускает keep-alive/идентификацию. + /// private void MarkOperational() { isOperational = true; @@ -1367,12 +1879,15 @@ namespace DanroJump.Hardware.Com if (keepAlive) { - keepAliveTask = Task.Run(() => KeepAliveLoopAsync(localCts.Token), localCts.Token); + keepAliveTask = RunLoggedTask(() => KeepAliveLoopAsync(localCts.Token), localCts.Token, $"{PortName} keep alive"); } - _ = Task.Run(() => ResolveIdentityAsync(localCts.Token), localCts.Token); + _ = RunLoggedTask(() => ResolveIdentityAsync(localCts.Token), localCts.Token, $"{PortName} identity resolution"); } + /// + /// Запрашивает роль и MAC-адрес устройства после успешной авторизации. + /// private async Task ResolveIdentityAsync(CancellationToken token) { try @@ -1410,6 +1925,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Периодически проверяет, что плата отвечает на запросы ввода, и инициирует переподключение. + /// private async Task KeepAliveLoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) @@ -1445,6 +1963,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Переоткрывает порт и повторно проходит авторизацию после потери keep-alive. + /// private async Task ReconnectAsync(CancellationToken token) { isOperational = false; @@ -1470,6 +1991,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Обрабатывает событие монеты с debounce и подтверждением плате. + /// private void HandleCoin() { long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -1483,6 +2007,9 @@ namespace DanroJump.Hardware.Com CoinAccepted?.Invoke(DeviceId); } + /// + /// Разбирает строку INPUT, обновляет биты ввода и отдельно отслеживает датчик хоппера. + /// private void HandleInput(string line) { inputReceivedInCycle = 1; @@ -1511,6 +2038,9 @@ namespace DanroJump.Hardware.Com InputReceived?.Invoke(new ComInputMessage(DeviceId, LastInputBits)); } + /// + /// Отправляет команду в порт, опционально обходя проверку рабочего состояния для handshake. + /// private bool Send(string command, bool bypassOperational) { if (string.IsNullOrWhiteSpace(command)) @@ -1543,6 +2073,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Пытается завершить ожидающий запрос роли устройства. + /// private bool TryCompleteRole(string line) { if (roleCompletion == null || roleCompletion.Task.IsCompleted) @@ -1572,6 +2105,9 @@ namespace DanroJump.Hardware.Com return true; } + /// + /// Пытается завершить ожидающий запрос MAC-адреса устройства. + /// private bool TryCompleteMac(string line) { if (macCompletion == null || macCompletion.Task.IsCompleted) @@ -1590,6 +2126,9 @@ namespace DanroJump.Hardware.Com return true; } + /// + /// Единообразно переводит подключение в отключенное состояние и публикует событие. + /// private void NotifyDisconnected() { if (disposed) @@ -1603,6 +2142,9 @@ namespace DanroJump.Hardware.Com Disconnected?.Invoke(this); } + /// + /// Закрывает и освобождает serial-транспорт под lock. + /// private void ClosePort() { lock (ioLock) @@ -1640,6 +2182,9 @@ namespace DanroJump.Hardware.Com return BytesToHex(encryptor.TransformFinalBlock(plain, 0, plain.Length)); } + /// + /// Расшифровывает 16-байтовый AES-блок в hex-формате. + /// private string DecryptHexBlock16(string cipherHex) { byte[] cipher = HexToBytes(cipherHex); @@ -1656,6 +2201,9 @@ namespace DanroJump.Hardware.Com return new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } + /// + /// Восстанавливает AES-ключ из обфусцированного массива. + /// private static byte[] BuildAesKey() { byte[] key = new byte[32]; @@ -1667,6 +2215,9 @@ namespace DanroJump.Hardware.Com return key; } + /// + /// Переводит десятичное значение платы в обратный набор битов, удобный для игровых проверок. + /// private static string DecimalToReversedBinary(string decimalString) { if (!long.TryParse(decimalString, out long value)) @@ -1683,6 +2234,9 @@ namespace DanroJump.Hardware.Com : reversed.PadRight(DefaultInputBits.Length, '0'); } + /// + /// Генерирует случайный 16-байтовый блок для handshake. + /// private static string GenerateRandomBlock16Hex() { byte[] buffer = new byte[16]; @@ -1690,6 +2244,9 @@ namespace DanroJump.Hardware.Com return BytesToHex(buffer); } + /// + /// Преобразует hex-строку в массив байтов. + /// private static byte[] HexToBytes(string hex) { if (hex == null) @@ -1712,6 +2269,9 @@ namespace DanroJump.Hardware.Com return result; } + /// + /// Преобразует байты в uppercase hex-строку. + /// private static string BytesToHex(byte[] bytes) { StringBuilder builder = new(bytes.Length * 2); @@ -1723,11 +2283,17 @@ namespace DanroJump.Hardware.Com return builder.ToString(); } + /// + /// Проверяет hex-строку с требуемой длиной. + /// private static bool IsHexString(string value, int expectedLength) { return !string.IsNullOrWhiteSpace(value) && value.Length == expectedLength && IsHexString(value); } + /// + /// Проверяет, состоит ли строка только из hex-символов. + /// private static bool IsHexString(string value) { for (int i = 0; i < value.Length; i++) @@ -1741,6 +2307,9 @@ namespace DanroJump.Hardware.Com return true; } + /// + /// Состояния защищенного рукопожатия с контроллером. + /// private enum HandshakeStage { None, @@ -1751,11 +2320,24 @@ namespace DanroJump.Hardware.Com } } + /// + /// Сырые данные о статусе одной призовой ячейки из очереди ответов витрины. + /// private readonly struct PrizeBoxStatusEntry { + /// + /// Номер ячейки витрины. + /// public readonly int BoxNumber; + + /// + /// Последний полученный статус ячейки. + /// public readonly PrizeBoxStatus Status; + /// + /// Создает запись статуса одной ячейки. + /// public PrizeBoxStatusEntry(int boxNumber, PrizeBoxStatus status) { BoxNumber = boxNumber; @@ -1763,10 +2345,19 @@ namespace DanroJump.Hardware.Com } } + /// + /// Минимальное состояние паузы световой последовательности. + /// private readonly struct LightSequenceState { + /// + /// Была ли активная последовательность света на момент паузы. + /// public readonly bool HasPausedSequence; + /// + /// Создает состояние паузы световой последовательности. + /// public LightSequenceState(bool hasPausedSequence) { HasPausedSequence = hasPausedSequence; diff --git a/Assets/Scripts/Runtime/Hardware/Com/Models/ComModels.cs b/Assets/Scripts/Runtime/Hardware/Com/Models/ComModels.cs index 60f8580..c59fff1 100644 --- a/Assets/Scripts/Runtime/Hardware/Com/Models/ComModels.cs +++ b/Assets/Scripts/Runtime/Hardware/Com/Models/ComModels.cs @@ -2,16 +2,49 @@ namespace DanroJump.Hardware.Com { + /// + /// Набор параметров serial-подключения к одному COM-устройству. + /// public readonly struct SerialSettings { + /// + /// Имя COM-порта. + /// public readonly string PortName; + + /// + /// Скорость serial-подключения. + /// public readonly int BaudRate; + + /// + /// Количество бит данных в serial-пакете. + /// public readonly int DataBits; + + /// + /// Режим четности serial-порта. + /// public readonly SerialParity Parity; + + /// + /// Количество stop bits serial-порта. + /// public readonly SerialStopBits StopBits; + + /// + /// Таймаут чтения из serial-порта в миллисекундах. + /// public readonly int ReadTimeoutMs; + + /// + /// Таймаут записи в serial-порт в миллисекундах. + /// public readonly int WriteTimeoutMs; + /// + /// Создает настройки порта с минимальной защитой таймаутов от нулевых значений. + /// public SerialSettings( string portName, int baudRate, @@ -31,11 +64,24 @@ namespace DanroJump.Hardware.Com } } + /// + /// Сообщение аппаратного ввода, привязанное к конкретному устройству. + /// public readonly struct ComInputMessage { + /// + /// Идентификатор устройства-источника. + /// public readonly string DeviceId; + + /// + /// Биты аппаратного ввода. + /// public readonly string InputBits; + /// + /// Создает снимок входных битов от устройства. + /// public ComInputMessage(string deviceId, string inputBits) { DeviceId = deviceId ?? ""; @@ -43,10 +89,24 @@ namespace DanroJump.Hardware.Com } } + /// + /// Результат попытки открыть ячейку призовой витрины. + /// public readonly struct PrizeOpenResult { + /// + /// Была ли выдача приза успешной. + /// public readonly bool IsSuccess; + + /// + /// Номер призовой ячейки. + /// public readonly int BoxNumber; + + /// + /// Сообщение о результате операции. + /// public readonly string Message; private PrizeOpenResult(bool isSuccess, int boxNumber, string message) @@ -56,22 +116,41 @@ namespace DanroJump.Hardware.Com Message = message ?? ""; } + /// + /// Создает успешный результат выдачи приза. + /// public static PrizeOpenResult Success(int boxNumber) { return new PrizeOpenResult(true, boxNumber, $"Ячейка {boxNumber} успешно открыта."); } + /// + /// Создает результат с причиной отказа в выдаче. + /// public static PrizeOpenResult Fail(int boxNumber, string message) { return new PrizeOpenResult(false, boxNumber, message); } } + /// + /// Один шаг световой последовательности на аппаратной плате. + /// public readonly struct LightStep { + /// + /// Световой режим платы. + /// public readonly LightMode Mode; + + /// + /// Количество циклов эффекта. + /// public readonly float Cycles; + /// + /// Создает шаг светового эффекта с безопасным числом циклов. + /// public LightStep(LightMode mode, float cycles) { Mode = mode; @@ -79,6 +158,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Статус ячейки призовой витрины, полученный от контроллера. + /// public enum PrizeBoxStatus { Ok, @@ -92,6 +174,9 @@ namespace DanroJump.Hardware.Com Error } + /// + /// Значения четности serial-порта, независимые от конкретной реализации System.IO.Ports. + /// public enum SerialParity { None, @@ -101,6 +186,9 @@ namespace DanroJump.Hardware.Com Space } + /// + /// Значения stop bits serial-порта, независимые от конкретной реализации System.IO.Ports. + /// public enum SerialStopBits { None, @@ -109,6 +197,9 @@ namespace DanroJump.Hardware.Com OnePointFive } + /// + /// Коды световых эффектов, которые понимает аппаратная плата. + /// public enum LightMode { Off = -1, diff --git a/Assets/Scripts/Runtime/Hardware/Com/Models/LightTimings.cs b/Assets/Scripts/Runtime/Hardware/Com/Models/LightTimings.cs index 52bfbf9..26e21ce 100644 --- a/Assets/Scripts/Runtime/Hardware/Com/Models/LightTimings.cs +++ b/Assets/Scripts/Runtime/Hardware/Com/Models/LightTimings.cs @@ -3,6 +3,9 @@ using UnityEngine; namespace DanroJump.Hardware.Com { + /// + /// Справочник длительности аппаратных световых эффектов. + /// public static class LightTimings { private static readonly Dictionary CycleMs = new() @@ -22,16 +25,25 @@ namespace DanroJump.Hardware.Com { LightMode.DiagonalWave3, 4000 } }; + /// + /// Возвращает длительность одного цикла эффекта в миллисекундах. + /// public static int GetCycleMs(LightMode mode) { return CycleMs.TryGetValue(mode, out int ms) ? ms : 0; } + /// + /// Возвращает длительность одного цикла эффекта в секундах. + /// public static float GetCycleSeconds(LightMode mode) { return GetCycleMs(mode) / 1000f; } + /// + /// Переводит количество циклов эффекта в длительность команды для платы. + /// public static int CyclesToCommandSeconds(LightMode mode, float cycles) { return Mathf.RoundToInt(GetCycleSeconds(mode) * Mathf.Max(0f, cycles)); diff --git a/Assets/Scripts/Runtime/Hardware/Com/Transports/ReflectionComPortProvider.cs b/Assets/Scripts/Runtime/Hardware/Com/Transports/ReflectionComPortProvider.cs index 6ff5761..457ed4e 100644 --- a/Assets/Scripts/Runtime/Hardware/Com/Transports/ReflectionComPortProvider.cs +++ b/Assets/Scripts/Runtime/Hardware/Com/Transports/ReflectionComPortProvider.cs @@ -4,10 +4,16 @@ using UnityEngine; namespace DanroJump.Hardware.Com { + /// + /// Получает список COM-портов через reflection, чтобы проект не падал в Unity-профилях без System.IO.Ports. + /// public sealed class ReflectionComPortProvider : IComPortProvider { private static readonly Type SerialPortType = Type.GetType("System.IO.Ports.SerialPort, System"); + /// + /// Возвращает имена доступных serial-портов или пустой список, если API недоступен. + /// public string[] GetPortNames() { if (SerialPortType == null) @@ -28,6 +34,9 @@ namespace DanroJump.Hardware.Com } } + /// + /// Возвращает исходную ошибку reflection-вызова без TargetInvocationException-обертки. + /// private static Exception Unwrap(Exception exception) { return exception is TargetInvocationException { InnerException: not null } diff --git a/Assets/Scripts/Runtime/Hardware/Com/Transports/ReflectionSerialComTransport.cs b/Assets/Scripts/Runtime/Hardware/Com/Transports/ReflectionSerialComTransport.cs index 32a7f86..8af8216 100644 --- a/Assets/Scripts/Runtime/Hardware/Com/Transports/ReflectionSerialComTransport.cs +++ b/Assets/Scripts/Runtime/Hardware/Com/Transports/ReflectionSerialComTransport.cs @@ -4,19 +4,46 @@ using System.Text; namespace DanroJump.Hardware.Com { + /// + /// Создает COM-транспорт через reflection-обертку над System.IO.Ports.SerialPort. + /// public sealed class ReflectionSerialComTransportFactory : IComTransportFactory { + /// + /// Создает транспорт для указанного serial-порта. + /// public IComTransport Create(SerialSettings settings) { return ReflectionSerialComTransport.Create(settings); } } + /// + /// Reflection-адаптер serial-порта, который изолирует проект от прямой ссылки на System.IO.Ports. + /// internal sealed class ReflectionSerialComTransport : IComTransport { + private const BindingFlags InstancePublicFlags = BindingFlags.Public | BindingFlags.Instance; + private static readonly Type SerialPortType = Type.GetType("System.IO.Ports.SerialPort, System"); private static readonly Type ParityType = Type.GetType("System.IO.Ports.Parity, System"); private static readonly Type StopBitsType = Type.GetType("System.IO.Ports.StopBits, System"); + private static readonly PropertyInfo IsOpenProperty = SerialPortType?.GetProperty("IsOpen", InstancePublicFlags); + private static readonly PropertyInfo DataBitsProperty = SerialPortType?.GetProperty("DataBits", InstancePublicFlags); + private static readonly PropertyInfo ParityProperty = SerialPortType?.GetProperty("Parity", InstancePublicFlags); + private static readonly PropertyInfo StopBitsProperty = SerialPortType?.GetProperty("StopBits", InstancePublicFlags); + private static readonly PropertyInfo ReadTimeoutProperty = SerialPortType?.GetProperty("ReadTimeout", InstancePublicFlags); + private static readonly PropertyInfo WriteTimeoutProperty = SerialPortType?.GetProperty("WriteTimeout", InstancePublicFlags); + private static readonly PropertyInfo NewLineProperty = SerialPortType?.GetProperty("NewLine", InstancePublicFlags); + private static readonly PropertyInfo EncodingProperty = SerialPortType?.GetProperty("Encoding", InstancePublicFlags); + private static readonly PropertyInfo DtrEnableProperty = SerialPortType?.GetProperty("DtrEnable", InstancePublicFlags); + private static readonly PropertyInfo RtsEnableProperty = SerialPortType?.GetProperty("RtsEnable", InstancePublicFlags); + private static readonly MethodInfo OpenMethod = SerialPortType?.GetMethod("Open", InstancePublicFlags, null, Type.EmptyTypes, null); + private static readonly MethodInfo CloseMethod = SerialPortType?.GetMethod("Close", InstancePublicFlags, null, Type.EmptyTypes, null); + private static readonly MethodInfo DiscardInBufferMethod = SerialPortType?.GetMethod("DiscardInBuffer", InstancePublicFlags, null, Type.EmptyTypes, null); + private static readonly MethodInfo DiscardOutBufferMethod = SerialPortType?.GetMethod("DiscardOutBuffer", InstancePublicFlags, null, Type.EmptyTypes, null); + private static readonly MethodInfo ReadLineMethod = SerialPortType?.GetMethod("ReadLine", InstancePublicFlags, null, Type.EmptyTypes, null); + private static readonly MethodInfo WriteLineMethod = SerialPortType?.GetMethod("WriteLine", InstancePublicFlags, null, new[] { typeof(string) }, null); private readonly object instance; @@ -25,8 +52,14 @@ namespace DanroJump.Hardware.Com this.instance = instance; } - public bool IsOpen => (bool)GetProperty("IsOpen"); + /// + /// Показывает, открыт ли underlying serial-порт. + /// + public bool IsOpen => GetBoolProperty(IsOpenProperty); + /// + /// Создает и настраивает экземпляр SerialPort через reflection. + /// public static ReflectionSerialComTransport Create(SerialSettings settings) { if (SerialPortType == null) @@ -34,50 +67,73 @@ namespace DanroJump.Hardware.Com throw new InvalidOperationException("System.IO.Ports.SerialPort is not available."); } + EnsureRequiredMetadata(); + object serialPort = Activator.CreateInstance(SerialPortType, settings.PortName, settings.BaudRate); var transport = new ReflectionSerialComTransport(serialPort); - transport.SetProperty("DataBits", settings.DataBits); - transport.SetProperty("Parity", ParseEnum(ParityType, settings.Parity.ToString())); - transport.SetProperty("StopBits", ParseEnum(StopBitsType, settings.StopBits.ToString())); - transport.SetProperty("ReadTimeout", settings.ReadTimeoutMs); - transport.SetProperty("WriteTimeout", settings.WriteTimeoutMs); - transport.SetProperty("NewLine", "\n"); - transport.SetProperty("Encoding", Encoding.UTF8); - transport.SetProperty("DtrEnable", true); - transport.SetProperty("RtsEnable", true); + transport.SetProperty(DataBitsProperty, settings.DataBits); + transport.SetProperty(ParityProperty, ParseEnum(ParityType, settings.Parity.ToString())); + transport.SetProperty(StopBitsProperty, ParseEnum(StopBitsType, settings.StopBits.ToString())); + transport.SetProperty(ReadTimeoutProperty, settings.ReadTimeoutMs); + transport.SetProperty(WriteTimeoutProperty, settings.WriteTimeoutMs); + transport.SetProperty(NewLineProperty, "\n"); + transport.SetProperty(EncodingProperty, Encoding.UTF8); + transport.SetProperty(DtrEnableProperty, true); + transport.SetProperty(RtsEnableProperty, true); return transport; } + /// + /// Открывает serial-порт. + /// public void Open() { - Invoke("Open"); + Invoke(OpenMethod); } + /// + /// Закрывает serial-порт. + /// public void Close() { - Invoke("Close"); + Invoke(CloseMethod); } + /// + /// Очищает входной буфер serial-порта. + /// public void DiscardInBuffer() { - Invoke("DiscardInBuffer"); + Invoke(DiscardInBufferMethod); } + /// + /// Очищает выходной буфер serial-порта. + /// public void DiscardOutBuffer() { - Invoke("DiscardOutBuffer"); + Invoke(DiscardOutBufferMethod); } + /// + /// Читает одну строку из serial-порта. + /// public string ReadLine() { - return (string)Invoke("ReadLine"); + return (string)Invoke(ReadLineMethod); } + /// + /// Записывает одну строку в serial-порт. + /// public void WriteLine(string value) { - Invoke("WriteLine", value); + Invoke(WriteLineMethod, value); } + /// + /// Освобождает underlying SerialPort, если он поддерживает IDisposable. + /// public void Dispose() { if (instance is IDisposable disposable) @@ -86,27 +142,65 @@ namespace DanroJump.Hardware.Com } } - private object GetProperty(string propertyName) + /// + /// Проверяет наличие всех reflection-членов заранее, чтобы ошибки были явными. + /// + private static void EnsureRequiredMetadata() { - return SerialPortType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance) - ?.GetValue(instance); + Require(DataBitsProperty, "DataBits"); + Require(ParityProperty, "Parity"); + Require(StopBitsProperty, "StopBits"); + Require(ReadTimeoutProperty, "ReadTimeout"); + Require(WriteTimeoutProperty, "WriteTimeout"); + Require(NewLineProperty, "NewLine"); + Require(EncodingProperty, "Encoding"); + Require(DtrEnableProperty, "DtrEnable"); + Require(RtsEnableProperty, "RtsEnable"); + Require(IsOpenProperty, "IsOpen"); + Require(OpenMethod, "Open()"); + Require(CloseMethod, "Close()"); + Require(DiscardInBufferMethod, "DiscardInBuffer()"); + Require(DiscardOutBufferMethod, "DiscardOutBuffer()"); + Require(ReadLineMethod, "ReadLine()"); + Require(WriteLineMethod, "WriteLine(string)"); } - private void SetProperty(string propertyName, object value) + private static void Require(MemberInfo member, string memberName) { - SerialPortType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance) - ?.SetValue(instance, value); + if (member == null) + { + throw new MissingMemberException(SerialPortType?.FullName ?? "System.IO.Ports.SerialPort", memberName); + } } - private object Invoke(string methodName, params object[] args) + /// + /// Читает bool-свойство SerialPort через заранее найденный PropertyInfo. + /// + private bool GetBoolProperty(PropertyInfo property) + { + return property.GetValue(instance) is bool value && value; + } + + /// + /// Записывает свойство SerialPort через заранее найденный PropertyInfo. + /// + private void SetProperty(PropertyInfo property, object value) + { + property.SetValue(instance, value); + } + + /// + /// Вызывает метод SerialPort через reflection и пробрасывает реальную inner-ошибку. + /// + private object Invoke(MethodInfo method, params object[] args) { try { - return SerialPortType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance) - ?.Invoke(instance, args); + return method.Invoke(instance, args); } catch (TargetInvocationException exception) when (exception.InnerException != null) { + // Наружу пробрасываем реальную ошибку serial-порта, а не reflection-обертку. throw exception.InnerException; } } diff --git a/Assets/Scripts/Runtime/SceneFlow.meta b/Assets/Scripts/Runtime/SceneFlow.meta new file mode 100644 index 0000000..b6ded81 --- /dev/null +++ b/Assets/Scripts/Runtime/SceneFlow.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b6d7c5a1f6f38b64a88886460e039fc4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Runtime/SceneFlow/ISceneFlowService.cs b/Assets/Scripts/Runtime/SceneFlow/ISceneFlowService.cs new file mode 100644 index 0000000..39e39d0 --- /dev/null +++ b/Assets/Scripts/Runtime/SceneFlow/ISceneFlowService.cs @@ -0,0 +1,22 @@ +using System; + +namespace DanroJump.SceneFlow +{ + /// + /// Единая точка переходов между сценами приложения. + /// + public interface ISceneFlowService + { + void LoadScene(string sceneName); + + void OpenMainMenu(); + + void StartGameplay(string sceneName); + + void OpenSettingsScene(string sceneName); + + void ReturnToMainMenuAfterGameOver(TimeSpan delay); + + bool SceneExists(string sceneName); + } +} diff --git a/Assets/Scripts/Runtime/SceneFlow/ISceneFlowService.cs.meta b/Assets/Scripts/Runtime/SceneFlow/ISceneFlowService.cs.meta new file mode 100644 index 0000000..f4d1da2 --- /dev/null +++ b/Assets/Scripts/Runtime/SceneFlow/ISceneFlowService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 98604a7ec49ccea44aa08826410963ae \ No newline at end of file diff --git a/Assets/Scripts/Runtime/SceneFlow/SceneFlowService.cs b/Assets/Scripts/Runtime/SceneFlow/SceneFlowService.cs new file mode 100644 index 0000000..144f3ca --- /dev/null +++ b/Assets/Scripts/Runtime/SceneFlow/SceneFlowService.cs @@ -0,0 +1,111 @@ +using System; +using System.IO; +using System.Threading; +using Cysharp.Threading.Tasks; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace DanroJump.SceneFlow +{ + /// + /// Централизует загрузку сцен, чтобы gameplay и UI не зависели напрямую от SceneManager. + /// + public sealed class SceneFlowService : ISceneFlowService, IDisposable + { + private const string DefaultMainMenuSceneName = "MainMenu"; + + private CancellationTokenSource delayedTransitionCancellation; + + public void LoadScene(string sceneName) + { + if (string.IsNullOrWhiteSpace(sceneName)) + { + return; + } + + CancelDelayedTransition(); + if (!SceneExists(sceneName)) + { + Debug.LogWarning($"Scene '{sceneName}' is not available in Build Settings."); + return; + } + + SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single); + } + + public void OpenMainMenu() + { + LoadScene(DefaultMainMenuSceneName); + } + + public void StartGameplay(string sceneName) + { + LoadScene(sceneName); + } + + public void OpenSettingsScene(string sceneName) + { + LoadScene(sceneName); + } + + public void ReturnToMainMenuAfterGameOver(TimeSpan delay) + { + CancelDelayedTransition(); + delayedTransitionCancellation = new CancellationTokenSource(); + ReturnToMainMenuAfterDelayAsync(delay, delayedTransitionCancellation.Token).Forget(); + } + + public bool SceneExists(string sceneName) + { + if (string.IsNullOrWhiteSpace(sceneName)) + { + return false; + } + + for (var index = 0; index < SceneManager.sceneCountInBuildSettings; index++) + { + var scenePath = SceneUtility.GetScenePathByBuildIndex(index); + var candidateName = Path.GetFileNameWithoutExtension(scenePath); + + if (candidateName == sceneName) + { + return true; + } + } + + return false; + } + + public void Dispose() + { + CancelDelayedTransition(); + } + + private async UniTaskVoid ReturnToMainMenuAfterDelayAsync(TimeSpan delay, CancellationToken cancellationToken) + { + try + { + await UniTask.Delay(delay, DelayType.DeltaTime, PlayerLoopTiming.Update, cancellationToken); + if (!cancellationToken.IsCancellationRequested) + { + OpenMainMenu(); + } + } + catch (OperationCanceledException) + { + } + } + + private void CancelDelayedTransition() + { + if (delayedTransitionCancellation == null) + { + return; + } + + delayedTransitionCancellation.Cancel(); + delayedTransitionCancellation.Dispose(); + delayedTransitionCancellation = null; + } + } +} diff --git a/Assets/Scripts/Runtime/SceneFlow/SceneFlowService.cs.meta b/Assets/Scripts/Runtime/SceneFlow/SceneFlowService.cs.meta new file mode 100644 index 0000000..603c2c7 --- /dev/null +++ b/Assets/Scripts/Runtime/SceneFlow/SceneFlowService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 457cf195fcf99534d91c9ea182118109 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings.meta b/Assets/Scripts/Runtime/Settings.meta new file mode 100644 index 0000000..9429b28 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9ad67257ad8ccbe45bb828799d2093da +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Runtime/Settings/Core.meta b/Assets/Scripts/Runtime/Settings/Core.meta new file mode 100644 index 0000000..2e21ab2 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b44d9f58d991ddb46972d3162b2acde2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Runtime/Settings/Core/AddressableServiceSettingsDatabaseProvider.cs b/Assets/Scripts/Runtime/Settings/Core/AddressableServiceSettingsDatabaseProvider.cs new file mode 100644 index 0000000..34d3c6e --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/AddressableServiceSettingsDatabaseProvider.cs @@ -0,0 +1,91 @@ +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.ResourceManagement.AsyncOperations; + +namespace DanroJump.Settings +{ + /// + /// Загружает базу сервисных настроек из прямой ссылки или через Addressables. + /// + public sealed class AddressableServiceSettingsDatabaseProvider : IServiceSettingsDatabaseProvider + { + private readonly string address; + private readonly ServiceSettingsDatabase fallbackDatabase; + private AsyncOperationHandle? loadedHandle; + + /// + /// Создает provider для addressable-базы настроек. + /// + public AddressableServiceSettingsDatabaseProvider( + string address, + ServiceSettingsDatabase fallbackDatabase = null) + { + this.address = address; + this.fallbackDatabase = fallbackDatabase; + } + + /// + /// Возвращает прямую fallback-базу без блокировки Addressables или загружает базу по адресу. + /// + public ServiceSettingsDatabase Load() + { + ReleaseLoadedHandle(); + + if (fallbackDatabase != null) + { + return fallbackDatabase; + } + + if (string.IsNullOrWhiteSpace(address)) + { + return null; + } + + try + { + var handle = Addressables.LoadAssetAsync(address); + var database = handle.WaitForCompletion(); + + // Удерживаем handle только для успешно загруженного addressable-asset. + if (handle.Status == AsyncOperationStatus.Succeeded && database != null) + { + loadedHandle = handle; + return database; + } + + if (handle.IsValid()) + { + Addressables.Release(handle); + } + + Debug.LogWarning( + $"[{nameof(AddressableServiceSettingsDatabaseProvider)}] Failed to load settings database at address '{address}'. Fallback will be used."); + } + catch (System.Exception exception) + { + Debug.LogWarning( + $"[{nameof(AddressableServiceSettingsDatabaseProvider)}] Failed to load settings database at address '{address}': {exception.Message}"); + } + + return null; + } + + /// + /// Освобождает addressable-handle, если база была загружена через Addressables. + /// + public void Dispose() + { + ReleaseLoadedHandle(); + } + + private void ReleaseLoadedHandle() + { + if (loadedHandle.HasValue && loadedHandle.Value.IsValid()) + { + Addressables.Release(loadedHandle.Value); + } + + loadedHandle = null; + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/AddressableServiceSettingsDatabaseProvider.cs.meta b/Assets/Scripts/Runtime/Settings/Core/AddressableServiceSettingsDatabaseProvider.cs.meta new file mode 100644 index 0000000..5975329 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/AddressableServiceSettingsDatabaseProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 92fa680792030ec439f2d44cecd90173 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/IServiceSettingApplier.cs b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingApplier.cs new file mode 100644 index 0000000..ed67aa0 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingApplier.cs @@ -0,0 +1,13 @@ +namespace DanroJump.Settings +{ + /// + /// Применяет изменение настройки к runtime-системам проекта. + /// + public interface IServiceSettingApplier + { + /// + /// Вызывается после изменения или загрузки настройки. + /// + void Apply(ServiceSettingDefinition definition, ServiceSettingValue value); + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/IServiceSettingApplier.cs.meta b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingApplier.cs.meta new file mode 100644 index 0000000..6e9b110 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingApplier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1223aeca642bade459ae7eba97c77748 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsDatabaseProvider.cs b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsDatabaseProvider.cs new file mode 100644 index 0000000..b3cd282 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsDatabaseProvider.cs @@ -0,0 +1,15 @@ +using System; + +namespace DanroJump.Settings +{ + /// + /// Источник базы сервисных настроек. + /// + public interface IServiceSettingsDatabaseProvider : IDisposable + { + /// + /// Загружает базу настроек. + /// + ServiceSettingsDatabase Load(); + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsDatabaseProvider.cs.meta b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsDatabaseProvider.cs.meta new file mode 100644 index 0000000..1a849cf --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsDatabaseProvider.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 081d44ba547211841ada275faa6cdc95 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsRepository.cs b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsRepository.cs new file mode 100644 index 0000000..20888dc --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsRepository.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace DanroJump.Settings +{ + /// + /// Репозиторий сохраненных значений сервисных настроек. + /// + public interface IServiceSettingsRepository + { + /// + /// Путь к хранилищу настроек. + /// + string Path { get; } + + /// + /// True, если сохранение уже существует. + /// + bool Exists { get; } + + /// + /// Загружает значения настроек из хранилища. + /// + bool TryLoad(out Dictionary values); + + /// + /// Сохраняет значения настроек в хранилище. + /// + bool Save(IReadOnlyDictionary values); + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsRepository.cs.meta b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsRepository.cs.meta new file mode 100644 index 0000000..c996c11 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsRepository.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 594d56f9806ef3a4ba13e76b5d186eb7 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsService.cs b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsService.cs new file mode 100644 index 0000000..47fc3b3 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsService.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace DanroJump.Settings +{ + /// + /// Read-only API сервисных настроек для UI и gameplay-систем. + /// + public interface IReadOnlyServiceSettings + { + ServiceSettingsDatabase Database { get; } + event Action SettingsLoaded; + event Action SettingsSaved; + event Action SettingChanged; + + bool TryGetDefinition(string key, out ServiceSettingDefinition definition); + IReadOnlyDictionary Snapshot(); + ServiceSettingValue Get(string key); + bool GetBool(string key); + int GetInt(string key); + float GetFloat(string key); + string GetString(string key); + int GetOptionIndex(string key); + } + + /// + /// Полный API сервисных настроек с загрузкой, сохранением и изменением значений. + /// + public interface IServiceSettingsService : IReadOnlyServiceSettings + { + void Load(); + void Save(); + void ReloadExternalChanges(); + void ApplyAll(); + void ResetAllToDefaults(); + void ResetGroupToDefaults(string groupName); + bool Set(string key, ServiceSettingValue value); + bool SetExternal(string key, ServiceSettingValue value); + bool SetBool(string key, bool value); + bool SetInt(string key, int value); + bool SetFloat(string key, float value); + bool SetString(string key, string value); + bool SetOptionIndex(string key, int optionIndex); + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsService.cs.meta b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsService.cs.meta new file mode 100644 index 0000000..1b65d79 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/IServiceSettingsService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 727d6c80986410948ac58a76487c216e \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/JsonServiceSettingsRepository.cs b/Assets/Scripts/Runtime/Settings/Core/JsonServiceSettingsRepository.cs new file mode 100644 index 0000000..f2d20dc --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/JsonServiceSettingsRepository.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace DanroJump.Settings +{ + /// + /// JSON-репозиторий сервисных настроек в Application.persistentDataPath. + /// + public sealed class JsonServiceSettingsRepository : IServiceSettingsRepository + { + private const string DefaultFileName = "service-settings.json"; + + /// + /// Создает репозиторий с безопасным именем файла настроек. + /// + public JsonServiceSettingsRepository(string fileName) + { + var safeFileName = GetSafeFileName(fileName); + Path = System.IO.Path.Combine(Application.persistentDataPath, safeFileName); + } + + /// + /// Полный путь к JSON-файлу настроек. + /// + public string Path { get; } + + /// + /// Показывает, существует ли файл настроек на диске. + /// + public bool Exists => File.Exists(Path); + + /// + /// Загружает настройки из JSON и разворачивает compact-значения. + /// + public bool TryLoad(out Dictionary values) + { + values = new Dictionary(StringComparer.Ordinal); + + if (!Exists) + { + return false; + } + + try + { + var json = File.ReadAllText(Path); + var data = JsonUtility.FromJson(json); + if (data?.entries == null) + { + return false; + } + + foreach (var entry in data.entries) + { + if (entry == null || string.IsNullOrWhiteSpace(entry.key)) + { + continue; + } + + values[entry.key] = ServiceSettingValue.FromCompact(entry.value); + } + + return true; + } + catch (Exception exception) + { + Debug.LogWarning($"[{nameof(JsonServiceSettingsRepository)}] Failed to load settings from '{Path}': {exception.Message}"); + return false; + } + } + + /// + /// Сохраняет настройки в JSON в компактном сериализуемом виде. + /// + public bool Save(IReadOnlyDictionary values) + { + if (values == null) + { + Debug.LogWarning($"[{nameof(JsonServiceSettingsRepository)}] Settings snapshot is null. Save skipped."); + return false; + } + + try + { + var directory = System.IO.Path.GetDirectoryName(Path); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + var data = new ServiceSettingsSaveData(); + foreach (var pair in values) + { + if (string.IsNullOrWhiteSpace(pair.Key)) + { + continue; + } + + data.entries.Add(new ServiceSettingsSaveData.Entry + { + key = pair.Key, + value = pair.Value.ToCompact() + }); + } + + WriteAllTextSafely(Path, JsonUtility.ToJson(data, true)); + return true; + } + catch (Exception exception) + { + Debug.LogWarning($"[{nameof(JsonServiceSettingsRepository)}] Failed to save settings to '{Path}': {exception.Message}"); + return false; + } + } + + /// + /// Пишет файл через временную копию, чтобы не оставить пустой JSON при аварийном завершении записи. + /// + private static void WriteAllTextSafely(string path, string contents) + { + var tempPath = path + ".tmp"; + var backupPath = path + ".bak"; + + try + { + File.WriteAllText(tempPath, contents); + + if (File.Exists(path)) + { + File.Replace(tempPath, path, backupPath, true); + return; + } + + File.Move(tempPath, path); + } + finally + { + if (File.Exists(tempPath)) + { + File.Delete(tempPath); + } + } + } + + /// + /// Оставляет только имя файла, чтобы настройки не могли выйти за persistentDataPath. + /// + private static string GetSafeFileName(string fileName) + { + var requestedFileName = string.IsNullOrWhiteSpace(fileName) ? DefaultFileName : fileName.Trim(); + var safeFileName = System.IO.Path.GetFileName(requestedFileName); + + if (string.IsNullOrWhiteSpace(safeFileName) || + safeFileName == "." || + safeFileName == ".." || + safeFileName.IndexOfAny(System.IO.Path.GetInvalidFileNameChars()) >= 0) + { + Debug.LogWarning( + $"[{nameof(JsonServiceSettingsRepository)}] Unsafe settings file name '{fileName}' was replaced with '{DefaultFileName}'."); + return DefaultFileName; + } + + if (!string.Equals(requestedFileName, safeFileName, StringComparison.Ordinal)) + { + Debug.LogWarning( + $"[{nameof(JsonServiceSettingsRepository)}] Settings file path '{fileName}' was reduced to safe file name '{safeFileName}'."); + } + + return safeFileName; + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/JsonServiceSettingsRepository.cs.meta b/Assets/Scripts/Runtime/Settings/Core/JsonServiceSettingsRepository.cs.meta new file mode 100644 index 0000000..c0631b6 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/JsonServiceSettingsRepository.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2c5a04c3237c0f7438e67ce412778ebe \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/NullServiceSettingApplier.cs b/Assets/Scripts/Runtime/Settings/Core/NullServiceSettingApplier.cs new file mode 100644 index 0000000..237221e --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/NullServiceSettingApplier.cs @@ -0,0 +1,15 @@ +namespace DanroJump.Settings +{ + /// + /// Пустой applier настроек для случаев, когда значения нужно хранить без runtime-побочных эффектов. + /// + public sealed class NullServiceSettingApplier : IServiceSettingApplier + { + /// + /// Намеренно ничего не применяет. + /// + public void Apply(ServiceSettingDefinition definition, ServiceSettingValue value) + { + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/NullServiceSettingApplier.cs.meta b/Assets/Scripts/Runtime/Settings/Core/NullServiceSettingApplier.cs.meta new file mode 100644 index 0000000..6e9c9ad --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/NullServiceSettingApplier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d00f6bddb2579684181e7935284792e0 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingDefinition.cs b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingDefinition.cs new file mode 100644 index 0000000..7f385c2 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingDefinition.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace DanroJump.Settings +{ + /// + /// Предпочитаемый способ отображения настройки в сервисном UI. + /// + public enum ServiceSettingDisplayMode + { + Auto, + Toggle, + Slider, + Stepper, + TextInput, + Button, + OptionPicker + } + + /// + /// Описание одной сервисной настройки: ключ, UI-метаданные, тип, дефолт и ограничения. + /// + [Serializable] + public sealed class ServiceSettingDefinition + { + [SerializeField] private string key; + [SerializeField] private string displayName; + [SerializeField] [TextArea] private string description; + [SerializeField] private string group = "General"; + [SerializeField] private bool shownInServiceMenu = true; + [SerializeField] private bool exposedForExternalChanges; + [SerializeField] private ServiceSettingValueType valueType; + [SerializeField] private ServiceSettingDisplayMode displayMode = ServiceSettingDisplayMode.Auto; + + [Header("Default values")] + [SerializeField] private bool defaultBool = true; + [SerializeField] private int defaultInt; + [SerializeField] private float defaultFloat = 1f; + [SerializeField] private string defaultString = ""; + [SerializeField] private int defaultOptionIndex; + + [Header("Ranges")] + [SerializeField] private int minInt; + [SerializeField] private int maxInt = 10; + [SerializeField] private int intStep = 1; + [SerializeField] private float minFloat; + [SerializeField] private float maxFloat = 1f; + [SerializeField] private float floatStep = 0.1f; + [SerializeField] private bool displayFloatAsPercentage; + + [Header("Options")] + [SerializeField] private List options = new(); + + [Header("Button")] + [SerializeField] private string buttonText; + [SerializeField] private string buttonData; + [SerializeField] private Color buttonColor = Color.white; + [SerializeField] private Sprite buttonSprite; + + /// + /// Уникальный ключ настройки. + /// + public string Key => key; + + /// + /// Название настройки для сервисного UI. + /// + public string DisplayName => displayName; + + /// + /// Описание настройки для подсказок обслуживающему персоналу. + /// + public string Description => description; + + /// + /// Группа настройки в сервисном меню. + /// + public string Group => group; + + /// + /// Показывать ли настройку в сервисном меню. + /// + public bool ShownInServiceMenu => shownInServiceMenu; + + /// + /// Можно ли менять настройку через внешний JSON. + /// + public bool ExposedForExternalChanges => exposedForExternalChanges; + + /// + /// Тип значения настройки. + /// + public ServiceSettingValueType ValueType => valueType; + + /// + /// Предпочитаемый вид UI-контрола для настройки. + /// + public ServiceSettingDisplayMode DisplayMode => displayMode; + + /// + /// Минимальное int-значение с защитой от перепутанных границ. + /// + public int MinInt => Mathf.Min(minInt, maxInt); + + /// + /// Максимальное int-значение с защитой от перепутанных границ. + /// + public int MaxInt => Mathf.Max(minInt, maxInt); + + /// + /// Шаг изменения int-значения. + /// + public int IntStep => Mathf.Max(1, intStep); + + /// + /// Минимальное float-значение с защитой от перепутанных границ. + /// + public float MinFloat => Mathf.Min(minFloat, maxFloat); + + /// + /// Максимальное float-значение с защитой от перепутанных границ. + /// + public float MaxFloat => Mathf.Max(minFloat, maxFloat); + + /// + /// Шаг изменения float-значения. + /// + public float FloatStep => Mathf.Max(0.0001f, floatStep); + + /// + /// Отображать ли float-значение как проценты. + /// + public bool DisplayFloatAsPercentage => displayFloatAsPercentage; + + /// + /// Список вариантов для option-настройки. + /// + public IReadOnlyList Options => options; + + /// + /// Текст кнопки действия. Если не задан, UI использует название настройки. + /// + public string ButtonText => buttonText; + + /// + /// Дополнительные данные кнопки: URL, имя окна, идентификатор команды или другая payload-строка. + /// + public string ButtonData => buttonData; + + /// + /// Цвет кнопки действия, перенесенный из legacy-базы. + /// + public Color ButtonColor => buttonColor; + + /// + /// Опциональный спрайт кнопки действия. + /// + public Sprite ButtonSprite => buttonSprite; + + /// + /// Значение по умолчанию, собранное из полей под выбранный тип. + /// + public ServiceSettingValue DefaultValue => valueType switch + { + ServiceSettingValueType.Bool => ServiceSettingValue.Bool(defaultBool), + ServiceSettingValueType.Int => ServiceSettingValue.Int(defaultInt), + ServiceSettingValueType.Float => ServiceSettingValue.Float(defaultFloat), + ServiceSettingValueType.String => ServiceSettingValue.String(defaultString), + ServiceSettingValueType.Button => ServiceSettingValue.Button(), + ServiceSettingValueType.Option => ServiceSettingValue.Option(GetDefaultOptionIndex()), + _ => default + }; + + /// + /// Приводит входное значение к типу и диапазонам настройки. + /// + public ServiceSettingValue Sanitize(ServiceSettingValue value) + { + if (valueType == ServiceSettingValueType.Button) + { + return ServiceSettingValue.Button(); + } + + if (value.type != valueType && !(valueType == ServiceSettingValueType.Option && value.type == ServiceSettingValueType.Int)) + { + return Sanitize(DefaultValue); + } + + return valueType switch + { + ServiceSettingValueType.Bool => ServiceSettingValue.Bool(value.boolValue), + ServiceSettingValueType.Int => ServiceSettingValue.Int(Mathf.Clamp(value.intValue, MinInt, MaxInt)), + ServiceSettingValueType.Float => ServiceSettingValue.Float(Mathf.Clamp(value.floatValue, MinFloat, MaxFloat)), + ServiceSettingValueType.String => ServiceSettingValue.String(value.stringValue), + ServiceSettingValueType.Option => ServiceSettingValue.Option(SanitizeOptionIndex(value.intValue)), + _ => DefaultValue + }; + } + + /// + /// Возвращает безопасный индекс варианта по умолчанию. + /// + private int GetDefaultOptionIndex() + { + return SanitizeOptionIndex(defaultOptionIndex); + } + + /// + /// Ограничивает option-index доступным диапазоном. + /// + private int SanitizeOptionIndex(int index) + { + return options is { Count: > 0 } ? Mathf.Clamp(index, 0, options.Count - 1) : 0; + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingDefinition.cs.meta b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingDefinition.cs.meta new file mode 100644 index 0000000..d8554ba --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingDefinition.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8f945cb9af5e9194399b7dcb1dccdbac \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingValue.cs b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingValue.cs new file mode 100644 index 0000000..e2b7e7d --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingValue.cs @@ -0,0 +1,224 @@ +using System; +using System.Globalization; +using UnityEngine; + +namespace DanroJump.Settings +{ + /// + /// Тип значения сервисной настройки. + /// + public enum ServiceSettingValueType + { + Bool, + Int, + Float, + String, + Button, + Option + } + + /// + /// Универсальное значение сервисной настройки с явным типом. + /// + [Serializable] + public struct ServiceSettingValue : IEquatable + { + /// + /// Активный тип значения. + /// + public ServiceSettingValueType type; + + /// + /// Значение для bool-настроек. + /// + public bool boolValue; + + /// + /// Значение для int и option-настроек. + /// + public int intValue; + + /// + /// Значение для float-настроек. + /// + public float floatValue; + + /// + /// Значение для string-настроек. + /// + public string stringValue; + + /// + /// Создает bool-значение. + /// + public static ServiceSettingValue Bool(bool value) + { + return new ServiceSettingValue { type = ServiceSettingValueType.Bool, boolValue = value }; + } + + /// + /// Создает int-значение. + /// + public static ServiceSettingValue Int(int value) + { + return new ServiceSettingValue { type = ServiceSettingValueType.Int, intValue = value }; + } + + /// + /// Создает float-значение. + /// + public static ServiceSettingValue Float(float value) + { + return new ServiceSettingValue { type = ServiceSettingValueType.Float, floatValue = value }; + } + + /// + /// Создает string-значение. + /// + public static ServiceSettingValue String(string value) + { + return new ServiceSettingValue { type = ServiceSettingValueType.String, stringValue = value ?? string.Empty }; + } + + /// + /// Создает marker-значение для кнопки действия. + /// + public static ServiceSettingValue Button() + { + return new ServiceSettingValue { type = ServiceSettingValueType.Button }; + } + + /// + /// Создает option-значение по индексу варианта. + /// + public static ServiceSettingValue Option(int optionIndex) + { + return new ServiceSettingValue { type = ServiceSettingValueType.Option, intValue = optionIndex }; + } + + /// + /// Упаковывает значение в компактный строковый формат для JSON. + /// + public ServiceSettingValueCompact ToCompact() + { + return new ServiceSettingValueCompact + { + type = (int)type, + value = type switch + { + ServiceSettingValueType.Bool => boolValue ? "1" : "0", + ServiceSettingValueType.Int => intValue.ToString(CultureInfo.InvariantCulture), + ServiceSettingValueType.Float => floatValue.ToString("R", CultureInfo.InvariantCulture), + ServiceSettingValueType.String => stringValue ?? string.Empty, + ServiceSettingValueType.Button => string.Empty, + ServiceSettingValueType.Option => intValue.ToString(CultureInfo.InvariantCulture), + _ => string.Empty + } + }; + } + + /// + /// Восстанавливает typed-значение из компактного JSON-представления. + /// + public static ServiceSettingValue FromCompact(ServiceSettingValueCompact compact) + { + var valueType = Enum.IsDefined(typeof(ServiceSettingValueType), compact.type) + ? (ServiceSettingValueType)compact.type + : ServiceSettingValueType.String; + + return valueType switch + { + ServiceSettingValueType.Bool => Bool(compact.value == "1" || + bool.TryParse(compact.value, out var boolValue) && boolValue), + ServiceSettingValueType.Int => Int(ParseInt(compact.value)), + ServiceSettingValueType.Float => Float(ParseFloat(compact.value)), + ServiceSettingValueType.String => String(compact.value), + ServiceSettingValueType.Button => Button(), + ServiceSettingValueType.Option => Option(ParseInt(compact.value)), + _ => String(compact.value) + }; + } + + /// + /// Сравнивает значения настроек с учетом типа и приблизительного float-сравнения. + /// + public bool Equals(ServiceSettingValue other) + { + return type == other.type && + boolValue == other.boolValue && + intValue == other.intValue && + Mathf.Approximately(floatValue, other.floatValue) && + stringValue == other.stringValue; + } + + /// + /// Сравнивает значение с boxed-объектом. + /// + public override bool Equals(object obj) + { + return obj is ServiceSettingValue other && Equals(other); + } + + /// + /// Возвращает hash code для использования значения в словарях и наборах. + /// + public override int GetHashCode() + { + return HashCode.Combine(type, boolValue, intValue, floatValue, stringValue); + } + + /// + /// Возвращает строковое представление активного typed-значения. + /// + public override string ToString() + { + return type switch + { + ServiceSettingValueType.Bool => boolValue.ToString(), + ServiceSettingValueType.Int => intValue.ToString(CultureInfo.InvariantCulture), + ServiceSettingValueType.Float => floatValue.ToString(CultureInfo.InvariantCulture), + ServiceSettingValueType.String => stringValue ?? string.Empty, + ServiceSettingValueType.Button => string.Empty, + ServiceSettingValueType.Option => intValue.ToString(CultureInfo.InvariantCulture), + _ => string.Empty + }; + } + + /// + /// Безопасно читает int из invariant-строки. + /// + private static int ParseInt(string value) + { + return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + ? result + : 0; + } + + /// + /// Безопасно читает float из invariant-строки. + /// + private static float ParseFloat(string value) + { + return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) + ? result + : 0f; + } + } + + /// + /// Компактное сериализуемое представление значения настройки. + /// + [Serializable] + public struct ServiceSettingValueCompact + { + /// + /// Числовой код типа значения. + /// + public int type; + + /// + /// Значение, упакованное в строку для JSON. + /// + public string value; + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingValue.cs.meta b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingValue.cs.meta new file mode 100644 index 0000000..ea56b09 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingValue.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b84da5e7fb664024799c182f72a1bcc7 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsActions.cs b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsActions.cs new file mode 100644 index 0000000..674473e --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsActions.cs @@ -0,0 +1,135 @@ +using UnityEngine; +using Zenject; +using DanroJump.SceneFlow; + +namespace DanroJump.Settings +{ + /// + /// MonoBehaviour-фасад для вызова действий сервиса настроек из UI/Unity Events. + /// + public sealed class ServiceSettingsActions : MonoBehaviour + { + private IServiceSettingsService settings; + private ISceneFlowService sceneFlow; + private bool ownsSceneFlow; + + /// + /// Получает сервис настроек через Extenject. + /// + [Inject] + public void Construct( + IServiceSettingsService injectedSettings, + [InjectOptional] ISceneFlowService injectedSceneFlow = null) + { + settings = injectedSettings; + sceneFlow = injectedSceneFlow; + } + + /// + /// Вручную назначает сервис настроек, если объект используется вне DI. + /// + public void Configure(IServiceSettingsService service) + { + settings = service; + } + + /// + /// Сохраняет настройки. + /// + public void Save() + { + settings?.Save(); + } + + /// + /// Перечитывает настройки, разрешенные для изменения через внешний JSON. + /// + public void ReloadExternalChanges() + { + settings?.ReloadExternalChanges(); + } + + /// + /// Повторно применяет текущие значения настроек. + /// + public void ApplyAll() + { + settings?.ApplyAll(); + } + + /// + /// Сбрасывает все настройки к значениям по умолчанию. + /// + public void ResetAllToDefaults() + { + settings?.ResetAllToDefaults(); + } + + /// + /// Сбрасывает одну группу настроек к значениям по умолчанию. + /// + public void ResetGroupToDefaults(string groupName) + { + settings?.ResetGroupToDefaults(groupName); + } + + /// + /// Открывает папку persistentDataPath с внешним JSON настроек. + /// + public void OpenPersistentDataFolder() + { + Application.OpenURL($"file://{Application.persistentDataPath}"); + } + + /// + /// Загружает сцену по имени. + /// + public void LoadScene(string sceneName) + { + if (!string.IsNullOrWhiteSpace(sceneName)) + { + GetSceneFlow().LoadScene(sceneName); + } + } + + /// + /// Возвращает приложение в главное меню. + /// + public void LoadMainMenu() + { + GetSceneFlow().OpenMainMenu(); + } + + private ISceneFlowService GetSceneFlow() + { + if (sceneFlow != null) + { + return sceneFlow; + } + + sceneFlow = new SceneFlowService(); + ownsSceneFlow = true; + return sceneFlow; + } + + private void OnDestroy() + { + if (ownsSceneFlow && sceneFlow is System.IDisposable disposable) + { + disposable.Dispose(); + } + } + + /// + /// Завершает приложение или останавливает Play Mode в редакторе. + /// + public void QuitApplication() + { +#if UNITY_EDITOR + UnityEditor.EditorApplication.isPlaying = false; +#else + Application.Quit(); +#endif + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsActions.cs.meta b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsActions.cs.meta new file mode 100644 index 0000000..51e5acb --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsActions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f626af96b53a1f34db055f3ef60554bb \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsBehaviour.cs b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsBehaviour.cs new file mode 100644 index 0000000..c0619c3 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsBehaviour.cs @@ -0,0 +1,97 @@ +using UnityEngine; + +namespace DanroJump.Settings +{ + /// + /// Автономный bootstrap-компонент сервисных настроек для сцен без ProjectContext installer. + /// + [DefaultExecutionOrder(-900)] + public sealed class ServiceSettingsBehaviour : MonoBehaviour + { + [SerializeField] private string databaseAddress = ServiceSettingsAddressKeys.Database; + [SerializeField] private ServiceSettingsDatabase fallbackDatabase; + [SerializeField] private string fileName = "service-settings.json"; + [SerializeField] private bool loadOnAwake = true; + [SerializeField] private bool dontDestroyOnLoad = true; + [SerializeField] private bool saveOnApplicationQuit = true; + + private static ServiceSettingsBehaviour instance; + private IServiceSettingsDatabaseProvider databaseProvider; + /// + /// Созданный runtime-сервис настроек. + /// + public IServiceSettingsService Service { get; private set; } + + /// + /// Создает сервис настроек и при необходимости загружает сохраненные значения. + /// + private void Awake() + { + if (dontDestroyOnLoad) + { + if (instance != null && instance != this) + { + Debug.LogWarning( + $"[{nameof(ServiceSettingsBehaviour)}] Another persistent instance already exists. Destroying duplicate.", + this); + Destroy(gameObject); + return; + } + + instance = this; + DontDestroyOnLoad(gameObject); + } + + Service = CreateService(); + + if (loadOnAwake) + { + Service?.Load(); + } + } + + /// + /// Сохраняет настройки при выходе из приложения. + /// + private void OnApplicationQuit() + { + if (saveOnApplicationQuit) + { + Service?.Save(); + } + } + + /// + /// Освобождает provider базы настроек. + /// + private void OnDestroy() + { + if (instance == this) + { + instance = null; + } + + databaseProvider?.Dispose(); + databaseProvider = null; + } + + /// + /// Создает сервис настроек из Addressables-базы и JSON-репозитория. + /// + public IServiceSettingsService CreateService() + { + databaseProvider = new AddressableServiceSettingsDatabaseProvider(databaseAddress, fallbackDatabase); + var database = databaseProvider.Load(); + + if (database == null) + { + Debug.LogError($"[{nameof(ServiceSettingsBehaviour)}] Service settings database was not loaded.", this); + return null; + } + + return new ServiceSettingsService( + database, + new JsonServiceSettingsRepository(fileName)); + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsBehaviour.cs.meta b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsBehaviour.cs.meta new file mode 100644 index 0000000..7293cd0 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1453de15186c9994fa3adad0fe6677a7 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsDatabase.cs b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsDatabase.cs new file mode 100644 index 0000000..215694e --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsDatabase.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace DanroJump.Settings +{ + /// + /// ScriptableObject-база всех операторских настроек проекта. + /// + [CreateAssetMenu( + fileName = "ServiceSettingsDatabase", + menuName = "Danro Jump/Settings/Service Settings Database")] + public sealed class ServiceSettingsDatabase : ScriptableObject + { + [SerializeField] private List entries = new(); + + /// + /// Все описания сервисных настроек в порядке отображения. + /// + public IReadOnlyList Entries => entries; + + /// + /// Находит описание настройки по ключу. + /// + public ServiceSettingDefinition Find(string key) + { + return entries.Find(entry => + entry != null && + string.Equals(entry.Key, key, StringComparison.Ordinal)); + } + + /// + /// Безопасно пытается найти настройку по ключу. + /// + public bool TryFind(string key, out ServiceSettingDefinition definition) + { + definition = Find(key); + return definition != null; + } + + /// + /// Проверяет пустые и дублирующиеся ключи в редакторе. + /// + private void OnValidate() + { + var keys = new HashSet(); + foreach (var entry in entries) + { + if (entry == null) + { + continue; + } + + if (string.IsNullOrWhiteSpace(entry.Key)) + { + Debug.LogWarning($"[{nameof(ServiceSettingsDatabase)}] Empty key in {name}.", this); + continue; + } + + if (!keys.Add(entry.Key)) + { + Debug.LogWarning($"[{nameof(ServiceSettingsDatabase)}] Duplicate key '{entry.Key}' in {name}.", this); + } + } + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsDatabase.cs.meta b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsDatabase.cs.meta new file mode 100644 index 0000000..00d43e7 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsDatabase.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2a6ecd20e77604845ab518440999c6e5 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsSaveData.cs b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsSaveData.cs new file mode 100644 index 0000000..cf0d2b8 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsSaveData.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace DanroJump.Settings +{ + /// + /// DTO для сохранения сервисных настроек в JSON-файл. + /// + [Serializable] + public sealed class ServiceSettingsSaveData + { + /// + /// Версия формата сохранения настроек. + /// + public int version = 1; + + /// + /// Список сохраненных значений настроек. + /// + public List entries = new(); + + /// + /// Одна сохраненная настройка в формате ключ-значение. + /// + [Serializable] + public sealed class Entry + { + /// + /// Ключ настройки. + /// + public string key; + + /// + /// Компактное значение настройки. + /// + public ServiceSettingValueCompact value; + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsSaveData.cs.meta b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsSaveData.cs.meta new file mode 100644 index 0000000..e1f8c72 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsSaveData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9011464d4b32df24a8708aed9a61d9fa \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsService.cs b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsService.cs new file mode 100644 index 0000000..261b5ac --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsService.cs @@ -0,0 +1,360 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace DanroJump.Settings +{ + /// + /// Runtime-сервис операторских настроек: хранит значения, валидирует их по базе и сохраняет во внешний файл. + /// + public sealed class ServiceSettingsService : IServiceSettingsService + { + private readonly ServiceSettingsDatabase database; + private readonly IServiceSettingsRepository repository; + private readonly IServiceSettingApplier applier; + private readonly Dictionary values = new(StringComparer.Ordinal); + + /// + /// Создает сервис поверх базы описаний, репозитория сохранения и опционального applier. + /// + public ServiceSettingsService( + ServiceSettingsDatabase database, + IServiceSettingsRepository repository, + IServiceSettingApplier applier = null) + { + this.database = database; + this.repository = repository; + this.applier = applier ?? new NullServiceSettingApplier(); + } + + /// + /// База описаний настроек, используемая сервисом. + /// + public ServiceSettingsDatabase Database => database; + + public event Action SettingsLoaded; + public event Action SettingsSaved; + public event Action SettingChanged; + + /// + /// Загружает сохраненные значения, дополняет дефолтами и применяет их. + /// + public void Load() + { + values.Clear(); + + if (repository != null && repository.TryLoad(out var loadedValues)) + { + foreach (var pair in loadedValues) + { + if (TryGetDefinition(pair.Key, out var definition) && IsStoredValueDefinition(definition)) + { + values[pair.Key] = definition.Sanitize(pair.Value); + } + } + } + + EnsureDefaults(); + ApplyAll(); + SettingsLoaded?.Invoke(); + } + + /// + /// Сохраняет текущий snapshot настроек в репозиторий. + /// + public void Save() + { + if (repository == null || !repository.Save(values)) + { + return; + } + + SettingsSaved?.Invoke(); + } + + /// + /// Перечитывает только значения, разрешенные для внешних изменений. + /// + public void ReloadExternalChanges() + { + if (repository == null || !repository.TryLoad(out var loadedValues)) + { + return; + } + + foreach (var pair in loadedValues) + { + SetExternal(pair.Key, pair.Value); + } + } + + /// + /// Повторно применяет все значения через applier. + /// + public void ApplyAll() + { + if (database == null) + { + return; + } + + foreach (var definition in database.Entries) + { + if (!IsStoredValueDefinition(definition)) + { + continue; + } + + applier.Apply(definition, Get(definition.Key)); + } + } + + /// + /// Сбрасывает все настройки к дефолтам из базы и сохраняет результат. + /// + public void ResetAllToDefaults() + { + if (database == null) + { + return; + } + + foreach (var definition in database.Entries) + { + if (!IsStoredValueDefinition(definition)) + { + continue; + } + + Set(definition.Key, definition.DefaultValue); + } + + Save(); + } + + /// + /// Сбрасывает одну группу настроек к дефолтам. + /// + public void ResetGroupToDefaults(string groupName) + { + if (database == null || string.IsNullOrWhiteSpace(groupName)) + { + return; + } + + foreach (var definition in database.Entries) + { + if (!IsStoredValueDefinition(definition) || + !string.Equals(definition.Group, groupName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + Set(definition.Key, definition.DefaultValue); + } + + Save(); + } + + /// + /// Устанавливает значение настройки после проверки и sanitization. + /// + public bool Set(string key, ServiceSettingValue value) + { + if (!TryGetDefinition(key, out var definition)) + { + Debug.LogWarning($"[{nameof(ServiceSettingsService)}] Unknown setting key '{key}'."); + return false; + } + + if (definition.ValueType == ServiceSettingValueType.Button) + { + Debug.LogWarning($"[{nameof(ServiceSettingsService)}] Button setting '{key}' does not store a value."); + return false; + } + + var sanitized = definition.Sanitize(value); + if (values.TryGetValue(key, out var current) && current.Equals(sanitized)) + { + return true; + } + + // Значение применяем сразу, а Save отвечает только за запись на диск. + values[key] = sanitized; + applier.Apply(definition, sanitized); + SettingChanged?.Invoke(key, sanitized); + return true; + } + + /// + /// Устанавливает значение из внешнего файла только для разрешенных настроек. + /// + public bool SetExternal(string key, ServiceSettingValue value) + { + if (!TryGetDefinition(key, out var definition)) + { + Debug.LogWarning($"[{nameof(ServiceSettingsService)}] Unknown external setting key '{key}'."); + return false; + } + + if (!definition.ExposedForExternalChanges) + { + Debug.LogWarning($"[{nameof(ServiceSettingsService)}] Setting '{key}' is not exposed for external changes."); + return false; + } + + return Set(key, value); + } + + /// + /// Устанавливает bool-настройку по ключу. + /// + public bool SetBool(string key, bool value) + { + return Set(key, ServiceSettingValue.Bool(value)); + } + + /// + /// Устанавливает int-настройку по ключу. + /// + public bool SetInt(string key, int value) + { + return Set(key, ServiceSettingValue.Int(value)); + } + + /// + /// Устанавливает float-настройку по ключу. + /// + public bool SetFloat(string key, float value) + { + return Set(key, ServiceSettingValue.Float(value)); + } + + /// + /// Устанавливает string-настройку по ключу. + /// + public bool SetString(string key, string value) + { + return Set(key, ServiceSettingValue.String(value)); + } + + /// + /// Устанавливает option-настройку по индексу варианта. + /// + public bool SetOptionIndex(string key, int optionIndex) + { + return Set(key, ServiceSettingValue.Option(optionIndex)); + } + + /// + /// Пытается найти описание настройки по ключу. + /// + public bool TryGetDefinition(string key, out ServiceSettingDefinition definition) + { + definition = null; + return !string.IsNullOrWhiteSpace(key) && + database != null && + database.TryFind(key, out definition); + } + + /// + /// Возвращает копию текущих значений для безопасного чтения снаружи. + /// + public IReadOnlyDictionary Snapshot() + { + return new Dictionary(values, StringComparer.Ordinal); + } + + /// + /// Возвращает текущее значение или дефолт из базы. + /// + public ServiceSettingValue Get(string key) + { + if (values.TryGetValue(key, out var value)) + { + return value; + } + + return TryGetDefinition(key, out var definition) ? definition.DefaultValue : default; + } + + /// + /// Возвращает bool-значение настройки. + /// + public bool GetBool(string key) + { + return Get(key).boolValue; + } + + /// + /// Возвращает int-значение настройки. + /// + public int GetInt(string key) + { + return Get(key).intValue; + } + + /// + /// Возвращает float-значение настройки. + /// + public float GetFloat(string key) + { + return Get(key).floatValue; + } + + /// + /// Возвращает string-значение настройки. + /// + public string GetString(string key) + { + return Get(key).stringValue ?? string.Empty; + } + + /// + /// Возвращает индекс выбранного option-варианта. + /// + public int GetOptionIndex(string key) + { + return Get(key).intValue; + } + + /// + /// Заполняет отсутствующие значения дефолтами и заново sanitizes загруженные данные. + /// + private void EnsureDefaults() + { + if (database == null) + { + return; + } + + foreach (var definition in database.Entries) + { + if (!IsStoredValueDefinition(definition)) + { + continue; + } + + values[definition.Key] = values.TryGetValue(definition.Key, out var existingValue) + ? definition.Sanitize(existingValue) + : definition.Sanitize(definition.DefaultValue); + } + } + + /// + /// Проверяет, что описание настройки пригодно для работы сервиса. + /// + private static bool IsValidDefinition(ServiceSettingDefinition definition) + { + return definition != null && !string.IsNullOrWhiteSpace(definition.Key); + } + + /// + /// Проверяет, что настройка хранит значение. Кнопки являются действиями и не попадают в JSON. + /// + private static bool IsStoredValueDefinition(ServiceSettingDefinition definition) + { + return IsValidDefinition(definition) && definition.ValueType != ServiceSettingValueType.Button; + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsService.cs.meta b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsService.cs.meta new file mode 100644 index 0000000..8481e88 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Core/ServiceSettingsService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6796aba03a75864458389f88a664e271 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Installers.meta b/Assets/Scripts/Runtime/Settings/Installers.meta new file mode 100644 index 0000000..1550832 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Installers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6a48d9bf13a4d3d4cbce16a2e0efec8d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Runtime/Settings/Installers/ServiceSettingsInstaller.cs b/Assets/Scripts/Runtime/Settings/Installers/ServiceSettingsInstaller.cs new file mode 100644 index 0000000..01c71d3 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Installers/ServiceSettingsInstaller.cs @@ -0,0 +1,59 @@ +using UnityEngine; +using Zenject; +using DanroJump.SceneFlow; + +namespace DanroJump.Settings +{ + /// + /// ProjectContext/SceneContext installer сервисных настроек. + /// + public sealed class ServiceSettingsInstaller : MonoInstaller + { + [SerializeField] private string databaseAddress = ServiceSettingsAddressKeys.Database; + [SerializeField] private ServiceSettingsDatabase fallbackDatabase; + [SerializeField] private string fileName = "service-settings.json"; + [SerializeField] private bool loadImmediately = true; + [SerializeField] private bool bindActionsInScene = true; + + /// + /// Загружает базу настроек, создает сервис и регистрирует его в DI-контейнере. + /// + public override void InstallBindings() + { + var databaseProvider = new AddressableServiceSettingsDatabaseProvider(databaseAddress, fallbackDatabase); + var database = databaseProvider.Load(); + + if (database == null) + { + Debug.LogError($"{nameof(ServiceSettingsInstaller)} could not load {nameof(ServiceSettingsDatabase)}.", this); + databaseProvider.Dispose(); + return; + } + + var repository = new JsonServiceSettingsRepository(fileName); + var service = new ServiceSettingsService(database, repository); + + if (loadImmediately) + { + service.Load(); + } + + Container.BindInstance(database).AsSingle(); + Container.Bind().FromInstance(databaseProvider).AsSingle(); + Container.Bind().FromInstance(databaseProvider).AsSingle(); + Container.Bind().FromInstance(repository).AsSingle(); + Container.Bind().FromInstance(service).AsSingle(); + Container.Bind().FromInstance(service).AsSingle(); + Container.BindInterfacesAndSelfTo().AsSingle(); + + if (bindActionsInScene) + { + // ServiceSettingsActions может жить в сцене и получать injection после создания сервиса. + foreach (var actions in FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) + { + Container.QueueForInject(actions); + } + } + } + } +} diff --git a/Assets/Scripts/Runtime/Settings/Installers/ServiceSettingsInstaller.cs.meta b/Assets/Scripts/Runtime/Settings/Installers/ServiceSettingsInstaller.cs.meta new file mode 100644 index 0000000..322f0e3 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Installers/ServiceSettingsInstaller.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9aa54c60d21f3754a9bbead2e5c6875b \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Keys.meta b/Assets/Scripts/Runtime/Settings/Keys.meta new file mode 100644 index 0000000..60d2eee --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Keys.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ac1f8d13f54af1949b67bd39936a3f13 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsAddressKeys.cs b/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsAddressKeys.cs new file mode 100644 index 0000000..be66776 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsAddressKeys.cs @@ -0,0 +1,10 @@ +namespace DanroJump.Settings +{ + /// + /// Addressables-ключи сервисных настроек. + /// + public static class ServiceSettingsAddressKeys + { + public const string Database = "settings/service-settings-database"; + } +} diff --git a/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsAddressKeys.cs.meta b/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsAddressKeys.cs.meta new file mode 100644 index 0000000..3d6014a --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsAddressKeys.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 94c52775e5605664092bc368b8ce35b6 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsKeys.cs b/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsKeys.cs new file mode 100644 index 0000000..fbf689d --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsKeys.cs @@ -0,0 +1,44 @@ +namespace DanroJump.Settings +{ + /// + /// Канонические ключи сервисных настроек проекта. + /// + public static class ServiceSettingsKeys + { + public const string MusicVolume = "volume.music"; + public const string SoundsVolume = "volume.sounds"; + + public const string FreePlay = "payment.freeGame"; + public const string ShowSupportNumber = "payment.showSupportNumber"; + public const string SupportNumber = "payment.supportNumber"; + public const string PaymentImpulseCost = "payment.paymentImpulseCost"; + public const string StartGameCost = "payment.startGameCost"; + public const string ContinueGameCost = "payment.continueGameCost"; + public const string Currency = "payment.currency"; + public const string GamePrice = StartGameCost; + + public const string ShowBigPrizeText = "prizes.showBigPrizeText"; + public const string GiveSmallPrize = "prizes.giveSmallPrize"; + public const string GiveBigPrize = "prizes.giveBigPrize"; + public const string ErrorGivePrize = "prizes.errorGivePrize"; + public const string ShowPrizeError = "prizes.showPrizeError"; + public const string PrizePhoneNumber = "prizes.phoneNumber"; + public const string BigPrize = "prizes.bigPrize"; + + public const string StartingLives = "gameplay.startLives"; + public const string AdditionalLives = "gameplay.additionalLives"; + public const string CanContinueGame = "gameplay.canContinueGame"; + public const string InactivityTimeout = "gameplay.inactivityTimeout"; + public const string GameDifficulty = "gameplay.gameDifficulty"; + + public const string StatisticsButton = "btn.statistics"; + public const string PhoneNumberButton = "btn.phoneNumber"; + public const string BigPrizeButton = "btn.bigPrize"; + public const string SupportNumberButton = "btn.supportNumber"; + public const string CurrencyButton = "btn.currency"; + public const string HopperTestButton = "btn.hopper_test"; + public const string LevelsPrizesButton = "btn.levels_prizes"; + public const string QrSettingsButton = "btn.qr_settings"; + public const string StandSettingsButton = "btn.stand_settings"; + } +} diff --git a/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsKeys.cs.meta b/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsKeys.cs.meta new file mode 100644 index 0000000..2382cb9 --- /dev/null +++ b/Assets/Scripts/Runtime/Settings/Keys/ServiceSettingsKeys.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 397eb7a02e405024ea555c3d1001ce42 \ No newline at end of file diff --git a/Assets/Scripts/Runtime/UI/Focus.meta b/Assets/Scripts/Runtime/UI/Focus.meta new file mode 100644 index 0000000..5200b53 --- /dev/null +++ b/Assets/Scripts/Runtime/UI/Focus.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3c41b11af3e1c9b429a090faefb7d17a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Runtime/UI/Focus/UiFocusRing.cs b/Assets/Scripts/Runtime/UI/Focus/UiFocusRing.cs new file mode 100644 index 0000000..d96d2f8 --- /dev/null +++ b/Assets/Scripts/Runtime/UI/Focus/UiFocusRing.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UIElements; + +namespace DanroJump.UI.Focus +{ + /// + /// Собирает линейное кольцо фокусируемых UI Toolkit-элементов для навигации с клавиатуры или автомата. + /// + public sealed class UiFocusRing + { + private readonly List elements = new(); + + /// + /// Текущий список фокусируемых элементов. + /// + public IReadOnlyList Elements => elements; + + /// + /// Количество элементов в кольце фокуса. + /// + public int Count => elements.Count; + + /// + /// Пересобирает список видимых и активных элементов, доступных для фокуса. + /// + public void Rebuild(VisualElement root) + { + elements.Clear(); + + if (root == null) + { + return; + } + + CollectFocusableElements(root); + } + + /// + /// Проверяет, входит ли элемент в текущее кольцо фокуса. + /// + public bool Contains(VisualElement element) + { + return elements.Contains(element); + } + + /// + /// Возвращает первый элемент кольца фокуса. + /// + public VisualElement GetFirst() + { + return elements.Count > 0 ? elements[0] : null; + } + + /// + /// Возвращает следующий элемент с зацикливанием по краям списка. + /// + public VisualElement GetNext(VisualElement current, int direction) + { + if (elements.Count == 0) + { + return null; + } + + var currentIndex = current != null ? elements.IndexOf(current) : -1; + if (direction == 0) + { + return currentIndex >= 0 ? elements[currentIndex] : elements[0]; + } + + if (currentIndex < 0) + { + return direction > 0 ? elements[0] : elements[^1]; + } + + var targetIndex = (currentIndex + direction + elements.Count) % elements.Count; + return elements[targetIndex]; + } + + /// + /// Ставит фокус на первый доступный элемент. + /// + public void FocusFirst() + { + TryFocus(GetFirst()); + } + + /// + /// Перемещает фокус относительно текущего элемента. + /// + public bool FocusRelativeTo(VisualElement current, int direction) + { + var next = GetNext(current, direction); + if (next == null) + { + return false; + } + + return TryFocus(next); + } + + /// + /// Ставит фокус на элемент, если он все еще доступен в текущем кольце. + /// + public bool TryFocus(VisualElement element) + { + if (element == null || !elements.Contains(element) || !IsFocusable(element)) + { + return false; + } + + element.Focus(); + return true; + } + + /// + /// Обходит визуальное дерево в порядке отображения и собирает доступные элементы. + /// + private void CollectFocusableElements(VisualElement root) + { + if (!CanTraverse(root)) + { + return; + } + + if (IsFocusable(root)) + { + elements.Add(root); + } + + for (var index = 0; index < root.childCount; index++) + { + CollectFocusableElements(root[index]); + } + } + + /// + /// Проверяет базовые условия доступности элемента для фокуса. + /// + private static bool IsFocusable(VisualElement element) + { + return element.focusable && + CanTraverse(element); + } + + /// + /// Проверяет, нужно ли учитывать элемент и его дочернее дерево при сборке фокуса. + /// + private static bool CanTraverse(VisualElement element) + { + return element != null && + element.enabledInHierarchy && + element.resolvedStyle.display != DisplayStyle.None && + element.resolvedStyle.visibility != Visibility.Hidden; + } + } +} diff --git a/Assets/Scripts/Runtime/UI/Focus/UiFocusRing.cs.meta b/Assets/Scripts/Runtime/UI/Focus/UiFocusRing.cs.meta new file mode 100644 index 0000000..e7d9b84 --- /dev/null +++ b/Assets/Scripts/Runtime/UI/Focus/UiFocusRing.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9c666bf7aa8dea943be11d9b6c22380d \ No newline at end of file diff --git a/Assets/Scripts/Runtime/UI/Gameplay.meta b/Assets/Scripts/Runtime/UI/Gameplay.meta new file mode 100644 index 0000000..826453e --- /dev/null +++ b/Assets/Scripts/Runtime/UI/Gameplay.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7808576ecdba38e40b7984cec1630269 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Runtime/UI/Gameplay/GameplayHudController.cs b/Assets/Scripts/Runtime/UI/Gameplay/GameplayHudController.cs new file mode 100644 index 0000000..7a12969 --- /dev/null +++ b/Assets/Scripts/Runtime/UI/Gameplay/GameplayHudController.cs @@ -0,0 +1,218 @@ +using System.Collections; +using DanroJump.Gameplay; +using UnityEngine; +using UnityEngine.UIElements; +using Zenject; + +namespace DanroJump.UI.Gameplay +{ + /// + /// Отображает runtime-показатели игрового забега в UI Toolkit HUD. + /// + public sealed class GameplayHudController : MonoBehaviour + { + [SerializeField] private VisualTreeAsset visualTreeAsset; + [SerializeField] private PanelSettings panelSettings; + [SerializeField] private int sortingOrder = 10; + [SerializeField] private string scoreLabelName = "ScoreValue"; + [SerializeField] private string heightLabelName = "HeightValue"; + [SerializeField] private string bestHeightLabelName = "BestHeightValue"; + [SerializeField] private string livesLabelName = "LivesValue"; + [SerializeField] private string coinsLabelName = "CoinsValue"; + [SerializeField] private string deathsLabelName = "DeathsValue"; + [SerializeField] private string stateLabelName = "StateValue"; + + private UIDocument document; + private GameplaySession session; + private Label scoreLabel; + private Label heightLabel; + private Label bestHeightLabel; + private Label livesLabel; + private Label coinsLabel; + private Label deathsLabel; + private Label stateLabel; + private Coroutine refreshRoutine; + + /// + /// Получает игровую сессию из DI-контейнера сцены. + /// + [Inject] + public void Construct(GameplaySession injectedSession) + { + session = injectedSession; + } + + private void Start() + { + EnsureDocument(); + BindLabels(); + ResolveSessionFromSceneContext(); + Refresh(); + } + + /// + /// Подписывается на изменения сессии и запускает кадровую подстраховку обновления HUD. + /// + private void OnEnable() + { + if (session != null) + { + session.Changed += Refresh; + } + + refreshRoutine ??= StartCoroutine(RefreshContinuously()); + Refresh(); + } + + /// + /// Останавливает обновление HUD и снимает подписку с игровой сессии. + /// + private void OnDisable() + { + if (refreshRoutine != null) + { + StopCoroutine(refreshRoutine); + refreshRoutine = null; + } + + if (session != null) + { + session.Changed -= Refresh; + } + } + + /// + /// Создает или настраивает UIDocument, чтобы HUD мог жить на отдельном GameObject без ручной сборки. + /// + private void EnsureDocument() + { + if (document == null && !TryGetComponent(out document)) + { + document = gameObject.AddComponent(); + } + + if (document == null) + { + return; + } + + document.visualTreeAsset = visualTreeAsset; + document.panelSettings = panelSettings; + document.sortingOrder = sortingOrder; + } + + /// + /// Находит текстовые поля в UXML по именам, заданным в инспекторе. + /// + private void BindLabels() + { + var root = document != null ? document.rootVisualElement : null; + if (root == null) + { + return; + } + + scoreLabel = FindLabel(root, scoreLabelName); + heightLabel = FindLabel(root, heightLabelName); + bestHeightLabel = FindLabel(root, bestHeightLabelName); + livesLabel = FindLabel(root, livesLabelName); + coinsLabel = FindLabel(root, coinsLabelName); + deathsLabel = FindLabel(root, deathsLabelName); + stateLabel = FindLabel(root, stateLabelName); + } + + /// + /// Обновляет все значения HUD из текущей игровой сессии. + /// + private void Refresh() + { + if (session == null) + { + SetLabel(stateLabel, "WAITING"); + return; + } + + SetLabel(scoreLabel, session.Score.ToString()); + SetLabel(heightLabel, $"{session.MaxRunHeight:0} м"); + SetLabel(bestHeightLabel, $"{session.RecordHeight:0} м"); + SetLabel(livesLabel, session.RemainingLives.ToString()); + SetLabel(coinsLabel, session.Coins.ToString()); + SetLabel(deathsLabel, session.DeathCount.ToString()); + SetLabel(stateLabel, FormatState(session.State)); + } + + /// + /// Безопасно обновляет текст label, если элемент найден в UXML. + /// + private static void SetLabel(Label label, string value) + { + if (label != null) + { + label.text = value; + } + } + + /// + /// Аварийно получает сессию из SceneContext, если HUD включился раньше DI-инъекции. + /// + private void ResolveSessionFromSceneContext() + { + if (session != null) + { + return; + } + + var sceneContext = FindFirstObjectByType(); + if (sceneContext == null || sceneContext.Container == null) + { + return; + } + + try + { + session = sceneContext.Container.Resolve(); + session.Changed += Refresh; + } + catch (ZenjectException) + { + Debug.LogWarning($"{nameof(GameplayHudController)} could not resolve {nameof(GameplaySession)} from SceneContext yet.", this); + } + } + + /// + /// Ищет Label по имени, заданному в настройках контроллера. + /// + private static Label FindLabel(VisualElement root, string labelName) + { + return root != null && !string.IsNullOrEmpty(labelName) + ? root.Q