feat: Implement balance system and fix pause bug
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled

- Добавлена система баланса игры (BalanceManager) для управления внутриигровой валютой
- Баланс начисляется за импульсы оплаты и списывается за начало/продолжение игры
- При старте игры весь внесенный баланс списывается, а жизни начисляются в соответствии с количеством оплат
- Добавлены проверки достаточности баланса для начала и продолжения игры
- Интегрирован BalanceManager с PaymentManager и GameManager
- Исправлен баг с возможностью снятия паузы при показе баннера проигрыша/победы
- Добавлена блокировка переключения паузы через IsActive флаг в PauseMenu
- Жизни больше не могут уйти в минус при повторной смерти во время показа баннера
- Настройки стоимости импульса, начала и продолжения игры берутся из SettingsManager
This commit is contained in:
Данил Ярош 2026-02-09 11:31:34 +03:00
parent 009152ac5b
commit 3944534cd0
16 changed files with 3641 additions and 1970 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,7 @@ GameObject:
- component: {fileID: 6566017136146807207}
- component: {fileID: 5633451267958679388}
- component: {fileID: 6824661392768214298}
- component: {fileID: -381940896145096749}
- component: {fileID: -3912971501865491093}
m_Layer: 0
m_Name: Globals
@ -159,6 +160,29 @@ MonoBehaviour:
OnCreditRemoved:
m_PersistentCalls:
m_Calls: []
--- !u!114 &-381940896145096749
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4763496076470024150}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 729da18f5e9741c28ca2d1e11d3fb43b, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Managers.BalanceManager
currentBalance: 0
saveBalanceBetweenSessions: 0
OnBalanceChanged:
m_PersistentCalls:
m_Calls: []
OnBalanceAdded:
m_PersistentCalls:
m_Calls: []
OnBalanceSpent:
m_PersistentCalls:
m_Calls: []
--- !u!114 &-3912971501865491093
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@ -138,7 +138,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &28542763
RectTransform:
m_ObjectHideFlags: 0
@ -1279,9 +1279,9 @@ RectTransform:
m_Father: {fileID: 2054206196}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 0}
m_AnchorMax: {x: 1, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_SizeDelta: {x: 20, y: -17}
m_Pivot: {x: 1, y: 1}
--- !u!114 &273493570
MonoBehaviour:
@ -1518,7 +1518,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u041E\u041F\u041B\u0410\u0422\u0410 \u041D\u0415 \u0412\u041D\u0415\u0421\u0415\u041D\u0410"
m_text: "\u0411\u0410\u041B\u0410\u041D\u0421\u0410 \u041D\u0415\u0414\u041E\u0421\u0422\u0410\u0422\u041E\u0427\u041D\u041E"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
@ -1633,9 +1633,9 @@ RectTransform:
m_Father: {fileID: 2054206196}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 20}
m_SizeDelta: {x: -17, y: 20}
m_Pivot: {x: 0, y: 0}
--- !u!114 &328533400
MonoBehaviour:
@ -1788,10 +1788,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 545548130}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 307.1872, y: -384.23657}
m_SizeDelta: {x: 600, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &430243133
MonoBehaviour:
@ -1969,7 +1969,7 @@ RectTransform:
m_Father: {fileID: 112549352}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
@ -3199,10 +3199,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 545548130}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 307.1872, y: -534.2366}
m_SizeDelta: {x: 600, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &674695656
MonoBehaviour:
@ -3716,10 +3716,10 @@ RectTransform:
- {fileID: 1631191833}
m_Father: {fileID: 519510896}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 307.1872, y: -209.23657}
m_SizeDelta: {x: 400, y: 150}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &780569109
GameObject:
@ -3981,10 +3981,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 545548130}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 307.1872, y: -234.23657}
m_SizeDelta: {x: 600, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &783722930
MonoBehaviour:
@ -4122,9 +4122,9 @@ RectTransform:
m_Father: {fileID: 265801313}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 20}
m_SizeDelta: {x: -17, y: 20}
m_Pivot: {x: 0, y: 0}
--- !u!114 &787002084
MonoBehaviour:
@ -4395,10 +4395,10 @@ RectTransform:
- {fileID: 457224350}
m_Father: {fileID: 519510896}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 307.1872, y: -409.23657}
m_SizeDelta: {x: 400, y: 150}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &817313575
GameObject:
@ -4569,10 +4569,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 545548130}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 307.1872, y: -84.23657}
m_SizeDelta: {x: 600, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &820785317
MonoBehaviour:
@ -5926,7 +5926,7 @@ RectTransform:
m_Father: {fileID: 1819180688}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
@ -6171,7 +6171,7 @@ RectTransform:
m_Father: {fileID: 837630039}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}
@ -7052,7 +7052,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &1460883867
RectTransform:
m_ObjectHideFlags: 0
@ -7221,9 +7221,9 @@ RectTransform:
m_Father: {fileID: 265801313}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_SizeDelta: {x: -17, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1461740380
MonoBehaviour:
@ -7703,7 +7703,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 100, y: 0}
m_SizeDelta: {x: -100, y: 0}
m_SizeDelta: {x: -100, y: -630.0134}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1537614490
MonoBehaviour:
@ -7812,9 +7812,9 @@ RectTransform:
m_Father: {fileID: 2054206196}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_SizeDelta: {x: -17, y: -17}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1549457647
MonoBehaviour:
@ -8128,6 +8128,195 @@ Transform:
- {fileID: 1696942874}
m_Father: {fileID: 1996696314}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1625478927
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1625478928}
- component: {fileID: 1625478930}
- component: {fileID: 1625478929}
- component: {fileID: 1625478931}
m_Layer: 5
m_Name: BalanceText
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1625478928
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1625478927}
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: 1851115885}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -375}
m_SizeDelta: {x: 400, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1625478929
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1625478927}
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: "\u0422\u0435\u043A\u0443\u0449\u0438\u0439 \u0431\u0430\u043B\u0430\u043D\u0441:
0"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, 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: 42.65
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 1
m_VerticalAlignment: 256
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 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!222 &1625478930
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1625478927}
m_CullTransparentMesh: 1
--- !u!114 &1625478931
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1625478927}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d67352bcb19c4561bb19609a290e8f84, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.BalanceDisplay
balanceText: {fileID: 1625478929}
textFormat: "\u0422\u0435\u043A\u0443\u0449\u0438\u0439 \u0431\u0430\u043B\u0430\u043D\u0441:
{0}"
animateOnChange: 1
animationDuration: 0.3
scaleCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 0.44773006
value: 1.0396395
inSlope: -0.116342045
outSlope: -0.116342045
tangentMode: 0
weightedMode: 0
inWeight: 0.112806365
outWeight: 0.33333334
- serializedVersion: 3
time: 0.99993896
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!1 &1628533780
GameObject:
m_ObjectHideFlags: 0
@ -9281,7 +9470,7 @@ MonoBehaviour:
rememberGroupFoldout: 1
groupOrder:
- "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438"
sortByNameInsideGroup: 1
sortByNameInsideGroup: 0
toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3}
sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3}
valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3}
@ -9433,7 +9622,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -0.00000016372034}
m_AnchoredPosition: {x: 0, y: 0.00086001726}
m_SizeDelta: {x: 0, y: 300}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1792218764
@ -9771,6 +9960,7 @@ RectTransform:
- {fileID: 1200884212}
- {fileID: 1532558928}
- {fileID: 1227305891}
- {fileID: 1625478928}
m_Father: {fileID: 2082092901}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@ -10069,7 +10259,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &1932206569
RectTransform:
m_ObjectHideFlags: 0
@ -10595,8 +10785,8 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1071857129}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMin: {x: 0.000000059604645, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 20}
m_Pivot: {x: 0.5, y: 0.5}

