added WinBannerController and LoseBannerController, implemented game end UI logic

This commit is contained in:
Данил Ярош 2025-12-16 17:00:39 +03:00
parent dafd317242
commit dc18ca9ff3
25 changed files with 3529 additions and 842 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

@ -0,0 +1,184 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1001 &8454563988256824744
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_Pivot.x
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_Pivot.y
value: 0.5
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_AnchorMin.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2735431105283990082, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: actions.Array.size
value: 4
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'actions.Array.data[0]'
value: 1880696228193501281
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'actions.Array.data[1]'
value: 1880696228193501282
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'actions.Array.data[2]'
value: 1880696228193501283
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'actions.Array.data[3]'
value: 1880696228193501284
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'managedReferences[-2]'
value:
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'managedReferences[1880696228193501280]'
value: Assembly-CSharp RR.Common.Behaviour.FreezePlayerAction
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'managedReferences[1880696228193501281]'
value: Assembly-CSharp RR.Common.Behaviour.MessageAppearAction
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'managedReferences[1880696228193501282]'
value: Assembly-CSharp RR.Common.Behaviour.WaitAction
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'managedReferences[1880696228193501283]'
value: Assembly-CSharp RR.Common.Behaviour.MessageDisappearAction
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: 'managedReferences[1880696228193501284]'
value: Assembly-CSharp RR.Common.Behaviour.DeleteGameObjectAction
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501281].endScale
value: 1
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501282].duration
value: 2
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501283].endScale
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501280].freezeMode
value: 7
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501281].startScale
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501283].startScale
value: 1
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501284].targetObject
value:
objectReference: {fileID: 4826131747682954655}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501281].transitionSpeed
value: 5
objectReference: {fileID: 0}
- target: {fileID: 3905633718102506837, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: managedReferences[1880696228193501283].transitionSpeed
value: 3
objectReference: {fileID: 0}
- target: {fileID: 4011995222962286647, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
propertyPath: m_Name
value: MessageBubble_Custom_1
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
--- !u!1 &4826131747682954655 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 4011995222962286647, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
m_PrefabInstance: {fileID: 8454563988256824744}
m_PrefabAsset: {fileID: 0}

View File

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

View File

@ -1180,12 +1180,19 @@ MonoBehaviour:
m_GameObject: {fileID: 294888496}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c815924369a0a64458e3c85a564de8b8, type: 3}
m_Script: {fileID: 11500000, guid: 8e0feede366043ef951637fa0f816177, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.ModalFocusTrap
modalRoot: {fileID: 294888496}
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.WinBannerController
firstSelected: {fileID: 1256436981}
enforce: 1
blockBackground: 1
enforceFocus: 1
canvasGroup: {fileID: 294888500}
fadeDuration: 0.15
closeAction: {fileID: 0}
closeButton: {fileID: 0}
winText: {fileID: 630106233}
mainMenuButton: {fileID: 1256436981}
qrCodesContainer: {fileID: 954402733}
--- !u!1 &322757142
GameObject:
m_ObjectHideFlags: 0
@ -2712,7 +2719,6 @@ GameObject:
- component: {fileID: 1069927039}
- component: {fileID: 1069927042}
- component: {fileID: 1069927041}
- component: {fileID: 1069927043}
m_Layer: 5
m_Name: LoseBanner
m_TagString: Untagged
@ -2793,7 +2799,13 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 7fd0f5dd52ebd7f41a46cb02a7f83497, type: 3}
m_Name:
m_EditorClassIdentifier: '::'
bannersController: {fileID: 1724863673}
firstSelected: {fileID: 1871409157}
blockBackground: 1
enforceFocus: 1
canvasGroup: {fileID: 1069927042}
fadeDuration: 0.15
closeAction: {fileID: 0}
closeButton: {fileID: 0}
loseText: {fileID: 1131560888}
creditText: {fileID: 322757144}
continueButton: {fileID: 1871409157}
@ -2813,21 +2825,6 @@ CanvasGroup:
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!114 &1069927043
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1069927037}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c815924369a0a64458e3c85a564de8b8, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.ModalFocusTrap
modalRoot: {fileID: 1069927037}
firstSelected: {fileID: 1871409157}
enforce: 1
--- !u!1 &1131560886
GameObject:
m_ObjectHideFlags: 0
@ -3860,6 +3857,20 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1527626192}
m_CullTransparentMesh: 1
--- !u!114 &1573983874
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3}
m_Name: UI/Cancel
m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference
m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
m_ActionId: ab443aed-9e90-4080-8896-92640a5b6838
--- !u!1 &1579229932
GameObject:
m_ObjectHideFlags: 0
@ -4136,7 +4147,6 @@ GameObject:
- component: {fileID: 1724863672}
- component: {fileID: 1724863676}
- component: {fileID: 1724863675}
- component: {fileID: 1724863674}
- component: {fileID: 1724863673}
m_Layer: 5
m_Name: GameEndPanel
@ -4178,22 +4188,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 1b61ec11c32d8aa42b00ee29ce87def2, type: 3}
m_Name:
m_EditorClassIdentifier: '::'
winPanel: {fileID: 294888500}
mainMenuButtonWin: {fileID: 1256436981}
winText: {fileID: 630106233}
losePanel: {fileID: 1069927042}
--- !u!225 &1724863674
CanvasGroup:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1724863671}
m_Enabled: 1
m_Alpha: 1
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
winBanner: {fileID: 294888501}
loseBanner: {fileID: 1069927041}
backgroundOverlay: {fileID: 1724863675}
overlayColor: {r: 0, g: 0, b: 0, a: 0.39215687}
--- !u!114 &1724863675
MonoBehaviour:
m_ObjectHideFlags: 0
@ -4580,8 +4578,14 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 55d821bd8bf8bb14c857f497ec6f2a0d, type: 3}
m_Name:
m_EditorClassIdentifier:
close: {fileID: 5054652661136980948, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
panel: {fileID: 1946308725}
firstSelected: {fileID: 1065887711}
blockBackground: 1
enforceFocus: 1
canvasGroup: {fileID: 1946308725}
fadeDuration: 0.15
closeAction: {fileID: 1573983874}
closeButton: {fileID: 1065887711}
pauseToggle: {fileID: 2080140806}
resumeButton: {fileID: 1065887711}
quitButton: {fileID: 1294478665}
bannerController: {fileID: 1724863673}
@ -4845,6 +4849,20 @@ MonoBehaviour:
livesText: {fileID: 250473979}
coinsText: {fileID: 122148826}
levelText: {fileID: 1162184471}
--- !u!114 &2080140806
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fc1515ab76e54f068e2f2207940fab32, type: 3}
m_Name: UI/Cancel
m_EditorClassIdentifier: Unity.InputSystem::UnityEngine.InputSystem.InputActionReference
m_Asset: {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
m_ActionId: ab443aed-9e90-4080-8896-92640a5b6838
--- !u!1 &2082762285
GameObject:
m_ObjectHideFlags: 0

View File

@ -193714,8 +193714,7 @@ MonoBehaviour:
- rid: 1880696163501343116
isOneTime: 1
messagePlaceholderPrefab: {fileID: 230978959095769727, guid: 0f453bdec7ad567458080bacb277b1c0, type: 3}
messageBubblePrefab: {fileID: 4011995222962286647, guid: 969225a5614fedf41878291f57bdeb88, type: 3}
defaultMessageOffset: {x: 0, y: 0.5, z: 0}
messageBubblePrefab: {fileID: 4826131747682954655, guid: 88d5927bc948ef94f89df153ca3ad9da, type: 3}
references:
version: 2
RefIds:
@ -193724,130 +193723,118 @@ MonoBehaviour:
data:
text: "\u041F\u0440\u0438\u0432\u0435\u0442! \u0422\u044B \u043D\u0435 \u043F\u043E\u043C\u043E\u0436\u0435\u0448\u044C
\u043C\u043D\u0435?"
target: {fileID: 2072794048}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: -3550973283731103874, guid: 4db2fbc7e829aec46b75cceb207881b9, type: 3}
- rid: 1880696163501343106
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "... \u041F\u0440\u0438\u0432\u0435\u0442"
target: {fileID: 0}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: 420805981189327916, guid: 8526ea9be1b0a56429731ec9f3edf875, type: 3}
- rid: 1880696163501343107
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u041C\u043E\u0436\u0435\u0442, \u0441\u043D\u0430\u0447\u0430\u043B\u0430
\u0441\u043A\u0430\u0436\u0435\u0448\u044C, \u043A\u0430\u043A \u0442\u044B
\u0440\u0430\u0437\u0433\u043E\u0432\u0430\u0440\u0438\u0432\u0430\u0435\u0448\u044C?"
target: {fileID: 0}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: 420805981189327916, guid: 8526ea9be1b0a56429731ec9f3edf875, type: 3}
- rid: 1880696163501343108
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u041C\u0435\u043D\u044F \u0437\u043E\u0432\u0443\u0442 \u0422\u0430\u043C\u0430
\u0413\u043E\u0447\u0438\u043D\u0433"
target: {fileID: 2072794048}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: -3550973283731103874, guid: 4db2fbc7e829aec46b75cceb207881b9, type: 3}
- rid: 1880696163501343109
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u042F \u043E\u0447\u0435\u043D\u044C \u0443\u043C\u043D\u044B\u0439
\u0440\u043E\u0431\u043E\u0442-\u0442\u0430\u043C\u0430\u0433\u043E\u0447\u0438"
target: {fileID: 2072794048}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: -3550973283731103874, guid: 4db2fbc7e829aec46b75cceb207881b9, type: 3}
- rid: 1880696163501343110
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u0410 \u0440\u0430\u0437\u0433\u043E\u0432\u0430\u0440\u0438\u0432\u0430\u044E
\u043F\u0440\u0438 \u043F\u043E\u043C\u043E\u0449\u0438 \u0434\u0438\u043D\u0430\u043C\u0438\u043A\u043E\u0432
\u0432\u043D\u0443\u0442\u0440\u0438"
target: {fileID: 2072794048}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: -3550973283731103874, guid: 4db2fbc7e829aec46b75cceb207881b9, type: 3}
- rid: 1880696163501343111
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u041C\u0435\u043D\u044F \u0437\u043E\u0432\u0443\u0442 \u0410\u043B\u0438\u0441\u0430.
\u042F \u043F\u043E\u043C\u043E\u0433\u0443 \u0442\u0435\u0431\u0435!"
target: {fileID: 0}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: 420805981189327916, guid: 8526ea9be1b0a56429731ec9f3edf875, type: 3}
- rid: 1880696163501343112
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u041D\u043E \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u043A\u0430\u0436\u0438,
\u0447\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u043E?"
target: {fileID: 0}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: 420805981189327916, guid: 8526ea9be1b0a56429731ec9f3edf875, type: 3}
- rid: 1880696163501343113
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u041F\u0430\u0441\u043A\u043E\u0441\u0442\u043D\u0438\u043A \u0414\u0430\u043D\u0440\u043E
\u0441\u043B\u043E\u043C\u0430\u043B \u043C\u043E\u0439 \u0430\u0432\u0442\u043E\u043C\u0430\u0442!"
target: {fileID: 2072794048}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: -3550973283731103874, guid: 4db2fbc7e829aec46b75cceb207881b9, type: 3}
- rid: 1880696163501343114
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u0410 \u044F \u0432\u044B\u043F\u0430\u043B \u0438\u0437 \u043D\u0435\u0433\u043E,
\u0438 \u043F\u043E\u0442\u0435\u0440\u044F\u043B\u0441\u044F..."
target: {fileID: 2072794048}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: -3550973283731103874, guid: 4db2fbc7e829aec46b75cceb207881b9, type: 3}
- rid: 1880696163501343115
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u041D\u0443 \u0442\u0430\u043A \u0447\u0442\u043E, \u043F\u043E\u043C\u043E\u0436\u0435\u0448\u044C
\u043C\u043D\u0435?"
target: {fileID: 2072794048}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: -3550973283731103874, guid: 4db2fbc7e829aec46b75cceb207881b9, type: 3}
- rid: 1880696163501343116
type: {class: AddMessageAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
text: "\u0414\u0430! \u0414\u0430\u0432\u0430\u0439 \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043C
\u0435\u0433\u043E!"
target: {fileID: 0}
offset: {x: 0, y: 0, z: 0}
speakerIcon: {fileID: 420805981189327916, guid: 8526ea9be1b0a56429731ec9f3edf875, type: 3}
- rid: 1880696163501343124
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 2
duration: 3
- rid: 1880696163501343125
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 1
duration: 3
- rid: 1880696163501343126
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 2
duration: 3
- rid: 1880696163501343127
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 1
duration: 3
- rid: 1880696163501343128
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 1
duration: 3
- rid: 1880696163501343129
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 2
duration: 3
- rid: 1880696163501343131
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 1
duration: 3
- rid: 1880696163501343132
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 1
duration: 3
- rid: 1880696163501343133
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 1
duration: 3
- rid: 1880696163501343134
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 1
duration: 3
- rid: 1880696163501343135
type: {class: WaitAction, ns: RR.Common.Behaviour, asm: Assembly-CSharp}
data:
duration: 1.5
duration: 4
--- !u!61 &1094121057
BoxCollider2D:
m_ObjectHideFlags: 0
@ -390772,11 +390759,6 @@ Transform:
m_CorrespondingSourceObject: {fileID: 3897414768386052035, guid: fd2391a459d3e9d4bba8faa914f8e433, type: 3}
m_PrefabInstance: {fileID: 2070698805}
m_PrefabAsset: {fileID: 0}
--- !u!1 &2072794048 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 5993946430821065449, guid: 7fd3a37dc702ece42b51e5867473f0ff, type: 3}
m_PrefabInstance: {fileID: 5189102261111869253}
m_PrefabAsset: {fileID: 0}
--- !u!1 &2073324771
GameObject:
m_ObjectHideFlags: 0

