feat: Add text input system and balance management with settings improvements
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- Добавлена система ввода текста через модальное окно с ячейками для аркадного автомата - Реализован TextInputModal с поддержкой кастомных алфавитов и навигации - Добавлен TextInputCell для ввода отдельных символов с циклической навигацией - Создан TextInputSettingsBinder для интеграции ввода текста с системой настроек - Реализована компактная сериализация настроек (только нужные поля в JSON) - Внедрена система баланса игрока (BalanceManager) - Баланс начисляется при оплате импульсом, списывается при начале/продолжении игры - Добавлен InactivityManager для выхода в меню при отсутствии активности (5 минут) - Улучшена система настроек - Добавлены флаги shownInSettings и exposed для управления видимостью настроек - Реализована автоматическая регистрация/отписка SettingsButtonBinder при активации/деактивации - Исправлена загрузка настроек с проверкой типов через Sanitize() - SettingsApplier упрощен - оставлена только поддержка AudioManager - Добавлен ValueSelectorRow для выбора целочисленных значений с шагом - Удалены примеры и документация из production кода (QUICK_SETUP.md, TextInputExample, AdvancedLoggingExample) - Убран весь отладочный вывод Debug.Log из системы настроек и ввода текста - Очищены неразрывные пробелы из файлов проекта - Добавлены префабы UI (InputCell)
This commit is contained in:
parent
3944534cd0
commit
2417f58f61
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,6 +4,9 @@
|
||||
*_[Tt]emp.*
|
||||
*[Tt]emp*
|
||||
|
||||
*_[Tt]est.*
|
||||
*[Tt]est*
|
||||
|
||||
#
|
||||
## --------------------------------------------
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
462
Assets/Prefabs/UI/InputCell.prefab
Normal file
462
Assets/Prefabs/UI/InputCell.prefab
Normal file
@ -0,0 +1,462 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &264925314075203901
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7476718851314200889}
|
||||
- component: {fileID: 8712991107362882286}
|
||||
- component: {fileID: 6850433135119910662}
|
||||
- component: {fileID: 485977605481230277}
|
||||
m_Layer: 5
|
||||
m_Name: InputCell
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &7476718851314200889
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 264925314075203901}
|
||||
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: 4445986152597778029}
|
||||
- {fileID: 8627206344477583323}
|
||||
- {fileID: 675627477356491008}
|
||||
- {fileID: 7722770590907352781}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 150}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &8712991107362882286
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 264925314075203901}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &6850433135119910662
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 264925314075203901}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0, g: 0, b: 0, a: 0.7058824}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &485977605481230277
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 264925314075203901}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 6828bed9c9414fe4bfd284a92d73f36b, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.TextInputCell
|
||||
symbolText: {fileID: 91819688666493321}
|
||||
backgroundImage: {fileID: 6850433135119910662}
|
||||
selectionHighlight: {fileID: 5330937721467949768}
|
||||
normalColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
selectedColor: {r: 0.2, g: 0.53333336, b: 0.74509805, a: 1}
|
||||
highlightColor: {r: 0.2, g: 0.53333336, b: 0.74509805, a: 0.3}
|
||||
--- !u!1 &2423717193611656995
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 675627477356491008}
|
||||
- component: {fileID: 6586906075369708484}
|
||||
- component: {fileID: 4382475361883771700}
|
||||
m_Layer: 5
|
||||
m_Name: ArrowUp
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &675627477356491008
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2423717193611656995}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0.7071068, w: 0.7071068}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7476718851314200889}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 90}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 110}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6586906075369708484
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2423717193611656995}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &4382475361883771700
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2423717193611656995}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 1197703181795215433, guid: 8991ce55ada140746b953a82d2b1f196, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &5016213976666468048
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 8627206344477583323}
|
||||
- component: {fileID: 3658248278055859851}
|
||||
- component: {fileID: 91819688666493321}
|
||||
m_Layer: 5
|
||||
m_Name: Symbol
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &8627206344477583323
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5016213976666468048}
|
||||
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: 7476718851314200889}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 1}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &3658248278055859851
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5016213976666468048}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &91819688666493321
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 5016213976666468048}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: N
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 135.15
|
||||
m_fontSizeBase: 200
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 1
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 250
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 2
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &6711382326363563761
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7722770590907352781}
|
||||
- component: {fileID: 4164371370080920532}
|
||||
- component: {fileID: 1120335031969087060}
|
||||
m_Layer: 5
|
||||
m_Name: ArrowDown
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
--- !u!224 &7722770590907352781
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6711382326363563761}
|
||||
m_LocalRotation: {x: 0, y: 0, z: -0.7071068, w: 0.7071068}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7476718851314200889}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: -90}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -110}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4164371370080920532
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6711382326363563761}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &1120335031969087060
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6711382326363563761}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 1197703181795215433, guid: 8991ce55ada140746b953a82d2b1f196, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &9063052322907577816
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4445986152597778029}
|
||||
- component: {fileID: 6601481174515317520}
|
||||
- component: {fileID: 5330937721467949768}
|
||||
m_Layer: 5
|
||||
m_Name: Highlight
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &4445986152597778029
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 9063052322907577816}
|
||||
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: 7476718851314200889}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 1}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &6601481174515317520
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 9063052322907577816}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &5330937721467949768
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 9063052322907577816}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.20038268, g: 0.5322009, b: 0.745283, a: 0.62352943}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 4211293783942383924, guid: a64f98d7a00501442ae40845562f1c20, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
7
Assets/Prefabs/UI/InputCell.prefab.meta
Normal file
7
Assets/Prefabs/UI/InputCell.prefab.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6839c0e6ef91e3846910bce6b887d6e6
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@ namespace RR.Common.Managers
|
||||
[SerializeField] private int currentBalance;
|
||||
|
||||
[Header("Настройки сохранения")]
|
||||
[SerializeField] private bool saveBalanceBetweenSessions = false;
|
||||
[SerializeField] private bool saveBalanceBetweenSessions;
|
||||
|
||||
// События для уведомления UI и других систем
|
||||
public UnityEvent<int> OnBalanceChanged;
|
||||
@ -35,7 +35,7 @@ namespace RR.Common.Managers
|
||||
private int _impulseCost = 10;
|
||||
private int _startGameCost = 10;
|
||||
private int _continueGameCost = 5;
|
||||
private bool _freeGame = false;
|
||||
private bool _freeGame;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using RR.Common.Controls;
|
||||
using UnityEngine;
|
||||
@ -28,6 +28,8 @@ namespace RR.Game2D.Core.Managers
|
||||
public PaymentManager Payment {get; private set;}
|
||||
// Balance control for game
|
||||
public BalanceManager Balance {get; private set;}
|
||||
// Inactivity control for game
|
||||
public InactivityManager Inactivity {get; private set;}
|
||||
|
||||
// Plate control for game
|
||||
public ComWrapper Plate { get; private set; }
|
||||
@ -150,6 +152,10 @@ namespace RR.Game2D.Core.Managers
|
||||
if (Balance == null) Balance = globals.GetComponent<BalanceManager>();
|
||||
if (Balance == null) Balance = BalanceManager.Instance;
|
||||
|
||||
// Initialize Inactivity control
|
||||
if (Inactivity == null) Inactivity = gameObject.AddComponent<InactivityManager>();
|
||||
if (Inactivity == null) Inactivity = InactivityManager.Instance;
|
||||
|
||||
// Apply loaded settings (new system applies automatically in Awake)
|
||||
|
||||
if (Plate == null) Plate = globals.GetComponent<ComWrapper>();
|
||||
@ -325,6 +331,9 @@ public void TogglePause() => SetPause(!IsPaused);
|
||||
|
||||
if (Plate) Plate.StopBlink();
|
||||
|
||||
// Включаем отслеживание бездействия
|
||||
if (Inactivity != null) Inactivity.EnableInactivityCheck();
|
||||
|
||||
LoadLevel(1, 1);
|
||||
}
|
||||
|
||||
@ -348,6 +357,10 @@ public void TogglePause() => SetPause(!IsPaused);
|
||||
LivesChanged?.Invoke(Lives);
|
||||
Coins = 0;
|
||||
CoinsChanged?.Invoke(Coins);
|
||||
|
||||
// Сбрасываем таймер бездействия
|
||||
if (Inactivity != null) Inactivity.ResetInactivityTimer();
|
||||
|
||||
RespawnPlayer();
|
||||
}
|
||||
|
||||
@ -359,6 +372,10 @@ public void TogglePause() => SetPause(!IsPaused);
|
||||
private void GameOver() // => LoadScene("MainMenu", false);
|
||||
{
|
||||
float totalTime = Time.unscaledTime - _gameStartTime;
|
||||
|
||||
// Отключаем отслеживание бездействия
|
||||
if (Inactivity != null) Inactivity.DisableInactivityCheck();
|
||||
|
||||
GameOverEvent?.Invoke(totalTime);
|
||||
}
|
||||
// --
|
||||
@ -366,6 +383,10 @@ public void TogglePause() => SetPause(!IsPaused);
|
||||
private void GameWin() // => LoadScene("MainMenu", false);
|
||||
{
|
||||
float totalTime = Time.unscaledTime - _gameStartTime;
|
||||
|
||||
// Отключаем отслеживание бездействия
|
||||
if (Inactivity != null) Inactivity.DisableInactivityCheck();
|
||||
|
||||
GameWonEvent?.Invoke(totalTime);
|
||||
}
|
||||
|
||||
|
||||
292
Assets/Scripts/Managers/InactivityManager.cs
Normal file
292
Assets/Scripts/Managers/InactivityManager.cs
Normal file
@ -0,0 +1,292 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using RR.Common.Controls;
|
||||
using RR.Common.Settings;
|
||||
using RR.Game2D.Core.Managers;
|
||||
|
||||
namespace RR.Common.Managers
|
||||
{
|
||||
/// <summary>
|
||||
/// Менеджер отслеживания бездействия игрока.
|
||||
/// При отсутствии ввода в течение заданного времени выкидывает игрока в главное меню.
|
||||
/// </summary>
|
||||
public class InactivityManager : MonoBehaviour
|
||||
{
|
||||
public static InactivityManager Instance { get; private set; }
|
||||
|
||||
[Header("Настройки бездействия")]
|
||||
[SerializeField] private float inactivityTimeout = 300f; // 5 минут в секундах
|
||||
[SerializeField] private bool enableInactivityCheck = true;
|
||||
|
||||
[Header("Отладка")]
|
||||
[SerializeField] private bool showDebugInfo;
|
||||
|
||||
private float lastInputTime;
|
||||
private bool isInMainMenu = true;
|
||||
private bool isGameActive;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
Initialize();
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
lastInputTime = Time.unscaledTime;
|
||||
|
||||
// Подписываемся на все возможные источники ввода
|
||||
SubscribeToInputEvents();
|
||||
|
||||
// Подписываемся на события смены сцены
|
||||
UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
|
||||
|
||||
// Подписываемся на изменения настроек
|
||||
var settingsManager = SettingsManager.Instance;
|
||||
if (settingsManager != null)
|
||||
{
|
||||
settingsManager.OnSettingsLoaded += LoadSettingsValues;
|
||||
settingsManager.OnSettingsSaved += LoadSettingsValues;
|
||||
LoadSettingsValues();
|
||||
}
|
||||
|
||||
Debug.Log("[InactivityManager] Инициализирован");
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
UnsubscribeFromInputEvents();
|
||||
UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded;
|
||||
|
||||
var settingsManager = SettingsManager.Instance;
|
||||
if (settingsManager != null)
|
||||
{
|
||||
settingsManager.OnSettingsLoaded -= LoadSettingsValues;
|
||||
settingsManager.OnSettingsSaved -= LoadSettingsValues;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Загружает значения из настроек
|
||||
/// </summary>
|
||||
private void LoadSettingsValues()
|
||||
{
|
||||
var settings = SettingsManager.Instance;
|
||||
if (settings == null)
|
||||
{
|
||||
Debug.LogWarning("[InactivityManager] SettingsManager is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
inactivityTimeout = settings.GetInt("gameplay.inactivityTimeout");
|
||||
|
||||
if (inactivityTimeout == 0)
|
||||
{
|
||||
Debug.Log("[InactivityManager] Отслеживание бездействия отключено (таймаут = 0)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[InactivityManager] Таймаут бездействия: {inactivityTimeout}s");
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeToInputEvents()
|
||||
{
|
||||
// Подписываемся на события ComWrapper (кнопки автомата)
|
||||
if (ComWrapper.OnInputReceived != null)
|
||||
{
|
||||
ComWrapper.OnInputReceived.AddListener(OnComInput);
|
||||
}
|
||||
|
||||
// InputSystem.onAnyButtonPress - это событие, которое срабатывает каждый раз
|
||||
// когда нажимается любая кнопка. Мы просто слушаем его в Update через CheckForAnyInput
|
||||
}
|
||||
|
||||
private void UnsubscribeFromInputEvents()
|
||||
{
|
||||
if (ComWrapper.OnInputReceived != null)
|
||||
{
|
||||
ComWrapper.OnInputReceived.RemoveListener(OnComInput);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Проверяем на наличие любого ввода каждый кадр
|
||||
CheckForAnyInput();
|
||||
|
||||
// Проверяем таймер бездействия
|
||||
if (enableInactivityCheck && !isInMainMenu && isGameActive)
|
||||
{
|
||||
float timeSinceLastInput = Time.unscaledTime - lastInputTime;
|
||||
|
||||
if (showDebugInfo && Time.frameCount % 60 == 0)
|
||||
{
|
||||
Debug.Log($"[InactivityManager] Время бездействия: {timeSinceLastInput:F1}s / {inactivityTimeout}s");
|
||||
}
|
||||
|
||||
if (timeSinceLastInput >= inactivityTimeout)
|
||||
{
|
||||
OnInactivityTimeout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет на наличие любого ввода с клавиатуры, мыши или геймпада
|
||||
/// </summary>
|
||||
private void CheckForAnyInput()
|
||||
{
|
||||
if (isInMainMenu || !isGameActive)
|
||||
return;
|
||||
|
||||
// Проверяем клавиатуру
|
||||
if (Keyboard.current != null && Keyboard.current.anyKey.wasPressedThisFrame)
|
||||
{
|
||||
ResetInactivityTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем мышь
|
||||
if (Mouse.current != null)
|
||||
{
|
||||
if (Mouse.current.leftButton.wasPressedThisFrame ||
|
||||
Mouse.current.rightButton.wasPressedThisFrame ||
|
||||
Mouse.current.middleButton.wasPressedThisFrame ||
|
||||
Mouse.current.delta.ReadValue().sqrMagnitude > 0.1f)
|
||||
{
|
||||
ResetInactivityTimer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем геймпад
|
||||
if (Gamepad.current != null)
|
||||
{
|
||||
foreach (var control in Gamepad.current.allControls)
|
||||
{
|
||||
if (control is UnityEngine.InputSystem.Controls.ButtonControl button && button.wasPressedThisFrame)
|
||||
{
|
||||
ResetInactivityTimer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnComInput(string input)
|
||||
{
|
||||
if (!isInMainMenu && isGameActive)
|
||||
{
|
||||
ResetInactivityTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сбрасывает таймер бездействия
|
||||
/// </summary>
|
||||
public void ResetInactivityTimer()
|
||||
{
|
||||
lastInputTime = Time.unscaledTime;
|
||||
|
||||
if (showDebugInfo)
|
||||
{
|
||||
Debug.Log("[InactivityManager] Таймер сброшен");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается при достижении таймаута бездействия
|
||||
/// </summary>
|
||||
private void OnInactivityTimeout()
|
||||
{
|
||||
Debug.Log("[InactivityManager] Таймаут бездействия! Возврат в главное меню...");
|
||||
|
||||
enableInactivityCheck = false; // Отключаем проверку до следующей игры
|
||||
|
||||
// Останавливаем игру
|
||||
if (GameManager.Instance != null)
|
||||
{
|
||||
GameManager.Instance.SetPause(false);
|
||||
GameManager.Instance.LoadScene("MainMenu", false);
|
||||
}
|
||||
|
||||
ResetInactivityTimer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик загрузки сцены
|
||||
/// </summary>
|
||||
private void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode)
|
||||
{
|
||||
isInMainMenu = scene.name == "MainMenu";
|
||||
|
||||
if (isInMainMenu)
|
||||
{
|
||||
enableInactivityCheck = false;
|
||||
isGameActive = false;
|
||||
Debug.Log("[InactivityManager] Главное меню загружено - проверка отключена");
|
||||
}
|
||||
|
||||
ResetInactivityTimer();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Включает проверку бездействия (вызывается при старте игры)
|
||||
/// </summary>
|
||||
public void EnableInactivityCheck()
|
||||
{
|
||||
enableInactivityCheck = true;
|
||||
isGameActive = true;
|
||||
ResetInactivityTimer();
|
||||
Debug.Log("[InactivityManager] Проверка бездействия включена");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отключает проверку бездействия
|
||||
/// </summary>
|
||||
public void DisableInactivityCheck()
|
||||
{
|
||||
enableInactivityCheck = false;
|
||||
isGameActive = false;
|
||||
Debug.Log("[InactivityManager] Проверка бездействия отключена");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает время таймаута бездействия.
|
||||
/// Если установлено 0, отслеживание бездействия отключается.
|
||||
/// </summary>
|
||||
public void SetInactivityTimeout(float timeoutSeconds)
|
||||
{
|
||||
inactivityTimeout = Mathf.Max(0f, timeoutSeconds); // 0 = отключено, минимум 0
|
||||
|
||||
if (inactivityTimeout == 0f)
|
||||
{
|
||||
Debug.Log("[InactivityManager] Отслеживание бездействия отключено (таймаут = 0)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[InactivityManager] Таймаут установлен: {inactivityTimeout}s");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает текущее время таймаута
|
||||
/// </summary>
|
||||
public float GetInactivityTimeout() => inactivityTimeout;
|
||||
|
||||
/// <summary>
|
||||
/// Получает время с момента последнего ввода
|
||||
/// </summary>
|
||||
public float GetTimeSinceLastInput() => Time.unscaledTime - lastInputTime;
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/Managers/InactivityManager.cs.meta
Normal file
3
Assets/Scripts/Managers/InactivityManager.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e51a2ca5cae54b7d9d78b28a63eea271
|
||||
timeCreated: 1770626227
|
||||
@ -7,6 +7,12 @@ namespace RR.Common.Settings
|
||||
{
|
||||
public static void Apply(SettingEntry def, SettingValue v)
|
||||
{
|
||||
if (def == null)
|
||||
{
|
||||
Debug.LogWarning("[SettingsApplier] Cannot apply settings: SettingEntry is null");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (def.applyTarget)
|
||||
{
|
||||
case ApplyTarget.None:
|
||||
|
||||
@ -56,7 +56,6 @@ namespace RR.Common.Settings
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(buttonKey))
|
||||
{
|
||||
Debug.LogWarning("[SettingsButtonDispatcher] Dispatch called with empty key");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -66,13 +65,14 @@ namespace RR.Common.Settings
|
||||
return InvokeBinders(list);
|
||||
}
|
||||
|
||||
// Fallback: ищем все биндеры в сцене (включая неактивные)
|
||||
// Fallback: ищем ВСЕ биндеры в сцене (включая на неактивных объектах)
|
||||
var allBinders = Object.FindObjectsByType<SettingsButtonBinder>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
var matchingBinders = new List<SettingsButtonBinder>();
|
||||
|
||||
foreach (var binder in allBinders)
|
||||
{
|
||||
if (binder.Key == buttonKey)
|
||||
// Проверяем что биндер имеет нужный ключ и компонент enabled
|
||||
if (binder.enabled && binder.Key == buttonKey)
|
||||
{
|
||||
matchingBinders.Add(binder);
|
||||
// Регистрируем для будущих вызовов
|
||||
|
||||
@ -45,6 +45,51 @@ namespace RR.Common.Settings
|
||||
public static SettingValue Int(int v) => new() { type = SettingValueType.Int, i = v };
|
||||
public static SettingValue Float(float v) => new() { type = SettingValueType.Float, f = v };
|
||||
public static SettingValue String(string v) => new() { type = SettingValueType.String, s = v };
|
||||
|
||||
/// <summary>
|
||||
/// Создает компактную версию для сериализации (только нужные поля)
|
||||
/// </summary>
|
||||
public SettingValueCompact ToCompact()
|
||||
{
|
||||
return new SettingValueCompact
|
||||
{
|
||||
type = (int)type,
|
||||
value = type switch
|
||||
{
|
||||
SettingValueType.Bool => b ? "1" : "0",
|
||||
SettingValueType.Int => i.ToString(),
|
||||
SettingValueType.Float => f.ToString("F6"),
|
||||
SettingValueType.String => s ?? "",
|
||||
_ => ""
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Создает SettingValue из компактной версии
|
||||
/// </summary>
|
||||
public static SettingValue FromCompact(SettingValueCompact compact)
|
||||
{
|
||||
var type = (SettingValueType)compact.type;
|
||||
return type switch
|
||||
{
|
||||
SettingValueType.Bool => Bool(compact.value == "1"),
|
||||
SettingValueType.Int => Int(int.TryParse(compact.value, out int i) ? i : 0),
|
||||
SettingValueType.Float => Float(float.TryParse(compact.value, out float f) ? f : 0f),
|
||||
SettingValueType.String => String(compact.value ?? ""),
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Компактная версия SettingValue для сериализации (экономит место в JSON)
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct SettingValueCompact
|
||||
{
|
||||
public int type;
|
||||
public string value;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@ -16,7 +16,7 @@ namespace RR.Common.Settings
|
||||
public class Entry
|
||||
{
|
||||
public string key;
|
||||
public SettingValue value;
|
||||
public SettingValueCompact value; // Используем компактную версию
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,7 @@ namespace RR.Common.Settings
|
||||
if (!database)
|
||||
{
|
||||
Debug.LogError("[SettingsManager] Database is not assigned! Settings will not work.");
|
||||
return;
|
||||
}
|
||||
|
||||
LoadOrDefaults();
|
||||
@ -227,8 +228,9 @@ namespace RR.Common.Settings
|
||||
continue;
|
||||
}
|
||||
|
||||
// Применяем настройку
|
||||
SettingValue clean = def.Sanitize(e.value);
|
||||
// Конвертируем из компактного формата и применяем настройку
|
||||
SettingValue value = SettingValue.FromCompact(e.value);
|
||||
SettingValue clean = def.Sanitize(value);
|
||||
map[e.key] = clean;
|
||||
SettingsApplier.Apply(def, clean);
|
||||
OnSettingChanged?.Invoke(e.key, clean);
|
||||
@ -276,7 +278,7 @@ namespace RR.Common.Settings
|
||||
foreach (SettingEntry def in database.entries)
|
||||
{
|
||||
if (def == null || string.IsNullOrWhiteSpace(def.key)) continue;
|
||||
if (!string.Equals(def.group, groupName, System.StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (!string.Equals(def.group, groupName, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
map[def.key] = def.DefaultValue;
|
||||
SettingsApplier.Apply(def, def.DefaultValue);
|
||||
@ -305,19 +307,32 @@ namespace RR.Common.Settings
|
||||
foreach (SettingsSaveData.Entry e in data.entries)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(e.key)) continue;
|
||||
|
||||
|
||||
// Получаем определение
|
||||
SettingEntry def = Define(e.key);
|
||||
|
||||
// Если checkExposed = true, проверяем флаг exposed
|
||||
if (checkExposed)
|
||||
{
|
||||
SettingEntry def = Define(e.key);
|
||||
if (def != null && !def.exposed)
|
||||
{
|
||||
Debug.LogWarning($"[SettingsManager] Skipping '{e.key}' from file - not exposed.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Конвертируем из компактного формата
|
||||
SettingValue value = SettingValue.FromCompact(e.value);
|
||||
|
||||
map[e.key] = e.value;
|
||||
// Применяем Sanitize если есть определение
|
||||
if (def != null)
|
||||
{
|
||||
map[e.key] = def.Sanitize(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
map[e.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -362,8 +377,15 @@ namespace RR.Common.Settings
|
||||
{
|
||||
SettingsSaveData data = new();
|
||||
|
||||
// Конвертируем значения в компактный формат
|
||||
foreach (var kv in map)
|
||||
data.entries.Add(new SettingsSaveData.Entry { key = kv.Key, value = kv.Value });
|
||||
{
|
||||
data.entries.Add(new SettingsSaveData.Entry
|
||||
{
|
||||
key = kv.Key,
|
||||
value = kv.Value.ToCompact() // Конвертация в компактный формат
|
||||
});
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@ -91,7 +91,7 @@ MonoBehaviour:
|
||||
buttonSprite: {fileID: 0}
|
||||
options: []
|
||||
defaultOptionIndex: 0
|
||||
- key: payment.supportNumber
|
||||
- key: payment.showSupportNumber
|
||||
displayName: "\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u043D\u043E\u043C\u0435\u0440
|
||||
\u043F\u043E\u0434\u0434\u0435\u0440\u0436\u043A\u0438"
|
||||
description:
|
||||
@ -118,6 +118,32 @@ MonoBehaviour:
|
||||
buttonSprite: {fileID: 0}
|
||||
options: []
|
||||
defaultOptionIndex: 0
|
||||
- key: payment.supportNumber
|
||||
displayName: "\u041D\u043E\u043C\u0435\u0440 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u043A\u0438"
|
||||
description:
|
||||
group: "\u041E\u043F\u043B\u0430\u0442\u0430 \u0438 \u0441\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C"
|
||||
shownInSettings: 0
|
||||
exposed: 0
|
||||
valueType: 3
|
||||
defaultBool: 1
|
||||
defaultInt: 0
|
||||
defaultFloat: 0
|
||||
defaultString: +79999999999
|
||||
minInt: 0
|
||||
maxInt: 0
|
||||
intDisplayFormat: 0
|
||||
intStep: 1
|
||||
minFloat: 0
|
||||
maxFloat: 1
|
||||
displayAsPercentage: 0
|
||||
applyTarget: 0
|
||||
audioTargetType: 0
|
||||
buttonText:
|
||||
buttonData:
|
||||
buttonColor: {r: 0, g: 0, b: 0, a: 0}
|
||||
buttonSprite: {fileID: 0}
|
||||
options: []
|
||||
defaultOptionIndex: 0
|
||||
- key: payment.paymentImpulseCost
|
||||
displayName: "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C \u0438\u043C\u043F\u0443\u043B\u044C\u0441\u0430
|
||||
\u043E\u043F\u043B\u0430\u0442\u044B"
|
||||
@ -360,15 +386,15 @@ MonoBehaviour:
|
||||
options: []
|
||||
defaultOptionIndex: 0
|
||||
- key: gameplay.additionalLives
|
||||
displayName: "\u0416\u0438\u0437\u043D\u0438 \u0437\u0430 \u0434\u043E\u043F.
|
||||
\u043C\u043E\u043D\u0435\u0442\u044B"
|
||||
displayName: "\u0414\u043E\u043F. \u0436\u0438\u0437\u043D\u0438 \u0437\u0430
|
||||
\u043A\u0440\u0435\u0434\u0438\u0442\u044B"
|
||||
description:
|
||||
group: "\u0418\u0433\u0440\u043E\u0432\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441"
|
||||
shownInSettings: 1
|
||||
exposed: 0
|
||||
valueType: 1
|
||||
defaultBool: 0
|
||||
defaultInt: 1
|
||||
defaultInt: 3
|
||||
defaultFloat: 0
|
||||
defaultString:
|
||||
minInt: 0
|
||||
@ -386,6 +412,33 @@ MonoBehaviour:
|
||||
buttonSprite: {fileID: 0}
|
||||
options: []
|
||||
defaultOptionIndex: 0
|
||||
- key: gameplay.inactivityTimeout
|
||||
displayName: "\u041C\u0430\u043A\u0441. \u0432\u0440\u0435\u043C\u044F \u0431\u0435\u0437
|
||||
\u0432\u0432\u043E\u0434\u0430"
|
||||
description:
|
||||
group: "\u0418\u0433\u0440\u043E\u0432\u043E\u0439 \u043F\u0440\u043E\u0446\u0435\u0441\u0441"
|
||||
shownInSettings: 1
|
||||
exposed: 0
|
||||
valueType: 1
|
||||
defaultBool: 0
|
||||
defaultInt: 300
|
||||
defaultFloat: 300
|
||||
defaultString:
|
||||
minInt: 0
|
||||
maxInt: 3000
|
||||
intDisplayFormat: 1
|
||||
intStep: 10
|
||||
minFloat: 10
|
||||
maxFloat: 3000
|
||||
displayAsPercentage: 0
|
||||
applyTarget: 0
|
||||
audioTargetType: 0
|
||||
buttonText:
|
||||
buttonData:
|
||||
buttonColor: {r: 0, g: 0, b: 0, a: 0}
|
||||
buttonSprite: {fileID: 0}
|
||||
options: []
|
||||
defaultOptionIndex: 0
|
||||
- key: btn.statistics
|
||||
displayName: "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430"
|
||||
description:
|
||||
|
||||
@ -31,15 +31,27 @@ namespace RR.Common.UI
|
||||
|
||||
private SettingEntry def;
|
||||
private bool isRegistered;
|
||||
private TextInputSettingsBinder textInputBinder;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Регистрируемся сразу при создании (даже если объект неактивен)
|
||||
RegisterIfNeeded();
|
||||
Debug.Log($"[SettingsButtonBinder] Awake() called for key '{key}' on GameObject '{gameObject.name}'");
|
||||
|
||||
// Получаем ссылку на TextInputSettingsBinder если он есть
|
||||
textInputBinder = GetComponent<TextInputSettingsBinder>();
|
||||
|
||||
if (textInputBinder != null)
|
||||
{
|
||||
Debug.Log($"[SettingsButtonBinder] Found TextInputSettingsBinder on GameObject '{gameObject.name}'");
|
||||
}
|
||||
|
||||
// НЕ регистрируемся в Awake - только при OnEnable
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Debug.Log($"[SettingsButtonBinder] Start() called for key '{key}'");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key)) return;
|
||||
|
||||
if (SettingsManager.Instance)
|
||||
@ -60,18 +72,29 @@ namespace RR.Common.UI
|
||||
{
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(OnButtonClick);
|
||||
Debug.Log($"[SettingsButtonBinder] Attached OnButtonClick to button for key '{key}'");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// На случай если Awake не был вызван (объект был неактивен при загрузке сцены)
|
||||
// Регистрируемся только когда объект активен
|
||||
RegisterIfNeeded();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Отписываемся при деактивации
|
||||
if (isRegistered)
|
||||
{
|
||||
SettingsButtonDispatcher.Unregister(key, this);
|
||||
isRegistered = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Отписываемся только при уничтожении
|
||||
// Отписываемся только при уничтожении (на всякий случай)
|
||||
if (isRegistered)
|
||||
{
|
||||
SettingsButtonDispatcher.Unregister(key, this);
|
||||
@ -97,6 +120,13 @@ namespace RR.Common.UI
|
||||
/// </summary>
|
||||
public void Invoke()
|
||||
{
|
||||
// Если есть TextInputSettingsBinder, вызываем его
|
||||
if (textInputBinder != null)
|
||||
{
|
||||
textInputBinder.OpenTextInput();
|
||||
}
|
||||
|
||||
// Вызываем обычные действия кнопки
|
||||
onButtonClick?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,13 +112,19 @@ namespace RR.Common.UI
|
||||
yield return null;
|
||||
|
||||
// Принудительно обновляем layout для корректной навигации
|
||||
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(contentRoot as RectTransform);
|
||||
if (contentRoot != null && contentRoot is RectTransform rectTransform)
|
||||
{
|
||||
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
|
||||
}
|
||||
|
||||
// Ещё кадр для применения layout
|
||||
yield return null;
|
||||
|
||||
// Находим первый Selectable элемент
|
||||
FirstSelectable = contentRoot.GetComponentInChildren<UnityEngine.UI.Selectable>();
|
||||
if (contentRoot != null)
|
||||
{
|
||||
FirstSelectable = contentRoot.GetComponentInChildren<UnityEngine.UI.Selectable>();
|
||||
}
|
||||
|
||||
// Оповещаем о завершении
|
||||
OnBuildComplete?.Invoke();
|
||||
@ -224,6 +230,12 @@ namespace RR.Common.UI
|
||||
|
||||
private void SpawnSettingRow(SettingEntry entry, Transform parent)
|
||||
{
|
||||
if (entry == null || parent == null)
|
||||
{
|
||||
Debug.LogWarning("[SettingsUIGenerator] Cannot spawn setting row: entry or parent is null");
|
||||
return;
|
||||
}
|
||||
|
||||
SettingUIItem prefab = PickPrefab(entry);
|
||||
if (!prefab)
|
||||
{
|
||||
@ -307,8 +319,15 @@ namespace RR.Common.UI
|
||||
}
|
||||
spawnedGroups.Clear();
|
||||
|
||||
for (int i = contentRoot.childCount - 1; i >= 0; i--)
|
||||
Destroy(contentRoot.GetChild(i).gameObject);
|
||||
if (contentRoot != null)
|
||||
{
|
||||
for (int i = contentRoot.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Transform child = contentRoot.GetChild(i);
|
||||
if (child != null)
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
spawnedItems.Clear();
|
||||
}
|
||||
|
||||
246
Assets/Scripts/Settings/UI/TextInputSettingsBinder.cs
Normal file
246
Assets/Scripts/Settings/UI/TextInputSettingsBinder.cs
Normal file
@ -0,0 +1,246 @@
|
||||
using RR.Common.Settings;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Common.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Биндер для связывания кнопки настроек с модальным окном ввода текста.
|
||||
/// При нажатии кнопки открывает TextInputModal с текущим значением настройки,
|
||||
/// при подтверждении ввода сохраняет новое значение в настройку.
|
||||
///
|
||||
/// ИСПОЛЬЗОВАНИЕ:
|
||||
/// 1. Создайте в MainSettingsDatabase настройку типа String (например, "prize.description")
|
||||
/// 2. На объект с SettingsButtonBinder добавьте компонент TextInputSettingsBinder
|
||||
/// 3. Укажите ссылку на TextInputModal (должен быть в сцене)
|
||||
/// 4. Настройте параметры:
|
||||
/// - settingKey: ключ настройки (если пусто, используется ключ кнопки)
|
||||
/// - modalTitle: заголовок окна ввода
|
||||
/// - maxLength: максимальная длина текста
|
||||
/// - customAlphabet: кастомный алфавит (опционально)
|
||||
/// 5. При нажатии кнопки в настройках автоматически откроется окно ввода
|
||||
/// 6. При подтверждении ввода значение сохранится в настройку
|
||||
///
|
||||
/// ПРИМЕР НАСТРОЙКИ В MainSettingsDatabase:
|
||||
/// {
|
||||
/// "key": "prize.phoneNumber",
|
||||
/// "displayName": "Номер телефона",
|
||||
/// "description": "Введите номер телефона для связи",
|
||||
/// "group": "Prizes",
|
||||
/// "valueType": "String",
|
||||
/// "defaultString": "",
|
||||
/// "shownInSettings": true,
|
||||
/// "exposed": true
|
||||
/// }
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(SettingsButtonBinder))]
|
||||
public class TextInputSettingsBinder : MonoBehaviour
|
||||
{
|
||||
[Header("Text Input Settings")]
|
||||
[Tooltip("Ключ настройки для сохранения/загрузки значения")]
|
||||
[SerializeField] private string settingKey;
|
||||
|
||||
[Tooltip("Ссылка на модальное окно ввода текста")]
|
||||
[SerializeField] private TextInputModal textInputModal;
|
||||
|
||||
[Header("Modal Settings")]
|
||||
[Tooltip("Заголовок модального окна")]
|
||||
[SerializeField] private string modalTitle = "Введите текст";
|
||||
|
||||
[Tooltip("Максимальная длина текста (количество ячеек)")]
|
||||
[SerializeField] private int maxLength = 10;
|
||||
|
||||
[Tooltip("Алфавит для ввода (если пусто - использует дефолтный)")]
|
||||
[SerializeField] private string[] customAlphabet;
|
||||
|
||||
private SettingsButtonBinder buttonBinder;
|
||||
private SettingEntry settingEntry;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
buttonBinder = GetComponent<SettingsButtonBinder>();
|
||||
|
||||
// Если ключ настройки не задан, используем ключ кнопки
|
||||
if (string.IsNullOrWhiteSpace(settingKey))
|
||||
{
|
||||
settingKey = buttonBinder.Key;
|
||||
}
|
||||
|
||||
Debug.Log($"[TextInputSettingsBinder] settingKey: '{settingKey}'");
|
||||
|
||||
// Если TextInputModal не назначен, ищем его на сцене
|
||||
if (textInputModal == null)
|
||||
{
|
||||
textInputModal = FindFirstObjectByType<TextInputModal>();
|
||||
|
||||
if (textInputModal == null)
|
||||
{
|
||||
Debug.LogError("[TextInputSettingsBinder] TextInputModal not found in scene! Please add TextInputModal to the scene or assign it manually.", this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[TextInputSettingsBinder] Found TextInputModal on scene: {textInputModal.name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(settingKey))
|
||||
{
|
||||
Debug.LogError("[TextInputSettingsBinder] Setting key is not set!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем определение настройки
|
||||
if (SettingsManager.Instance)
|
||||
{
|
||||
settingEntry = SettingsManager.Instance.Define(settingKey);
|
||||
|
||||
if (settingEntry == null)
|
||||
{
|
||||
Debug.LogWarning($"[TextInputSettingsBinder] Setting '{settingKey}' not found in SettingsManager!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (textInputModal != null)
|
||||
{
|
||||
textInputModal.OnTextConfirmed += OnTextConfirmed;
|
||||
textInputModal.OnTextCancelled += OnTextCancelled;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (textInputModal != null)
|
||||
{
|
||||
textInputModal.OnTextConfirmed -= OnTextConfirmed;
|
||||
textInputModal.OnTextCancelled -= OnTextCancelled;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Открывает модальное окно ввода текста с текущим значением настройки
|
||||
/// </summary>
|
||||
public void OpenTextInput()
|
||||
{
|
||||
if (textInputModal == null)
|
||||
{
|
||||
Debug.LogError("[TextInputSettingsBinder] TextInputModal is not assigned!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Загружаем текущее значение из настройки
|
||||
string currentValue = LoadCurrentValue();
|
||||
|
||||
// ВАЖНО: Порядок вызовов:
|
||||
// 1. Сначала устанавливаем алфавит (если задан)
|
||||
if (customAlphabet != null && customAlphabet.Length > 0)
|
||||
{
|
||||
textInputModal.SetAlphabet(customAlphabet);
|
||||
}
|
||||
|
||||
// 2. Потом устанавливаем количество ячеек (это создаст ячейки с сохраненным текстом)
|
||||
if (maxLength > 0)
|
||||
{
|
||||
textInputModal.SetCellCount(maxLength);
|
||||
}
|
||||
|
||||
// 3. Открываем модалку с текущим значением (ячейки уже созданы, просто обновим значения)
|
||||
textInputModal.OpenForInput(modalTitle, currentValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Загружает текущее значение настройки
|
||||
/// </summary>
|
||||
private string LoadCurrentValue()
|
||||
{
|
||||
if (SettingsManager.Instance == null)
|
||||
{
|
||||
Debug.LogWarning("[TextInputSettingsBinder] SettingsManager is not available!");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Проверяем, существует ли настройка
|
||||
var entry = SettingsManager.Instance.Define(settingKey);
|
||||
if (entry == null)
|
||||
{
|
||||
Debug.LogWarning($"[TextInputSettingsBinder] Setting '{settingKey}' not found!");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Пытаемся загрузить значение как строку
|
||||
try
|
||||
{
|
||||
string value = SettingsManager.Instance.GetString(settingKey);
|
||||
return value ?? "";
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[TextInputSettingsBinder] Failed to load value for '{settingKey}': {ex.Message}");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается при подтверждении ввода текста
|
||||
/// </summary>
|
||||
private void OnTextConfirmed(string text)
|
||||
{
|
||||
if (SettingsManager.Instance == null)
|
||||
{
|
||||
Debug.LogError("[TextInputSettingsBinder] SettingsManager is not available!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем значение в настройку
|
||||
SettingsManager.Instance.SetString(settingKey, text);
|
||||
|
||||
// Автоматически сохраняем настройки на диск
|
||||
SettingsManager.Instance.SaveSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается при отмене ввода текста
|
||||
/// </summary>
|
||||
private void OnTextCancelled()
|
||||
{
|
||||
// Ничего не делаем при отмене
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает заголовок модального окна
|
||||
/// </summary>
|
||||
public void SetModalTitle(string title)
|
||||
{
|
||||
modalTitle = title;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает максимальную длину текста
|
||||
/// </summary>
|
||||
public void SetMaxLength(int length)
|
||||
{
|
||||
maxLength = length;
|
||||
if (textInputModal != null)
|
||||
{
|
||||
textInputModal.SetCellCount(length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает пользовательский алфавит
|
||||
/// </summary>
|
||||
public void SetCustomAlphabet(string[] alphabet)
|
||||
{
|
||||
customAlphabet = alphabet;
|
||||
if (textInputModal != null && alphabet != null && alphabet.Length > 0)
|
||||
{
|
||||
textInputModal.SetAlphabet(alphabet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd0446f37d5e454387522c6223014f06
|
||||
timeCreated: 1770641010
|
||||
3
Assets/Scripts/UI/TextInput.meta
Normal file
3
Assets/Scripts/UI/TextInput.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c26f2b5339624e658c1edefc0f358d7b
|
||||
timeCreated: 1770629979
|
||||
116
Assets/Scripts/UI/TextInput/TextInputAlphabets.cs
Normal file
116
Assets/Scripts/UI/TextInput/TextInputAlphabets.cs
Normal file
@ -0,0 +1,116 @@
|
||||
namespace RR.Common.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Предустановленные алфавиты для TextInputModal
|
||||
/// </summary>
|
||||
public static class TextInputAlphabets
|
||||
{
|
||||
/// <summary>
|
||||
/// Только цифры (0-9)
|
||||
/// </summary>
|
||||
public static readonly string[] Numeric = new string[]
|
||||
{
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Цифры и спецсимволы для телефонных номеров
|
||||
/// </summary>
|
||||
public static readonly string[] PhoneNumber = new string[]
|
||||
{
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"+", "-", "(", ")", " "
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Английские буквы в верхнем регистре
|
||||
/// </summary>
|
||||
public static readonly string[] UppercaseEnglish = new string[]
|
||||
{
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Английские буквы в нижнем регистре
|
||||
/// </summary>
|
||||
public static readonly string[] LowercaseEnglish = new string[]
|
||||
{
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
|
||||
"u", "v", "w", "x", "y", "z"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Английские буквы (обоих регистров) и цифры
|
||||
/// </summary>
|
||||
public static readonly string[] Alphanumeric = new string[]
|
||||
{
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z",
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
|
||||
"u", "v", "w", "x", "y", "z"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Английские буквы в верхнем регистре, цифры и базовые спецсимволы
|
||||
/// </summary>
|
||||
public static readonly string[] Standard = new string[]
|
||||
{
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z",
|
||||
" ", "-", "_", ".", ",", "!"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Русские буквы в верхнем регистре
|
||||
/// </summary>
|
||||
public static readonly string[] UppercaseRussian = new string[]
|
||||
{
|
||||
"А", "Б", "В", "Г", "Д", "Е", "Ё", "Ж", "З", "И", "Й",
|
||||
"К", "Л", "М", "Н", "О", "П", "Р", "С", "Т", "У", "Ф",
|
||||
"Х", "Ц", "Ч", "Ш", "Щ", "Ъ", "Ы", "Ь", "Э", "Ю", "Я"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Русские буквы в нижнем регистре
|
||||
/// </summary>
|
||||
public static readonly string[] LowercaseRussian = new string[]
|
||||
{
|
||||
"а", "б", "в", "г", "д", "е", "ё", "ж", "з", "и", "й",
|
||||
"к", "л", "м", "н", "о", "п", "р", "с", "т", "у", "ф",
|
||||
"х", "ц", "ч", "ш", "щ", "ъ", "ы", "ь", "э", "ю", "я"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Русские буквы (верхний регистр), цифры и базовые спецсимволы
|
||||
/// </summary>
|
||||
public static readonly string[] RussianStandard = new string[]
|
||||
{
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"А", "Б", "В", "Г", "Д", "Е", "Ё", "Ж", "З", "И", "Й",
|
||||
"К", "Л", "М", "Н", "О", "П", "Р", "С", "Т", "У", "Ф",
|
||||
"Х", "Ц", "Ч", "Ш", "Щ", "Ъ", "Ы", "Ь", "Э", "Ю", "Я",
|
||||
" ", "-", "_"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Email-friendly алфавит (английские буквы в нижнем регистре, цифры, @ и точка)
|
||||
/// </summary>
|
||||
public static readonly string[] Email = new string[]
|
||||
{
|
||||
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
|
||||
"k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
|
||||
"u", "v", "w", "x", "y", "z",
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"@", ".", "_", "-"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/UI/TextInput/TextInputAlphabets.cs.meta
Normal file
3
Assets/Scripts/UI/TextInput/TextInputAlphabets.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 02b4c67a814d4efa9f5104db5b88a7d0
|
||||
timeCreated: 1770630028
|
||||
246
Assets/Scripts/UI/TextInput/TextInputCell.cs
Normal file
246
Assets/Scripts/UI/TextInput/TextInputCell.cs
Normal file
@ -0,0 +1,246 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
namespace RR.Common.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Ячейка для ввода одного символа.
|
||||
/// Символы переключаются циклически при нажатии кнопки "вверх".
|
||||
/// Наследуется от Selectable для поддержки навигации.
|
||||
/// </summary>
|
||||
public class TextInputCell : Selectable
|
||||
{
|
||||
[Header("UI References")]
|
||||
[SerializeField] private TextMeshProUGUI symbolText;
|
||||
[SerializeField] private Image backgroundImage;
|
||||
[SerializeField] private Image selectionHighlight;
|
||||
|
||||
[Header("Visual Settings")]
|
||||
[SerializeField] private Color normalColor = Color.white;
|
||||
[SerializeField] private Color selectedColor = Color.yellow;
|
||||
[SerializeField] private Color highlightColor = new Color(1f, 1f, 0f, 0.3f);
|
||||
|
||||
private string[] alphabet;
|
||||
private int currentIndex;
|
||||
private bool isSelected;
|
||||
|
||||
/// <summary>
|
||||
/// Событие, вызываемое при изменении символа
|
||||
/// </summary>
|
||||
public event System.Action<TextInputCell> OnSymbolChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Текущий выбранный символ
|
||||
/// </summary>
|
||||
public string CurrentSymbol => alphabet != null && alphabet.Length > 0 ? alphabet[currentIndex] : "";
|
||||
|
||||
/// <summary>
|
||||
/// Индекс текущего символа
|
||||
/// </summary>
|
||||
public int CurrentIndex => currentIndex;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
if (selectionHighlight)
|
||||
{
|
||||
selectionHighlight.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
// Устанавливаем targetGraphic для визуальной обратной связи
|
||||
if (backgroundImage != null)
|
||||
{
|
||||
targetGraphic = backgroundImage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переопределяем DoStateTransition для кастомной визуализации
|
||||
/// </summary>
|
||||
protected override void DoStateTransition(SelectionState state, bool instant)
|
||||
{
|
||||
base.DoStateTransition(state, instant);
|
||||
|
||||
if (!gameObject.activeInHierarchy)
|
||||
return;
|
||||
|
||||
// Обновляем визуализацию в зависимости от состояния
|
||||
bool shouldHighlight = (state == SelectionState.Highlighted ||
|
||||
state == SelectionState.Selected ||
|
||||
state == SelectionState.Pressed) && isSelected;
|
||||
|
||||
if (selectionHighlight != null)
|
||||
{
|
||||
selectionHighlight.gameObject.SetActive(shouldHighlight);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переопределяем OnMove для обработки вверх/вниз и изменения символа
|
||||
/// </summary>
|
||||
public override void OnMove(UnityEngine.EventSystems.AxisEventData eventData)
|
||||
{
|
||||
switch (eventData.moveDir)
|
||||
{
|
||||
case UnityEngine.EventSystems.MoveDirection.Up:
|
||||
// Вверх всегда меняет символ
|
||||
NextSymbol();
|
||||
eventData.Use(); // Отмечаем событие как обработанное
|
||||
break;
|
||||
|
||||
case UnityEngine.EventSystems.MoveDirection.Down:
|
||||
// Вниз всегда меняет символ (на предыдущий)
|
||||
PreviousSymbol();
|
||||
eventData.Use();
|
||||
break;
|
||||
|
||||
case UnityEngine.EventSystems.MoveDirection.Left:
|
||||
case UnityEngine.EventSystems.MoveDirection.Right:
|
||||
// Для влево/вправо используем стандартную навигацию
|
||||
base.OnMove(eventData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует ячейку с алфавитом
|
||||
/// </summary>
|
||||
/// <param name="newAlphabet">Алфавит символов</param>
|
||||
/// <param name="startIndex">Начальный индекс (используется только если symbol = null)</param>
|
||||
/// <param name="symbol">Начальный символ (если указан, ищется в алфавите)</param>
|
||||
public void Initialize(string[] newAlphabet, int startIndex = 0, string symbol = null)
|
||||
{
|
||||
this.alphabet = newAlphabet;
|
||||
|
||||
// Если указан символ, ищем его в алфавите
|
||||
if (!string.IsNullOrEmpty(symbol))
|
||||
{
|
||||
bool found = false;
|
||||
|
||||
// Ищем точное совпадение
|
||||
for (int i = 0; i < newAlphabet.Length; i++)
|
||||
{
|
||||
if (newAlphabet[i] == symbol)
|
||||
{
|
||||
this.currentIndex = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Если не найдено, ищем без учета регистра
|
||||
if (!found)
|
||||
{
|
||||
for (int i = 0; i < newAlphabet.Length; i++)
|
||||
{
|
||||
if (string.Equals(newAlphabet[i], symbol, System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
this.currentIndex = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если символ не найден, устанавливаем первый символ из алфавита
|
||||
if (!found)
|
||||
{
|
||||
this.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Если символ не указан, используем startIndex
|
||||
this.currentIndex = Mathf.Clamp(startIndex, 0, newAlphabet.Length - 1);
|
||||
}
|
||||
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переключает на следующий символ в алфавите (циклически)
|
||||
/// </summary>
|
||||
public void NextSymbol()
|
||||
{
|
||||
if (alphabet == null || alphabet.Length == 0) return;
|
||||
|
||||
currentIndex = (currentIndex + 1) % alphabet.Length;
|
||||
UpdateDisplay();
|
||||
OnSymbolChanged?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переключает на предыдущий символ в алфавите (циклически)
|
||||
/// </summary>
|
||||
public void PreviousSymbol()
|
||||
{
|
||||
if (alphabet == null || alphabet.Length == 0) return;
|
||||
|
||||
currentIndex--;
|
||||
if (currentIndex < 0)
|
||||
currentIndex = alphabet.Length - 1;
|
||||
|
||||
UpdateDisplay();
|
||||
OnSymbolChanged?.Invoke(this);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает индекс символа
|
||||
/// </summary>
|
||||
public void SetSymbolIndex(int index)
|
||||
{
|
||||
if (alphabet == null || alphabet.Length == 0) return;
|
||||
|
||||
currentIndex = Mathf.Clamp(index, 0, alphabet.Length - 1);
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает состояние выделения ячейки
|
||||
/// </summary>
|
||||
public void SetCellSelected(bool selected)
|
||||
{
|
||||
isSelected = selected;
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет визуальное отображение ячейки
|
||||
/// </summary>
|
||||
private void UpdateDisplay()
|
||||
{
|
||||
// Обновляем текст символа
|
||||
if (symbolText && alphabet != null && alphabet.Length > 0)
|
||||
{
|
||||
symbolText.text = alphabet[currentIndex];
|
||||
symbolText.color = isSelected ? selectedColor : normalColor;
|
||||
}
|
||||
|
||||
// Обновляем фон
|
||||
if (backgroundImage)
|
||||
{
|
||||
backgroundImage.color = isSelected ? selectedColor : normalColor;
|
||||
}
|
||||
|
||||
// Обновляем подсветку выделения
|
||||
if (selectionHighlight)
|
||||
{
|
||||
selectionHighlight.gameObject.SetActive(isSelected);
|
||||
selectionHighlight.color = highlightColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сбрасывает ячейку к первому символу
|
||||
/// </summary>
|
||||
public new void Reset()
|
||||
{
|
||||
currentIndex = 0;
|
||||
UpdateDisplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/UI/TextInput/TextInputCell.cs.meta
Normal file
3
Assets/Scripts/UI/TextInput/TextInputCell.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6828bed9c9414fe4bfd284a92d73f36b
|
||||
timeCreated: 1770629979
|
||||
497
Assets/Scripts/UI/TextInput/TextInputModal.cs
Normal file
497
Assets/Scripts/UI/TextInput/TextInputModal.cs
Normal file
@ -0,0 +1,497 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
namespace RR.Common.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Модальное окно для ввода текста через выбор символов.
|
||||
/// Использует ячейки TextInputCell для ввода каждого символа.
|
||||
/// </summary>
|
||||
public class TextInputModal : Modal
|
||||
{
|
||||
[Header("Text Input Settings")]
|
||||
[SerializeField] private int cellCount = 10;
|
||||
[SerializeField] private string[] alphabet = new string[]
|
||||
{
|
||||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
|
||||
"K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
|
||||
"U", "V", "W", "X", "Y", "Z", " ", "-", "_"
|
||||
};
|
||||
|
||||
[Header("UI References")]
|
||||
[SerializeField] private Transform cellsContainer;
|
||||
[SerializeField] private GameObject cellPrefab;
|
||||
[SerializeField] private TextMeshProUGUI titleText;
|
||||
[SerializeField] private TextMeshProUGUI previewText;
|
||||
[SerializeField] private Button confirmButton;
|
||||
[SerializeField] private Button cancelButton;
|
||||
|
||||
private List<TextInputCell> cells = new List<TextInputCell>();
|
||||
private int currentCellIndex;
|
||||
|
||||
// Параметры для отложенной генерации (когда модалка станет активной)
|
||||
private bool needsRegeneration = false;
|
||||
private string pendingText = "";
|
||||
|
||||
// События
|
||||
public event Action<string> OnTextConfirmed;
|
||||
public event Action OnTextCancelled;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
// НЕ генерируем ячейки здесь - они будут созданы через SetCellCount() или SetAlphabet()
|
||||
|
||||
// Настраиваем кнопки
|
||||
if (confirmButton)
|
||||
{
|
||||
confirmButton.onClick.AddListener(ConfirmInput);
|
||||
}
|
||||
|
||||
if (cancelButton)
|
||||
{
|
||||
cancelButton.onClick.AddListener(CancelInput);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
|
||||
// Отписываемся от всех событий ячеек
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (cell != null)
|
||||
{
|
||||
cell.OnSymbolChanged -= OnCellSymbolChanged;
|
||||
}
|
||||
}
|
||||
|
||||
// Отписываемся от кнопок
|
||||
if (confirmButton)
|
||||
{
|
||||
confirmButton.onClick.RemoveListener(ConfirmInput);
|
||||
}
|
||||
|
||||
if (cancelButton)
|
||||
{
|
||||
cancelButton.onClick.RemoveListener(CancelInput);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!gameObject.activeInHierarchy) return;
|
||||
|
||||
// Синхронизируем currentCellIndex с текущим выбранным элементом EventSystem
|
||||
SyncCurrentCellWithEventSystem();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Синхронизирует currentCellIndex с текущим выбранным элементом EventSystem
|
||||
/// </summary>
|
||||
private void SyncCurrentCellWithEventSystem()
|
||||
{
|
||||
if (UnityEngine.EventSystems.EventSystem.current == null) return;
|
||||
|
||||
var selected = UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject;
|
||||
if (selected == null) return;
|
||||
|
||||
// Проверяем, является ли выбранный объект одной из наших ячеек
|
||||
var selectedCell = selected.GetComponent<TextInputCell>();
|
||||
if (selectedCell != null)
|
||||
{
|
||||
int index = cells.IndexOf(selectedCell);
|
||||
if (index >= 0 && index != currentCellIndex)
|
||||
{
|
||||
currentCellIndex = index;
|
||||
// Обновляем визуальное выделение без установки фокуса (чтобы избежать рекурсии)
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
{
|
||||
cells[i].SetCellSelected(i == currentCellIndex);
|
||||
}
|
||||
UpdatePreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Генерирует ячейки для ввода текста
|
||||
/// </summary>
|
||||
/// <param name="initialText">Начальный текст для заполнения ячеек</param>
|
||||
private void GenerateCells(string initialText = "")
|
||||
{
|
||||
Debug.Log($"[TextInputModal] GenerateCells: cellCount={cellCount}, initialText='{initialText}', activeInHierarchy={gameObject.activeInHierarchy}");
|
||||
|
||||
if (cellPrefab == null || cellsContainer == null)
|
||||
{
|
||||
Debug.LogError("[TextInputModal] Cell prefab or container is not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Отписываемся от событий старых ячеек и очищаем их
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (cell != null)
|
||||
{
|
||||
cell.OnSymbolChanged -= OnCellSymbolChanged;
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем существующие ячейки
|
||||
foreach (Transform child in cellsContainer)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
cells.Clear();
|
||||
|
||||
// Создаем новые ячейки с начальными символами
|
||||
for (int i = 0; i < cellCount; i++)
|
||||
{
|
||||
GameObject cellObj = Instantiate(cellPrefab, cellsContainer);
|
||||
TextInputCell cell = cellObj.GetComponent<TextInputCell>();
|
||||
|
||||
if (cell != null)
|
||||
{
|
||||
// Определяем начальный символ для этой ячейки
|
||||
string symbol = (i < initialText.Length) ? initialText[i].ToString() : null;
|
||||
|
||||
// Инициализируем ячейку с алфавитом и символом
|
||||
cell.Initialize(alphabet, 0, symbol);
|
||||
|
||||
// Подписываемся на изменение символа
|
||||
cell.OnSymbolChanged += OnCellSymbolChanged;
|
||||
|
||||
cells.Add(cell);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[TextInputModal] Cell prefab doesn't have TextInputCell component!");
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[TextInputModal] Created {cells.Count} cells. Setting up navigation...");
|
||||
|
||||
// Настраиваем циклическую горизонтальную навигацию
|
||||
SetupCyclicNavigation();
|
||||
|
||||
// Выделяем первую ячейку и устанавливаем фокус
|
||||
if (cells.Count > 0)
|
||||
{
|
||||
currentCellIndex = 0;
|
||||
SetFirstSelected(cells[0]);
|
||||
UpdateCellSelection();
|
||||
}
|
||||
|
||||
// Обновляем preview с актуальными данными из ячеек
|
||||
UpdatePreview();
|
||||
|
||||
Debug.Log($"[TextInputModal] GenerateCells completed successfully");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Настраивает циклическую горизонтальную навигацию между всеми элементами
|
||||
/// </summary>
|
||||
private void SetupCyclicNavigation()
|
||||
{
|
||||
if (cells.Count == 0) return;
|
||||
|
||||
// Создаем список всех элементов для навигации: ячейки + кнопки
|
||||
List<Selectable> allElements = new List<Selectable>();
|
||||
|
||||
// Добавляем все ячейки
|
||||
allElements.AddRange(cells);
|
||||
|
||||
// Добавляем кнопки в конец
|
||||
if (confirmButton != null) allElements.Add(confirmButton);
|
||||
if (cancelButton != null) allElements.Add(cancelButton);
|
||||
|
||||
// Настраиваем циклическую горизонтальную навигацию для всех элементов
|
||||
for (int i = 0; i < allElements.Count; i++)
|
||||
{
|
||||
var element = allElements[i];
|
||||
var nav = new Navigation();
|
||||
nav.mode = Navigation.Mode.Explicit;
|
||||
|
||||
// Предыдущий элемент (циклично)
|
||||
int prevIndex = (i - 1 + allElements.Count) % allElements.Count;
|
||||
nav.selectOnLeft = allElements[prevIndex];
|
||||
|
||||
// Следующий элемент (циклично)
|
||||
int nextIndex = (i + 1) % allElements.Count;
|
||||
nav.selectOnRight = allElements[nextIndex];
|
||||
|
||||
// Вверх/вниз: только для ячеек (не для кнопок)
|
||||
if (element is TextInputCell)
|
||||
{
|
||||
// Для ячеек вверх/вниз обрабатываются в OnMove (изменение символа)
|
||||
nav.selectOnUp = null;
|
||||
nav.selectOnDown = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Для кнопок вверх/вниз не используются
|
||||
nav.selectOnUp = null;
|
||||
nav.selectOnDown = null;
|
||||
}
|
||||
|
||||
element.navigation = nav;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Открывает модальное окно с заданным заголовком и начальным текстом
|
||||
/// </summary>
|
||||
public void OpenForInput(string title, string initialText = "")
|
||||
{
|
||||
Debug.Log($"[TextInputModal] OpenForInput called. Title: '{title}', InitialText: '{initialText}'");
|
||||
|
||||
if (titleText)
|
||||
{
|
||||
titleText.text = title;
|
||||
}
|
||||
|
||||
// Сохраняем текст для генерации после активации
|
||||
pendingText = initialText ?? "";
|
||||
|
||||
// Помечаем что нужна регенерация (если еще не помечено)
|
||||
if (cells.Count != cellCount || cells.Count == 0)
|
||||
{
|
||||
needsRegeneration = true;
|
||||
}
|
||||
|
||||
// Проверяем что ячейки не null
|
||||
if (!needsRegeneration && cells.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
{
|
||||
if (cells[i] == null)
|
||||
{
|
||||
Debug.LogWarning($"[TextInputModal] Cell {i} is null, will regenerate");
|
||||
needsRegeneration = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[TextInputModal] needsRegeneration: {needsRegeneration}, calling Open()");
|
||||
|
||||
// Open() активирует модалку и вызовет генерацию ячеек
|
||||
Open();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Переопределяем Open для генерации ячеек ПОСЛЕ активации
|
||||
/// </summary>
|
||||
public override void Open()
|
||||
{
|
||||
Debug.Log($"[TextInputModal] Open() called. needsRegeneration: {needsRegeneration}, pendingText: '{pendingText}'");
|
||||
Debug.Log($"[TextInputModal] BEFORE base.Open() - gameObject.activeSelf: {gameObject.activeSelf}, activeInHierarchy: {gameObject.activeInHierarchy}");
|
||||
|
||||
// Сначала активируем модалку
|
||||
base.Open();
|
||||
|
||||
Debug.Log($"[TextInputModal] AFTER base.Open() - gameObject.activeSelf: {gameObject.activeSelf}, activeInHierarchy: {gameObject.activeInHierarchy}");
|
||||
|
||||
// Теперь, когда модалка активна, генерируем ячейки если нужно
|
||||
if (needsRegeneration)
|
||||
{
|
||||
Debug.Log($"[TextInputModal] Generating cells NOW (after activation) with text: '{pendingText}'");
|
||||
GenerateCells(pendingText);
|
||||
needsRegeneration = false;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(pendingText))
|
||||
{
|
||||
Debug.Log($"[TextInputModal] Setting text to existing cells: '{pendingText}'");
|
||||
SetText(pendingText);
|
||||
}
|
||||
|
||||
// Очищаем pending text
|
||||
pendingText = "";
|
||||
|
||||
Debug.Log($"[TextInputModal] Open() completed. cells.Count: {cells.Count}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает текст в ячейки (переинициализирует ячейки с новыми символами)
|
||||
/// </summary>
|
||||
public void SetText(string text)
|
||||
{
|
||||
if (text == null) text = "";
|
||||
|
||||
Debug.Log($"[TextInputModal] SetText called with: '{text}'");
|
||||
|
||||
// Проверяем, что ячейки созданы
|
||||
if (cells.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[TextInputModal] No cells available, cannot set text!");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[TextInputModal] Setting text to {cells.Count} cells");
|
||||
|
||||
// Переинициализируем каждую ячейку с новым символом
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
{
|
||||
if (cells[i] == null)
|
||||
{
|
||||
Debug.LogWarning($"[TextInputModal] Cell {i} is null!");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Определяем символ для этой ячейки
|
||||
string symbol = (i < text.Length) ? text[i].ToString() : null;
|
||||
|
||||
Debug.Log($"[TextInputModal] Cell {i}: initializing with symbol '{symbol}', alphabet has {(cells[i] != null ? alphabet.Length : 0)} symbols");
|
||||
|
||||
// Переинициализируем ячейку с новым символом
|
||||
cells[i].Initialize(alphabet, 0, symbol);
|
||||
}
|
||||
|
||||
Debug.Log($"[TextInputModal] SetText completed, updating preview");
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает текущий введенный текст
|
||||
/// </summary>
|
||||
public string GetText()
|
||||
{
|
||||
string result = "";
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
string symbol = cell.CurrentSymbol;
|
||||
// Добавляем символ, но первый символ алфавита (обычно пробел или 0) считается "пустым"
|
||||
if (cell.CurrentIndex == 0 && string.IsNullOrEmpty(symbol.Trim()))
|
||||
{
|
||||
result += " "; // Добавляем пробел для пустых ячеек
|
||||
}
|
||||
else
|
||||
{
|
||||
result += symbol;
|
||||
}
|
||||
}
|
||||
return result.TrimEnd(); // Убираем пробелы в конце
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вызывается когда символ в ячейке изменяется
|
||||
/// </summary>
|
||||
private void OnCellSymbolChanged(TextInputCell cell)
|
||||
{
|
||||
UpdatePreview();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет выделение ячеек
|
||||
/// </summary>
|
||||
private void UpdateCellSelection()
|
||||
{
|
||||
for (int i = 0; i < cells.Count; i++)
|
||||
{
|
||||
cells[i].SetCellSelected(i == currentCellIndex);
|
||||
}
|
||||
|
||||
// Устанавливаем фокус EventSystem на текущую ячейку
|
||||
if (currentCellIndex >= 0 && currentCellIndex < cells.Count && cells[currentCellIndex] != null)
|
||||
{
|
||||
if (UnityEngine.EventSystems.EventSystem.current != null)
|
||||
{
|
||||
UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(cells[currentCellIndex].gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обновляет предпросмотр текста
|
||||
/// </summary>
|
||||
private void UpdatePreview()
|
||||
{
|
||||
if (previewText)
|
||||
{
|
||||
previewText.text = GetText();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Подтверждает ввод текста
|
||||
/// </summary>
|
||||
private void ConfirmInput()
|
||||
{
|
||||
string text = GetText();
|
||||
OnTextConfirmed?.Invoke(text);
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отменяет ввод текста
|
||||
/// </summary>
|
||||
private void CancelInput()
|
||||
{
|
||||
OnTextCancelled?.Invoke();
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает алфавит для всех ячеек
|
||||
/// </summary>
|
||||
public void SetAlphabet(string[] newAlphabet)
|
||||
{
|
||||
Debug.Log($"[TextInputModal] SetAlphabet called with {(newAlphabet != null ? newAlphabet.Length : 0)} symbols");
|
||||
|
||||
if (newAlphabet == null || newAlphabet.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("[TextInputModal] Cannot set empty alphabet!");
|
||||
return;
|
||||
}
|
||||
|
||||
alphabet = newAlphabet;
|
||||
|
||||
// Помечаем что нужна регенерация (она произойдет при открытии модалки)
|
||||
needsRegeneration = true;
|
||||
|
||||
Debug.Log($"[TextInputModal] Alphabet set, marked for regeneration");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает количество ячеек и перегенерирует их
|
||||
/// </summary>
|
||||
public void SetCellCount(int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cellCount = count;
|
||||
|
||||
// Помечаем что нужна регенерация (она произойдет при открытии модалки)
|
||||
needsRegeneration = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сбрасывает все ячейки к первому символу
|
||||
/// </summary>
|
||||
public void ResetAllCells()
|
||||
{
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (cell != null)
|
||||
{
|
||||
cell.Initialize(alphabet);
|
||||
}
|
||||
}
|
||||
|
||||
currentCellIndex = 0;
|
||||
UpdateCellSelection();
|
||||
UpdatePreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/UI/TextInput/TextInputModal.cs.meta
Normal file
3
Assets/Scripts/UI/TextInput/TextInputModal.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39d5fa6256b9422d89259fdfee507c20
|
||||
timeCreated: 1770630009
|
||||
157
Assets/Scripts/UI/TextInput/TextInputModal_Test.cs
Normal file
157
Assets/Scripts/UI/TextInput/TextInputModal_Test.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Common.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Пример использования TextInputModal для различных типов ввода
|
||||
/// </summary>
|
||||
public class TextInputModal_Test : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TextInputModal textInputModal;
|
||||
|
||||
/// <summary>
|
||||
/// Открывает модальное окно для ввода номера телефона
|
||||
/// </summary>
|
||||
public void OpenPhoneNumberInput()
|
||||
{
|
||||
if (textInputModal == null) return;
|
||||
|
||||
// Устанавливаем алфавит для телефонных номеров
|
||||
textInputModal.SetAlphabet(TextInputAlphabets.PhoneNumber);
|
||||
|
||||
// Устанавливаем количество ячеек (например, 11 для российского номера)
|
||||
textInputModal.SetCellCount(11);
|
||||
|
||||
// Подписываемся на события
|
||||
textInputModal.OnTextConfirmed += OnPhoneNumberEntered;
|
||||
textInputModal.OnTextCancelled += OnInputCancelled;
|
||||
|
||||
// Открываем окно
|
||||
textInputModal.OpenForInput("Введите номер телефона", "+7");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Открывает модальное окно для ввода имени игрока
|
||||
/// </summary>
|
||||
public void OpenPlayerNameInput()
|
||||
{
|
||||
if (textInputModal == null) return;
|
||||
|
||||
// Устанавливаем алфавит для имени (русские буквы)
|
||||
textInputModal.SetAlphabet(TextInputAlphabets.RussianStandard);
|
||||
|
||||
// Устанавливаем количество ячеек
|
||||
textInputModal.SetCellCount(15);
|
||||
|
||||
// Подписываемся на события
|
||||
textInputModal.OnTextConfirmed += OnPlayerNameEntered;
|
||||
textInputModal.OnTextCancelled += OnInputCancelled;
|
||||
|
||||
// Открываем окно
|
||||
textInputModal.OpenForInput("Введите ваше имя");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Открывает модальное окно для ввода кода приза
|
||||
/// </summary>
|
||||
public void OpenPrizeCodeInput()
|
||||
{
|
||||
if (textInputModal == null) return;
|
||||
|
||||
// Устанавливаем алфавит только из цифр
|
||||
textInputModal.SetAlphabet(TextInputAlphabets.Numeric);
|
||||
|
||||
// Устанавливаем количество ячеек (например, 6 для кода)
|
||||
textInputModal.SetCellCount(6);
|
||||
|
||||
// Подписываемся на события
|
||||
textInputModal.OnTextConfirmed += OnPrizeCodeEntered;
|
||||
textInputModal.OnTextCancelled += OnInputCancelled;
|
||||
|
||||
// Открываем окно
|
||||
textInputModal.OpenForInput("Введите код приза");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Открывает модальное окно для ввода email
|
||||
/// </summary>
|
||||
public void OpenEmailInput()
|
||||
{
|
||||
if (textInputModal == null) return;
|
||||
|
||||
// Устанавливаем алфавит для email
|
||||
textInputModal.SetAlphabet(TextInputAlphabets.Email);
|
||||
|
||||
// Устанавливаем количество ячеек
|
||||
textInputModal.SetCellCount(20);
|
||||
|
||||
// Подписываемся на события
|
||||
textInputModal.OnTextConfirmed += OnEmailEntered;
|
||||
textInputModal.OnTextCancelled += OnInputCancelled;
|
||||
|
||||
// Открываем окно
|
||||
textInputModal.OpenForInput("Введите email");
|
||||
}
|
||||
|
||||
private void OnPhoneNumberEntered(string phoneNumber)
|
||||
{
|
||||
Debug.Log($"Введен номер телефона: {phoneNumber}");
|
||||
|
||||
// Отписываемся от событий
|
||||
textInputModal.OnTextConfirmed -= OnPhoneNumberEntered;
|
||||
textInputModal.OnTextCancelled -= OnInputCancelled;
|
||||
|
||||
// Здесь можно сохранить номер телефона
|
||||
PlayerPrefs.SetString("PlayerPhone", phoneNumber);
|
||||
}
|
||||
|
||||
private void OnPlayerNameEntered(string playerName)
|
||||
{
|
||||
Debug.Log($"Введено имя игрока: {playerName}");
|
||||
|
||||
// Отписываемся от событий
|
||||
textInputModal.OnTextConfirmed -= OnPlayerNameEntered;
|
||||
textInputModal.OnTextCancelled -= OnInputCancelled;
|
||||
|
||||
// Здесь можно сохранить имя игрока
|
||||
PlayerPrefs.SetString("PlayerName", playerName);
|
||||
}
|
||||
|
||||
private void OnPrizeCodeEntered(string prizeCode)
|
||||
{
|
||||
Debug.Log($"Введен код приза: {prizeCode}");
|
||||
|
||||
// Отписываемся от событий
|
||||
textInputModal.OnTextConfirmed -= OnPrizeCodeEntered;
|
||||
textInputModal.OnTextCancelled -= OnInputCancelled;
|
||||
|
||||
// Здесь можно проверить код приза
|
||||
// CheckPrizeCode(prizeCode);
|
||||
}
|
||||
|
||||
private void OnEmailEntered(string email)
|
||||
{
|
||||
Debug.Log($"Введен email: {email}");
|
||||
|
||||
// Отписываемся от событий
|
||||
textInputModal.OnTextConfirmed -= OnEmailEntered;
|
||||
textInputModal.OnTextCancelled -= OnInputCancelled;
|
||||
|
||||
// Здесь можно сохранить email
|
||||
PlayerPrefs.SetString("PlayerEmail", email);
|
||||
}
|
||||
|
||||
private void OnInputCancelled()
|
||||
{
|
||||
Debug.Log("Ввод отменен");
|
||||
|
||||
// Отписываемся от всех событий
|
||||
textInputModal.OnTextConfirmed -= OnPhoneNumberEntered;
|
||||
textInputModal.OnTextConfirmed -= OnPlayerNameEntered;
|
||||
textInputModal.OnTextConfirmed -= OnPrizeCodeEntered;
|
||||
textInputModal.OnTextConfirmed -= OnEmailEntered;
|
||||
textInputModal.OnTextCancelled -= OnInputCancelled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
Assets/Scripts/UI/TextInput/TextInputModal_Test.cs.meta
Normal file
3
Assets/Scripts/UI/TextInput/TextInputModal_Test.cs.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb6889322203465cb37c5f8bbdee790c
|
||||
timeCreated: 1770630211
|
||||
@ -6,7 +6,7 @@ using RR.Common.Utility;
|
||||
/// Продвинутый пример использования системы логирования.
|
||||
/// Демонстрирует различные сценарии использования FileLogger и CompositeLogger.
|
||||
/// </summary>
|
||||
public class AdvancedLoggingExample : MonoBehaviour
|
||||
public class AdvancedLogging_Test : MonoBehaviour
|
||||
{
|
||||
private CompositeLogger compositeLogger;
|
||||
private FileLogger mainFileLogger;
|
||||
@ -28,13 +28,19 @@ namespace RR.Common.Utility
|
||||
{
|
||||
// Оставляем только цифры
|
||||
string digitsOnly = System.Text.RegularExpressions.Regex.Replace(number, @"\D", "");
|
||||
|
||||
if (digitsOnly.StartsWith("+") && digitsOnly.Length == 12)
|
||||
{
|
||||
return digitsOnly[2..];
|
||||
}
|
||||
|
||||
// Проверяем формат
|
||||
if ((digitsOnly.StartsWith("8") || digitsOnly.StartsWith("7")) && digitsOnly.Length == 11)
|
||||
{
|
||||
return digitsOnly[1..];
|
||||
}
|
||||
else if (digitsOnly.Length == 10)
|
||||
|
||||
if (digitsOnly.Length == 10)
|
||||
{
|
||||
return digitsOnly;
|
||||
}
|
||||
|
||||
42766
Assets/_Recovery/0 (1).unity
Normal file
42766
Assets/_Recovery/0 (1).unity
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/_Recovery/0 (1).unity.meta
Normal file
7
Assets/_Recovery/0 (1).unity.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71c4377b7b16cdc469bf877b9a66695d
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -2,13 +2,13 @@
|
||||
"dependencies": {
|
||||
"com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity",
|
||||
"com.unity.2d.enhancers": "1.0.0",
|
||||
"com.unity.ai.assistant": "1.0.0-pre.12",
|
||||
"com.unity.ai.generators": "1.0.0-pre.20",
|
||||
"com.unity.ai.assistant": "1.6.0-pre.2",
|
||||
"com.unity.ai.generators": "1.5.0-pre.2",
|
||||
"com.unity.ai.inference": "2.4.1",
|
||||
"com.unity.cinemachine": "3.1.4",
|
||||
"com.unity.collab-proxy": "2.10.2",
|
||||
"com.unity.feature.2d": "2.0.2",
|
||||
"com.unity.ide.rider": "3.0.38",
|
||||
"com.unity.ide.rider": "3.0.39",
|
||||
"com.unity.ide.visualstudio": "2.0.25",
|
||||
"com.unity.inputsystem": "1.17.0",
|
||||
"com.unity.multiplayer.center": "1.0.1",
|
||||
|
||||
@ -128,24 +128,27 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.ai.assistant": {
|
||||
"version": "1.0.0-pre.12",
|
||||
"version": "1.6.0-pre.2",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.ai.toolkit": "1.0.0-pre.18",
|
||||
"com.unity.ai.generators": "1.6.0-pre.2",
|
||||
"com.unity.serialization": "3.1.1",
|
||||
"com.unity.modules.uielements": "1.0.0",
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.1",
|
||||
"com.unity.modules.unitywebrequest": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.ai.generators": {
|
||||
"version": "1.0.0-pre.20",
|
||||
"depth": 0,
|
||||
"version": "1.6.0-pre.2",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.ai.toolkit": "1.0.0-pre.20",
|
||||
"com.unity.2d.sprite": "1.0.0",
|
||||
"com.unity.ai.toolkit": "1.6.0-pre.2",
|
||||
"com.unity.mathematics": "1.3.2",
|
||||
"com.unity.cloud.gltfast": "6.14.1",
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.1"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
@ -164,8 +167,8 @@
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.ai.toolkit": {
|
||||
"version": "1.0.0-pre.20",
|
||||
"depth": 1,
|
||||
"version": "1.6.0-pre.2",
|
||||
"depth": 2,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.nuget.newtonsoft-json": "3.2.1"
|
||||
@ -192,6 +195,19 @@
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.cloud.gltfast": {
|
||||
"version": "6.14.1",
|
||||
"depth": 1,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
"com.unity.burst": "1.8.24",
|
||||
"com.unity.collections": "1.2.4",
|
||||
"com.unity.mathematics": "1.2.6",
|
||||
"com.unity.modules.jsonserialize": "1.0.0",
|
||||
"com.unity.modules.unitywebrequest": "1.0.0"
|
||||
},
|
||||
"url": "https://packages.unity.com"
|
||||
},
|
||||
"com.unity.collab-proxy": {
|
||||
"version": "2.10.2",
|
||||
"depth": 0,
|
||||
@ -247,7 +263,7 @@
|
||||
}
|
||||
},
|
||||
"com.unity.ide.rider": {
|
||||
"version": "3.0.38",
|
||||
"version": "3.0.39",
|
||||
"depth": 0,
|
||||
"source": "registry",
|
||||
"dependencies": {
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
{
|
||||
"CustomInstructionsGUID": "",
|
||||
"CheckpointEnabled": false,
|
||||
"GitInstanceTypeValue": 0,
|
||||
"CustomGitPath": "",
|
||||
"CheckpointRetentionWeeks": 2
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user