View File

@ -252,6 +252,25 @@ namespace RR.Common.Controls
/// </summary>
public static int GetCredits() => CreditsCount;
/// <summary>
/// Устанавливает количество кредитов напрямую (для синхронизации с балансом)
/// </summary>
public static void SetCredits(int amount)
{
int oldCredits = CreditsCount;
CreditsCount = Mathf.Max(0, amount);
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
if (CreditsCount > oldCredits)
{
OnCreditAdded?.Invoke(CreditsCount);
}
else if (CreditsCount < oldCredits)
{
OnCreditRemoved?.Invoke(CreditsCount);
}
}
//===========================end: PUBLIC API===========================//
private void Update()

View File

@ -0,0 +1,329 @@
using RR.Common.Controls;
using RR.Common.Settings;
using UnityEngine;
using UnityEngine.Events;
namespace RR.Common.Managers
{
/// <summary>
/// Менеджер игрового баланса. Управляет внутриигровой валютой,
/// которая начисляется за импульсы оплаты и списывается за начало/продолжение игры.
/// </summary>
public class BalanceManager : MonoBehaviour
{
public static BalanceManager Instance { get; private set; }
[Header("Текущий баланс")]
[SerializeField] private int currentBalance;
[Header("Настройки сохранения")]
[SerializeField] private bool saveBalanceBetweenSessions = false;
// События для уведомления UI и других систем
public UnityEvent<int> OnBalanceChanged;
public UnityEvent<int, int> OnBalanceAdded; // (добавлено, новый баланс)
public UnityEvent<int, int> OnBalanceSpent; // (потрачено, новый баланс)
/// <summary>
/// Текущий баланс игрока
/// </summary>
public int CurrentBalance => currentBalance;
private const string BALANCE_SAVE_KEY = "PlayerBalance";
// Кешированные значения из настроек
private int _impulseCost = 10;
private int _startGameCost = 10;
private int _continueGameCost = 5;
private bool _freeGame = false;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
Initialize();
}
private void Initialize()
{
// Инициализация событий
OnBalanceChanged ??= new UnityEvent<int>();
OnBalanceChanged ??= new UnityEvent<int>();
OnBalanceAdded ??= new UnityEvent<int, int>();
OnBalanceSpent ??= new UnityEvent<int, int>();
// Загрузка баланса из сохранений
if (saveBalanceBetweenSessions)
{
currentBalance = PlayerPrefs.GetInt(BALANCE_SAVE_KEY, 0);
}
else
{
currentBalance = 0;
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, 0);
}
// Подписка на события настроек для кеширования значений
var settingsManager = SettingsManager.Instance;
if (settingsManager != null)
{
settingsManager.OnSettingsLoaded += CacheSettingsValues;
settingsManager.OnSettingsSaved += CacheSettingsValues;
CacheSettingsValues(); // Загрузка при инициализации
}
// Подписка на события оплаты
if (PaymentManager.Instance != null)
{
PaymentManager.Instance.OnCreditAdded.AddListener(OnImpulsePaid);
}
Debug.Log($"[BalanceManager] Инициализирован. Баланс: {currentBalance}");
}
private void OnDestroy()
{
if (PaymentManager.Instance != null)
{
PaymentManager.Instance.OnCreditAdded.RemoveListener(OnImpulsePaid);
}
var settingsManager = SettingsManager.Instance;
if (settingsManager != null)
{
settingsManager.OnSettingsLoaded -= CacheSettingsValues;
settingsManager.OnSettingsSaved -= CacheSettingsValues;
}
}
/// <summary>
/// Кеширует значения из настроек
/// </summary>
private void CacheSettingsValues()
{
var settings = SettingsManager.Instance;
if (settings == null)
{
Debug.LogError("[BalanceManager] SettingsManager is null, cannot cache settings!");
return;
}
_impulseCost = settings.GetInt("payment.paymentImpulseCost");
_startGameCost = settings.GetInt("payment.startGameCost");
_continueGameCost = settings.GetInt("payment.continueGameCost");
_freeGame = settings.GetBool("payment.freeGame");
// Валидация значений
if (_impulseCost <= 0)
{
Debug.LogError($"[BalanceManager] Invalid payment.paymentImpulseCost: {_impulseCost}");
_impulseCost = 10;
}
if (_startGameCost <= 0)
{
Debug.LogError($"[BalanceManager] Invalid payment.startGameCost: {_startGameCost}");
_startGameCost = 10;
}
if (_continueGameCost <= 0)
{
Debug.LogError($"[BalanceManager] Invalid payment.continueGameCost: {_continueGameCost}");
_continueGameCost = 5;
}
}
/// <summary>
/// Обработчик оплаты импульса - начисляет баланс
/// </summary>
private void OnImpulsePaid(int creditsCount)
{
// creditsCount - это количество добавленных кредитов (всегда 1 для одного импульса)
// Начисляем стоимость импульса
AddBalance(_impulseCost);
}
/// <summary>
/// Добавляет баланс игроку
/// </summary>
/// <param name="amount">Количество добавляемого баланса</param>
public void AddBalance(int amount)
{
if (amount <= 0)
{
Debug.LogWarning($"[BalanceManager] Trying to add invalid amount: {amount}");
return;
}
currentBalance += amount;
// Сохранение если включено
if (saveBalanceBetweenSessions)
{
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
PlayerPrefs.Save();
}
// Синхронизация кредитов с балансом
SyncCreditsWithBalance();
// Уведомление подписчиков
OnBalanceAdded?.Invoke(amount, currentBalance);
OnBalanceChanged?.Invoke(currentBalance);
}
/// <summary>
/// Проверяет, достаточно ли баланса для начала игры
/// </summary>
public bool CanStartGame()
{
// Проверка бесплатной игры
if (_freeGame)
return true;
return currentBalance >= _startGameCost;
}
/// <summary>
/// Проверяет, достаточно ли баланса для продолжения игры
/// </summary>
public bool CanContinueGame()
{
// Проверка бесплатной игры
if (_freeGame)
return true;
return currentBalance >= _continueGameCost;
}
/// <summary>
/// Списывает баланс за начало игры
/// </summary>
/// <returns>true если списание успешно</returns>
public bool SpendForStart()
{
// Проверка бесплатной игры
if (_freeGame)
return true;
return SpendBalance(_startGameCost, "начало игры");
}
/// <summary>
/// Списывает баланс за продолжение игры
/// </summary>
/// <returns>true если списание успешно</returns>
public bool SpendForContinue()
{
// Проверка бесплатной игры
if (_freeGame)
return true;
return SpendBalance(_continueGameCost, "продолжение игры");
}
/// <summary>
/// Списывает указанное количество баланса
/// </summary>
/// <param name="amount">Количество списываемого баланса</param>
/// <param name="reason">Причина списания (для логирования)</param>
/// <returns>true если списание успешно</returns>
public bool SpendBalance(int amount, string reason = "")
{
if (amount <= 0)
{
Debug.LogWarning($"[BalanceManager] Trying to spend invalid amount: {amount}");
return false;
}
if (currentBalance < amount)
{
return false;
}
currentBalance -= amount;
// Сохранение если включено
if (saveBalanceBetweenSessions)
{
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
PlayerPrefs.Save();
}
// Синхронизация кредитов с балансом
SyncCreditsWithBalance();
// Уведомление подписчиков
OnBalanceSpent?.Invoke(amount, currentBalance);
OnBalanceChanged?.Invoke(currentBalance);
return true;
}
/// <summary>
/// Сбрасывает баланс до нуля
/// </summary>
public void ResetBalance()
{
currentBalance = 0;
if (saveBalanceBetweenSessions)
{
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, 0);
PlayerPrefs.Save();
}
// Синхронизация кредитов с балансом
SyncCreditsWithBalance();
OnBalanceChanged?.Invoke(currentBalance);
}
/// <summary>
/// Синхронизирует кредиты в ComWrapper с текущим балансом
/// по формуле: кредиты = баланс / стоимость_импульса (округление вниз)
/// </summary>
private void SyncCreditsWithBalance()
{
if (_impulseCost <= 0)
{
Debug.LogError("[BalanceManager] Cannot sync credits - invalid impulse cost");
return;
}
int targetCredits = currentBalance / _impulseCost;
int currentCredits = ComWrapper.GetCredits();
if (targetCredits == currentCredits)
return; // Кредиты уже синхронизированы
// Устанавливаем кредиты напрямую через SetCredits
ComWrapper.SetCredits(targetCredits);
}
/// <summary>
/// Устанавливает баланс напрямую (для тестирования/отладки)
/// </summary>
public void SetBalance(int amount)
{
currentBalance = Mathf.Max(0, amount);
if (saveBalanceBetweenSessions)
{
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
PlayerPrefs.Save();
}
// Синхронизация кредитов с балансом
SyncCreditsWithBalance();
OnBalanceChanged?.Invoke(currentBalance);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 729da18f5e9741c28ca2d1e11d3fb43b
timeCreated: 1770384164

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using RR.Common.Controls;
using UnityEngine;
@ -26,6 +26,8 @@ namespace RR.Game2D.Core.Managers
public AudioManager Audio {get; private set;}
// Payment control for game
public PaymentManager Payment {get; private set;}
// Balance control for game
public BalanceManager Balance {get; private set;}
// Plate control for game
public ComWrapper Plate { get; private set; }
@ -144,6 +146,10 @@ namespace RR.Game2D.Core.Managers
// Initialize Payment control
if (Payment == null) Payment = globals.GetComponent<PaymentManager>();
// Initialize Balance control
if (Balance == null) Balance = globals.GetComponent<BalanceManager>();
if (Balance == null) Balance = BalanceManager.Instance;
// Apply loaded settings (new system applies automatically in Awake)
if (Plate == null) Plate = globals.GetComponent<ComWrapper>();
@ -264,7 +270,52 @@ public void TogglePause() => SetPause(!IsPaused);
/// </summary>
public void NewGame()
{
Lives = MaxLives;
Debug.Log("[GameManager] NewGame called");
// Проверка баланса перед началом игры
if (Balance != null && !Balance.CanStartGame())
{
Logger?.Log(gameObject.name, "Cannot start game - insufficient balance");
Debug.LogWarning("[GameManager] Cannot start - insufficient balance");
return;
}
int livesToGive = StartingLives;
int balanceToSpend = 0;
if (Balance != null)
{
int currentBalance = Balance.CurrentBalance;
int impulseCost = Settings?.GetInt("payment.paymentImpulseCost") ?? 10;
// Рассчитываем количество оплат
int totalImpulses = currentBalance / impulseCost;
// Первая оплата даёт базовые жизни
// Каждая дополнительная оплата даёт LivesIncrement жизней
if (totalImpulses > 0)
{
int additionalImpulses = totalImpulses - 1;
int bonusLives = additionalImpulses * LivesIncrement;
livesToGive = StartingLives + bonusLives;
Debug.Log($"[GameManager] Balance: {currentBalance}, Impulses: {totalImpulses}, Base lives: {StartingLives}, Bonus lives: {bonusLives}, Total lives: {livesToGive}");
}
// Списываем ВЕСЬ баланс
balanceToSpend = currentBalance;
if (!Balance.SpendBalance(balanceToSpend, "начало игры (полный баланс)"))
{
Logger?.Log(gameObject.name, "Failed to spend balance for game start");
Debug.LogError("[GameManager] Failed to spend balance!");
return;
}
Debug.Log($"[GameManager] Spent entire balance: {balanceToSpend}, Lives given: {livesToGive}");
}
Lives = livesToGive;
LivesChanged?.Invoke(Lives);
Coins = 0;
CoinsChanged?.Invoke(Coins);
@ -272,16 +323,27 @@ public void TogglePause() => SetPause(!IsPaused);
_gameStartTime = Time.time;
Logger.Log(gameObject.name,$"Game start time = {_gameStartTime}");
if (!Payment.IsFreePlayEnabled()) Payment.ProcessPayment(Payment.CreditsCount);
if (Plate) Plate.StopBlink();
LoadLevel(1, 1);
}
public void ContinueGame()
{
if (!Payment.ProcessPayment()) return;
// Проверка баланса перед продолжением игры
if (Balance != null && !Balance.CanContinueGame())
{
Logger?.Log(gameObject.name, "Cannot continue game - insufficient balance");
return;
}
// Списание баланса за продолжение игры
if (Balance != null && !Balance.SpendForContinue())
{
Logger?.Log(gameObject.name, "Failed to spend balance for continue");
return;
}
Lives = StartingLives + 1;
LivesChanged?.Invoke(Lives);
Coins = 0;

View File

@ -119,8 +119,8 @@ MonoBehaviour:
options: []
defaultOptionIndex: 0
- key: payment.paymentImpulseCost
displayName: "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C \u043E\u0434\u043D\u043E\u0433\u043E
\u0438\u043C\u043F\u0443\u043B\u044C\u0441\u0430"
displayName: "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C \u0438\u043C\u043F\u0443\u043B\u044C\u0441\u0430
\u043E\u043F\u043B\u0430\u0442\u044B"
description:
group: "\u041E\u043F\u043B\u0430\u0442\u0430 \u0438 \u0441\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C"
shownInSettings: 1
@ -145,6 +145,60 @@ MonoBehaviour:
buttonSprite: {fileID: 0}
options: []
defaultOptionIndex: 0
- key: payment.startGameCost
displayName: "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C \u043D\u0430\u0447\u0430\u043B\u0430
\u0438\u0433\u0440\u044B"
description:
group: "\u041E\u043F\u043B\u0430\u0442\u0430 \u0438 \u0441\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C"
shownInSettings: 1
exposed: 0
valueType: 1
defaultBool: 0
defaultInt: 10
defaultFloat: 0
defaultString:
minInt: 10
maxInt: 1000
intDisplayFormat: 1
intStep: 5
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.continueGameCost
displayName: "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0435\u043D\u0438\u044F
\u0438\u0433\u0440\u044B"
description:
group: "\u041E\u043F\u043B\u0430\u0442\u0430 \u0438 \u0441\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C"
shownInSettings: 1
exposed: 0
valueType: 1
defaultBool: 0
defaultInt: 10
defaultFloat: 0
defaultString:
minInt: 5
maxInt: 1000
intDisplayFormat: 1
intStep: 5
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: prizes.giveSmallPrize
displayName: "\u0412\u044B\u0434\u0430\u0432\u0430\u0442\u044C \u043C\u0430\u043B\u0435\u043D\u044C\u043A\u0438\u0439
\u043F\u0440\u0438\u0437"

View File

@ -20,10 +20,26 @@ namespace RR.Common.Settings.UI
public void Setup(SettingEntry entry, int currentValue)
{
_entry = entry;
_currentValue = Mathf.Clamp(currentValue, entry.minInt, entry.maxInt);
_step = Mathf.Max(1, entry.intStep);
if (entry == null)
{
Debug.LogError("[ValueSelectorRow] Cannot setup with null entry!");
return;
}
_entry = entry;
// Валидация step
if (entry.intStep <= 0)
{
Debug.LogWarning($"[ValueSelectorRow] Invalid step {entry.intStep} for key {entry.key}, using 1");
_step = 1;
}
else
{
_step = entry.intStep;
}
_currentValue = Mathf.Clamp(currentValue, entry.minInt, entry.maxInt);
UpdateUI();
}
@ -61,9 +77,7 @@ namespace RR.Common.Settings.UI
int newValue = Mathf.Max(_entry.minInt, _currentValue - _step);
if (newValue != _currentValue)
{
_currentValue = newValue;
UpdateUI();
SettingsManager.Instance?.SetInt(_entry.key, _currentValue);
SetValue(newValue);
}
}
@ -72,9 +86,7 @@ namespace RR.Common.Settings.UI
int newValue = Mathf.Min(_entry.maxInt, _currentValue + _step);
if (newValue != _currentValue)
{
_currentValue = newValue;
UpdateUI();
SettingsManager.Instance?.SetInt(_entry.key, _currentValue);
SetValue(newValue);
}
}
@ -89,8 +101,23 @@ namespace RR.Common.Settings.UI
if (_entry == null)
return;
_currentValue = Mathf.Clamp(value, _entry.minInt, _entry.maxInt);
UpdateUI();
int clampedValue = Mathf.Clamp(value, _entry.minInt, _entry.maxInt);
if (clampedValue != _currentValue)
{
_currentValue = clampedValue;
UpdateUI();
// Сохраняем изменения через SettingsManager
var manager = SettingsManager.Instance;
if (manager != null)
{
manager.SetInt(_entry.key, _currentValue);
}
else
{
Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!");
}
}
}
public string Key => _entry?.key;