View File

@ -207,7 +207,6 @@ GameObject:
- component: {fileID: 28542764}
- component: {fileID: 28542767}
- component: {fileID: 28542766}
- component: {fileID: 28542768}
m_Layer: 5
m_Name: LevelSelectModal
m_TagString: Untagged
@ -285,12 +284,15 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 5b96039a6a73f764295ca4b3de5b5318, type: 3}
m_Name:
m_EditorClassIdentifier:
close: {fileID: 5054652661136980948, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
firstSelected: {fileID: 1846005930}
blockBackground: 1
enforceFocus: 1
canvasGroup: {fileID: 28542767}
fadeDuration: 0.15
closeAction: {fileID: 0}
closeButton: {fileID: 1846005930}
content: {fileID: 1792218763}
levelButtonPrefab: {fileID: -2647512120749192222, guid: 292805c897bde6f4fb6d6cfb3010ae3a, type: 3}
closeButton: {fileID: 1846005930}
fadeDuration: 0.15
--- !u!225 &28542767
CanvasGroup:
m_ObjectHideFlags: 0
@ -303,21 +305,6 @@ CanvasGroup:
m_Interactable: 1
m_BlocksRaycasts: 1
m_IgnoreParentGroups: 0
--- !u!114 &28542768
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 28542762}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c815924369a0a64458e3c85a564de8b8, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.ModalFocusTrap
modalRoot: {fileID: 28542762}
firstSelected: {fileID: 1846005930}
enforce: 1
--- !u!1 &37257833
GameObject:
m_ObjectHideFlags: 0
@ -9123,7 +9110,6 @@ GameObject:
- component: {fileID: 1460883872}
- component: {fileID: 1460883871}
- component: {fileID: 1460883870}
- component: {fileID: 1460883869}
- component: {fileID: 1460883868}
m_Layer: 5
m_Name: AdminStatsModal
@ -9165,6 +9151,13 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: df89ed1d48bc36140864491f04301407, type: 3}
m_Name:
m_EditorClassIdentifier: '::'
firstSelected: {fileID: 683903128}
blockBackground: 1
enforceFocus: 1
canvasGroup: {fileID: 1460883870}
fadeDuration: 0.15
closeAction: {fileID: 0}
closeButton: {fileID: 683903128}
gamesPlayedTextPrefix: "\u0418\u0433\u0440 \u0441\u044B\u0433\u0440\u0430\u043D\u043E: "
gamesPlayedText: {fileID: 820785317}
gamesPlayedNulTextPrefix: "\u0418\u0433\u0440 \u0441\u044B\u0433\u0440\u0430\u043D\u043E
@ -9177,24 +9170,6 @@ MonoBehaviour:
prizesGivenNulText: {fileID: 674695656}
resetGamesPlayedNulButton: {fileID: 728958718}
resetPrizesGivenNulButton: {fileID: 804461262}
closeButton: {fileID: 683903128}
canvasGroup: {fileID: 1460883870}
fadeDuration: 0.15
--- !u!114 &1460883869
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1460883866}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c815924369a0a64458e3c85a564de8b8, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.ModalFocusTrap
modalRoot: {fileID: 1412406033}
firstSelected: {fileID: 683903128}
enforce: 1
--- !u!225 &1460883870
CanvasGroup:
m_ObjectHideFlags: 0
@ -9480,7 +9455,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.132, y: 0.5}
m_AnchorMax: {x: 0.846, y: 0.77500004}
m_AnchoredPosition: {x: -54.50001, y: 5}
m_AnchoredPosition: {x: -54.500015, y: 5}
m_SizeDelta: {x: -107, y: -10}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1474919456
@ -11939,7 +11914,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.000019041576}
m_AnchoredPosition: {x: 0, y: 0.00088984595}
m_SizeDelta: {x: 0, y: 300}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1792218764
@ -13085,7 +13060,6 @@ GameObject:
- component: {fileID: 1932206569}
- component: {fileID: 1932206571}
- component: {fileID: 1932206570}
- component: {fileID: 1932206572}
- component: {fileID: 1932206575}
- component: {fileID: 1932206573}
m_Layer: 5
@ -13153,21 +13127,6 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1932206568}
m_CullTransparentMesh: 1
--- !u!114 &1932206572
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1932206568}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c815924369a0a64458e3c85a564de8b8, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.ModalFocusTrap
modalRoot: {fileID: 1932206568}
firstSelected: {fileID: 917079365}
enforce: 1
--- !u!114 &1932206573
MonoBehaviour:
m_ObjectHideFlags: 0
@ -13180,11 +13139,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 85d4ced96061fd14592d8a7f7fb824de, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUI
firstSelected: {fileID: 917079365}
blockBackground: 1
enforceFocus: 1
canvasGroup: {fileID: 1932206575}
fadeDuration: 0.15
closeAction: {fileID: 0}
closeButton: {fileID: 0}
triggerKey: {fileID: -3077985071382975143, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
saveButton: {fileID: 917079365}
canvasGroup: {fileID: 1932206575}
content: {fileID: 1537614489}
fadeDuration: 0.15
--- !u!225 &1932206575
CanvasGroup:
m_ObjectHideFlags: 0
@ -13317,7 +13281,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.132, y: 0.21750003}
m_AnchorMax: {x: 0.846, y: 0.5}
m_AnchoredPosition: {x: -54.50001, y: -79}
m_AnchoredPosition: {x: -54.500015, y: -79}
m_SizeDelta: {x: -107, y: -63}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1959311160

View File

