feat: Add text input system and balance management with settings improvements
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:
Данил Ярош 2026-02-11 16:27:33 +03:00
parent 3944534cd0
commit 2417f58f61
36 changed files with 51237 additions and 118 deletions

3
.gitignore vendored
View File

@ -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

View 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

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6839c0e6ef91e3846910bce6b887d6e6
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -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()
{

View File

@ -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);
}

View 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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e51a2ca5cae54b7d9d78b28a63eea271
timeCreated: 1770626227

View File

@ -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:

View File

@ -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);
// Регистрируем для будущих вызовов

View File

@ -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]

View File

@ -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
{

View File

@ -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:

View File

@ -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();
}
}

View File

@ -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();
}

View 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);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bd0446f37d5e454387522c6223014f06
timeCreated: 1770641010

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c26f2b5339624e658c1edefc0f358d7b
timeCreated: 1770629979

View 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",
"@", ".", "_", "-"
};
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 02b4c67a814d4efa9f5104db5b88a7d0
timeCreated: 1770630028

View 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();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6828bed9c9414fe4bfd284a92d73f36b
timeCreated: 1770629979

View 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();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 39d5fa6256b9422d89259fdfee507c20
timeCreated: 1770630009

View 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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fb6889322203465cb37c5f8bbdee790c
timeCreated: 1770630211

View File

@ -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;

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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",

View File

@ -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": {

View File

@ -0,0 +1,7 @@
{
"CustomInstructionsGUID": "",
"CheckpointEnabled": false,
"GitInstanceTypeValue": 0,
"CustomGitPath": "",
"CheckpointRetentionWeeks": 2
}