View File

@ -0,0 +1,164 @@
using RR.Common.Managers;
using TMPro;
using UnityEngine;
namespace RR.Common.UI
{
/// <summary>
/// UI компонент для отображения текущего баланса игрока.
/// Автоматически обновляется при изменении баланса.
/// </summary>
public class BalanceDisplay : MonoBehaviour
{
[Header("Настройки отображения")]
[SerializeField] private TextMeshProUGUI balanceText;
[SerializeField] private string textFormat = "Текущий баланс: {0}";
[Header("Опциональная анимация")]
[SerializeField] private bool animateOnChange = true;
[SerializeField] private float animationDuration = 0.3f;
[SerializeField] private AnimationCurve scaleCurve = AnimationCurve.EaseInOut(0, 1, 1, 1);
private BalanceManager _balanceManager;
private Coroutine _animationCoroutine;
private Vector3 _originalScale;
private void Awake()
{
if (balanceText != null)
{
_originalScale = balanceText.transform.localScale;
}
}
private void Start()
{
_balanceManager = BalanceManager.Instance;
if (_balanceManager != null)
{
// Подписываемся на события изменения баланса
_balanceManager.OnBalanceChanged.AddListener(OnBalanceChanged);
// Устанавливаем начальное значение
UpdateBalanceDisplay(_balanceManager.CurrentBalance);
}
else
{
Debug.LogWarning("[BalanceDisplay] BalanceManager not found! Balance display will not work.");
if (balanceText != null)
{
balanceText.text = string.Format(textFormat, 0);
}
}
}
private void OnDestroy()
{
if (_balanceManager != null)
{
_balanceManager.OnBalanceChanged.RemoveListener(OnBalanceChanged);
}
}
/// <summary>
/// Обработчик изменения баланса
/// </summary>
private void OnBalanceChanged(int newBalance)
{
UpdateBalanceDisplay(newBalance);
if (animateOnChange && balanceText != null)
{
AnimateBalanceChange();
}
}
/// <summary>
/// Обновляет текст баланса
/// </summary>
private void UpdateBalanceDisplay(int balance)
{
if (balanceText != null)
{
balanceText.text = string.Format(textFormat, balance);
}
}
/// <summary>
/// Анимирует изменение баланса (масштабирование)
/// </summary>
private void AnimateBalanceChange()
{
if (_animationCoroutine != null)
{
StopCoroutine(_animationCoroutine);
}
_animationCoroutine = StartCoroutine(ScaleAnimation());
}
private System.Collections.IEnumerator ScaleAnimation()
{
float elapsed = 0f;
while (elapsed < animationDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = elapsed / animationDuration;
// Применяем кривую анимации
float scaleMultiplier = scaleCurve.Evaluate(t);
// Небольшое увеличение и возврат к исходному размеру
float scale = Mathf.Lerp(1.2f, 1f, t);
balanceText.transform.localScale = _originalScale * scale * scaleMultiplier;
yield return null;
}
// Гарантируем возврат к исходному масштабу
balanceText.transform.localScale = _originalScale;
_animationCoroutine = null;
}
/// <summary>
/// Принудительное обновление отображения (для отладки)
/// </summary>
public void ForceUpdate()
{
if (_balanceManager != null)
{
UpdateBalanceDisplay(_balanceManager.CurrentBalance);
}
}
/// <summary>
/// Изменить формат отображения текста
/// </summary>
public void SetTextFormat(string format)
{
textFormat = format;
if (_balanceManager != null)
{
UpdateBalanceDisplay(_balanceManager.CurrentBalance);
}
}
#if UNITY_EDITOR
/// <summary>
/// Для предварительного просмотра в редакторе
/// </summary>
private void OnValidate()
{
if (Application.isPlaying) return;
if (balanceText != null)
{
balanceText.text = string.Format(textFormat, 0);
}
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d67352bcb19c4561bb19609a290e8f84
timeCreated: 1770385844

View File

@ -24,8 +24,9 @@ namespace RR.Common.UI
[Header("Тексты")]
[SerializeField] private string creditTextFormat = "У вас осталось: {0}";
private RR.Common.Managers.PaymentManager _paymentManager;
private RR.Common.Managers.BalanceManager _balanceManager;
private Coroutine _continueCountdownCoroutine;
private bool _isSubscribedToBalance = false;
/// <summary>
/// Событие, вызываемое при нажатии "Продолжить" или закрытии баннера.
@ -46,16 +47,22 @@ namespace RR.Common.UI
{
base.OnEnable();
_paymentManager = GameManager.Instance?.Payment;
if (_paymentManager != null)
// Инициализируем BalanceManager
if (_balanceManager == null)
{
int credits = _paymentManager.CreditsCount;
_balanceManager = GameManager.Instance?.Balance;
}
// Подписываемся на событие только если еще не подписаны
if (_balanceManager != null && !_isSubscribedToBalance)
{
int balance = _balanceManager.CurrentBalance;
if (creditText)
{
creditText.text = string.Format(creditTextFormat, credits);
creditText.text = string.Format(creditTextFormat, balance);
}
_paymentManager.OnCreditAdded.AddListener(UnlockContinueButton);
_balanceManager.OnBalanceChanged.AddListener(UnlockContinueButton);
_isSubscribedToBalance = true;
}
}
@ -68,9 +75,11 @@ namespace RR.Common.UI
_continueCountdownCoroutine = null;
}
if (_paymentManager != null)
// Безопасная отписка от события
if (_balanceManager != null && _isSubscribedToBalance)
{
_paymentManager.OnCreditAdded.RemoveListener(UnlockContinueButton);
_balanceManager.OnBalanceChanged.RemoveListener(UnlockContinueButton);
_isSubscribedToBalance = false;
}
base.OnDisable();
@ -82,14 +91,27 @@ namespace RR.Common.UI
/// <param name="gameTime">Время игры в секундах</param>
public void ShowLose(float gameTime)
{
// Инициализируем BalanceManager до открытия окна
if (_balanceManager == null)
{
_balanceManager = GameManager.Instance?.Balance;
}
// Подписываемся на событие изменения баланса, если еще не подписаны
if (_balanceManager != null && !_isSubscribedToBalance)
{
_balanceManager.OnBalanceChanged.AddListener(UnlockContinueButton);
_isSubscribedToBalance = true;
}
if (loseText)
{
loseText.text = $"Время игры: {TimeFormatHelper.FormatTime(gameTime)}";
}
if (creditText && _paymentManager != null)
if (creditText && _balanceManager != null)
{
creditText.text = string.Format(creditTextFormat, (object)_paymentManager.CreditsCount);
creditText.text = string.Format(creditTextFormat, (object)_balanceManager.CurrentBalance);
}
if (continueButtonCountdown <= 0f)
@ -103,8 +125,9 @@ namespace RR.Common.UI
if (continueButton)
{
continueButton.gameObject.SetActive(true);
var settings = RR.Common.Settings.SettingsManager.Instance;
continueButton.interactable = (settings != null && settings.GetBool("payment.freeGame")) || (_paymentManager != null && _paymentManager.CreditsCount > 0);
// Проверяем возможность продолжить игру через BalanceManager
bool canContinue = _balanceManager != null && _balanceManager.CanContinueGame();
continueButton.interactable = canContinue;
}
Open();
@ -160,23 +183,20 @@ namespace RR.Common.UI
}
}
private void UnlockContinueButton(int newCredits)
private void UnlockContinueButton(int newBalance)
{
if (_paymentManager == null) return;
if (_balanceManager == null) return;
var settings = RR.Common.Settings.SettingsManager.Instance;
if (_paymentManager.IsFreePlayEnabled() || (settings != null && settings.GetBool("payment.freeGame")))
{
if (continueButton)
{
continueButton.interactable = true;
}
return;
}
// Проверяем возможность продолжить через BalanceManager
if (continueButton)
{
continueButton.interactable = _paymentManager.CreditsCount > 0;
continueButton.interactable = _balanceManager.CanContinueGame();
}
// Обновляем текст баланса
if (creditText)
{
creditText.text = string.Format(creditTextFormat, newBalance);
}
}

View File

@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
@ -16,12 +16,12 @@ namespace RR.Common.UI
[Header("Тексты")]
[SerializeField] private TextMeshProUGUI paymentText;
private RR.Common.Managers.PaymentManager _paymentManager;
private RR.Common.Managers.BalanceManager _balanceManager;
private RR.Common.Settings.SettingsManager _settingsManager;
private void Start()
{
_paymentManager = RR.Common.Managers.PaymentManager.Instance;
_balanceManager = RR.Common.Managers.BalanceManager.Instance;
_settingsManager = RR.Common.Settings.SettingsManager.Instance;
Bootstrap();
@ -31,8 +31,8 @@ namespace RR.Common.UI
quitButton.gameObject.SetActive(false);
}
if (_paymentManager != null)
_paymentManager.OnCreditsChanged.AddListener(OnCreditsChanged);
if (_balanceManager != null)
_balanceManager.OnBalanceChanged.AddListener(OnBalanceChanged);
if (_settingsManager != null)
{
@ -43,8 +43,8 @@ namespace RR.Common.UI
private void OnDestroy()
{
if (_paymentManager != null)
_paymentManager.OnCreditsChanged.RemoveListener(OnCreditsChanged);
if (_balanceManager != null)
_balanceManager.OnBalanceChanged.RemoveListener(OnBalanceChanged);
if (_settingsManager != null)
{
@ -84,9 +84,9 @@ namespace RR.Common.UI
Application.Quit();
}
private void OnCreditsChanged(int credits)
private void OnBalanceChanged(int balance)
{
UpdatePlayButtonState(credits);
UpdatePlayButtonState();
}
private void OnSettingsChanged()
@ -94,12 +94,11 @@ namespace RR.Common.UI
UpdatePlayButtonState();
}
private void UpdatePlayButtonState(int credits = -1)
private void UpdatePlayButtonState()
{
if (credits < 0)
credits = _paymentManager?.CreditsCount ?? 0;
if (!newGameButton) return;
// Проверяем только через SettingsManager, так как PaymentManager обновляется с задержкой
// Проверяем бесплатную игру
if (_settingsManager != null && _settingsManager.GetBool("payment.freeGame"))
{
newGameButton.interactable = true;
@ -107,10 +106,9 @@ namespace RR.Common.UI
return;
}
paymentText.gameObject.SetActive(true);
if (!newGameButton) return;
bool canStartGame = credits >= 1;
// Проверяем баланс через BalanceManager
bool canStartGame = _balanceManager != null && _balanceManager.CanStartGame();
newGameButton.interactable = canStartGame;
paymentText.gameObject.SetActive(!canStartGame);
}

View File

@ -22,7 +22,10 @@ namespace RR.Common.UI
[Header("Связь с другими элементами")]
[SerializeField] private GameEndUIController bannerController;
private bool _isActive = true;
/// <summary>
/// Флаг активности меню. Деактивируется когда показан баннер проигрыша/победы.
/// </summary>
public bool IsActive { get; private set; } = true;
protected override void Awake()
{
@ -41,9 +44,9 @@ namespace RR.Common.UI
if (bannerController)
{
bannerController.WinBannerShown += _ => { _isActive = false; };
bannerController.LoseBannerShown += _ => { _isActive = false; };
bannerController.BannerDismissed += () => { _isActive = true; };
bannerController.WinBannerShown += _ => { IsActive = false; };
bannerController.LoseBannerShown += _ => { IsActive = false; };
bannerController.BannerDismissed += () => { IsActive = true; };
}
// Изначально скрыто - не вызываем base.Awake(), делаем это вручную
@ -52,6 +55,9 @@ namespace RR.Common.UI
private void OnPauseToggle(InputAction.CallbackContext ctx)
{
// Блокируем паузу если меню неактивно (например, показан баннер проигрыша/победы)
if (!IsActive) return;
GameManager.Instance.TogglePause();
}
@ -68,7 +74,7 @@ namespace RR.Common.UI
/// </summary>
private void HandlePauseChanged(bool paused)
{
if (!_isActive) return;
if (!IsActive) return;
if (paused)
Open();
@ -81,6 +87,9 @@ namespace RR.Common.UI
/// </summary>
public override void Close()
{
// Блокируем закрытие если меню неактивно (показан баннер)
if (!IsActive) return;
base.Close();
// Если игра ещё на паузе, снимаем её
@ -93,7 +102,14 @@ namespace RR.Common.UI
/// <summary>
/// Метод для возобновления игры.
/// </summary>
public static void OnResumeClicked() => GameManager.Instance.TogglePause();
public static void OnResumeClicked()
{
// Блокируем если меню неактивно (показан баннер)
var pauseMenu = FindFirstObjectByType<PauseMenu>();
if (pauseMenu != null && !pauseMenu.IsActive) return;
GameManager.Instance.TogglePause();
}
/// <summary>
/// Метод для выхода в главное меню.

View File

@ -1,5 +1,6 @@
using RR.Game2D.Core.Managers;
using RR.Game2D.Core.Managers;
using RR.Common.Settings;
using RR.Common.Managers;
using TMPro;
using UnityEngine;
@ -17,10 +18,12 @@ namespace RR.Common.UI
private GameManager _gameManager;
private SettingsManager _settingsManager;
private BalanceManager _balanceManager;
private void Start()
{
_gameManager = GameManager.Instance;
_balanceManager = BalanceManager.Instance;
if (_gameManager == null) return;
@ -28,14 +31,21 @@ namespace RR.Common.UI
if (_settingsManager == null) _settingsManager = SettingsManager.Instance;
UpdateLivesText(_gameManager.MaxLives);
UpdateLivesText();
UpdateLivesIncreaseText();
_gameManager.MaxLivesChanged += UpdateLivesText;
// Подписываемся на изменение баланса для пересчета жизней
if (_balanceManager != null)
{
_balanceManager.OnBalanceChanged.AddListener(OnBalanceChanged);
}
if (_settingsManager != null)
{
_settingsManager.OnSettingsLoaded += UpdateLivesIncreaseText;
_settingsManager.OnSettingsSaved += UpdateLivesIncreaseText;
_settingsManager.OnSettingsLoaded += OnSettingsChanged;
_settingsManager.OnSettingsSaved += OnSettingsChanged;
}
}
@ -43,24 +53,59 @@ namespace RR.Common.UI
{
if (_gameManager == null) _gameManager = GameManager.Instance;
if (_gameManager != null) _gameManager.MaxLivesChanged -= UpdateLivesText;
if (_balanceManager != null)
{
_balanceManager.OnBalanceChanged.RemoveListener(OnBalanceChanged);
}
if (_settingsManager != null)
{
_settingsManager.OnSettingsLoaded -= UpdateLivesIncreaseText;
_settingsManager.OnSettingsSaved -= UpdateLivesIncreaseText;
_settingsManager.OnSettingsLoaded -= OnSettingsChanged;
_settingsManager.OnSettingsSaved -= OnSettingsChanged;
}
}
private void OnBalanceChanged(int newBalance)
{
UpdateLivesText();
}
private void OnSettingsChanged()
{
UpdateLivesText();
}
private void UpdateLivesIncreaseText()
{
if (_settingsManager != null)
livesIncreaseText.text = string.Format(livesIncreaseString, _settingsManager.GetInt("gameplay.additionalLives"));
}
private void UpdateLivesText(int newLives)
private void UpdateLivesText()
{
livesText.text = string.Format(livesString, (object)newLives);
if (_gameManager == null || _balanceManager == null || _settingsManager == null)
return;
int currentBalance = _balanceManager.CurrentBalance;
int impulseCost = _settingsManager.GetInt("payment.paymentImpulseCost");
int startLives = _settingsManager.GetInt("gameplay.startLives");
int livesIncrement = _settingsManager.GetInt("gameplay.additionalLives");
if (impulseCost <= 0) impulseCost = 10;
// Рассчитываем количество жизней на основе баланса
int totalImpulses = currentBalance / impulseCost;
int totalLives = startLives;
if (totalImpulses > 0)
{
int additionalImpulses = totalImpulses - 1;
totalLives = startLives + (additionalImpulses * livesIncrement);
}
livesText.text = string.Format(livesString, totalLives);
}
}
}