@ -1,142 +1,100 @@
using System;
using System.Collections;
using RR.Common.Controls;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using RR.Common.Controls;
public class AdminStatsWindowUIController : MonoBehaviour
namespace RR.Common.UI
{
[Header("Тексты статистики")]
[SerializeField] private string gamesPlayedTextPrefix = "Игр сыграно: ";
[SerializeField] private TextMeshProUGUI gamesPlayedText;
[SerializeField] private string gamesPlayedNulTextPrefix = "Игр сыграно (обнуляемый): ";
[SerializeField] private TextMeshProUGUI gamesPlayedNulText;
[SerializeField] private string prizesGivenTextPrefix = "Призов выдано: ";
[SerializeField] private TextMeshProUGUI prizesGivenText;
[SerializeField] private string prizesGivenNulTextPrefix = "Призов выдано (обнуляемый): ";
[SerializeField] private TextMeshProUGUI prizesGivenNulText;
[Header("Кнопки")]
[SerializeField] private Button resetGamesPlayedNulButton;
[SerializeField] private Button resetPrizesGivenNulButton;
[SerializeField] private Button closeButton;
[Header("UI")]
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private float fadeDuration = 0.15f;
private string _gamesPlayedKey;
private string _gamesPlayedNulKey;
private string _prizesGivenKey;
private string _prizesGivenNulKey;
private void Awake()
{
_gamesPlayedKey = PrizeSystem.key_starts;
_gamesPlayedNulKey = PrizeSystem.key_startsNul;
_prizesGivenKey = PrizeSystem.key_prizesgiven;
_prizesGivenNulKey = PrizeSystem.key_prizesgivenNul;
if (resetGamesPlayedNulButton) resetGamesPlayedNulButton.onClick.AddListener(() => ResetPlayerPrefsStat(_gamesPlayedNulKey));
if (resetPrizesGivenNulButton) resetPrizesGivenNulButton.onClick.AddListener( () => ResetPlayerPrefsStat(_prizesGivenNulKey));
if (closeButton) closeButton.onClick.AddListener(Toggle);
gameObject.SetActive(false);
}
private void OnDestroy()
{
if (resetGamesPlayedNulButton) resetGamesPlayedNulButton.onClick.RemoveListener(() => ResetPlayerPrefsStat(_gamesPlayedNulKey));
if (resetPrizesGivenNulButton) resetPrizesGivenNulButton.onClick.RemoveListener( () => ResetPlayerPrefsStat(_prizesGivenNulKey));
if (closeButton) closeButton.onClick.RemoveListener(Toggle);
}
public void Toggle()
{
if (gameObject.activeSelf)
{
Close();
}
else
{
Open();
}
}
private void Open()
{
gameObject.SetActive(true);
UpdateText();
StopAllCoroutines();
StartCoroutine(Fade(0f, 1f, fadeDuration));
}
private void Close()
{
StopAllCoroutines();
StartCoroutine(Fade(1f, 0f, fadeDuration, () => { gameObject.SetActive(false); }));
}
private void UpdateText()
{
if (gamesPlayedText)
{
int gamesPlayed = PlayerPrefs.GetInt(_gamesPlayedKey, 0);
gamesPlayedText.text = gamesPlayedTextPrefix + gamesPlayed;
}
if (gamesPlayedNulText)
{
int gamesPlayedNul = PlayerPrefs.GetInt(_gamesPlayedNulKey, 0);
gamesPlayedNulText.text = gamesPlayedNulTextPrefix + gamesPlayedNul;
}
if (prizesGivenText)
{
int prizesGiven = PlayerPrefs.GetInt(_prizesGivenKey, 0);
prizesGivenText.text = prizesGivenTextPrefix + prizesGiven;
}
if (prizesGivenNulText)
{
int prizesGivenNul = PlayerPrefs.GetInt(_prizesGivenNulKey, 0);
prizesGivenNulText.text = prizesGivenNulTextPrefix + prizesGivenNul;
}
}
private void ResetPlayerPrefsStat(string key)
{
PlayerPrefs.SetInt(key, 0);
UpdateText();
}
/// <summary>
/// Метод для плавного перехода (затухания)
/// Модальное окно статистики администратора.
/// Отображает статистику игр и призов с возможностью сброса.
/// </summary>
/// <param name="from">Начальная прозрачность</param>
/// <param name="to">Конечная прозрачность</param>
/// <param name="time">Время перехода</param>
/// <param name="onDone">Действие при выполнении перехода</param>
private IEnumerator Fade(float from, float to, float time, System.Action onDone = null)
public class AdminStatsWindowUIController : Modal
{
if (!canvasGroup) { onDone?.Invoke(); yield break; }
float t = 0f;
canvasGroup.alpha = from;
canvasGroup.blocksRaycasts = true;
while (t < time)
[Header("Тексты статистики")]
[SerializeField] private string gamesPlayedTextPrefix = "Игр сыграно: ";
[SerializeField] private TextMeshProUGUI gamesPlayedText;
[SerializeField] private string gamesPlayedNulTextPrefix = "Игр сыграно (обнуляемый): ";
[SerializeField] private TextMeshProUGUI gamesPlayedNulText;
[SerializeField] private string prizesGivenTextPrefix = "Призов выдано: ";
[SerializeField] private TextMeshProUGUI prizesGivenText;
[SerializeField] private string prizesGivenNulTextPrefix = "Призов выдано (обнуляемый): ";
[SerializeField] private TextMeshProUGUI prizesGivenNulText;
[Header("Кнопки")]
[SerializeField] private Button resetGamesPlayedNulButton;
[SerializeField] private Button resetPrizesGivenNulButton;
private string _gamesPlayedKey;
private string _gamesPlayedNulKey;
private string _prizesGivenKey;
private string _prizesGivenNulKey;
protected override void Awake()
{
t += Time.unscaledDeltaTime;
canvasGroup.alpha = Mathf.Lerp(from, to, t / time);
yield return null;
base.Awake();
_gamesPlayedKey = PrizeSystem.key_starts;
_gamesPlayedNulKey = PrizeSystem.key_startsNul;
_prizesGivenKey = PrizeSystem.key_prizesgiven;
_prizesGivenNulKey = PrizeSystem.key_prizesgivenNul;
if (resetGamesPlayedNulButton)
resetGamesPlayedNulButton.onClick.AddListener(() => ResetPlayerPrefsStat(_gamesPlayedNulKey));
if (resetPrizesGivenNulButton)
resetPrizesGivenNulButton.onClick.AddListener(() => ResetPlayerPrefsStat(_prizesGivenNulKey));
}
private void OnDestroy()
{
if (resetGamesPlayedNulButton)
resetGamesPlayedNulButton.onClick.RemoveListener(() => ResetPlayerPrefsStat(_gamesPlayedNulKey));
if (resetPrizesGivenNulButton)
resetPrizesGivenNulButton.onClick.RemoveListener(() => ResetPlayerPrefsStat(_prizesGivenNulKey));
}
/// <summary>
/// Переопределяем Open для обновления текста при открытии.
/// </summary>
public override void Open()
{
UpdateText();
base.Open();
}
private void UpdateText()
{
if (gamesPlayedText)
{
int gamesPlayed = PlayerPrefs.GetInt(_gamesPlayedKey, 0);
gamesPlayedText.text = gamesPlayedTextPrefix + gamesPlayed;
}
if (gamesPlayedNulText)
{
int gamesPlayedNul = PlayerPrefs.GetInt(_gamesPlayedNulKey, 0);
gamesPlayedNulText.text = gamesPlayedNulTextPrefix + gamesPlayedNul;
}
if (prizesGivenText)
{
int prizesGiven = PlayerPrefs.GetInt(_prizesGivenKey, 0);
prizesGivenText.text = prizesGivenTextPrefix + prizesGiven;
}
if (prizesGivenNulText)
{
int prizesGivenNul = PlayerPrefs.GetInt(_prizesGivenNulKey, 0);
prizesGivenNulText.text = prizesGivenNulTextPrefix + prizesGivenNul;
}
}
private void ResetPlayerPrefsStat(string key)
{
PlayerPrefs.SetInt(key, 0);
UpdateText();
}
canvasGroup.alpha = to;
canvasGroup.blocksRaycasts = to > 0.99f;
onDone?.Invoke();
}
}

View File

@ -1,27 +1,38 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using RR.Game2D.Core.Managers;
using TMPro;
using UnityEngine.EventSystems;
using Random = System.Random;
namespace RR.Common.UI
{
/// <summary>
/// Контроллер панели завершения игры.
/// Управляет отображением баннеров выигрыша и проигрыша.
/// </summary>
public class GameEndUIController : MonoBehaviour
{
[Header("Меню выигрыша")]
[SerializeField] private CanvasGroup winPanel;
[SerializeField] private Button mainMenuButtonWin;
[SerializeField] private TextMeshProUGUI winText;
[Header("Баннеры")]
[SerializeField] private WinBannerController winBanner;
[SerializeField] private LoseBannerController loseBanner;
[Header("Меню проигрыша")]
[SerializeField] private CanvasGroup losePanel;
[Header("Фон")]
[SerializeField] private Image backgroundOverlay;
[SerializeField] private Color overlayColor = new Color(0, 0, 0, 100f/255f);
/// <summary>
/// Событие, вызываемое при показе баннера выигрыша.
/// </summary>
public event Action<float> WinBannerShown;
/// <summary>
/// Событие, вызываемое при показе баннера проигрыша.
/// </summary>
public event Action<float> LoseBannerShown;
/// <summary>
/// Событие, вызываемое при закрытии любого баннера.
/// </summary>
public event Action BannerDismissed;
private void OnEnable()
@ -30,6 +41,16 @@ namespace RR.Common.UI
GameManager.Instance.GameWonEvent += OnGameWon;
GameManager.Instance.GameOverEvent += OnGameOver;
// Подписываемся на события закрытия баннеров
if (winBanner)
{
winBanner.WinBannerDismissed += OnBannerDismissed;
}
if (loseBanner)
{
loseBanner.ContinueClicked += OnBannerDismissed;
}
}
private void OnDisable()
@ -38,105 +59,81 @@ namespace RR.Common.UI
GameManager.Instance.GameWonEvent -= OnGameWon;
GameManager.Instance.GameOverEvent -= OnGameOver;
// Отписываемся от событий закрытия баннеров
if (winBanner)
{
winBanner.WinBannerDismissed -= OnBannerDismissed;
}
if (loseBanner)
{
loseBanner.ContinueClicked -= OnBannerDismissed;
}
}
private void Awake()
{
if (mainMenuButtonWin) mainMenuButtonWin.onClick.AddListener(OnMainMenuClicked);
if (losePanel) losePanel.GetComponent<LoseBannerController>().ContinueClicked += OnContinueClicked;
// Изначально скрыто
SetVisible(winPanel, false, true);
SetVisible(losePanel, false, true);
GetComponent<Image>().color = new Color(0, 0,0, 0);
// Изначально скрываем фон
if (backgroundOverlay)
{
backgroundOverlay.color = new Color(0, 0, 0, 0);
}
}
private void OnMainMenuClicked()
{
if(winPanel.interactable) SetVisible(winPanel, false);
if(losePanel.interactable) SetVisible(losePanel, false);
BannerDismissed?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
}
private void OnContinueClicked()
{
if(winPanel.interactable) SetVisible(winPanel, false);
if(losePanel.interactable) SetVisible(losePanel, false);
GetComponent<Image>().color = new Color(0, 0,0, 0);
BannerDismissed?.Invoke();
}
private void OnGameWon(float gameTime)
{
GameManager.Instance.SetPause(true);
if (GameManager.Instance)
{
GameManager.Instance.SetPause(true);
}
GetComponent<Image>().color = new Color(0, 0,0, 100f/255f);
ShowOverlay();
winText.text = $"Время игры: {FormatTime(gameTime)}";
SetVisible(winPanel, true);
EventSystem.current.firstSelectedGameObject = mainMenuButtonWin.gameObject;
EventSystem.current.SetSelectedGameObject(mainMenuButtonWin.gameObject);
if (winBanner)
{
winBanner.ShowWin(gameTime);
}
WinBannerShown?.Invoke(gameTime);
}
private void OnGameOver(float gameTime)
{
GameManager.Instance.SetPause(true);
if (GameManager.Instance)
{
GameManager.Instance.SetPause(true);
}
GetComponent<Image>().color = new Color(0, 0,0, 100f/255f);
ShowOverlay();
SetVisible(losePanel, true);
if (loseBanner)
{
loseBanner.ShowLose(gameTime);
}
LoseBannerShown?.Invoke(gameTime);
}
private void SetVisible(CanvasGroup canvasGroup, bool visible, bool instant = false)
private void ShowOverlay()
{
if (!canvasGroup) return;
canvasGroup.interactable = visible;
canvasGroup.blocksRaycasts = visible;
if (instant)
if (backgroundOverlay)
{
canvasGroup.alpha = visible ? 1f : 0f;
backgroundOverlay.color = overlayColor;
}
else
{
StopAllCoroutines();
StartCoroutine(Fade(canvasGroup, visible ? 1f : 0f, 0.15f));
}
}
/// <summary>
/// Метод для плавного изменения видимости
/// </summary>
/// <param name="cg">Группа холстов</param>
/// <param name="target">Целевое значение альфа-канала</param>
/// <param name="duration">Продолжительность перехода</param>
private static IEnumerator Fade(CanvasGroup cg, float target, float duration)
{
float start = cg.alpha;
float t = 0f;
while (t < duration)
{
t += Time.unscaledDeltaTime;
cg.alpha = Mathf.Lerp(start, target, t / duration);
yield return null;
}
cg.alpha = target;
}
public static string FormatTime(float gameTime)
private void HideOverlay()
{
TimeSpan interval = TimeSpan.FromSeconds(gameTime);
return interval.ToString(@"mm\:ss");
if (backgroundOverlay)
{
backgroundOverlay.color = new Color(0, 0, 0, 0);
}
}
private void OnBannerDismissed()
{
HideOverlay();
BannerDismissed?.Invoke();
}
}
}

View File

@ -1,52 +1,30 @@
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
namespace RR.Common.UI
{
public class LevelSelectModal : MonoBehaviour
/// <summary>
/// Модальное окно выбора уровня.
/// Наследуется от Modal для автоматического управления фокусом и блокировки фона.
/// </summary>
public class LevelSelectModal : Modal
{
[Header("Управление")]
[SerializeField] private InputActionReference close;
[Header("UI")]
[SerializeField] private CanvasGroup canvasGroup;
[Header("Контент")]
[SerializeField] private Transform content; // Grid parent
[SerializeField] private LevelButton levelButtonPrefab;
[SerializeField] private Button closeButton;
[SerializeField] private float fadeDuration = 0.15f;
private readonly List<LevelButton> _levelsPool = new();
private void Awake()
{
if (closeButton) closeButton.onClick.AddListener(Close);
gameObject.SetActive(false);
}
// ==== ПУБЛИЧНЫЙ API ====
/// <summary>
/// Метод для открытия модального окна
/// Метод для открытия модального окна.
/// Переопределяет базовый метод для сборки кнопок перед открытием.
/// </summary>
public void Open()
public override void Open()
{
BuildIfNeeded();
gameObject.SetActive(true);
StopAllCoroutines();
StartCoroutine(Fade(0f, 1f, fadeDuration));
}
/// <summary>
/// Метод для закрытия модального окна
/// </summary>
private void Close()
{
StopAllCoroutines();
StartCoroutine(Fade(1f, 0f, fadeDuration, () => gameObject.SetActive(false)));
base.Open();
}
// ==== ВНУТРЕННЕЕ ====
@ -68,41 +46,15 @@ namespace RR.Common.UI
.ThenBy(e => e.stage)
.ToList();
foreach (LevelButton btn in levels.Select(lv => Instantiate(levelButtonPrefab, content)))
foreach (LevelEntry _ in levels)
{
LevelButton btn = Instantiate(levelButtonPrefab, content);
_levelsPool.Add(btn);
}
for (int index = 0; index < _levelsPool.Count; index++)
{
LevelButton btn = _levelsPool[index];
// if (index == 0) btn.Setup(
// $"{levels[index].world}-{levels[index].stage}",
// levels[index].world,
// levels[index].stage,
// onDown: closeButton,
// onUp: closeButton,
// onLeft: closeButton,
// onRight: _levelsPool.Count > 1 ? _levelsPool[1].GetComponent<Button>() : closeButton
// );
// else if (index == _levelsPool.Count - 1) btn.Setup(
// $"{levels[index].world}-{levels[index].stage}",
// levels[index].world,
// levels[index].stage,
// onDown: closeButton,
// onUp: closeButton,
// onLeft: _levelsPool.Count > 1 ? _levelsPool[index - 1].GetComponent<Button>() : closeButton,
// onRight: closeButton
// );
// else btn.Setup(
// $"{levels[index].world}-{levels[index].stage}",
// levels[index].world,
// levels[index].stage,
// onUp: closeButton,
// onDown: closeButton,
// onLeft: _levelsPool[index - 1].GetComponent<Button>(),
// onRight: _levelsPool[index + 1].GetComponent<Button>()
// );
btn.Setup(
$"{levels[index].world}-{levels[index].stage}",
@ -119,16 +71,9 @@ namespace RR.Common.UI
break;
}
}
if (_levelsPool.Count <= 0) return;
// Navigation closeNav = closeButton.navigation;
// closeNav.mode = Navigation.Mode.Explicit;
// closeNav.selectOnUp = _levelsPool.First().GetComponent<Button>();
// closeNav.selectOnDown = _levelsPool.First().GetComponent<Button>();
// closeButton.navigation = closeNav;
}
private struct LevelEntry { public string name; public int world; public int stage; }
private struct LevelEntry { public int world; public int stage; }
/// <summary>
/// Метод для перебора уровней в игре
@ -140,52 +85,20 @@ namespace RR.Common.UI
for (int i = 0; i < n; i++)
{
string path = SceneUtility.GetScenePathByBuildIndex(i);
string buttonName = System.IO.Path.GetFileNameWithoutExtension(path);
string sceneName = System.IO.Path.GetFileNameWithoutExtension(path);
// фильтр "только уровни" по формату имени W-S:
string[] parts = buttonName.Split('-');
string[] parts = sceneName.Split('-');
if (parts.Length != 2) continue;
if (!int.TryParse(parts[0], out int world)) continue;
if (!int.TryParse(parts[1], out int stage)) continue;
// Исключим служебные сцены, даже если вдруг совпадут по формату (на всякий случай)
if (buttonName.Equals("Level UI", System.StringComparison.OrdinalIgnoreCase)) continue;
if (buttonName.Equals("MainMenu", System.StringComparison.OrdinalIgnoreCase)) continue;
// Исключим служебные сцены
if (sceneName.Equals("Level UI", System.StringComparison.OrdinalIgnoreCase)) continue;
if (sceneName.Equals("MainMenu", System.StringComparison.OrdinalIgnoreCase)) continue;
yield return new LevelEntry { name = buttonName, world = world, stage = stage };
yield return new LevelEntry { world = world, stage = stage };
}
}
/// <summary>
/// Метод для плавного перехода (затухания)
/// </summary>
/// <param name="from">Начальная прозрачность</param>
/// <param name="to">Конечная прозрачнеость</param>
/// <param name="time">Время перехода</param>
/// <param name="onDone">Действие при выполнении перехода</param>
private IEnumerator Fade(float from, float to, float time, System.Action onDone = null)
{
if (!canvasGroup) { onDone?.Invoke(); yield break; }
float t = 0f;
canvasGroup.alpha = from;
canvasGroup.blocksRaycasts = true;
while (t < time)
{
t += Time.unscaledDeltaTime;
canvasGroup.alpha = Mathf.Lerp(from, to, t / time);
yield return null;
}
canvasGroup.alpha = to;
canvasGroup.interactable = to > 0.99f;
canvasGroup.blocksRaycasts = to > 0.99f;
onDone?.Invoke();
}
private void Update()
{
// ESC закрыть
if (gameObject.activeSelf && close.action.WasPressedThisFrame())
Close();
}
}
}

View File

@ -5,17 +5,17 @@ using UnityEngine;
using UnityEngine.UI;
using TMPro;
using RR.Common.UI;
using RR.Game2D.Core.Managers;
using UnityEngine.EventSystems;
namespace RR.Common.UI
{
public class LoseBannerController : MonoBehaviour
/// <summary>
/// Контроллер баннера проигрыша.
/// Модальное окно, которое отображается при поражении игрока.
/// </summary>
public class LoseBannerController : Modal
{
[SerializeField] private GameEndUIController bannersController;
[Header("Кнопки")]
[Header("Элементы баннера проигрыша")]
[SerializeField] private TextMeshProUGUI loseText;
[SerializeField] private TextMeshProUGUI creditText;
[SerializeField] private Button continueButton;
@ -28,25 +28,40 @@ namespace RR.Common.UI
private PaymentManager _paymentManager;
private Coroutine _continueCountdownCoroutine;
/// <summary>
/// Событие, вызываемое при нажатии "Продолжить" или закрытии баннера.
/// </summary>
public event Action ContinueClicked;
private void OnEnable()
protected override void Awake()
{
bannersController.LoseBannerShown += OnLoseBannerShown;
base.Awake();
if (continueButton)
{
continueButton.onClick.AddListener(OnContinueButtonClicked);
}
}
_paymentManager = GameManager.Instance.Payment;
protected override void OnEnable()
{
base.OnEnable();
_paymentManager = GameManager.Instance?.Payment;
if (_paymentManager != null)
{
int credits = _paymentManager.CreditsCount;
creditText.text = string.Format(creditTextFormat, credits);
if (creditText)
{
creditText.text = string.Format(creditTextFormat, credits);
}
_paymentManager.OnCreditAdded.AddListener(UnlockContinueButton);
}
}
private void OnDisable()
protected override void OnDisable()
{
bannersController.LoseBannerShown -= OnLoseBannerShown;
// Остановим корутину, если объект деактивируется
if (_continueCountdownCoroutine != null)
{
@ -58,43 +73,46 @@ namespace RR.Common.UI
{
_paymentManager.OnCreditAdded.RemoveListener(UnlockContinueButton);
}
base.OnDisable();
}
private void Awake()
/// <summary>
/// Показывает баннер проигрыша с указанным временем игры.
/// </summary>
/// <param name="gameTime">Время игры в секундах</param>
public void ShowLose(float gameTime)
{
if (continueButton) continueButton.onClick.AddListener(OnContinueClicked);
// Инициализируем видимость/интерактивность кнопок в безопасное состояние
if (continueButton) continueButton.gameObject.SetActive(false);
if (continueButton && continueButtonText) continueButtonText.text = string.Empty;
}
private void OnLoseBannerShown(float gameTime)
{
// EventSystem.current.firstSelectedGameObject = continueButton.gameObject;
EventSystem.current.SetSelectedGameObject(continueButton.gameObject);
loseText.text = $"Время игры: {GameEndUIController.FormatTime(gameTime)}";
creditText.text = string.Format(creditTextFormat, _paymentManager.CreditsCount);
if (loseText)
{
loseText.text = $"Время игры: {TimeFormatHelper.FormatTime(gameTime)}";
}
if (creditText && _paymentManager != null)
{
creditText.text = string.Format(creditTextFormat, _paymentManager.CreditsCount);
}
if (continueButtonCountdown <= 0f)
{
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
GameManager.Instance?.SetPause(false);
GameManager.Instance?.LoadScene("MainMenu", false);
return;
}
if (continueButton)
{
continueButton.gameObject.SetActive(true);
continueButton.interactable = _paymentManager.CreditsCount > 0;
continueButton.interactable = _paymentManager != null && _paymentManager.CreditsCount > 0;
}
Open();
// Остановим предыдущую корутину, если есть
if (_continueCountdownCoroutine != null)
{
StopCoroutine(_continueCountdownCoroutine);
_continueCountdownCoroutine = null;
}
_continueCountdownCoroutine = StartCoroutine(ContinueCountdownCoroutine(continueButtonCountdown));
@ -102,19 +120,15 @@ namespace RR.Common.UI
private IEnumerator ContinueCountdownCoroutine(float duration)
{
float elapsed = 0f;
// Обновляем сразу первоначальный текст
int initialSeconds = Mathf.RoundToInt(duration);
UpdateContinueText(initialSeconds);
while (elapsed < duration)
{
// Using yield return null and manual timing with unscaled time for more reliable updates
float startTime = Time.unscaledTime;
yield return null;
// Wait for approximately 1 second using unscaled time
while (Time.unscaledTime - startTime < 1.0f)
{
yield return null;
@ -125,9 +139,12 @@ namespace RR.Common.UI
UpdateContinueText(remaining);
}
if (continueButton)
// Таймер истёк — закрываем баннер и выходим в меню
Close();
ContinueClicked?.Invoke();
if (GameManager.Instance)
{
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
}
@ -138,11 +155,15 @@ namespace RR.Common.UI
private void UpdateContinueText(int secondsRemaining)
{
if (continueButtonText)
{
continueButtonText.text = $"Продолжить? ({secondsRemaining})";
}
}
private void UnlockContinueButton(int newCredits)
{
if (_paymentManager == null) return;
if (_paymentManager.IsFreePlayEnabled() || SettingsManager.Instance.GetCurrentGameSettings().isFreeGame)
{
if (continueButton)
@ -158,7 +179,7 @@ namespace RR.Common.UI
}
}
private void OnContinueClicked()
private void OnContinueButtonClicked()
{
// При клике на "Продолжить" — остановим таймер и продолжим игру
if (_continueCountdownCoroutine != null)
@ -167,9 +188,14 @@ namespace RR.Common.UI
_continueCountdownCoroutine = null;
}
Close();
ContinueClicked?.Invoke();
GameManager.Instance.SetPause(false);
GameManager.Instance.ContinueGame();
if (GameManager.Instance)
{
GameManager.Instance.SetPause(false);
GameManager.Instance.ContinueGame();
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4f4ee3155ab74532bb53c936f11318c1
timeCreated: 1765785807

View File

@ -0,0 +1,333 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
namespace RR.Common.UI
{
/// <summary>
/// Базовый компонент модального окна.
/// Автоматически регистрируется в ModalManager при активации.
/// </summary>
[DisallowMultipleComponent]
public class Modal : MonoBehaviour
{
[Header("Настройки модального окна")]
[Tooltip("Первый элемент, который получит фокус при открытии окна.")]
[SerializeField] private Selectable firstSelected;
[Tooltip("Блокировать интерактивность элементов за пределами модального окна.")]
[SerializeField] private bool blockBackground = true;
[Tooltip("Автоматически возвращать фокус внутрь окна, если он уходит наружу.")]
[SerializeField] private bool enforceFocus = true;
[Header("Анимация")]
[Tooltip("CanvasGroup для анимации появления/исчезновения.")]
[SerializeField] private CanvasGroup canvasGroup;
[Tooltip("Длительность анимации появления/исчезновения.")]
[SerializeField] private float fadeDuration = 0.15f;
[Header("Закрытие")]
[Tooltip("InputAction для закрытия модального окна (например, Escape).")]
[SerializeField] private InputActionReference closeAction;
[Tooltip("Кнопка закрытия модального окна.")]
[SerializeField] private Button closeButton;
/// <summary>
/// Первый элемент для фокуса при открытии.
/// </summary>
public Selectable FirstSelected => firstSelected;
/// <summary>
/// Блокировать ли фон при открытии.
/// </summary>
public bool BlockBackground => blockBackground;
/// <summary>
/// Следить ли за фокусом и возвращать его внутрь окна.
/// </summary>
public bool EnforceFocus => enforceFocus;
/// <summary>
/// Событие, вызываемое перед открытием окна.
/// </summary>
public event Action OnOpening;
/// <summary>
/// Событие, вызываемое после открытия окна.
/// </summary>
public event Action OnOpened;
/// <summary>
/// Событие, вызываемое перед закрытием окна.
/// </summary>
public event Action OnClosing;
/// <summary>
/// Событие, вызываемое после закрытия окна.
/// </summary>
public event Action OnClosed;
protected bool _isOpen;
protected Coroutine _fadeCoroutine;
/// <summary>
/// Открыто ли модальное окно.
/// </summary>
public bool IsOpen => _isOpen;
protected virtual void Awake()
{
if (closeButton)
{
closeButton.onClick.AddListener(Close);
}
// Если canvasGroup не указан или указан на родительском объекте,
// создаём/получаем CanvasGroup на самом модальном окне
if (!canvasGroup || canvasGroup.gameObject != gameObject)
{
canvasGroup = GetComponent<CanvasGroup>();
if (!canvasGroup)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
// Изначально скрыто
if (canvasGroup)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
gameObject.SetActive(false);
}
protected virtual void OnEnable()
{
// Регистрируемся в менеджере
ModalManager.Instance.Push(this);
// Подписываемся на действие закрытия
if (closeAction)
{
closeAction.action.Enable();
closeAction.action.performed += OnCloseActionPerformed;
}
}
protected virtual void OnDisable()
{
// Отписываемся от действия закрытия
if (closeAction)
{
closeAction.action.performed -= OnCloseActionPerformed;
}
// Удаляемся из менеджера
ModalManager.Instance.Pop(this);
}
private void OnCloseActionPerformed(InputAction.CallbackContext ctx)
{
Close();
}
/// <summary>
/// Открывает модальное окно.
/// </summary>
public virtual void Open()
{
if (_isOpen) return;
OnOpening?.Invoke();
gameObject.SetActive(true);
_isOpen = true;
if (canvasGroup)
{
if (_fadeCoroutine != null)
{
StopCoroutine(_fadeCoroutine);
}
_fadeCoroutine = StartCoroutine(FadeCoroutine(0f, 1f, fadeDuration, () =>
{
OnOpened?.Invoke();
}));
}
else
{
OnOpened?.Invoke();
}
}
/// <summary>
/// Закрывает модальное окно.
/// </summary>
public virtual void Close()
{
if (!_isOpen && !gameObject.activeSelf) return;
OnClosing?.Invoke();
if (canvasGroup && fadeDuration > 0f)
{
if (_fadeCoroutine != null)
{
StopCoroutine(_fadeCoroutine);
}
_fadeCoroutine = StartCoroutine(FadeCoroutine(canvasGroup.alpha, 0f, fadeDuration, () =>
{
gameObject.SetActive(false);
_isOpen = false;
OnClosed?.Invoke();
}));
}
else
{
if (canvasGroup)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
gameObject.SetActive(false);
_isOpen = false;
OnClosed?.Invoke();
}
}
/// <summary>
/// Мгновенно открывает окно без анимации.
/// </summary>
public void OpenInstant()
{
if (_isOpen) return;
OnOpening?.Invoke();
gameObject.SetActive(true);
_isOpen = true;
if (canvasGroup)
{
canvasGroup.alpha = 1f;
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
OnOpened?.Invoke();
}
/// <summary>
/// Мгновенно закрывает окно без анимации.
/// </summary>
public void CloseInstant()
{
if (!_isOpen && !gameObject.activeSelf) return;
OnClosing?.Invoke();
if (canvasGroup)
{
canvasGroup.alpha = 0f;
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
gameObject.SetActive(false);
_isOpen = false;
OnClosed?.Invoke();
}
/// <summary>
/// Переключает состояние окна (открыто/закрыто).
/// </summary>
public void Toggle()
{
if (_isOpen)
Close();
else
Open();
}
private IEnumerator FadeCoroutine(float from, float to, float duration, Action onComplete = null)
{
if (!canvasGroup)
{
onComplete?.Invoke();
yield break;
}
// При открытии сразу делаем интерактивным
if (to > from)
{
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / duration);
canvasGroup.alpha = Mathf.Lerp(from, to, t);
yield return null;
}
canvasGroup.alpha = to;
// При закрытии выключаем интерактивность
if (to < from)
{
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
onComplete?.Invoke();
_fadeCoroutine = null;
}
/// <summary>
/// Устанавливает первый элемент для фокуса (можно менять динамически).
/// </summary>
public void SetFirstSelected(Selectable selectable)
{
firstSelected = selectable;
}
#if UNITY_EDITOR
protected virtual void Reset()
{
// Пытаемся автоматически найти CanvasGroup
canvasGroup = GetComponent<CanvasGroup>();
if (!canvasGroup)
{
canvasGroup = GetComponentInChildren<CanvasGroup>();
}
// Ищем кнопку закрытия
Button[] buttons = GetComponentsInChildren<Button>(true);
foreach (Button btn in buttons)
{
string btnName = btn.name.ToLower();
if (btnName.Contains("close") || btnName.Contains("back") || btnName.Contains("cancel"))
{
closeButton = btn;
break;
}
}
// Ищем первый Selectable для фокуса
firstSelected = GetComponentInChildren<Selectable>(false);
}
#endif
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 46e5f9b83fbe4d6c90471d64468f6787
timeCreated: 1765785953

View File

@ -0,0 +1,386 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace RR.Common.UI
{
/// <summary>
/// Центральный менеджер модальных окон.
/// Управляет стеком открытых модальных окон и фокусом между ними.
/// </summary>
public class ModalManager : MonoBehaviour
{
private static ModalManager _instance;
/// <summary>
/// Singleton-инстанс менеджера модальных окон.
/// Создаётся автоматически при первом обращении.
/// </summary>
public static ModalManager Instance
{
get
{
if (_instance) return _instance;
// Ищем существующий в сцене
_instance = FindAnyObjectByType<ModalManager>();
if (_instance) return _instance;
// Создаём новый
GameObject go = new GameObject("[ModalManager]");
_instance = go.AddComponent<ModalManager>();
DontDestroyOnLoad(go);
return _instance;
}
}
/// <summary>
/// Информация о состоянии UI до открытия модального окна.
/// </summary>
private class ModalStackEntry
{
public Modal Modal;
public GameObject PreviousSelected;
public List<CanvasGroup> DisabledGroups = new();
}
private readonly Stack<ModalStackEntry> _stack = new();
/// <summary>
/// Событие, вызываемое при открытии любого модального окна.
/// </summary>
public event Action<Modal> OnModalOpened;
/// <summary>
/// Событие, вызываемое при закрытии любого модального окна.
/// </summary>
public event Action<Modal> OnModalClosed;
/// <summary>
/// Текущее активное модальное окно (на вершине стека).
/// </summary>
public Modal CurrentModal => _stack.Count > 0 ? _stack.Peek().Modal : null;
/// <summary>
/// Есть ли открытые модальные окна.
/// </summary>
public bool HasOpenModals => _stack.Count > 0;
/// <summary>
/// Количество открытых модальных окон.
/// </summary>
public int OpenModalCount => _stack.Count;
private void Awake()
{
if (_instance && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}
private void Update()
{
// Следим за тем, чтобы фокус оставался внутри текущего модального окна
if (_stack.Count == 0) return;
ModalStackEntry top = _stack.Peek();
if (!top.Modal || !top.Modal.gameObject.activeInHierarchy) return;
if (!top.Modal.EnforceFocus) return;
EnsureFocusInModal(top.Modal);
}
/// <summary>
/// Регистрирует модальное окно в стеке (вызывается из Modal.OnEnable).
/// </summary>
internal void Push(Modal modal)
{
if (modal == null) return;
// Проверяем, не зарегистрировано ли уже это окно
foreach (ModalStackEntry entry in _stack)
{
if (entry.Modal == modal) return;
}
ModalStackEntry newEntry = new ModalStackEntry
{
Modal = modal,
PreviousSelected = EventSystem.current ? EventSystem.current.currentSelectedGameObject : null
};
// Блокируем интерактивность всего, что не принадлежит этому модальному окну
if (modal.BlockBackground)
{
BlockBackgroundInteraction(modal, newEntry);
}
_stack.Push(newEntry);
// Устанавливаем фокус на первый элемент модального окна
SetInitialFocus(modal);
OnModalOpened?.Invoke(modal);
}
/// <summary>
/// Удаляет модальное окно из стека (вызывается из Modal.OnDisable).
/// </summary>
internal void Pop(Modal modal)
{
if (modal == null) return;
if (_stack.Count == 0) return;
// Ищем и удаляем модальное окно из стека
Stack<ModalStackEntry> temp = new Stack<ModalStackEntry>();
ModalStackEntry foundEntry = null;
while (_stack.Count > 0)
{
ModalStackEntry entry = _stack.Pop();
if (entry.Modal == modal)
{
foundEntry = entry;
break;
}
temp.Push(entry);
}
// Возвращаем остальные в стек
while (temp.Count > 0)
{
_stack.Push(temp.Pop());
}
if (foundEntry == null) return;
// Восстанавливаем интерактивность заблокированных групп
RestoreBackgroundInteraction(foundEntry);
// Восстанавливаем фокус
RestoreFocus(foundEntry);
OnModalClosed?.Invoke(modal);
}
/// <summary>
/// Закрывает все открытые модальные окна.
/// </summary>
public void CloseAll()
{
while (_stack.Count > 0)
{
ModalStackEntry entry = _stack.Peek();
if (entry.Modal != null)
{
entry.Modal.Close();
}
else
{
_stack.Pop();
}
}
}
/// <summary>
/// Закрывает текущее (верхнее) модальное окно.
/// </summary>
public void CloseCurrent()
{
if (_stack.Count == 0) return;
Modal current = _stack.Peek().Modal;
if (current != null)
{
current.Close();
}
}
private void BlockBackgroundInteraction(Modal modal, ModalStackEntry entry)
{
// Находим все CanvasGroup, которые не принадлежат модальному окну
CanvasGroup[] allGroups = FindObjectsByType<CanvasGroup>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (CanvasGroup group in allGroups)
{
if (!group || !group.interactable) continue;
if (IsUnderModal(group.transform, modal)) continue;
// Не блокируем родительские CanvasGroup модального окна
if (IsParentOfModal(group.transform, modal)) continue;
// Не блокируем CanvasGroup, если модальное окно является его потомком
if (modal.transform.IsChildOf(group.transform)) continue;
// Проверяем, не принадлежит ли эта группа другому модальному окну в стеке
bool belongsToOtherModal = false;
foreach (ModalStackEntry stackEntry in _stack)
{
if (stackEntry.Modal && IsUnderModal(group.transform, stackEntry.Modal))
{
belongsToOtherModal = true;
break;
}
}
if (belongsToOtherModal) continue;
group.interactable = false;
entry.DisabledGroups.Add(group);
}
// Если у модального окна нет CanvasGroup-родителя, блокируем все Selectable напрямую
if (entry.DisabledGroups.Count == 0)
{
BlockSelectablesOutsideModal(modal, entry);
}
}
private void BlockSelectablesOutsideModal(Modal modal, ModalStackEntry entry)
{
Selectable[] all = FindObjectsByType<Selectable>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
foreach (Selectable s in all)
{
if (!s || !s.interactable) continue;
if (IsUnderModal(s.transform, modal)) continue;
// Добавляем временную CanvasGroup для блокировки
CanvasGroup cg = s.GetComponent<CanvasGroup>();
if (!cg)
{
cg = s.gameObject.AddComponent<CanvasGroup>();
}
if (cg.interactable)
{
cg.interactable = false;
entry.DisabledGroups.Add(cg);
}
}
}
private void RestoreBackgroundInteraction(ModalStackEntry entry)
{
foreach (CanvasGroup group in entry.DisabledGroups)
{
if (group)
{
group.interactable = true;
}
}
entry.DisabledGroups.Clear();
}
private void SetInitialFocus(Modal modal)
{
if (!EventSystem.current) return;
GameObject toSelect = null;
if (modal.FirstSelected && modal.FirstSelected.gameObject.activeInHierarchy)
{
toSelect = modal.FirstSelected.gameObject;
}
else
{
Selectable any = modal.GetComponentInChildren<Selectable>(includeInactive: false);
if (any) toSelect = any.gameObject;
}
if (!toSelect) return;
// Сбрасываем и устанавливаем фокус
EventSystem.current.SetSelectedGameObject(null);
EventSystem.current.SetSelectedGameObject(toSelect);
}
private void RestoreFocus(ModalStackEntry entry)
{
if (!EventSystem.current) return;
// Если есть другие модальные окна в стеке, фокусируемся на верхнем
if (_stack.Count > 0)
{
Modal topModal = _stack.Peek().Modal;
if (topModal != null)
{
SetInitialFocus(topModal);
return;
}
}
// Иначе восстанавливаем предыдущий фокус
GameObject toSelect = null;
if (entry.PreviousSelected && entry.PreviousSelected.activeInHierarchy)
{
Selectable selectable = entry.PreviousSelected.GetComponent<Selectable>();
if (selectable && selectable.interactable)
{
toSelect = entry.PreviousSelected;
}
}
// Если предыдущий элемент недоступен, ищем любой доступный
if (!toSelect)
{
Selectable anySelectable = FindAnyObjectByType<Selectable>(FindObjectsInactive.Exclude);
if (anySelectable && anySelectable.interactable)
{
toSelect = anySelectable.gameObject;
}
}
EventSystem.current.SetSelectedGameObject(toSelect);
}
private void EnsureFocusInModal(Modal modal)
{
if (!EventSystem.current) return;
GameObject current = EventSystem.current.currentSelectedGameObject;
// Если текущий выделенный элемент находится внутри модального окна, всё ок
if (current && IsUnderModal(current.transform, modal))
{
return;
}
// Иначе возвращаем фокус внутрь модального окна
SetInitialFocus(modal);
}
private bool IsUnderModal(Transform t, Modal modal)
{
if (!modal) return false;
Transform root = modal.transform;
for (Transform cur = t; cur; cur = cur.parent)
{
if (cur == root) return true;
}
return false;
}
/// <summary>
/// Проверяет, является ли указанный Transform родителем модального окна.
/// </summary>
private bool IsParentOfModal(Transform t, Modal modal)
{
if (!modal || !t) return false;
Transform modalTransform = modal.transform;
for (Transform cur = modalTransform.parent; cur; cur = cur.parent)
{
if (cur == t) return true;
}
return false;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7437b0d73d7e4c7e90d74fe313806cba
timeCreated: 1765785807

View File

@ -1,177 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
namespace RR.Common.UI
{
[DisallowMultipleComponent]
public class ModalFocusTrap : MonoBehaviour
{
[Header("Modal")]
public GameObject modalRoot;
public Selectable firstSelected;
[Header("Behavior")]
[Tooltip("Следить и возвращать фокус внутрь модалки, если он уходит наружу.")]
public bool enforce = true;
private bool _wasOpenLastFrame;
private GameObject _prevSelectedGlobal;
private readonly List<Selectable> _outside = new();
private readonly Dictionary<Selectable, bool> _outsidePrev = new();
private Selectable _lastModalSelected;
private void Reset()
{
if (modalRoot != null) return;
Transform sibling = transform.parent ? transform.parent.Find("LevelSelectModal") : null;
if (sibling) modalRoot = sibling.gameObject;
}
private void Update()
{
if (!modalRoot) return;
bool isOpen = modalRoot.activeSelf;
switch (isOpen)
{
case true when !_wasOpenLastFrame:
OnModalOpened();
break;
case false when _wasOpenLastFrame:
OnModalClosed();
break;
}
_wasOpenLastFrame = isOpen;
if (isOpen && enforce) EnsureFocusInsideModal();
}
private void OnDisable()
{
if (_wasOpenLastFrame) OnModalClosed();
}
private void OnModalOpened()
{
// запомнить, что было выделено до открытия
_prevSelectedGlobal = EventSystem.current ? EventSystem.current.currentSelectedGameObject : null;
// выключить интерактив вне модалки
CollectOutsideSelectables();
foreach (Selectable s in _outside.Where(s => s))
{
_outsidePrev[s] = s.interactable;
s.interactable = false;
}
// сфокусировать элемент внутри модалки
GameObject toSelect = null;
if (firstSelected && firstSelected.gameObject.activeInHierarchy)
toSelect = firstSelected.gameObject;
else
{
Selectable any = modalRoot.GetComponentInChildren<Selectable>(includeInactive: false);
if (any) toSelect = any.gameObject;
}
if (!toSelect || !EventSystem.current) return;
EventSystem.current.SetSelectedGameObject(null);
EventSystem.current.SetSelectedGameObject(toSelect);
_lastModalSelected = toSelect.GetComponent<Selectable>();
}
private void OnModalClosed()
{
// Восстанавливаем интерактивность всех внешних элементов
foreach ((Selectable selectable, bool value) in _outsidePrev)
{
if (selectable) selectable.interactable = value;
}
_outsidePrev.Clear();
_outside.Clear();
// Возвращаем фокус на элемент, который был выделен до открытия модалки
if (EventSystem.current)
{
GameObject toSelect = null;
// Проверяем, что предыдущий элемент всё ещё активен и интерактивен
if (_prevSelectedGlobal && _prevSelectedGlobal.activeInHierarchy)
{
Selectable selectable = _prevSelectedGlobal.GetComponent<Selectable>();
if (selectable && selectable.interactable)
{
toSelect = _prevSelectedGlobal;
}
}
// Если предыдущий элемент недоступен, ищем любой доступный Selectable
if (!toSelect)
{
Selectable anySelectable = Object.FindAnyObjectByType<Selectable>(FindObjectsInactive.Exclude);
if (anySelectable && anySelectable.interactable && !IsUnderModal(anySelectable.transform))
{
toSelect = anySelectable.gameObject;
}
}
EventSystem.current.SetSelectedGameObject(toSelect);
}
_lastModalSelected = null;
_prevSelectedGlobal = null;
}
private void CollectOutsideSelectables()
{
_outside.Clear();
_outsidePrev.Clear();
Selectable[] all = Object.FindObjectsByType<Selectable>(
FindObjectsInactive.Include,
FindObjectsSortMode.None);
foreach (Selectable s in all)
{
if (!s || !s.gameObject.activeInHierarchy) continue;
if (!IsUnderModal(s.transform))
_outside.Add(s);
}
}
private bool IsUnderModal(Transform t)
{
if (!modalRoot) return false;
Transform root = modalRoot.transform;
for (Transform cur = t; cur; cur = cur.parent)
if (cur == root) return true;
return false;
}
private void EnsureFocusInsideModal()
{
if (!EventSystem.current) return;
GameObject cur = EventSystem.current.currentSelectedGameObject;
if (!cur || !IsUnderModal(cur.transform))
{
GameObject fallback =
(_lastModalSelected && _lastModalSelected.gameObject.activeInHierarchy) ? _lastModalSelected.gameObject :
(firstSelected && firstSelected.gameObject.activeInHierarchy) ? firstSelected.gameObject :
modalRoot.GetComponentInChildren<Selectable>()?.gameObject;
if (!fallback) return;
EventSystem.current.SetSelectedGameObject(fallback);
_lastModalSelected = fallback.GetComponent<Selectable>();
}
else
{
_lastModalSelected = cur.GetComponent<Selectable>();
}
}
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: c815924369a0a64458e3c85a564de8b8

View File

@ -1,143 +1,102 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
using UnityEngine.EventSystems;
using RR.Game2D.Core.Managers;
namespace RR.Common.UI
{
public class PauseMenu : MonoBehaviour
/// <summary>
/// Меню паузы. Наследуется от Modal для автоматического управления фокусом.
/// Открывается/закрывается через GameManager.PauseStateChanged.
/// </summary>
public class PauseMenu : Modal
{
[Header("Управление")]
[SerializeField] private InputActionReference close;
[Header("Управление паузой")]
[SerializeField] private InputActionReference pauseToggle;
[Header("UI")]
[SerializeField] private CanvasGroup panel;
[Header("Кнопки меню")]
[SerializeField] private Button resumeButton;
[SerializeField] private Button quitButton;
[Header("Связь с другими элементами")]
[SerializeField] private GameEndUIController bannerController;
//[SerializeField] private Button restartButton;
//[SerializeField] private KeyCode toggleKey = KeyCode.Escape; // Esc
private bool isActive = true;
private bool _isActive = true;
private void OnEnable()
protected override void Awake()
{
// Настраиваем обработчики кнопок
if (resumeButton) resumeButton.onClick.AddListener(OnResumeClicked);
if (quitButton) quitButton.onClick.AddListener(OnQuitClicked);
if (pauseToggle) pauseToggle.action.performed += OnPauseToggle;
// Устанавливаем первый выбранный элемент
SetFirstSelected(resumeButton);
if (GameManager.Instance != null)
GameManager.Instance.PauseStateChanged += HandlePauseChanged;
bannerController.WinBannerShown += (f) => { isActive = false; };
bannerController.LoseBannerShown += (f) => { isActive = false; };
bannerController.BannerDismissed += () => { isActive = true; };
if (bannerController)
{
bannerController.WinBannerShown += _ => { _isActive = false; };
bannerController.LoseBannerShown += _ => { _isActive = false; };
bannerController.BannerDismissed += () => { _isActive = true; };
}
// Изначально скрыто - не вызываем base.Awake(), делаем это вручную
gameObject.SetActive(false);
}
private void OnDisable()
private void OnPauseToggle(InputAction.CallbackContext ctx)
{
if (GameManager.Instance != null)
GameManager.Instance.PauseStateChanged -= HandlePauseChanged;
GameManager.Instance.TogglePause();
}
private void OnDestroy()
{
// ReSharper disable once EventUnsubscriptionViaAnonymousDelegate
if (close) close.action.performed -= _ => GameManager.Instance.TogglePause();
}
private void Awake()
{
if (resumeButton) resumeButton.onClick.AddListener(OnResumeClicked);
if (quitButton) quitButton.onClick.AddListener(OnQuitClicked);
//if (restartButton) restartButton.onClick.AddListener(OnRestartClicked);
// Изначально скрыто
SetVisible(false, true);
if (pauseToggle) pauseToggle.action.performed -= OnPauseToggle;
if (close) close.action.performed += _ => GameManager.Instance.TogglePause();
if (GameManager.Instance != null)
GameManager.Instance.PauseStateChanged -= HandlePauseChanged;
}
// private void Update()
// {
// if (close.action.WasPressedThisFrame() && !GameManager.Instance.IsPaused)
// GameManager.Instance.TogglePause();
// }
/// <summary>
/// Обработка события паузы
/// Обработка события паузы от GameManager.
/// </summary>
/// <param name="paused">Ставить ли на паузу</param>
private void HandlePauseChanged(bool paused)
{
if (!isActive) return;
if (!_isActive) return;
if (paused)
{
EventSystem.current.firstSelectedGameObject = resumeButton.gameObject;
EventSystem.current.SetSelectedGameObject(resumeButton.gameObject);
}
SetVisible(paused);
}
/// <summary>
/// Метод для изменения отображения
/// </summary>
/// <param name="visible">Отображать ли меню паузы</param>
/// <param name="instant">Мгновенно или нет</param>
private void SetVisible(bool visible, bool instant = false)
{
if (!panel) return;
panel.interactable = visible;
panel.blocksRaycasts = visible;
if (instant)
{
panel.alpha = visible ? 1f : 0f;
}
Open();
else
{
StopAllCoroutines();
StartCoroutine(Fade(panel, visible ? 1f : 0f, 0.15f));
}
Close();
}
/// <summary>
/// Метод для плавного изменения видимости
/// Переопределяем Close, чтобы при закрытии также снимать паузу.
/// </summary>
/// <param name="cg">Группа холстов</param>
/// <param name="target">Целевое значение альфа-канала</param>
/// <param name="duration">Продолжительность перехода</param>
private static IEnumerator Fade(CanvasGroup cg, float target, float duration)
public override void Close()
{
float start = cg.alpha;
float t = 0f;
while (t < duration)
base.Close();
// Если игра ещё на паузе, снимаем её
if (GameManager.Instance != null && GameManager.Instance.IsPaused)
{
t += Time.unscaledDeltaTime;
cg.alpha = Mathf.Lerp(start, target, t / duration);
yield return null;
GameManager.Instance.SetPause(false);
}
cg.alpha = target;
}
/// <summary>
/// Метод для возобновления игры
/// Метод для возобновления игры.
/// </summary>
public static void OnResumeClicked() => GameManager.Instance.TogglePause();
//public void OnRestartClicked()
//{
// GameManager.Instance.SetPause(false);
// GameManager.Instance.ResetLevel();
//}
/// <summary>
/// Метод для выхода в главное меню
/// Метод для выхода в главное меню.
/// </summary>
public void OnQuitClicked()
{

View File

@ -1,4 +1,3 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
@ -7,27 +6,31 @@ using RR.Game2D.Core.Managers;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно настроек игры.
/// Управляет отображением и сохранением настроек через SettingsManager.
/// </summary>
[RequireComponent(typeof(CanvasGroup))]
public class SettingsUI : MonoBehaviour
public class SettingsUI : Modal
{
[Header("Управление")]
[Header("Дополнительное управление")]
[SerializeField] private InputActionReference triggerKey;
[SerializeField] private Button saveButton;
[Header("UI")]
[SerializeField] private CanvasGroup canvasGroup;
[Header("Контент настроек")]
[SerializeField] private Transform content;
[SerializeField] private float fadeDuration = 0.15f;
private SettingsUIList settingsUIList;
private SettingsManager settingsManager;
private void Awake()
protected override void Awake()
{
base.Awake();
settingsManager = SettingsManager.Instance;
if (triggerKey != null) triggerKey.action.started += Toggle;
if (triggerKey != null)
triggerKey.action.started += OnTriggerKey;
if (settingsManager == null)
{
@ -38,9 +41,8 @@ namespace RR.Common.UI
settingsManager.OnSettingsLoaded += UpdateSettings;
settingsManager.OnSettingsSaved += SaveSettingsHandler;
if (saveButton) saveButton.onClick.AddListener(SaveAndClose);
gameObject.SetActive(false);
if (saveButton)
saveButton.onClick.AddListener(SaveAndClose);
}
private void OnDestroy()
@ -51,14 +53,16 @@ namespace RR.Common.UI
settingsManager.OnSettingsSaved -= SaveSettingsHandler;
}
if (triggerKey != null) triggerKey.action.started -= Toggle;
if (triggerKey != null)
triggerKey.action.started -= OnTriggerKey;
if (saveButton) saveButton.onClick.RemoveListener(SaveAndClose);
if (saveButton)
saveButton.onClick.RemoveListener(SaveAndClose);
}
private void Toggle(InputAction.CallbackContext ctx)
private void OnTriggerKey(InputAction.CallbackContext ctx)
{
if (gameObject.activeSelf)
if (IsOpen)
{
SaveAndClose();
}
@ -68,22 +72,22 @@ namespace RR.Common.UI
}
}
public void Open()
/// <summary>
/// Переопределяем Open для обновления настроек при открытии.
/// </summary>
public override void Open()
{
gameObject.SetActive(true);
StopAllCoroutines();
StartCoroutine(Fade(0f, 1f, fadeDuration));
}
private void Close()
{
StartCoroutine(Fade(1f, 0f, fadeDuration, () => { gameObject.SetActive(false); }));
UpdateSettings();
base.Open();
}
private void Save()
{
UpdateSettings();
settingsUIList.SaveSettings();
if (settingsUIList == null)
settingsUIList = content.GetComponent<SettingsUIList>();
if (settingsUIList != null)
settingsUIList.SaveSettings();
}
public void SaveAndClose()
@ -94,38 +98,17 @@ namespace RR.Common.UI
private void UpdateSettings()
{
if (settingsUIList == null) settingsUIList = content.GetComponent<SettingsUIList>();
if (settingsUIList != null) settingsUIList.UpdateSettings(settingsManager.GetCurrentGameSettings());
if (settingsUIList == null)
settingsUIList = content.GetComponent<SettingsUIList>();
if (settingsUIList != null)
settingsUIList.UpdateSettings(settingsManager.GetCurrentGameSettings());
}
private void SaveSettingsHandler()
{
GameManager.Logger.Log(name, "Saved settings");
}
/// <summary>
/// Метод для плавного перехода (затухания)
/// </summary>
/// <param name="from">Начальная прозрачность</param>
/// <param name="to">Конечная прозрачность</param>
/// <param name="time">Время перехода</param>
/// <param name="onDone">Действие при выполнении перехода</param>
private IEnumerator Fade(float from, float to, float time, System.Action onDone = null)
{
if (!canvasGroup) { onDone?.Invoke(); yield break; }
float t = 0f;
canvasGroup.alpha = from;
canvasGroup.blocksRaycasts = true;
while (t < time)
{
t += Time.unscaledDeltaTime;
canvasGroup.alpha = Mathf.Lerp(from, to, t / time);
yield return null;
}
canvasGroup.alpha = to;
canvasGroup.blocksRaycasts = to > 0.99f;
onDone?.Invoke();
}
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace RR.Common.UI
{
/// <summary>
/// Вспомогательный класс для форматирования времени в UI.
/// </summary>
public static class TimeFormatHelper
{
/// <summary>
/// Форматирует время в строку вида mm:ss.
/// </summary>
/// <param name="seconds">Время в секундах</param>
/// <returns>Отформатированная строка времени</returns>
public static string FormatTime(float seconds)
{
TimeSpan interval = TimeSpan.FromSeconds(seconds);
return interval.ToString(@"mm\:ss");
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5bba4010c66d4779b16aeaa3fda74069
timeCreated: 1765888933

View File

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using RR.Common.Managers;
using RR.Game2D.Core.Managers;
namespace RR.Common.UI
{
/// <summary>
/// Контроллер баннера выигрыша.
/// Модальное окно, которое отображается при победе игрока.
/// </summary>
public class WinBannerController : Modal
{
[Header("Элементы баннера выигрыша")]
[SerializeField] private TextMeshProUGUI winText;
[SerializeField] private Button mainMenuButton;
[Header("QR-коды")]
[Tooltip("Контейнер для размещения QR-кодов. QR-коды создаются динамически.")]
[SerializeField] private RectTransform qrCodesContainer;
/// <summary>
/// Событие, вызываемое при закрытии баннера выигрыша.
/// </summary>
public event Action WinBannerDismissed;
private readonly List<GameObject> _createdQRCodeObjects = new();
protected override void Awake()
{
base.Awake();
if (mainMenuButton)
{
mainMenuButton.onClick.AddListener(OnMainMenuClicked);
}
}
/// <summary>
/// Показывает баннер выигрыша с указанным временем игры.
/// </summary>
/// <param name="gameTime">Время игры в секундах</param>
public void ShowWin(float gameTime)
{
if (winText)
{
winText.text = $"Время игры: {TimeFormatHelper.FormatTime(gameTime)}";
}
// Отображаем QR-коды
DisplayQRCodes();
Open();
}
/// <summary>
/// Отображает QR-коды из PrizeSystemManager.
/// Создаёт объекты полностью динамически без префаба.
/// </summary>
private void DisplayQRCodes()
{
// Очищаем ранее созданные QR-коды
ClearQRCodes();
if (qrCodesContainer == null) return;
if (PrizeSystemManager.Instance == null) return;
Texture2D[] qrCodes = PrizeSystemManager.Instance.GetQRCodes();
if (qrCodes == null || qrCodes.Length == 0) return;
// Создаём QR-коды динамически
foreach (Texture2D qrTexture in qrCodes)
{
if (qrTexture == null) continue;
// Создаём GameObject с Image
GameObject qrObject = new GameObject("QRCode", typeof(RectTransform), typeof(Image));
qrObject.transform.SetParent(qrCodesContainer, false);
// Конвертируем Texture2D в Sprite
Sprite qrSprite = Sprite.Create(
qrTexture,
new Rect(0, 0, qrTexture.width, qrTexture.height),
new Vector2(0.5f, 0.5f)
);
// Настраиваем Image
Image image = qrObject.GetComponent<Image>();
image.sprite = qrSprite;
image.raycastTarget = false;
_createdQRCodeObjects.Add(qrObject);
}
}
/// <summary>
/// Очищает созданные QR-код объекты.
/// </summary>
private void ClearQRCodes()
{
foreach (GameObject obj in _createdQRCodeObjects)
{
if (obj != null)
{
Destroy(obj);
}
}
_createdQRCodeObjects.Clear();
}
private void OnMainMenuClicked()
{
Close();
WinBannerDismissed?.Invoke();
if (GameManager.Instance)
{
GameManager.Instance.SetPause(false);
GameManager.Instance.LoadScene("MainMenu", false);
}
}
protected override void OnDisable()
{
base.OnDisable();
ClearQRCodes();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8e0feede366043ef951637fa0f816177
timeCreated: 1765888795

View File

@ -701,7 +701,7 @@ PlayerSettings:
framebufferDepthMemorylessMode: 0
qualitySettingsNames: []
projectName: Mario-like platformer
organizationId: dis_bro
organizationId:
cloudEnabled: 0
legacyClampBlendShapeWeights: 0
hmiLoadingImage: {fileID: 0}