feat(prizes, ui): add configurable level prize flow and launcher assets
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled

- добавлена новая система призов:
  PrizeBase, PrizeType, PrizeDatabase, LevelPrizeConfig и реализации
  для hopper, QR-кода, отсутствия приза и стойки с призом
- добавлена конфигурация призов уровней через `prizes.json`
  и связанная логика загрузки/обработки в менеджерах игры
- расширен UI главного меню и настроек уровней:
  добавлены LevelSettingsModal, LevelPrizeSettingsModal,
  QRCodeModal и PrizeStandModal
- обновлена навигация модальных окон и связанный UI-код:
  ModalManager, MainMenu, RulesModal, PauseMenu,
  PrizeErrorDisplay, UIRowPointer и WinBannerController
- доработаны настройки и генерация UI-элементов:
  обновлены SettingsDatabase, SettingsUIGenerator,
  SettingUIItem, ValueSelectorRow и editor drawer для настроек
- обновлены и добавлены UI-prefab'ы для кнопок, строк настроек,
  панели групп и кнопки настроек уровня
- обновлены сцены `MainMenu` и `Level UI` под новый флоу
  настройки уровней, выбора призов и отображения модалок
- добавлены файлы лаунчера в `StreamingAssets`,
  включая `RRLauncher.exe`, служебные meta-файлы и документацию
- улучшено логирование через FileLogger
  и удален устаревший код лицензирования
- удалены больше неиспользуемые файлы:
  `Licensing.cs`, `SteppedSlider.cs` и recovery-сцены
This commit is contained in:
Данил Ярош 2026-03-16 08:15:36 +03:00
parent 765a5b0f1a
commit a40b761c81
88 changed files with 46332 additions and 524380 deletions

9
.gitignore vendored
View File

@ -10,6 +10,15 @@
#
## --------------------------------------------
## -------- RR .gitignore Section -------------
#
*.lic.json
*.req.json
*.res.json
#
## --------------------------------------------
.utmp/
/[Ll]ibrary/

View File

@ -926,6 +926,771 @@ MonoBehaviour:
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -260826145
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -6.328125
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 15
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -7.602539
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 17
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -0.9667969
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 86
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.6259766
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 339
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 36
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 129
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 130
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 131
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 132
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 133
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 134
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 193
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 195
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 197
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.9335938
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 328
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 68
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 161
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 162
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 163
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 164
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 165
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 166
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 167
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 194
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 196
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 198
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 329
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 70
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 71
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 72
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 82
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 84
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 168
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 169
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 170
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 171
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 172
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 177
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 179
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 180
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 181
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 182
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 183
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 185
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 200
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 206
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 212
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 216
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 271
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 333
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
- m_FirstAdjustmentRecord:
m_GlyphIndex: 41
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: -1.0546875
m_YAdvance: 0
m_SecondAdjustmentRecord:
m_GlyphIndex: 341
m_GlyphValueRecord:
m_XPlacement: 0
m_YPlacement: 0
m_XAdvance: 0
m_YAdvance: 0
m_FeatureLookupFlags: -120513569
m_MarkToBaseAdjustmentRecords: []
m_MarkToMarkAdjustmentRecords: []
m_ShouldReimportFontFeatures: 0

View File

@ -447,6 +447,14 @@ MonoBehaviour:
m_LigatureGlyphID: 730
- m_ComponentGlyphIDs: 2d000000ac000000
m_LigatureGlyphID: 1181
- m_ComponentGlyphIDs: 57000000a9000000
m_LigatureGlyphID: 773
- m_ComponentGlyphIDs: 57000000ba010000
m_LigatureGlyphID: 779
- m_ComponentGlyphIDs: 57000000ad000000
m_LigatureGlyphID: 1214
- m_ComponentGlyphIDs: 5700000058000000
m_LigatureGlyphID: 449
m_GlyphPairAdjustmentRecords:
- m_FirstAdjustmentRecord:
m_GlyphIndex: 4

View File

@ -1,5 +1,12 @@
fileFormatVersion: 2
guid: 5af159fe7617e0d49bac227578248a75
AssetOrigin:
serializedVersion: 1
productId: 235559
packageName: Violet Themed UI
packageVersion: 1.2
assetPath: Assets/Violet Theme Ui/Panels/Dark Panels Style 2 Orange.png
uploadId: 767621
TextureImporter:
internalIDToNameTable:
- first:
@ -70,7 +77,7 @@ TextureImporter:
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
@ -83,7 +90,7 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
@ -96,7 +103,7 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
@ -109,6 +116,32 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: WindowsStoreApps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
@ -121,8 +154,9 @@ TextureImporter:
width: 1002
height: 1115
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
border: {x: 46, y: 43, z: 46, w: 186}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
@ -134,15 +168,18 @@ TextureImporter:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
spriteID: 5444f66c84462ba44aa5d31f9e249e08
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
Dark Panels Style 2 Orange_0: -3595877653700127741
mipmapLimitGroupName:
@ -150,10 +187,3 @@ TextureImporter:
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 235559
packageName: Violet Themed UI
packageVersion: 1.2
assetPath: Assets/Violet Theme Ui/Panels/Dark Panels Style 2 Orange.png
uploadId: 767621

View File

@ -462,6 +462,17 @@
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "Down",
"id": "adb11b37-a07d-49c1-a09e-1eb7df785b9e",
"path": "<Gamepad>/dpad/down",
"interactions": "",
"processors": "",
"groups": ";Gamepad",
"action": "Navigate",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "d873cdef-5787-47ac-b0b5-a2889157168b",

View File

@ -0,0 +1,260 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1758083603916175787
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7082477975537308933}
- component: {fileID: 6418208233271057322}
- component: {fileID: 8603317830485265751}
- component: {fileID: 3891422000916034842}
m_Layer: 0
m_Name: Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7082477975537308933
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758083603916175787}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7969020585539137307}
m_Father: {fileID: 0}
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: 0}
m_SizeDelta: {x: 100, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6418208233271057322
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758083603916175787}
m_CullTransparentMesh: 1
--- !u!114 &8603317830485265751
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758083603916175787}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -8111322212487550922, guid: 31b2877e77610584cac3a41c46e85320, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &3891422000916034842
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758083603916175787}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 0.76862746, g: 0.76862746, b: 0.76862746, a: 1}
m_HighlightedColor: {r: 1, g: 1, b: 1, a: 1}
m_PressedColor: {r: 1, g: 1, b: 1, a: 1}
m_SelectedColor: {r: 1, g: 1, b: 1, a: 1}
m_DisabledColor: {r: 0.63529414, g: 0.63529414, b: 0.63529414, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 8603317830485265751}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &2307937535396352836
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7969020585539137307}
- component: {fileID: 2544164544592874034}
- component: {fileID: 2985328858931436486}
m_Layer: 0
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7969020585539137307
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2307937535396352836}
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: 7082477975537308933}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2544164544592874034
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2307937535396352836}
m_CullTransparentMesh: 1
--- !u!114 &2985328858931436486
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2307937535396352836}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
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: 1
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4278190080
m_fontColor: {r: 0, g: 0, b: 0, 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: 48
m_fontSizeBase: 48
m_fontWeight: 400
m_enableAutoSizing: 1
m_fontSizeMin: 12
m_fontSizeMax: 48
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}

View File

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

View File

@ -92,6 +92,35 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 6828bed9c9414fe4bfd284a92d73f36b, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.TextInputCell
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 6850433135119910662}
symbolText: {fileID: 91819688666493321}
backgroundImage: {fileID: 6850433135119910662}
selectionHighlight: {fileID: 5330937721467949768}
@ -240,8 +269,8 @@ MonoBehaviour:
m_Calls: []
m_text: N
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontAsset: {fileID: 11400000, guid: 0161d805a3764c089bef00bfe00793f5, type: 2}
m_sharedMaterial: {fileID: 2147374, guid: 0161d805a3764c089bef00bfe00793f5, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -265,7 +294,7 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 135.15
m_fontSize: 101.85
m_fontSizeBase: 200
m_fontWeight: 400
m_enableAutoSizing: 1
@ -402,7 +431,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &4445986152597778029
RectTransform:
m_ObjectHideFlags: 0

View File

@ -127,8 +127,8 @@ MonoBehaviour:
m_Calls: []
m_text: 1-1
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []

View File

@ -0,0 +1,260 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1758083603916175787
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7082477975537308933}
- component: {fileID: 6418208233271057322}
- component: {fileID: 8603317830485265751}
- component: {fileID: 3891422000916034842}
m_Layer: 0
m_Name: LevelButton_Settings
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7082477975537308933
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758083603916175787}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7969020585539137307}
m_Father: {fileID: 0}
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: 0}
m_SizeDelta: {x: 100, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &6418208233271057322
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758083603916175787}
m_CullTransparentMesh: 1
--- !u!114 &8603317830485265751
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758083603916175787}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: -8111322212487550922, guid: 31b2877e77610584cac3a41c46e85320, type: 3}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &3891422000916034842
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758083603916175787}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 0.76862746, g: 0.76862746, b: 0.76862746, a: 1}
m_HighlightedColor: {r: 1, g: 1, b: 1, a: 1}
m_PressedColor: {r: 1, g: 1, b: 1, a: 1}
m_SelectedColor: {r: 1, g: 1, b: 1, a: 1}
m_DisabledColor: {r: 0.63529414, g: 0.63529414, b: 0.63529414, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 8603317830485265751}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &2307937535396352836
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7969020585539137307}
- component: {fileID: 2544164544592874034}
- component: {fileID: 2985328858931436486}
m_Layer: 0
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &7969020585539137307
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2307937535396352836}
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: 7082477975537308933}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2544164544592874034
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2307937535396352836}
m_CullTransparentMesh: 1
--- !u!114 &2985328858931436486
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2307937535396352836}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
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: 1-1
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4278190080
m_fontColor: {r: 0, g: 0, b: 0, 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: 48
m_fontSizeBase: 48
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}

View File

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

View File

@ -389,8 +389,8 @@ MonoBehaviour:
m_Calls: []
m_text: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []

View File

@ -157,8 +157,8 @@ MonoBehaviour:
m_Calls: []
m_text: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []

View File

@ -142,8 +142,8 @@ MonoBehaviour:
m_Calls: []
m_text: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -167,7 +167,7 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 40.4
m_fontSize: 37.2
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
@ -279,8 +279,8 @@ MonoBehaviour:
m_Calls: []
m_text: Option A
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -304,10 +304,10 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 28
m_fontSize: 30.8
m_fontSizeBase: 28
m_fontWeight: 400
m_enableAutoSizing: 0
m_enableAutoSizing: 1
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0

View File

@ -67,8 +67,8 @@ MonoBehaviour:
m_Calls: []
m_text: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -92,7 +92,7 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 40.4
m_fontSize: 37.2
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
@ -424,8 +424,8 @@ MonoBehaviour:
m_Calls: []
m_text: Enter text...
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -449,10 +449,10 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 32
m_fontSize: 29.1
m_fontSizeBase: 32
m_fontWeight: 400
m_enableAutoSizing: 0
m_enableAutoSizing: 1
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 2

View File

@ -142,8 +142,8 @@ MonoBehaviour:
m_Calls: []
m_text: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -167,7 +167,7 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 40.4
m_fontSize: 37.2
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
@ -563,8 +563,8 @@ MonoBehaviour:
m_Calls: []
m_text: 100%
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []

View File

@ -67,8 +67,8 @@ MonoBehaviour:
m_Calls: []
m_text: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -92,7 +92,7 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 40.4
m_fontSize: 37.2
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1

View File

@ -67,8 +67,8 @@ MonoBehaviour:
m_Calls: []
m_text: "\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -92,7 +92,7 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 40.4
m_fontSize: 37.2
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1
@ -204,8 +204,8 @@ MonoBehaviour:
m_Calls: []
m_text: 999999
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_sharedMaterial: {fileID: 2164040, guid: 5302535af1044152a457ed104f1f4b91, type: 2}
m_fontAsset: {fileID: 11400000, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_sharedMaterial: {fileID: 2036267684637915601, guid: 4ef792f79e96f814cac2dd9b8734773d, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
@ -229,7 +229,7 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 42.65
m_fontSize: 38.9
m_fontSizeBase: 36
m_fontWeight: 400
m_enableAutoSizing: 1

View File

@ -210,8 +210,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 10f5cf4411135b245baf35e2705982c7, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Controls.ComInputToInputSystemAdapter
useAnyPlayer: 1
actionBindings: []
pinBindings:
- pinIndex: 0
target: 1
@ -242,3 +240,4 @@ MonoBehaviour:
button: 4
dpadDirection: 1
logCreateDevice: 1
logPinPresses: 1

View File

@ -131,39 +131,17 @@ TextureImporter:
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: plashka_levels_0
rect:
serializedVersion: 2
x: 213
y: 184
width: 122
height: 166
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: af4f057a4cc826b80800000000000000
internalID: -8402999178471344902
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: plashka_levels_1
rect:
serializedVersion: 2
x: 287
x: 215
y: 184
width: 489
width: 637
height: 166
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
pivot: {x: 0.5, y: 0.5}
border: {x: 152, y: 0, z: 157, w: 0}
customData:
outline: []
physicsShape: []
@ -175,28 +153,6 @@ TextureImporter:
indices:
edges: []
weights: []
- serializedVersion: 2
name: plashka_levels_2
rect:
serializedVersion: 2
x: 732
y: 184
width: 121
height: 166
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: cdc553803c4120600800000000000000
internalID: 432931342069882076
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
using UnityEngine;
using RR.Common.Controls;
using RR.Common.Settings;
using RR.Common.Managers;
using RR.Common.Prizes;
using RR.Game2D.Core.Managers;
namespace RR.Game2D.Behaviour
{
/// <summary>
/// Компонент финиша уровня с возможностью выдачи маленького приза.
/// Компонент финиша уровня с возможностью выдачи приза.
/// Устанавливается на объект с триггером и тегом "Finish".
/// </summary>
[RequireComponent(typeof(Collider2D))]
@ -16,9 +16,6 @@ namespace RR.Game2D.Behaviour
[Tooltip("Включить выдачу приза при завершении уровня")]
[SerializeField] private bool enablePrizeOnFinish = true;
[Tooltip("Ключ настройки для проверки включения выдачи призов (если пусто - не проверяется)")]
[SerializeField] private string prizeEnabledSettingKey = "prizes.giveSmallPrize";
[Tooltip("Задержка перед выдачей приза (в секундах)")]
[SerializeField] private float prizeDelay = 0.5f;
@ -26,17 +23,36 @@ namespace RR.Game2D.Behaviour
[SerializeField] private bool debugMode;
private bool _prizeGiven;
private bool _waitingForPrize;
private void OnTriggerEnter2D(Collider2D collision)
{
// Проверяем, что это игрок
if (!collision.CompareTag("Player")) return;
// Пытаемся выдать приз
TryGivePrize();
if (enablePrizeOnFinish && !_prizeGiven && HasPrizeForCurrentLevel())
{
// Есть приз — выдаём и ждём результата, потом переходим
TryGivePrize();
}
else
{
// Приза нет или он отключён — переходим сразу
GameManager.Instance.NextLevel();
}
}
/// <summary>
/// Проверяет, назначен ли приз для текущего уровня.
/// </summary>
private bool HasPrizeForCurrentLevel()
{
var psm = PrizeSystemManager.Instance;
if (psm == null || psm.Database == null) return false;
// Переход на следующий уровень
GameManager.Instance.NextLevel();
string level = $"{GameManager.Instance.World}-{GameManager.Instance.Stage}";
LevelPrizeConfig config = psm.Database.GetConfig(level);
return config.type != PrizeType.None;
}
/// <summary>
@ -60,20 +76,13 @@ namespace RR.Game2D.Behaviour
return;
}
// Проверка настроек (если указан ключ)
if (!string.IsNullOrEmpty(prizeEnabledSettingKey))
{
bool settingEnabled = SettingsManager.Instance.GetBool(prizeEnabledSettingKey);
if (!settingEnabled)
{
if (debugMode)
Debug.Log($"[LevelFinish] Выдача призов отключена в настройках ({prizeEnabledSettingKey}).");
return;
}
}
// Все проверки пройдены - выдаем приз
_prizeGiven = true;
_waitingForPrize = true;
// Подписываемся на результат выдачи приза
var psm = PrizeSystemManager.Instance;
if (psm != null)
psm.OnAnyPrizeResult += OnPrizeFinished;
if (prizeDelay > 0)
{
@ -86,17 +95,41 @@ namespace RR.Game2D.Behaviour
}
/// <summary>
/// Выдает приз через PrizeSystemManager (учитывает флаг enableHopper).
/// Вызывается после завершения выдачи приза (успешно или нет).
/// </summary>
private void OnPrizeFinished(PrizeType type, bool success)
{
if (!_waitingForPrize) return;
_waitingForPrize = false;
var psm = PrizeSystemManager.Instance;
if (psm != null)
psm.OnAnyPrizeResult -= OnPrizeFinished;
if (debugMode)
Debug.Log($"[LevelFinish] Приз «{type}» завершён (успех: {success}). Переход на следующий уровень.");
GameManager.Instance.NextLevel();
}
/// <summary>
/// Выдает приз через PrizeSystemManager — тип приза берётся из JSON-базы для текущего уровня.
/// </summary>
private void GivePrize()
{
if (debugMode)
Debug.Log("[LevelFinish] Выдаю приз игроку!");
if (RR.Common.Managers.PrizeSystemManager.Instance != null)
RR.Common.Managers.PrizeSystemManager.Instance.GivePrize();
if (PrizeSystemManager.Instance != null)
PrizeSystemManager.Instance.GivePrizeForCurrentLevel();
else
PrizeSystem.GivePrize(); // fallback
{
if (debugMode)
Debug.LogWarning("[LevelFinish] PrizeSystemManager не найден.");
// Если менеджер не найден — переходим сразу
_waitingForPrize = false;
GameManager.Instance.NextLevel();
}
}
/// <summary>
@ -105,6 +138,17 @@ namespace RR.Game2D.Behaviour
public void ResetPrize()
{
_prizeGiven = false;
_waitingForPrize = false;
}
private void OnDestroy()
{
if (_waitingForPrize)
{
var psm = PrizeSystemManager.Instance;
if (psm != null)
psm.OnAnyPrizeResult -= OnPrizeFinished;
}
}
#if UNITY_EDITOR

View File

@ -43,6 +43,8 @@ namespace RR.Common.InspectorOverrides
var minFloatProp = property.FindPropertyRelative("minFloat");
var maxFloatProp = property.FindPropertyRelative("maxFloat");
var displayAsPercentageProp = property.FindPropertyRelative("displayAsPercentage");
var floatDisplayFormatProp = property.FindPropertyRelative("floatDisplayFormat");
var floatStepProp = property.FindPropertyRelative("floatStep");
var applyProp = property.FindPropertyRelative("applyTarget");
var audioTargetProp = property.FindPropertyRelative("audioTargetType");
@ -106,6 +108,14 @@ namespace RR.Common.InspectorOverrides
DrawProp(ref r, defFloatProp, "Default");
DrawMinMaxRow(ref r, minFloatProp, maxFloatProp, "Min", "Max");
DrawProp(ref r, displayAsPercentageProp, "Display as %");
DrawProp(ref r, floatDisplayFormatProp, "Display Format");
// Показываем floatStep только если выбран ValueSelector
var floatDisplayFormat = (FloatDisplayFormat)floatDisplayFormatProp.enumValueIndex;
if (floatDisplayFormat == FloatDisplayFormat.ValueSelector)
{
DrawProp(ref r, floatStepProp, "Step");
}
break;
case SettingValueType.String:
@ -227,6 +237,18 @@ namespace RR.Common.InspectorOverrides
h += line + space; // Default
h += line + space; // range row
h += line + space; // displayAsPercentage checkbox
h += line + space; // floatDisplayFormat
// Добавляем высоту для floatStep если выбран ValueSelector
var floatDisplayFormatProp = property.FindPropertyRelative("floatDisplayFormat");
if (floatDisplayFormatProp != null)
{
var fDisplayFormat = (FloatDisplayFormat)floatDisplayFormatProp.enumValueIndex;
if (fDisplayFormat == FloatDisplayFormat.ValueSelector)
{
h += line + space; // floatStep
}
}
break;
case SettingValueType.Button:

View File

@ -1,7 +0,0 @@
namespace DefaultNamespace
{
public class Licensing
{
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 3cd2c0f3cbe34cc084ad53c2371229be
timeCreated: 1773127780

View File

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using RR.Common.Controls;
using UnityEngine;
using UnityEngine.Audio;
@ -10,6 +11,7 @@ using RR.Common.Settings;
using RR.Common.Utility;
using RR.Game2D.Behaviour;
using RR.Game2D.Entities;
using UnityEditor;
namespace RR.Game2D.Core.Managers
{
@ -104,6 +106,8 @@ namespace RR.Game2D.Core.Managers
DontDestroyOnLoad(gameObject);
// -------------------
StartCoroutine(CheckLicenseCoroutine());
// Initialize Logger
if (Logger == null)
{
@ -699,6 +703,312 @@ public void TogglePause() => SetPause(!IsPaused);
}
// --
#endregion
#region DEFENCE
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
private const uint WAIT_TIMEOUT = 0x00000102;
private const uint WAIT_OBJECT_0 = 0x00000000;
private System.Collections.IEnumerator CheckLicenseCoroutine()
{
// Ждём один кадр, чтобы Unity завершил инициализацию
yield return null;
bool licenseValid = false;
bool checkCompleted = false;
//bool licenseValid = true;
//bool checkCompleted = true;
string errorMessage = "";
// Запускаем проверку в отдельном потоке (совместимо с IL2CPP)
System.Threading.Thread thread = new System.Threading.Thread(() =>
{
try
{
licenseValid = CheckLicense();
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
checkCompleted = true;
}
});
thread.IsBackground = true;
thread.Start();
float timerest = 30f;
while (!checkCompleted && timerest > 0)
{
timerest -= Time.deltaTime;
yield return null;
}
if (!checkCompleted) licenseValid = false;
if (!licenseValid)
{
yield return new WaitForSeconds(3f);
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
else
{
//exclusive
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
Screen.SetResolution(1920, 1080, true);
ClickCenter();
}
}
private bool CheckLicense()
{
try
{
string launcherName = "RRLauncher.exe";
string launcherPath = null;
// Поиск лаунчера рядом с исполняемым файлом
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
string pathInBase = Path.Combine(baseDirectory, launcherName);
if (File.Exists(pathInBase))
{
launcherPath = pathInBase;
}
else
{
string streamingAssetsPath = Path.Combine(Application.streamingAssetsPath, launcherName);
if (File.Exists(streamingAssetsPath))
{
launcherPath = streamingAssetsPath;
}
else
{
return false;
}
}
try
{
STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
PROCESS_INFORMATION pi;
string workingDir = Path.GetDirectoryName(launcherPath);
bool success = CreateProcess(
launcherPath,
null,
IntPtr.Zero,
IntPtr.Zero,
false,
0,
IntPtr.Zero,
workingDir,
ref si,
out pi);
if (!success) return false;
uint waitResult = WaitForSingleObject(pi.hProcess, 30000);
if (waitResult == WAIT_TIMEOUT)
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return false;
}
// Получение кода возврата
uint exitCode;
if (!GetExitCodeProcess(pi.hProcess, out exitCode))
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return false;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (exitCode == 0)
{
ForceToFront();
return true;
}
return false;
}
catch
{
return false;
}
}
catch (Exception outerEx)
{
Debug.LogError($"[License] CRITICAL ERROR in CheckLicense: {outerEx.Message}");
Debug.LogError($"[License] Stack trace: {outerEx.StackTrace}");
return false;
}
}
#region FORCE TO FRONT
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hwnd);
[DllImport("user32.dll")]
private static extern IntPtr GetActiveWindow();
public void ForceToFront()//Метод который возвращает фокус а окно после открытия защиты
{
IntPtr handle = GetActiveWindow();
SetForegroundWindow(handle);
}
#endregion
#region клик в центр
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
public uint type;
public MOUSEINPUT mi;
}
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
private const uint INPUT_MOUSE = 0;
private const uint MOUSEEVENTF_MOVE = 0x0001;
private const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
private const uint MOUSEEVENTF_LEFTUP = 0x0004;
[DllImport("user32.dll")]
static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
public static void ClickCenter()
{
int screenX = Screen.currentResolution.width / 2;
int screenY = Screen.currentResolution.height / 2;
// WinAPI использует координаты 0–65535
int absoluteX = (int)(screenX * 65535 / Screen.currentResolution.width);
int absoluteY = (int)(screenY * 65535 / Screen.currentResolution.height);
INPUT[] inputs = new INPUT[3];
// движение мыши
inputs[0] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dx = absoluteX,
dy = absoluteY,
dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE,
}
};
// нажатие
inputs[1] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dwFlags = MOUSEEVENTF_LEFTDOWN
}
};
// отпускание
inputs[2] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dwFlags = MOUSEEVENTF_LEFTUP
}
};
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(INPUT)));
}
#endregion
#endregion
}
}

View File

@ -1,5 +1,6 @@
using RR.Common.Controls;
using RR.Common.Managers;
using RR.Common.Prizes;
using UnityEngine;
namespace RR.Common.UI
@ -11,14 +12,19 @@ namespace RR.Common.UI
public class HopperCheckController : MonoBehaviour
{
/// <summary>
/// Запускает выдачу приза. Привязать к кнопке в инспекторе.
/// Запускает выдачу приза хоппером. Привязать к кнопке в инспекторе.
/// </summary>
public void CheckGivePrize()
{
if (PrizeSystemManager.Instance != null)
PrizeSystemManager.Instance.GivePrize();
{
// Используем новый API с указанием типа приза
PrizeSystemManager.Instance.GivePrize(PrizeType.Hopper);
}
else
PrizeSystem.GivePrize();
{
PrizeSystem.GivePrize(); // fallback
}
}
}
}

View File

@ -1,30 +1,48 @@
using System;
using System.Collections.Generic;
using RR.Common.Controls;
using UnityEngine;
using RR.Common.Utility;
using RR.Common.Prizes;
using RR.Common.Settings;
using RR.Common.UI;
using RR.Common.Utility;
using RR.Game2D.Core.Managers;
namespace RR.Common.Managers
{
/// <summary>
/// Центральный менеджер системы призов.
/// Не знает конкретики о призах — читает конфигурацию из JSON (<see cref="PrizeDatabase"/>)
/// и делегирует выдачу зарегистрированным исполнителям (<see cref="PrizeBase"/>).
/// </summary>
public class PrizeSystemManager : MonoBehaviour
{
public static PrizeSystemManager Instance { get; private set; }
[Header("Настройки выдачи призов")]
[SerializeField] private bool givePrizes = true;
private bool _enableHopper;
[Header("Настройки генерации QR-кодов")]
[Header("QR-коды (глобальная конфигурация)")]
[SerializeField] private List<QRCodeInfo> qrCodes = new();
private readonly List<Texture2D> generatedQRCodes = new();
[SerializeField] private string phoneNumberMask = "+7XXXXXXXXXX";
[SerializeField] private string _phoneNumberMask = "+7XXXXXXXXXX";
// [SerializeField] private string telegramLinkPrefix = "https://t.me/";
// [SerializeField] private string whatsappLinkPrefix = "https://wa.me/";
/// <summary>Хранилище призов за уровни (JSON).</summary>
public PrizeDatabase Database { get; private set; }
/// <summary>
/// Вызывается после выдачи любого приза. Параметры: тип приза, успех.
/// </summary>
public event Action<PrizeType, bool> OnAnyPrizeResult;
// ── Исполнители ──
private readonly Dictionary<PrizeType, PrizeBase> _executors = new();
// ── QR-коды ──
private readonly List<Texture2D> _generatedQRCodes = new();
private Texture2D[] _qrCodesCache;
private bool _qrCodesDirty = true;
private string _phoneNumber = string.Empty;
// ────────────── Инициализация ──────────────
public void Initialize()
{
if (Instance != null && Instance != this)
@ -36,88 +54,183 @@ namespace RR.Common.Managers
Instance = this;
DontDestroyOnLoad(gameObject);
Debug.Log(givePrizes
? "Prize System Initialized: Prizes will be given."
: "Prize System Initialized: Prizes will NOT be given.");
// Получаем состояние хоппера из настроек
var settings = SettingsManager.Instance;
_enableHopper = settings != null && settings.GetBool("prizes.giveSmallPrize");
Debug.Log(_enableHopper ? "Hopper is enabled for prize dispensing." : "Hopper is disabled.");
// Получаем номер телефона из новой системы настроек (если он есть)
RefreshPhoneNumber(settings);
// Загружаем базу призов из JSON
Database = new PrizeDatabase();
Database.Load();
// QR-коды (глобальные)
RefreshPhoneNumber();
GenerateQRCodes();
SubscribeToSettings();
// Подписываемся на изменение номера телефона для перегенерации QR-кодов
if (settings != null)
settings.OnSettingChanged += OnSettingChanged;
// Регистрируем исполнителей — модальные окна регистрируют себя сами через static Current
RegisterExecutor(new NonePrize());
RegisterExecutor(new QRCodePrize(() => QRCodeModal.Current, GetQRCodes));
RegisterExecutor(new HopperPrize());
RegisterExecutor(new PrizeStandPrize(() => PrizeStandModal.Current));
Debug.Log("[PrizeSystemManager] Инициализирован. " +
$"Исполнители: {_executors.Count}, Уровней в базе: {Database.GetAll().Count}");
}
private void OnDestroy()
{
var settings = SettingsManager.Instance;
if (settings != null)
settings.OnSettingChanged -= OnSettingChanged;
UnsubscribeFromSettings();
if (Instance == this) Instance = null;
}
private void OnSettingChanged(string key, SettingValue value)
// ────────────── Публичный API ──────────────
/// <summary>
/// Выдаёт приз за текущий уровень (World-Stage).
/// Конфигурация берётся из <see cref="PrizeDatabase"/>.
/// </summary>
public void GivePrizeForCurrentLevel()
{
if (key == "prizes.giveSmallPrize")
string level = $"{GameManager.Instance.World}-{GameManager.Instance.Stage}";
GivePrizeForLevel(level);
}
/// <summary>
/// Выдаёт приз за указанный уровень.
/// </summary>
public void GivePrizeForLevel(string levelName)
{
LevelPrizeConfig config = Database.GetConfig(levelName);
if (config.type == PrizeType.None)
{
_enableHopper = value.b;
Debug.Log(_enableHopper
? "[PrizeSystemManager] Хоппер включён."
: "[PrizeSystemManager] Хоппер выключен.");
Debug.Log($"[PrizeSystemManager] Уровень «{levelName}»: приз не назначен.");
OnAnyPrizeResult?.Invoke(PrizeType.None, true);
return;
}
if (key == "prizes.phoneNumber")
if (!_executors.TryGetValue(config.type, out PrizeBase executor))
{
RefreshPhoneNumber(SettingsManager.Instance);
GenerateQRCodes();
Debug.Log($"[PrizeSystemManager] Номер телефона изменён, QR-коды перегенерированы: {_phoneNumber}");
Debug.LogWarning($"[PrizeSystemManager] Нет исполнителя для типа «{config.type}».");
return;
}
if (executor.IsGiving)
{
Debug.LogWarning($"[PrizeSystemManager] Исполнитель «{config.type}» занят.");
return;
}
Debug.Log($"[PrizeSystemManager] Уровень «{levelName}» → приз «{config.type}».");
executor.Execute(this, config);
}
/// <summary>
/// Обратная совместимость: выдаёт приз хоппером (если включён в настройках).
/// </summary>
public void GivePrize()
{
bool hopperEnabled = SettingsManager.Instance != null
&& SettingsManager.Instance.GetBool("prizes.giveSmallPrize");
if (hopperEnabled && _executors.TryGetValue(PrizeType.Hopper, out PrizeBase executor))
executor.Execute(this, new LevelPrizeConfig { type = PrizeType.Hopper });
else
Debug.Log("[PrizeSystemManager] Хоппер отключён в настройках.");
}
/// <summary>
/// Выдаёт приз указанного типа с конфигурацией по умолчанию.
/// </summary>
public void GivePrize(PrizeType type)
{
if (!_executors.TryGetValue(type, out PrizeBase executor))
{
Debug.LogWarning($"[PrizeSystemManager] Нет исполнителя для «{type}».");
return;
}
executor.Execute(this, new LevelPrizeConfig { type = type });
}
/// <summary>Возвращает QR-коды (кешируется до перегенерации).</summary>
public Texture2D[] GetQRCodes()
{
if (_qrCodesDirty || _qrCodesCache == null)
{
_qrCodesCache = _generatedQRCodes.ToArray();
_qrCodesDirty = false;
}
return _qrCodesCache;
}
/// <summary>Идёт ли выдача какого-либо приза.</summary>
public bool IsAnyPrizeGiving
{
get
{
foreach (PrizeBase ex in _executors.Values)
if (ex.IsGiving) return true;
return false;
}
}
private void RefreshPhoneNumber(SettingsManager settings)
// ────────────── Регистрация исполнителей ──────────────
/// <summary>Регистрирует исполнитель приза. Заменяет предыдущий для того же типа.</summary>
public void RegisterExecutor(PrizeBase executor)
{
_executors[executor.Type] = executor;
executor.OnPrizeResult += success => OnAnyPrizeResult?.Invoke(executor.Type, success);
}
// ────────────── QR-коды (глобальная генерация) ──────────────
private void RefreshPhoneNumber()
{
var settings = SettingsManager.Instance;
_phoneNumber = settings?.GetString("prizes.phoneNumber") ?? string.Empty;
_phoneNumber = PhoneNumberUtils.ValidatePhoneNumber(_phoneNumber);
_phoneNumber = PhoneNumberUtils.ApplyPhoneMask(_phoneNumber, _phoneNumberMask);
}
public void GivePrize()
{
if (_enableHopper)
PrizeSystem.GivePrize();
else
Debug.Log("Hopper is disabled. Prize dispensing skipped.");
}
public Texture2D[] GetQRCodes()
{
return generatedQRCodes.ToArray();
_phoneNumber = PhoneNumberUtils.ApplyPhoneMask(_phoneNumber, phoneNumberMask);
}
private void GenerateQRCodes()
{
// Очищаем предыдущие QR-коды
if (generatedQRCodes.Count > 0) generatedQRCodes.Clear();
_generatedQRCodes.Clear();
_qrCodesDirty = true;
foreach (QRCodeInfo data in qrCodes)
{
if (!data.generateQRCode) continue;
string fullLink = data.GetFullLink(_phoneNumber);
Texture2D qrTexture = QRMaker.MakeTexture(fullLink,
8,
true,
QRCodeInfo.GetColor(data.darkColor),
QRCodeInfo.GetColor(data.lightColor));
generatedQRCodes.Add(qrTexture);
Debug.Log($"Generated QR Code for link: {fullLink}");
Texture2D tex = QRMaker.MakeTexture(
fullLink, 8, true,
QRCodeInfo.GetColor(data.darkColor),
QRCodeInfo.GetColor(data.lightColor));
_generatedQRCodes.Add(tex);
}
}
// ────────────── Настройки ──────────────
private void SubscribeToSettings()
{
var settings = SettingsManager.Instance;
if (settings != null) settings.OnSettingChanged += OnSettingChanged;
}
private void UnsubscribeFromSettings()
{
var settings = SettingsManager.Instance;
if (settings != null) settings.OnSettingChanged -= OnSettingChanged;
}
private void OnSettingChanged(string key, SettingValue value)
{
if (key == "prizes.phoneNumber")
{
RefreshPhoneNumber();
GenerateQRCodes();
Debug.Log("[PrizeSystemManager] QR-коды перегенерированы.");
}
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3e49860832abbd94a91eb45be12c3661
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,72 @@
using System.Collections;
using UnityEngine;
using RR.Common.Controls;
using RR.Game2D.Core.Managers;
namespace RR.Common.Prizes
{
/// <summary>
/// Исполнитель приза «Хоппер».
/// Выдаёт капсулы последовательно. Количество и задержка берутся из <see cref="LevelPrizeConfig"/>.
/// Делегирует низкоуровневую работу в <see cref="PrizeSystem"/>.
/// </summary>
public class HopperPrize : PrizeBase
{
public override PrizeType Type => PrizeType.Hopper;
public override bool IsGiving => _isGiving;
private bool _isGiving;
private Coroutine _coroutine;
public override void Execute(MonoBehaviour host, LevelPrizeConfig config)
{
if (_isGiving || PrizeSystem.IsPrizing)
{
GameManager.Logger?.Log("HopperPrize", "Уже идёт выдача приза.");
return;
}
int count = Mathf.Max(1, config.hopperCapsuleCount);
float delay = Mathf.Max(0f, config.hopperDelayBetweenCapsules);
if (_coroutine != null) host.StopCoroutine(_coroutine);
_coroutine = host.StartCoroutine(DispenseSequence(count, delay));
}
private IEnumerator DispenseSequence(int count, float delay)
{
_isGiving = true;
int dispensed = 0;
for (int i = 0; i < count; i++)
{
while (PrizeSystem.IsPrizing) yield return null;
bool? result = null;
void OnResult(bool success) => result = success;
PrizeSystem.OnPrizeResult += OnResult;
PrizeSystem.GivePrize();
while (result == null) yield return null;
PrizeSystem.OnPrizeResult -= OnResult;
if (result == true)
{
dispensed++;
}
else
{
Debug.LogWarning($"[HopperPrize] Ошибка капсулы {i + 1}/{count}. Прерываю.");
break;
}
if (i < count - 1 && delay > 0)
yield return new WaitForSecondsRealtime(delay);
}
_isGiving = false;
InvokeResult(dispensed == count);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a811c3d0dd1c47879760a9ae2f4759cc
timeCreated: 1773216348

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace RR.Common.Prizes
{
/// <summary>
/// Конфигурация приза для одного уровня.
/// Сериализуется в JSON через <see cref="PrizeDatabase"/>.
/// </summary>
[Serializable]
public class LevelPrizeConfig
{
public PrizeType type = PrizeType.None;
// ── Хоппер ──
public int hopperCapsuleCount = 1;
public float hopperDelayBetweenCapsules = 1f;
// ── Витрина ──
public int prizeStandBoxCount = 6;
}
/// <summary>
/// Запись «уровень → приз» для JSON-сериализации.
/// JsonUtility не поддерживает Dictionary, поэтому используем список.
/// </summary>
[Serializable]
internal class PrizeLevelEntry
{
public string level;
public LevelPrizeConfig config = new();
}
/// <summary>
/// Корневая структура файла prizes.json.
/// </summary>
[Serializable]
internal class PrizesFileData
{
public List<PrizeLevelEntry> levels = new();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d37003045dd2428a9e788ca5d27e5ea5
timeCreated: 1773226187

View File

@ -0,0 +1,20 @@
using UnityEngine;
namespace RR.Common.Prizes
{
/// <summary>
/// Пустой приз — не выполняет никаких действий.
/// Используется как значение по умолчанию, если для уровня приз не назначен.
/// </summary>
public class NonePrize : PrizeBase
{
public override PrizeType Type => PrizeType.None;
public override void Execute(MonoBehaviour host, LevelPrizeConfig config)
{
// Ничего не делаем
InvokeResult(true);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 647f58a6e1ab49e18855aeae812fa0df
timeCreated: 1773226228

View File

@ -0,0 +1,38 @@
using System;
using UnityEngine;
namespace RR.Common.Prizes
{
/// <summary>
/// Абстрактный исполнитель приза.
/// Каждый тип приза реализует <see cref="Execute"/> для своей логики выдачи.
/// Исполнители создаются PrizeSystemManager, не хранят конфигурацию —
/// получают её через <see cref="LevelPrizeConfig"/> при вызове.
/// </summary>
public abstract class PrizeBase
{
/// <summary>Тип приза, который обрабатывает этот исполнитель.</summary>
public abstract PrizeType Type { get; }
/// <summary>Идёт ли процесс выдачи.</summary>
public virtual bool IsGiving => false;
/// <summary>
/// Вызывается после завершения выдачи. true — успех, false — ошибка.
/// </summary>
public event Action<bool> OnPrizeResult;
/// <summary>
/// Выполняет выдачу приза.
/// </summary>
/// <param name="host">MonoBehaviour для запуска корутин.</param>
/// <param name="config">Конфигурация приза для текущего уровня.</param>
public abstract void Execute(MonoBehaviour host, LevelPrizeConfig config);
/// <summary>Уведомляет подписчиков о результате.</summary>
protected void InvokeResult(bool success)
{
OnPrizeResult?.Invoke(success);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 794461cf42ac3294c8d7f8cab43ec2fb

View File

@ -0,0 +1,126 @@
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace RR.Common.Prizes
{
/// <summary>
/// Хранилище призов за уровни. Загружает и сохраняет конфигурацию в JSON-файл.
/// Если файла нет — создаёт новый с пустым списком (нет призов ни за один уровень).
/// </summary>
public class PrizeDatabase
{
private const string FileName = "prizes.json";
private readonly Dictionary<string, LevelPrizeConfig> _configs = new();
private string _filePath;
/// <summary>Путь к JSON-файлу.</summary>
public string FilePath => _filePath;
// ────────────── Загрузка / сохранение ──────────────
/// <summary>
/// Загружает конфигурацию из JSON-файла рядом с исполняемым файлом.
/// Если файл отсутствует — создаёт пустой.
/// </summary>
public void Load()
{
string root = Directory.GetParent(Application.dataPath)?.ToString()
?? Application.dataPath;
_filePath = Path.Combine(root, FileName);
_configs.Clear();
if (File.Exists(_filePath))
{
string json = File.ReadAllText(_filePath);
var data = JsonUtility.FromJson<PrizesFileData>(json);
if (data?.levels != null)
{
foreach (PrizeLevelEntry entry in data.levels)
{
if (!string.IsNullOrEmpty(entry.level))
_configs[entry.level] = entry.config ?? new LevelPrizeConfig();
}
}
Debug.Log($"[PrizeDatabase] Загружено {_configs.Count} записей из {_filePath}");
}
else
{
Save();
Debug.Log($"[PrizeDatabase] Файл не найден — создан пустой: {_filePath}");
}
}
/// <summary>
/// Сохраняет текущую конфигурацию в JSON-файл.
/// </summary>
public void Save()
{
if (string.IsNullOrEmpty(_filePath)) return;
var data = new PrizesFileData();
foreach (var kvp in _configs)
data.levels.Add(new PrizeLevelEntry { level = kvp.Key, config = kvp.Value });
string json = JsonUtility.ToJson(data, true);
File.WriteAllText(_filePath, json);
Debug.Log($"[PrizeDatabase] Сохранено {_configs.Count} записей в {_filePath}");
}
// ────────────── Чтение ──────────────
/// <summary>
/// Возвращает конфигурацию приза для указанного уровня.
/// Если для уровня нет записи — возвращает <see cref="PrizeType.None"/>.
/// </summary>
public LevelPrizeConfig GetConfig(string level)
{
return _configs.TryGetValue(level, out var config) ? config : new LevelPrizeConfig();
}
/// <summary>
/// Проверяет, есть ли запись для указанного уровня.
/// </summary>
public bool HasConfig(string level) => _configs.ContainsKey(level);
/// <summary>
/// Все записи (только для чтения).
/// </summary>
public IReadOnlyDictionary<string, LevelPrizeConfig> GetAll() => _configs;
// ────────────── Запись ──────────────
/// <summary>
/// Устанавливает конфигурацию приза для уровня и сохраняет файл.
/// </summary>
public void SetConfig(string level, LevelPrizeConfig config)
{
_configs[level] = config;
Save();
}
/// <summary>
/// Удаляет запись для уровня (приз = None) и сохраняет файл.
/// </summary>
public void RemoveConfig(string level)
{
if (_configs.Remove(level))
Save();
}
/// <summary>
/// Очищает все записи и сохраняет пустой файл.
/// </summary>
public void Clear()
{
_configs.Clear();
Save();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3b65e25d8b740d59d9c30caa8e22107
timeCreated: 1773226207

View File

@ -0,0 +1,107 @@
using System;
using UnityEngine;
using RR.Common.Controls;
using RR.Common.UI;
namespace RR.Common.Prizes
{
/// <summary>
/// Исполнитель приза «Витрина».
/// Показывает модальное окно с выбором ячейки, затем открывает её
/// через <see cref="PrizeStandController"/>. Количество ячеек берётся из <see cref="LevelPrizeConfig"/>.
/// </summary>
public class PrizeStandPrize : PrizeBase
{
public override PrizeType Type => PrizeType.PrizeStand;
public override bool IsGiving => _isGiving;
private readonly Func<PrizeStandModal> _resolveModal;
private bool _isGiving;
private PrizeStandModal _activeModal;
/// <param name="resolveModal">Делегат для поиска модального окна в текущих сценах.</param>
public PrizeStandPrize(Func<PrizeStandModal> resolveModal)
{
_resolveModal = resolveModal;
}
public override void Execute(MonoBehaviour host, LevelPrizeConfig config)
{
if (_isGiving)
{
Debug.LogWarning("[PrizeStandPrize] Уже идёт выдача.");
return;
}
_isGiving = true;
// Отписываемся от предыдущего модального окна, если было
if (_activeModal != null)
_activeModal.OnBoxSelected -= OnBoxSelected;
_activeModal = _resolveModal?.Invoke();
if (_activeModal != null)
{
_activeModal.OnBoxSelected += OnBoxSelected;
_activeModal.Show(config.prizeStandBoxCount);
}
else
{
Debug.LogError("[PrizeStandPrize] PrizeStandModal не найден на сцене.");
_isGiving = false;
InvokeResult(false);
}
}
private async void OnBoxSelected(int boxNumber)
{
// Отписываемся сразу после выбора
if (_activeModal != null)
{
_activeModal.OnBoxSelected -= OnBoxSelected;
_activeModal = null;
}
if (boxNumber <= 0)
{
_isGiving = false;
InvokeResult(false);
return;
}
PrizeStandController stand = ComSystemManager.GetPrizeStandController();
if (stand == null)
{
Debug.LogError("[PrizeStandPrize] Витрина не подключена.");
_isGiving = false;
InvokeResult(false);
return;
}
bool success = false;
void OnResult(bool s, string m)
{
stand.OpeningFinished -= OnResult;
success = s;
if (!s) Debug.LogWarning($"[PrizeStandPrize] Ошибка: {m}");
}
stand.OpeningFinished += OnResult;
try
{
await stand.OpenBoxAsync(boxNumber);
}
catch (Exception ex)
{
Debug.LogError($"[PrizeStandPrize] Исключение: {ex.Message}");
stand.OpeningFinished -= OnResult;
}
_isGiving = false;
InvokeResult(success);
}
}
}

View File

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

View File

@ -0,0 +1,18 @@
namespace RR.Common.Prizes
{
/// <summary>
/// Тип приза. Определяет, какой приз выдаётся за уровень.
/// </summary>
public enum PrizeType
{
/// <summary>Нет приза.</summary>
None = 0,
/// <summary>QR-код — модальное окно с QR-кодом.</summary>
QRCode = 1,
/// <summary>Хоппер — физическая выдача капсул.</summary>
Hopper = 2,
/// <summary>Витрина — модальное окно с выбором ячейки.</summary>
PrizeStand = 3
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 00f8ed3308f84f9d9505acb84b27ca12
timeCreated: 1773226177

View File

@ -0,0 +1,72 @@
using System;
using UnityEngine;
using RR.Common.UI;
namespace RR.Common.Prizes
{
/// <summary>
/// Исполнитель приза «QR-код».
/// Показывает модальное окно с QR-кодами. Сами QR-коды получает через делегат —
/// не знает откуда они берутся.
/// </summary>
public class QRCodePrize : PrizeBase
{
public override PrizeType Type => PrizeType.QRCode;
public override bool IsGiving => _isGiving;
private readonly Func<QRCodeModal> _resolveModal;
private readonly Func<Texture2D[]> _getQRCodes;
private bool _isGiving;
private QRCodeModal _activeModal;
/// <param name="resolveModal">Делегат для поиска модального окна в текущих сценах.</param>
/// <param name="getQRCodes">Делегат получения QR-текстур.</param>
public QRCodePrize(Func<QRCodeModal> resolveModal, Func<Texture2D[]> getQRCodes)
{
_resolveModal = resolveModal;
_getQRCodes = getQRCodes;
}
public override void Execute(MonoBehaviour host, LevelPrizeConfig config)
{
Texture2D[] codes = _getQRCodes?.Invoke();
if (codes == null || codes.Length == 0)
{
Debug.LogWarning("[QRCodePrize] Нет QR-кодов для отображения.");
InvokeResult(false);
return;
}
QRCodeModal modal = _resolveModal?.Invoke();
if (modal == null)
{
Debug.LogWarning("[QRCodePrize] QRCodeModal не найден на сцене.");
InvokeResult(false);
return;
}
_isGiving = true;
// Отписываемся от предыдущего, если было
if (_activeModal != null)
_activeModal.OnClosed -= OnModalClosed;
_activeModal = modal;
_activeModal.OnClosed += OnModalClosed;
_activeModal.ShowQRCodes(codes);
}
private void OnModalClosed()
{
if (_activeModal != null)
{
_activeModal.OnClosed -= OnModalClosed;
_activeModal = null;
}
_isGiving = false;
InvokeResult(true);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5c9c07112ada4098b8c5b2a8dd6d714f
timeCreated: 1773216325

View File

@ -32,6 +32,12 @@ namespace RR.Common.Settings
ValueSelector // Выбор значения (со стрелками или дропдауном)
}
public enum FloatDisplayFormat
{
Slider, // Обычный слайдер
ValueSelector // Выбор значения (со стрелками)
}
[Serializable]
public struct SettingValue
{
@ -129,6 +135,12 @@ namespace RR.Common.Settings
[Tooltip("Отображать float значение как проценты (0-100%)")]
public bool displayAsPercentage = false;
[Tooltip("Формат отображения для Float (слайдер или выбор значения)")]
public FloatDisplayFormat floatDisplayFormat = FloatDisplayFormat.Slider;
[Tooltip("Шаг для выбора значения Float (используется только для ValueSelector)")]
public float floatStep = 0.1f;
public ApplyTarget applyTarget = ApplyTarget.None;

View File

@ -31,6 +31,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 1
floatDisplayFormat: 1
floatStep: 0.01
applyTarget: 1
audioTargetType: 0
buttonText:
@ -57,6 +59,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 1
floatDisplayFormat: 1
floatStep: 0.01
applyTarget: 1
audioTargetType: 1
buttonText:
@ -83,6 +87,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -110,6 +116,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -136,6 +144,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -156,13 +166,15 @@ MonoBehaviour:
defaultInt: 10
defaultFloat: 0
defaultString:
minInt: 10
minInt: 1
maxInt: 1000
intDisplayFormat: 1
intStep: 5
intStep: 1
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -183,13 +195,15 @@ MonoBehaviour:
defaultInt: 10
defaultFloat: 0
defaultString:
minInt: 10
minInt: 1
maxInt: 1000
intDisplayFormat: 1
intStep: 5
intStep: 1
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -210,13 +224,15 @@ MonoBehaviour:
defaultInt: 10
defaultFloat: 0
defaultString:
minInt: 5
minInt: 1
maxInt: 1000
intDisplayFormat: 1
intStep: 5
intStep: 1
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -243,6 +259,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -270,6 +288,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -297,6 +317,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0412\u0432\u0435\u0441\u0442\u0438 \u0441\u0443\u043F\u0435\u0440\u043F\u0440\u0438\u0437"
@ -324,6 +346,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0412\u0432\u0435\u0441\u0442\u0438 \u0441\u0443\u043F\u0435\u0440\u043F\u0440\u0438\u0437"
@ -351,6 +375,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0412\u0432\u0435\u0441\u0442\u0438 \u0441\u0443\u043F\u0435\u0440\u043F\u0440\u0438\u0437"
@ -377,6 +403,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430"
@ -403,6 +431,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430"
@ -424,11 +454,13 @@ MonoBehaviour:
defaultString:
minInt: 1
maxInt: 20
intDisplayFormat: 0
intDisplayFormat: 1
intStep: 1
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -451,11 +483,13 @@ MonoBehaviour:
defaultString:
minInt: 0
maxInt: 20
intDisplayFormat: 0
intDisplayFormat: 1
intStep: 1
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -479,10 +513,12 @@ MonoBehaviour:
minInt: 0
maxInt: 3000
intDisplayFormat: 1
intStep: 10
intStep: 1
minFloat: 10
maxFloat: 3000
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText:
@ -509,6 +545,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430"
@ -536,6 +574,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0412\u0432\u0435\u0441\u0442\u0438 \u043D\u043E\u043C\u0435\u0440
@ -563,6 +603,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0412\u0432\u0435\u0441\u0442\u0438 \u0441\u0443\u043F\u0435\u0440\u043F\u0440\u0438\u0437"
@ -590,6 +632,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0412\u0432\u0435\u0441\u0442\u0438 \u043D\u043E\u043C\u0435\u0440
@ -617,6 +661,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0412\u0432\u0435\u0441\u0442\u0438 \u0432\u0430\u043B\u044E\u0442\u0443"
@ -643,6 +689,8 @@ MonoBehaviour:
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u0422\u0435\u0441\u0442 \u0432\u044B\u0434\u0430\u0447\u0438 \u043F\u0440\u0438\u0437\u0430"
@ -651,3 +699,31 @@ MonoBehaviour:
buttonSprite: {fileID: 0}
options: []
defaultOptionIndex: 0
- key: btn.levels_prizes
displayName: "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u0440\u0438\u0437\u043E\u0432"
description:
group: "\u041F\u0440\u0438\u0437\u044B \u0438 \u0432\u044B\u0434\u0430\u0447\u0430"
shownInSettings: 1
exposed: 0
valueType: 4
defaultBool: 1
defaultInt: 0
defaultFloat: 0
defaultString:
minInt: 0
maxInt: 0
intDisplayFormat: 0
intStep: 1
minFloat: 0
maxFloat: 1
displayAsPercentage: 0
floatDisplayFormat: 0
floatStep: 0.1
applyTarget: 0
audioTargetType: 0
buttonText: "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u0440\u0438\u0437\u043E\u0432"
buttonData:
buttonColor: {r: 0.16300285, g: 0.64997125, b: 0.9339623, a: 1}
buttonSprite: {fileID: 0}
options: []
defaultOptionIndex: 0

View File

@ -114,8 +114,16 @@ namespace RR.Common.UI
break;
case SettingValueType.Float:
UseSliderFloat(entry.minFloat, entry.maxFloat, v =>
SetSafe(entry.key, SettingValue.Float(v)));
// Для ValueSelector не создаём слайдер - его обрабатывает ValueSelectorRow
if (entry.floatDisplayFormat == FloatDisplayFormat.ValueSelector)
{
// ValueSelectorRow сам управляет значением
}
else
{
UseSliderFloat(entry.minFloat, entry.maxFloat, v =>
SetSafe(entry.key, SettingValue.Float(v)));
}
break;
case SettingValueType.Int:

View File

@ -263,6 +263,22 @@ namespace RR.Common.UI
Debug.LogWarning($"[SettingsUIGenerator] ValueSelectorRow component not found on prefab for '{entry.key}'");
}
}
// Если это Float ValueSelector, настраиваем компонент ValueSelectorRow
if (entry.valueType == SettingValueType.Float &&
entry.floatDisplayFormat == FloatDisplayFormat.ValueSelector)
{
ValueSelectorRow valueSelector = item.GetComponent<ValueSelectorRow>();
if (valueSelector != null)
{
float currentValue = SettingsManager.Instance.GetFloat(entry.key);
valueSelector.Setup(entry, currentValue);
}
else
{
Debug.LogWarning($"[SettingsUIGenerator] ValueSelectorRow component not found on prefab for '{entry.key}'");
}
}
spawnedItems.Add(item);
}
@ -272,7 +288,9 @@ namespace RR.Common.UI
return e.valueType switch
{
SettingValueType.Bool => toggleRowPrefab,
SettingValueType.Float => sliderRowPrefab,
SettingValueType.Float => e.floatDisplayFormat == FloatDisplayFormat.ValueSelector
? valueSelectorRowPrefab
: sliderRowPrefab,
SettingValueType.Int => e.intDisplayFormat == IntDisplayFormat.ValueSelector
? valueSelectorRowPrefab
: sliderRowPrefab ? sliderRowPrefab : dropdownRowPrefab,
@ -296,8 +314,16 @@ namespace RR.Common.UI
ValueSelectorRow valueSelector = spawnedItems[i].GetComponent<ValueSelectorRow>();
if (valueSelector != null && !string.IsNullOrEmpty(valueSelector.Key))
{
int value = SettingsManager.Instance.GetInt(valueSelector.Key);
valueSelector.SetValue(value);
if (valueSelector.IsFloat)
{
float fValue = SettingsManager.Instance.GetFloat(valueSelector.Key);
valueSelector.SetValue(fValue);
}
else
{
int iValue = SettingsManager.Instance.GetInt(valueSelector.Key);
valueSelector.SetValue(iValue);
}
}
}
}

View File

@ -7,6 +7,9 @@ namespace RR.Common.Settings.UI
{
/// <summary>
/// Компонент-Selectable для элемента настроек с выбором значения стрелками влево/вправо.
/// Поддерживает как int, так и float значения.
/// При зажатии кнопки скорость изменения плавно увеличивается.
/// Автоповтор целиком управляется Event System (inputActionsPerSecond).
/// Работает на одном префабе вместе с SettingUIItem.
/// </summary>
public class ValueSelectorRow : Selectable, IMoveHandler
@ -15,8 +18,31 @@ namespace RR.Common.Settings.UI
[SerializeField] private TMP_Text valueText;
private SettingEntry _entry;
private int _currentValue;
private int _step = 1;
private bool _isFloat;
private int _currentInt;
private float _currentFloat;
private int _intStep = 1;
private float _floatStep = 0.1f;
// ───────────────────── Ускорение при зажатии ─────────────────────
/// <summary>Максимальный множитель шага при длительном зажатии.</summary>
private const int MaxStepMultiplier = 10;
/// <summary>Сколько вызовов OnMove до начала роста множителя.</summary>
private const int MultiplierStartCount = 6;
/// <summary>Сколько вызовов OnMove до достижения максимального множителя.</summary>
private const int MultiplierMaxCount = 30;
/// <summary>Если между двумя OnMove прошло больше этого времени — сброс множителя.</summary>
private const float HoldResetTimeout = 0.25f;
private int _holdDirection; // -1 = left, +1 = right, 0 = idle
private int _holdCount; // Счётчик последовательных OnMove в одном направлении
private float _lastMoveTime; // Time.unscaledTime последнего OnMove
// ───────────────────── Int Setup ─────────────────────
public void Setup(SettingEntry entry, int currentValue)
{
@ -27,27 +53,55 @@ namespace RR.Common.Settings.UI
}
_entry = entry;
// Валидация step
_isFloat = false;
if (entry.intStep <= 0)
{
Debug.LogWarning($"[ValueSelectorRow] Invalid step {entry.intStep} for key {entry.key}, using 1");
_step = 1;
_intStep = 1;
}
else
{
_step = entry.intStep;
_intStep = entry.intStep;
}
_currentValue = Mathf.Clamp(currentValue, entry.minInt, entry.maxInt);
_currentInt = Mathf.Clamp(currentValue, entry.minInt, entry.maxInt);
UpdateUI();
}
// ───────────────────── Float Setup ─────────────────────
public void Setup(SettingEntry entry, float currentValue)
{
if (entry == null)
{
Debug.LogError("[ValueSelectorRow] Cannot setup with null entry!");
return;
}
_entry = entry;
_isFloat = true;
if (entry.floatStep <= 0f)
{
Debug.LogWarning($"[ValueSelectorRow] Invalid step {entry.floatStep} for key {entry.key}, using 0.1");
_floatStep = 0.1f;
}
else
{
_floatStep = entry.floatStep;
}
_currentFloat = Mathf.Clamp(currentValue, entry.minFloat, entry.maxFloat);
UpdateUI();
}
// ───────────────────── Navigation ─────────────────────
public new void OnMove(AxisEventData eventData)
{
if (_entry == null || !IsInteractable())
{
// Пробрасываем событие к базовому классу для стандартной навигации
base.OnMove(eventData);
return;
}
@ -55,71 +109,166 @@ namespace RR.Common.Settings.UI
switch (eventData.moveDir)
{
case MoveDirection.Left:
DecreaseValue();
ApplyMove(-1);
eventData.Use();
break;
case MoveDirection.Right:
IncreaseValue();
ApplyMove(1);
eventData.Use();
break;
case MoveDirection.Up:
case MoveDirection.Down:
// Пробрасываем вертикальную навигацию к базовому классу
ResetHold();
base.OnMove(eventData);
break;
}
}
private void DecreaseValue()
// ───────────────────── Move Logic ─────────────────────
private void ApplyMove(int direction)
{
int newValue = Mathf.Max(_entry.minInt, _currentValue - _step);
if (newValue != _currentValue)
float now = Time.unscaledTime;
// Если направление сменилось или прошло слишком много времени — сбрасываем
if (_holdDirection != direction || (now - _lastMoveTime) > HoldResetTimeout)
{
SetValue(newValue);
_holdDirection = direction;
_holdCount = 0;
}
_lastMoveTime = now;
_holdCount++;
// Вычисляем множитель шага по количеству последовательных нажатий
int multiplier = 1;
if (_holdCount > MultiplierStartCount)
{
float progress = Mathf.Clamp01(
(float)(_holdCount - MultiplierStartCount) / (MultiplierMaxCount - MultiplierStartCount));
multiplier = Mathf.Max(1, Mathf.RoundToInt(Mathf.Lerp(1f, MaxStepMultiplier, progress)));
}
if (direction > 0)
IncreaseValue(multiplier);
else
DecreaseValue(multiplier);
}
private void ResetHold()
{
_holdDirection = 0;
_holdCount = 0;
}
protected override void OnDisable()
{
base.OnDisable();
ResetHold();
}
public override void OnDeselect(BaseEventData eventData)
{
base.OnDeselect(eventData);
ResetHold();
}
// ───────────────────── Value Changes ─────────────────────
private void DecreaseValue(int multiplier)
{
if (_isFloat)
{
float newValue = Mathf.Max(_entry.minFloat, _currentFloat - _floatStep * multiplier);
if (!Mathf.Approximately(newValue, _currentFloat))
SetValue(newValue);
}
else
{
int newValue = Mathf.Max(_entry.minInt, _currentInt - _intStep * multiplier);
if (newValue != _currentInt)
SetValue(newValue);
}
}
private void IncreaseValue()
private void IncreaseValue(int multiplier)
{
int newValue = Mathf.Min(_entry.maxInt, _currentValue + _step);
if (newValue != _currentValue)
if (_isFloat)
{
SetValue(newValue);
float newValue = Mathf.Min(_entry.maxFloat, _currentFloat + _floatStep * multiplier);
if (!Mathf.Approximately(newValue, _currentFloat))
SetValue(newValue);
}
else
{
int newValue = Mathf.Min(_entry.maxInt, _currentInt + _intStep * multiplier);
if (newValue != _currentInt)
SetValue(newValue);
}
}
// ───────────────────── UI ─────────────────────
private void UpdateUI()
{
if (valueText != null)
valueText.text = _currentValue.ToString();
}
if (valueText == null) return;
public void SetValue(int value)
{
if (_entry == null)
return;
int clampedValue = Mathf.Clamp(value, _entry.minInt, _entry.maxInt);
if (clampedValue != _currentValue)
if (_isFloat)
{
_currentValue = clampedValue;
UpdateUI();
// Сохраняем изменения через SettingsManager
var manager = SettingsManager.Instance;
if (manager != null)
{
manager.SetInt(_entry.key, _currentValue);
}
if (_entry != null && _entry.displayAsPercentage)
valueText.text = (_currentFloat * 100f).ToString("0") + "%";
else
{
Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!");
}
valueText.text = _currentFloat.ToString("0.##");
}
else
{
valueText.text = _currentInt.ToString();
}
}
// ───────────────────── Public API (int) ─────────────────────
public void SetValue(int value)
{
if (_entry == null) return;
int clamped = Mathf.Clamp(value, _entry.minInt, _entry.maxInt);
if (clamped != _currentInt)
{
_currentInt = clamped;
UpdateUI();
var manager = SettingsManager.Instance;
if (manager != null)
manager.SetInt(_entry.key, _currentInt);
else
Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!");
}
}
// ───────────────────── Public API (float) ─────────────────────
public void SetValue(float value)
{
if (_entry == null) return;
float clamped = Mathf.Clamp(value, _entry.minFloat, _entry.maxFloat);
if (!Mathf.Approximately(clamped, _currentFloat))
{
_currentFloat = clamped;
UpdateUI();
var manager = SettingsManager.Instance;
if (manager != null)
manager.SetFloat(_entry.key, _currentFloat);
else
Debug.LogError("[ValueSelectorRow] SettingsManager.Instance is null!");
}
}
public bool IsFloat => _isFloat;
public string Key => _entry?.key;
}
}

View File

@ -0,0 +1,282 @@
using System.Collections.Generic;
using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно выбора уровня для настройки награды за его прохождение.
/// Строит список уровней из <see cref="SceneManager.sceneCountInBuildSettings"/> и
/// предоставляет выбор текущего уровня как контекст для дальнейшего редактирования призов.
/// </summary>
public class LevelSettingsModal : Modal
{
[Header("References")]
[SerializeField] private Transform contentRoot;
[SerializeField] private GameObject levelButtonPrefab;
[Header("Display")]
[SerializeField] private bool rebuildOnOpen = true;
[SerializeField] private bool includeOnlyLevelsFolder = true;
[SerializeField] private string levelsFolderName = "Levels";
[SerializeField] private List<string> ignoredSceneNames = new() { "MainMenu", "Level UI" };
[Header("Selection Visual")]
[SerializeField] private Vector3 normalScale = Vector3.one;
[SerializeField] private Vector3 selectedScale = new(1.05f, 1.05f, 1f);
[Header("Модальное окно настройки приза")]
[Tooltip("Ссылка на LevelPrizeSettingsModal. Открывается при нажатии на кнопку уровня.")]
[SerializeField] private LevelPrizeSettingsModal levelPrizeSettingsModal;
private readonly List<LevelButtonView> _spawnedButtons = new();
private LevelButtonView _selectedButton;
/// <summary>
/// Имя сцены выбранного уровня.
/// Используется внешними компонентами как ключ для загрузки или сохранения настроек приза.
/// </summary>
public string SelectedSceneName => _selectedButton != null ? _selectedButton.SceneName : string.Empty;
/// <summary>
/// Открывает окно выбора уровня.
/// Перед открытием при необходимости пересобирает список кнопок из актуального набора сцен в Build Settings.
/// </summary>
public override void Open()
{
if (rebuildOnOpen || _spawnedButtons.Count == 0)
{
Rebuild();
}
base.Open();
}
[ContextMenu("Rebuild Level Buttons")]
public void Rebuild()
{
if (!ValidateReferences())
{
return;
}
ClearButtons();
List<string> sceneNames = GetLevelSceneNames();
foreach (string t in sceneNames)
{
CreateButton(t);
}
if (_spawnedButtons.Count <= 0) return;
SelectButton(_spawnedButtons[0]);
SetFirstSelected(_spawnedButtons[0].Button);
}
/// <summary>
/// Проверяет, что в инспекторе назначены контейнер для кнопок и префаб кнопки уровня.
/// </summary>
/// <returns>
/// <see langword="true"/>, если окно готово к построению списка уровней;
/// иначе <see langword="false"/>.
/// </returns>
private bool ValidateReferences()
{
if (contentRoot == null)
{
Debug.LogError($"[{nameof(LevelSettingsModal)}] Content Root is not assigned on {name}.", this);
return false;
}
if (levelButtonPrefab == null)
{
Debug.LogError($"[{nameof(LevelSettingsModal)}] Level Button Prefab is not assigned on {name}.", this);
return false;
}
return true;
}
/// <summary>
/// Создает кнопку для конкретной сцены уровня и настраивает ее отображение.
/// Нажатие на кнопку только выбирает уровень для последующей настройки награды.
/// </summary>
/// <param name="sceneName">Имя сцены уровня, отображаемое в интерфейсе.</param>
private void CreateButton(string sceneName)
{
GameObject buttonObject = Instantiate(levelButtonPrefab, contentRoot);
buttonObject.name = $"LevelButton_{sceneName}";
Button button = buttonObject.GetComponentInChildren<Button>(true);
if (button == null)
{
Debug.LogError(
$"[{nameof(LevelSettingsModal)}] Prefab '{levelButtonPrefab.name}' must contain a Button component.",
buttonObject);
Destroy(buttonObject);
return;
}
TMP_Text tmpText = buttonObject.GetComponentInChildren<TMP_Text>(true);
if (tmpText != null)
{
tmpText.text = sceneName;
}
else
{
Text legacyText = buttonObject.GetComponentInChildren<Text>(true);
if (legacyText != null)
{
legacyText.text = sceneName;
}
}
LevelButtonView buttonView = new(buttonObject.transform, sceneName, buttonObject.transform as RectTransform, button);
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => OnLevelButtonClicked(buttonView));
ApplySelectedVisual(buttonView, false);
_spawnedButtons.Add(buttonView);
}
/// <summary>
/// Обрабатывает нажатие на кнопку уровня.
/// Не запускает уровень, а только делает его текущим выбранным элементом для настройки приза.
/// </summary>
/// <param name="buttonView">Представление нажатой кнопки уровня.</param>
private void OnLevelButtonClicked(LevelButtonView buttonView)
{
SelectButton(buttonView);
Debug.Log($"[{nameof(LevelSettingsModal)}] Selected level: {buttonView.SceneName}");
// Открываем окно настройки приза для выбранного уровня
if (levelPrizeSettingsModal != null)
levelPrizeSettingsModal.OpenForLevel(buttonView.SceneName);
}
/// <summary>
/// Обновляет текущий выбранный уровень и переключает визуальное выделение между кнопками.
/// </summary>
/// <param name="buttonView">Кнопка уровня, которая должна стать активной.</param>
private void SelectButton(LevelButtonView buttonView)
{
if (_selectedButton != null)
{
ApplySelectedVisual(_selectedButton, false);
}
_selectedButton = buttonView;
if (_selectedButton != null)
{
ApplySelectedVisual(_selectedButton, true);
}
}
/// <summary>
/// Включает или отключает визуальное состояние выбранного уровня.
/// Используется, чтобы в интерфейсе было видно, для какого уровня сейчас настраивается награда.
/// </summary>
/// <param name="buttonView">Кнопка, для которой обновляется внешний вид.</param>
/// <param name="isSelected">
/// <see langword="true"/>, если кнопка должна быть показана как выбранная;
/// иначе <see langword="false"/>.
/// </param>
private void ApplySelectedVisual(LevelButtonView buttonView, bool isSelected)
{
if (buttonView.RectTransform != null)
{
buttonView.RectTransform.localScale = isSelected ? selectedScale : normalScale;
}
}
/// <summary>
/// Собирает список имен сцен уровней, доступных для настройки наград.
/// Источником служат сцены, добавленные в Build Settings, с учетом настроенной фильтрации.
/// </summary>
/// <returns>Список имен сцен уровней для построения кнопок в окне настроек.</returns>
private List<string> GetLevelSceneNames()
{
var result = new List<string>();
int sceneCount = SceneManager.sceneCountInBuildSettings;
for (int i = 0; i < sceneCount; i++)
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(i);
if (string.IsNullOrWhiteSpace(scenePath))
{
continue;
}
if (includeOnlyLevelsFolder)
{
string folderToken = $"/{levelsFolderName}/";
if (!scenePath.Replace("\\", "/").Contains(folderToken))
{
continue;
}
}
string sceneName = Path.GetFileNameWithoutExtension(scenePath);
if (ignoredSceneNames.Contains(sceneName))
{
continue;
}
result.Add(sceneName);
}
return result;
}
/// <summary>
/// Удаляет ранее созданные кнопки уровней и сбрасывает текущий выбор.
/// Используется перед полной пересборкой списка.
/// </summary>
private void ClearButtons()
{
foreach (LevelButtonView buttonView in _spawnedButtons)
{
if (buttonView.Root != null)
{
Destroy(buttonView.Root.gameObject);
}
}
_spawnedButtons.Clear();
_selectedButton = null;
}
/// <summary>
/// Внутреннее представление кнопки уровня, используемое модальным окном
/// для хранения ссылки на UI-элементы и имя сцены выбранного уровня.
/// </summary>
private sealed class LevelButtonView
{
/// <summary>
/// Инициализирует представление кнопки уровня, используемое для выбора уровня
/// при настройке награды за его прохождение.
/// </summary>
/// <param name="root">Корневой объект экземпляра префаба кнопки.</param>
/// <param name="sceneName">Имя сцены уровня.</param>
/// <param name="rectTransform">Прямоугольная трансформация корневого UI-элемента.</param>
/// <param name="button">Компонент кнопки, принимающий пользовательский ввод.</param>
public LevelButtonView(Transform root, string sceneName, RectTransform rectTransform, Button button)
{
Root = root;
SceneName = sceneName;
RectTransform = rectTransform;
Button = button;
}
public Transform Root { get; }
public string SceneName { get; }
public RectTransform RectTransform { get; }
public Button Button { get; }
}
}
}

View File

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

View File

@ -1,4 +1,5 @@
using UnityEngine;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
@ -13,6 +14,11 @@ namespace RR.Common.UI
[SerializeField] private Button newGameButton;
[SerializeField] private Button quitButton;
[Header("Автовыбор при загрузке")]
[SerializeField] private Selectable defaultSelectable;
[Tooltip("Задержка перед автовыбором элемента (сек)")]
[SerializeField] private float selectDelay = 1f;
[Header("Тексты")]
[SerializeField] private TextMeshProUGUI paymentText;
[SerializeField] private TextMeshProUGUI supportNumberText;
@ -48,6 +54,17 @@ namespace RR.Common.UI
// Обновляем тексты сразу при старте
UpdateTextsFromSettings();
}
// Автовыбор элемента при загрузке сцены
if (defaultSelectable != null)
StartCoroutine(SelectAfterDelay());
}
private IEnumerator SelectAfterDelay()
{
yield return new WaitForSeconds(selectDelay);
if (defaultSelectable != null)
defaultSelectable.Select();
}
private void OnDestroy()

View File

@ -0,0 +1,358 @@
using System;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using UnityEngine.Serialization;
using UnityEngine.UI;
using RR.Common.Managers;
using RR.Common.Prizes;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно настройки приза за конкретный уровень.
/// Полностью управляется с клавиатуры/геймпада через Input System.
/// Навигация: вверх/вниз — переключение строк (зацикленно), влево/вправо — изменение значения.
/// Настройки сохраняются автоматически при закрытии окна.
/// </summary>
public class LevelPrizeSettingsModal : Modal
{
[Header("UI — Общее")]
[SerializeField] private TMP_Text titleText;
[SerializeField] private TMP_Text prizeTypeLabel;
[Header("UI — Параметры хоппера")]
[SerializeField] private GameObject hopperParamsGroup;
[SerializeField] private TMP_Text hopperCapsuleCountLabel;
[Header("UI — Параметры витрины")]
[FormerlySerializedAs("showcaseParamsGroup")]
[SerializeField] private GameObject prizeStandParamsGroup;
[FormerlySerializedAs("showcaseBoxCountLabel")]
[SerializeField] private TMP_Text prizeStandBoxCountLabel;
[Header("Строки (RectTransform)")]
[Tooltip("RectTransform строки выбора типа приза.")]
[SerializeField] private RectTransform prizeTypeRow;
[Header("Указатель")]
[Tooltip("Компонент UIRowPointer для визуальной стрелки-указателя (опционально).")]
[SerializeField] private UIRowPointer rowPointer;
[Header("Кнопка закрытия")]
[Tooltip("Кнопка закрытия окна. При фокусе на ней указатель прячется, но Submit срабатывает.")]
[SerializeField] private Button closeModalButton;
[Header("Навигация (Input System)")]
[Tooltip("InputAction навигации (Navigate, тип Value/Vector2)")]
[SerializeField] private InputActionReference navigateAction;
[Tooltip("Порог срабатывания по осям (0..1)")]
[SerializeField] private float deadZone = 0.5f;
[Header("Лимиты")]
[SerializeField] private int maxCapsules = 10;
[SerializeField] private int maxBoxes = 48;
// ── Строки ──
private const int ROW_TYPE = 0;
private const int ROW_PARAM = 1;
private const int ROW_CLOSE = 2;
private const int ROW_COUNT = 3;
// ── Текущее состояние ──
private string _levelName;
private LevelPrizeConfig _config;
private static readonly PrizeType[] PrizeTypes =
(PrizeType[])Enum.GetValues(typeof(PrizeType));
private int _currentTypeIndex;
private int _activeRow;
private bool _axisHeldX;
private bool _axisHeldY;
/// <summary>
/// Вызывается после сохранения настроек (при закрытии окна). Параметр — имя уровня.
/// </summary>
public event Action<string> OnApplied;
// ────────────── Lifecycle ──────────────
protected override void OnEnable()
{
base.OnEnable();
if (navigateAction != null && navigateAction.action != null)
{
navigateAction.action.Enable();
navigateAction.action.performed += OnNavigatePerformed;
navigateAction.action.canceled += OnNavigateCanceled;
}
}
protected override void OnDisable()
{
if (navigateAction != null && navigateAction.action != null)
{
navigateAction.action.performed -= OnNavigatePerformed;
navigateAction.action.canceled -= OnNavigateCanceled;
}
_axisHeldX = false;
_axisHeldY = false;
base.OnDisable();
}
// ────────────── Ввод ──────────────
private void OnNavigatePerformed(InputAction.CallbackContext ctx)
{
if (!_isOpen) return;
Vector2 v = ctx.ReadValue<Vector2>();
// ── Вертикаль: переключение строк ──
if (Mathf.Abs(v.y) >= deadZone)
{
if (!_axisHeldY)
{
_axisHeldY = true;
MoveRow(v.y > 0f ? -1 : 1);
}
}
else
{
_axisHeldY = false;
}
// ── Горизонталь: изменение значения ──
if (Mathf.Abs(v.x) >= deadZone)
{
if (!_axisHeldX)
{
_axisHeldX = true;
HandleLeftRight(v.x > 0f ? 1 : -1);
}
}
else
{
_axisHeldX = false;
}
}
private void OnNavigateCanceled(InputAction.CallbackContext ctx)
{
_axisHeldX = false;
_axisHeldY = false;
}
// ────────────── Навигация строк ──────────────
private void MoveRow(int delta)
{
int next = _activeRow;
for (int i = 0; i < ROW_COUNT; i++)
{
next = (next + delta + ROW_COUNT) % ROW_COUNT;
if (IsRowVisible(next))
{
_activeRow = next;
break;
}
}
RefreshRowHighlights();
}
private bool IsRowVisible(int row)
{
if (row == ROW_PARAM)
return _config.type == PrizeType.Hopper || _config.type == PrizeType.PrizeStand;
return true; // ROW_TYPE и ROW_CLOSE видны всегда
}
private void HandleLeftRight(int direction)
{
switch (_activeRow)
{
case ROW_TYPE:
CyclePrizeType(direction);
break;
case ROW_PARAM:
if (_config.type == PrizeType.Hopper)
AdjustCapsules(direction);
else if (_config.type == PrizeType.PrizeStand)
AdjustBoxes(direction);
break;
// ROW_CLOSE — лево/право ничего не делает
}
}
// ────────────── Публичный API ──────────────
/// <summary>
/// Открывает окно настройки приза для указанного уровня.
/// </summary>
public void OpenForLevel(string levelName)
{
_levelName = levelName;
PrizeDatabase db = PrizeSystemManager.Instance?.Database;
_config = db != null
? CopyConfig(db.GetConfig(levelName))
: new LevelPrizeConfig();
_currentTypeIndex = 0;
for (int i = 0; i < PrizeTypes.Length; i++)
{
if (PrizeTypes[i] == _config.type)
{
_currentTypeIndex = i;
break;
}
}
_activeRow = ROW_TYPE;
RefreshUI();
RefreshRowHighlights();
Open();
}
/// <summary>
/// Закрывает окно и сохраняет настройки.
/// </summary>
public override void Close()
{
Save();
base.Close();
}
// ────────────── Внутреннее ──────────────
private void CyclePrizeType(int direction)
{
_currentTypeIndex = (_currentTypeIndex + direction + PrizeTypes.Length) % PrizeTypes.Length;
_config.type = PrizeTypes[_currentTypeIndex];
// Если строка параметров стала невидимой — перескочить
if (_activeRow == ROW_PARAM && !IsRowVisible(ROW_PARAM))
_activeRow = ROW_TYPE;
RefreshUI();
RefreshRowHighlights();
}
private void AdjustCapsules(int delta)
{
_config.hopperCapsuleCount = Mathf.Clamp(_config.hopperCapsuleCount + delta, 1, maxCapsules);
RefreshUI();
}
private void AdjustBoxes(int delta)
{
_config.prizeStandBoxCount = Mathf.Clamp(_config.prizeStandBoxCount + delta, 1, maxBoxes);
RefreshUI();
}
private void Save()
{
if (string.IsNullOrEmpty(_levelName) || _config == null) return;
PrizeDatabase db = PrizeSystemManager.Instance?.Database;
if (db == null)
{
Debug.LogError("[LevelPrizeSettingsModal] PrizeDatabase недоступна.");
return;
}
db.SetConfig(_levelName, _config);
Debug.Log($"[LevelPrizeSettingsModal] Уровень «{_levelName}» → приз «{_config.type}» сохранён.");
OnApplied?.Invoke(_levelName);
}
// ────────────── UI ──────────────
private void RefreshUI()
{
if (titleText)
titleText.text = $"Приз за уровень {_levelName}";
if (prizeTypeLabel)
prizeTypeLabel.text = GetPrizeTypeName(_config.type);
bool isHopper = _config.type == PrizeType.Hopper;
bool isPrizeStand = _config.type == PrizeType.PrizeStand;
if (hopperParamsGroup) hopperParamsGroup.SetActive(isHopper);
if (prizeStandParamsGroup) prizeStandParamsGroup.SetActive(isPrizeStand);
if (hopperCapsuleCountLabel)
hopperCapsuleCountLabel.text = _config.hopperCapsuleCount.ToString();
if (prizeStandBoxCountLabel)
prizeStandBoxCountLabel.text = _config.prizeStandBoxCount.ToString();
}
private void RefreshRowHighlights()
{
if (_activeRow == ROW_CLOSE)
{
// Прячем указатель, фокус переходит на кнопку закрытия
if (rowPointer) rowPointer.Hide();
if (closeModalButton && EventSystem.current)
EventSystem.current.SetSelectedGameObject(closeModalButton.gameObject);
}
else
{
if (rowPointer)
{
RectTransform target = GetActiveRowTransform();
if (target) rowPointer.SetTarget(target);
}
}
}
private RectTransform GetActiveRowTransform()
{
switch (_activeRow)
{
case ROW_TYPE:
return prizeTypeRow;
case ROW_PARAM:
if (_config.type == PrizeType.Hopper && hopperParamsGroup)
return hopperParamsGroup.transform as RectTransform;
if (_config.type == PrizeType.PrizeStand && prizeStandParamsGroup)
return prizeStandParamsGroup.transform as RectTransform;
return prizeTypeRow;
default:
return prizeTypeRow;
}
}
private static string GetPrizeTypeName(PrizeType type) => type switch
{
PrizeType.None => "Нет приза",
PrizeType.QRCode => "QR-код",
PrizeType.Hopper => "Хоппер",
PrizeType.PrizeStand => "Витрина",
_ => type.ToString()
};
private static LevelPrizeConfig CopyConfig(LevelPrizeConfig src) => new()
{
type = src.type,
hopperCapsuleCount = src.hopperCapsuleCount,
hopperDelayBetweenCapsules = src.hopperDelayBetweenCapsules,
prizeStandBoxCount = src.prizeStandBoxCount
};
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 503701b468074130a5d4e963e9867215
timeCreated: 1773226679

View File

@ -112,7 +112,7 @@ namespace RR.Common.UI
protected virtual void OnEnable()
{
// Регистрируемся в менеджере
ModalManager.Instance.Push(this);
ModalManager.Instance?.Push(this);
// Подписываемся на действие закрытия
if (closeAction)
@ -131,7 +131,7 @@ namespace RR.Common.UI
}
// Удаляемся из менеджера
ModalManager.Instance.Pop(this);
ModalManager.Instance?.Pop(this);
}
private void OnCloseActionPerformed(InputAction.CallbackContext ctx)

View File

@ -13,6 +13,7 @@ namespace RR.Common.UI
public class ModalManager : MonoBehaviour
{
private static ModalManager _instance;
private static bool _applicationQuitting;
/// <summary>
/// Singleton-инстанс менеджера модальных окон.
@ -22,6 +23,7 @@ namespace RR.Common.UI
{
get
{
if (_applicationQuitting) return null;
if (_instance) return _instance;
// Ищем существующий в сцене
@ -93,6 +95,24 @@ namespace RR.Common.UI
}
}
private void OnApplicationQuit()
{
_applicationQuitting = true;
}
#if UNITY_EDITOR
/// <summary>
/// Сбрасываем статический флаг при выходе из Play Mode в редакторе,
/// иначе при повторном запуске Instance всегда будет null.
/// </summary>
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetStatics()
{
_applicationQuitting = false;
_instance = null;
}
#endif
private void Update()
{
// Следим за тем, чтобы фокус оставался внутри текущего модального окна

View File

@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
using RR.Game2D.Core.Managers;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно витрины с выбором ячейки/полки для выдачи приза.
/// Отображает сетку кнопок-ячеек. Навигация — через Input System (Navigate).
/// </summary>
public class PrizeStandModal : Modal
{
/// <summary>Текущий экземпляр на сцене. Регистрируется автоматически.</summary>
public static PrizeStandModal Current { get; private set; }
[Header("Контент")]
[Tooltip("Контейнер для кнопок ячеек (например, GridLayoutGroup).")]
[SerializeField] private Transform boxButtonsContainer;
[Tooltip("Префаб кнопки ячейки. Должен содержать Button и TMP_Text.")]
[SerializeField] private GameObject boxButtonPrefab;
[Header("UI")]
[Tooltip("Текст-заголовок окна (опционально).")]
[SerializeField] private TMP_Text titleText;
[Tooltip("Текст заголовка по умолчанию.")]
[SerializeField] private string defaultTitle = "Выберите ячейку";
[Header("Навигация")]
[Tooltip("InputAction для навигации (Navigate из Input System).")]
[SerializeField] private InputActionReference navigateAction;
/// <summary>
/// Вызывается при выборе ячейки. Параметр — номер ячейки (1-based).
/// Если игрок закрыл окно без выбора, вызывается с 0.
/// </summary>
public event Action<int> OnBoxSelected;
private readonly List<GameObject> _createdButtons = new();
private bool _selectionMade;
// ────────────── Lifecycle ──────────────
protected override void Awake()
{
Current = this;
base.Awake();
// При закрытии без выбора — уведомляем с 0
OnClosed += HandleClosedWithoutSelection;
}
private void OnDestroy()
{
if (Current == this) Current = null;
OnClosed -= HandleClosedWithoutSelection;
}
// ────────────── Публичный API ──────────────
/// <summary>
/// Показывает окно с указанным количеством ячеек.
/// </summary>
/// <param name="boxCount">Количество ячеек для отображения.</param>
public void Show(int boxCount)
{
_selectionMade = false;
BuildButtons(boxCount);
if (titleText != null)
titleText.text = defaultTitle;
Open();
GameManager.Instance?.SetPause(true);
}
// ────────────── Внутреннее ──────────────
private void BuildButtons(int count)
{
ClearButtons();
if (boxButtonsContainer == null || boxButtonPrefab == null)
{
Debug.LogError("[PrizeStandModal] Контейнер или префаб кнопки не назначены.");
return;
}
for (int i = 1; i <= count; i++)
{
GameObject btnObj = Instantiate(boxButtonPrefab, boxButtonsContainer);
btnObj.name = $"BoxButton_{i}";
// Настраиваем текст
TMP_Text label = btnObj.GetComponentInChildren<TMP_Text>();
if (label != null)
label.text = i.ToString();
// Настраиваем кнопку
Button btn = btnObj.GetComponent<Button>();
if (btn != null)
{
int boxNumber = i; // локальная копия для замыкания
btn.onClick.AddListener(() => OnBoxButtonClicked(boxNumber));
}
_createdButtons.Add(btnObj);
}
// Устанавливаем первый элемент для фокуса
if (_createdButtons.Count > 0)
{
Button firstBtn = _createdButtons[0].GetComponent<Button>();
if (firstBtn != null)
SetFirstSelected(firstBtn);
}
}
private void OnBoxButtonClicked(int boxNumber)
{
_selectionMade = true;
Debug.Log($"[PrizeStandModal] Выбрана ячейка #{boxNumber}.");
Close();
GameManager.Instance?.SetPause(false);
OnBoxSelected?.Invoke(boxNumber);
}
private void HandleClosedWithoutSelection()
{
if (!_selectionMade)
{
Debug.Log("[PrizeStandModal] Окно закрыто без выбора.");
GameManager.Instance?.SetPause(false);
OnBoxSelected?.Invoke(0);
}
}
private void ClearButtons()
{
foreach (GameObject obj in _createdButtons)
{
if (obj != null) Destroy(obj);
}
_createdButtons.Clear();
}
protected override void OnDisable()
{
base.OnDisable();
ClearButtons();
GameManager.Instance?.SetPause(false);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9ec130ebb3048714cbde73bc9deb13cd

View File

@ -0,0 +1,104 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using RR.Game2D.Core.Managers;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно для отображения QR-кодов.
/// Создаёт Image-объекты с QR-кодами динамически в указанном контейнере.
/// </summary>
public class QRCodeModal : Modal
{
/// <summary>Текущий экземпляр на сцене. Регистрируется автоматически.</summary>
public static QRCodeModal Current { get; private set; }
[Header("Контент")]
[Tooltip("Контейнер для размещения QR-кодов. QR-коды создаются динамически.")]
[SerializeField] private RectTransform qrCodesContainer;
private readonly List<GameObject> _createdObjects = new();
protected override void Awake()
{
Current = this;
base.Awake(); // деактивирует GO, но ссылка Current остаётся валидной
}
private void OnDestroy()
{
if (Current == this) Current = null;
}
/// <summary>
/// Показывает модальное окно с указанными QR-кодами.
/// </summary>
/// <param name="qrTextures">Массив текстур QR-кодов для отображения.</param>
public void ShowQRCodes(Texture2D[] qrTextures)
{
ClearQRCodes();
if (qrCodesContainer == null)
{
Debug.LogWarning("[QRCodeModal] Контейнер для QR-кодов не назначен.");
return;
}
if (qrTextures == null || qrTextures.Length == 0)
{
Debug.LogWarning("[QRCodeModal] Нет QR-кодов для отображения.");
return;
}
foreach (Texture2D tex in qrTextures)
{
if (tex == null) continue;
GameObject qrObj = new GameObject("QRCode", typeof(RectTransform), typeof(Image));
qrObj.transform.SetParent(qrCodesContainer, false);
Sprite sprite = Sprite.Create(
tex,
new Rect(0, 0, tex.width, tex.height),
new Vector2(0.5f, 0.5f));
Image image = qrObj.GetComponent<Image>();
image.sprite = sprite;
image.raycastTarget = false;
_createdObjects.Add(qrObj);
}
Open();
GameManager.Instance?.SetPause(true);
}
/// <summary>
/// Очищает созданные QR-код объекты.
/// </summary>
private void ClearQRCodes()
{
foreach (GameObject obj in _createdObjects)
{
if (obj != null) Destroy(obj);
}
_createdObjects.Clear();
}
public override void Close()
{
base.Close();
ClearQRCodes();
GameManager.Instance?.SetPause(false);
}
protected override void OnDisable()
{
base.OnDisable();
ClearQRCodes();
GameManager.Instance?.SetPause(false);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 59ab4793ed2e4d3186f00d094a22f03c
timeCreated: 1773216378

View File

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace RR.Common.UI
{
/// <summary>
/// Модальное окно с правилами игры.
/// Поддерживает несколько страниц с переключением физическими кнопками влево/вправо.
/// </summary>
public class RulesModal : Modal
{
// ───────────────────────── Страницы ─────────────────────────
[Header("Страницы правил")]
[Tooltip("Список страниц правил. Каждая страница — это отдельный GameObject, " +
"который будет показываться/скрываться при переключении.")]
[SerializeField] private List<GameObject> pages = new();
// ───────────────────────── Навигация ─────────────────────────
[Header("Навигация (Input System)")]
[Tooltip("InputAction навигации (Navigate, тип Value/Vector2)")]
[SerializeField] private InputActionReference navigateAction;
[Tooltip("Порог срабатывания по оси X (0..1)")]
[SerializeField] private float deadZone = 0.5f;
[Tooltip("Зацикливать навигацию (с последней страницы на первую и наоборот)")]
[SerializeField] private bool loop;
// ───────────────────────── Индикатор ─────────────────────────
[Header("Индикатор страниц (опционально)")]
[Tooltip("Текст с номером страницы (например '2 / 5'). Может быть null.")]
[SerializeField] private TMP_Text pageIndicatorText;
[Tooltip("Формат текста индикатора. {0} — текущая, {1} — всего.")]
[SerializeField] private string pageIndicatorFormat = "{0} / {1}";
[Tooltip("Контейнер с точками-индикаторами. Дочерние объекты = точки. Может быть null.")]
[SerializeField] private Transform dotsContainer;
[Tooltip("Цвет активной точки")]
[SerializeField] private Color dotActiveColor = Color.white;
[Tooltip("Цвет неактивной точки")]
[SerializeField] private Color dotInactiveColor = new Color(1f, 1f, 1f, 0.35f);
[Header("Стрелки навигации (опционально)")]
[Tooltip("Изображение стрелки 'назад'. Скрывается, если предыдущей страницы нет.")]
[SerializeField] private RawImage prevArrowImage;
[Tooltip("Изображение стрелки 'вперёд'. Скрывается, если следующей страницы нет.")]
[SerializeField] private RawImage nextArrowImage;
// ───────────────────────── Состояние ─────────────────────────
private int _currentPage;
private bool _axisHeld;
/// <summary>Текущий индекс страницы (0-based).</summary>
public int CurrentPage => _currentPage;
/// <summary>Общее количество страниц.</summary>
public int PageCount => pages.Count;
/// <summary>Вызывается при смене страницы. Параметр — новый индекс.</summary>
public event Action<int> OnPageChanged;
// ───────────────────────── Lifecycle ─────────────────────────
protected override void OnEnable()
{
base.OnEnable();
ShowPage(0);
if (navigateAction != null && navigateAction.action != null)
{
navigateAction.action.Enable();
navigateAction.action.performed += OnNavigatePerformed;
navigateAction.action.canceled += OnNavigateCanceled;
}
}
protected override void OnDisable()
{
if (navigateAction != null && navigateAction.action != null)
{
navigateAction.action.performed -= OnNavigatePerformed;
navigateAction.action.canceled -= OnNavigateCanceled;
}
_axisHeld = false;
base.OnDisable();
}
// ───────────────────────── Ввод (Input System) ─────────────────────────
private void OnNavigatePerformed(InputAction.CallbackContext ctx)
{
float x = ctx.ReadValue<Vector2>().x;
if (Mathf.Abs(x) < deadZone)
{
_axisHeld = false;
return;
}
if (_axisHeld) return;
_axisHeld = true;
if (x < 0f)
PrevPage();
else
NextPage();
}
private void OnNavigateCanceled(InputAction.CallbackContext ctx)
{
_axisHeld = false;
}
// ───────────────────────── Навигация ─────────────────────────
/// <summary>Перейти на предыдущую страницу.</summary>
public void PrevPage()
{
if (pages.Count == 0) return;
int target = _currentPage - 1;
if (target < 0)
target = loop ? pages.Count - 1 : 0;
ShowPage(target);
}
/// <summary>Перейти на следующую страницу.</summary>
public void NextPage()
{
if (pages.Count == 0) return;
int target = _currentPage + 1;
if (target >= pages.Count)
target = loop ? 0 : pages.Count - 1;
ShowPage(target);
}
/// <summary>Показать страницу по индексу.</summary>
public void ShowPage(int index)
{
if (pages.Count == 0) return;
index = Mathf.Clamp(index, 0, pages.Count - 1);
for (int i = 0; i < pages.Count; i++)
{
if (pages[i] != null)
pages[i].SetActive(i == index);
}
_currentPage = index;
UpdateIndicator();
OnPageChanged?.Invoke(_currentPage);
}
// ───────────────────────── UI Update ─────────────────────────
private void UpdateIndicator()
{
if (pageIndicatorText != null)
{
pageIndicatorText.text = string.Format(pageIndicatorFormat, _currentPage + 1, pages.Count);
}
if (dotsContainer != null)
{
for (int i = 0; i < dotsContainer.childCount; i++)
{
var graphic = dotsContainer.GetChild(i).GetComponent<Graphic>();
if (graphic != null)
graphic.color = i == _currentPage ? dotActiveColor : dotInactiveColor;
}
}
if (prevArrowImage != null)
{
prevArrowImage.gameObject.SetActive(loop || _currentPage > 0);
}
if (nextArrowImage != null)
{
nextArrowImage.gameObject.SetActive(loop || _currentPage < pages.Count - 1);
}
}
}
}

View File

@ -58,6 +58,9 @@ namespace RR.Common.UI
// Блокируем паузу если меню неактивно (например, показан баннер проигрыша/победы)
if (!IsActive) return;
// Блокируем паузу если открыто любое другое модальное окно
if (!_isOpen && IsAnyOtherModalOpen()) return;
GameManager.Instance.TogglePause();
}
@ -76,11 +79,31 @@ namespace RR.Common.UI
{
if (!IsActive) return;
// Не показываем меню паузы, если открыто другое модальное окно
if (paused && IsAnyOtherModalOpen()) return;
if (paused)
Open();
else
Close();
}
/// <summary>
/// Проверяет, открыто ли какое-либо модальное окно, кроме самого PauseMenu.
/// </summary>
private bool IsAnyOtherModalOpen()
{
var mm = ModalManager.Instance;
if (mm == null) return false;
// Если открытых окон нет — точно нет других
if (!mm.HasOpenModals) return false;
// Если текущее окно — мы сами, значит других нет
if (mm.CurrentModal == this) return false;
return true;
}
/// <summary>
/// Переопределяем Close, чтобы при закрытии также снимать паузу.

View File

@ -1,6 +1,8 @@
using UnityEngine;
using TMPro;
using RR.Common.Controls;
using RR.Common.Managers;
using RR.Common.Prizes;
using RR.Common.Settings;
namespace RR.Common.UI
@ -50,6 +52,10 @@ namespace RR.Common.UI
SettingsManager.Instance.OnSettingChanged += OnSettingChanged;
PrizeSystem.OnPrizeResult += OnPrizeResult;
// Подписываемся на новую систему призов (если доступна)
if (PrizeSystemManager.Instance != null)
PrizeSystemManager.Instance.OnAnyPrizeResult += OnAnyPrizeResult;
}
private void OnDisable()
@ -58,6 +64,9 @@ namespace RR.Common.UI
SettingsManager.Instance.OnSettingChanged -= OnSettingChanged;
PrizeSystem.OnPrizeResult -= OnPrizeResult;
if (PrizeSystemManager.Instance != null)
PrizeSystemManager.Instance.OnAnyPrizeResult -= OnAnyPrizeResult;
}
private void OnSettingChanged(string key, SettingValue value)
@ -82,6 +91,21 @@ namespace RR.Common.UI
}
}
private void OnAnyPrizeResult(PrizeType type, bool success)
{
// Показываем ошибку только для хоппера и витрины
if (type == PrizeType.QRCode) return;
if (success)
{
if (_isShowing) Hide();
}
else
{
Show();
}
}
/// <summary>
/// Показывает сообщение об ошибке выдачи приза.
/// </summary>

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 5b9d6ed2ec5d4f2ca08ad641d3290cc8
timeCreated: 1773045871

View File

@ -14,7 +14,7 @@ namespace RR.Common.UI
// Strings to display
[SerializeField] private string livesIncreaseString = "{0}";
[SerializeField] private string livesString = "У вас будет {0}";
[SerializeField] private string livesString = "{0}";
private GameManager _gameManager;
private SettingsManager _settingsManager;
@ -79,8 +79,15 @@ namespace RR.Common.UI
private void UpdateLivesIncreaseText()
{
if (_settingsManager != null)
livesIncreaseText.text = string.Format(livesIncreaseString, _settingsManager.GetInt("gameplay.additionalLives"));
if (_settingsManager == null) return;
int additionalLives = _settingsManager.GetInt("gameplay.additionalLives");
if (livesIncreaseText != null)
{
livesIncreaseText.transform.parent.gameObject.SetActive(additionalLives > 0);
livesIncreaseText.text = string.Format(livesIncreaseString, additionalLives);
}
}
private void UpdateLivesText()

View File

@ -0,0 +1,145 @@
using UnityEngine;
namespace RR.Common.UI
{
/// <summary>
/// Указатель-стрелка, который плавно перемещается к произвольному UI-элементу.
/// Управляется извне через <see cref="SetTarget(RectTransform)"/> или <see cref="SetTarget(int)"/>.
/// </summary>
public class UIRowPointer : MonoBehaviour
{
[Header("Указатель")]
[Tooltip("RectTransform указателя (стрелка, иконка и т.д.).")]
[SerializeField] private RectTransform pointer;
[Header("Цели (опционально)")]
[Tooltip("Фиксированные цели для SetTarget(int). Можно не заполнять, если используется SetTarget(RectTransform).")]
[SerializeField] private RectTransform[] targets;
[Header("Движение")]
[Tooltip("Скорость плавного перемещения. 0 — мгновенно.")]
[SerializeField] private float smoothSpeed = 12f;
[Tooltip("Смещение указателя относительно центра цели (в локальных координатах родителя указателя).")]
[SerializeField] private Vector2 offset;
[Header("Покачивание")]
[Tooltip("Включить покачивание указателя по горизонтали.")]
[SerializeField] private bool wobble = true;
[Tooltip("Амплитуда покачивания (пикселей).")]
[SerializeField] private float wobbleAmplitude = 4f;
[Tooltip("Скорость покачивания (циклов в секунду).")]
[SerializeField] private float wobbleSpeed = 2f;
// ── Состояние ──
private RectTransform _currentTarget;
private Vector2 _targetPos;
private Vector2 _baseX;
private bool _ready;
// ────────────── Публичный API ──────────────
/// <summary>
/// Перемещает указатель к произвольному <see cref="RectTransform"/>.
/// </summary>
public void SetTarget(RectTransform target)
{
if (target == null) return;
_currentTarget = target;
_targetPos = CalculatePosition(target);
if (!_ready)
{
if (pointer) pointer.anchoredPosition = _targetPos;
_ready = true;
}
ShowPointer();
}
/// <summary>
/// Перемещает указатель к цели по индексу из массива <see cref="targets"/>.
/// </summary>
public void SetTarget(int index)
{
if (targets == null || index < 0 || index >= targets.Length) return;
SetTarget(targets[index]);
}
/// <summary>
/// Скрывает указатель.
/// </summary>
public void Hide()
{
if (pointer) pointer.gameObject.SetActive(false);
_currentTarget = null;
}
/// <summary>
/// Задаёт массив целей из кода (если не задан через инспектор).
/// </summary>
public void SetTargets(RectTransform[] newTargets)
{
targets = newTargets;
}
// ────────────── Lifecycle ──────────────
private void Awake()
{
if (pointer) _baseX = new Vector2(pointer.anchoredPosition.x, 0f);
}
private void LateUpdate()
{
if (!pointer || _currentTarget == null) return;
// Пересчитываем позицию (цель может двигаться из-за layout)
_targetPos = CalculatePosition(_currentTarget);
// Плавное перемещение
Vector2 pos = smoothSpeed > 0
? Vector2.Lerp(pointer.anchoredPosition, _targetPos, Time.unscaledDeltaTime * smoothSpeed)
: _targetPos;
// Покачивание
if (wobble)
pos.x += Mathf.Sin(Time.unscaledTime * wobbleSpeed * Mathf.PI * 2f) * wobbleAmplitude;
pointer.anchoredPosition = pos;
}
// ────────────── Внутреннее ──────────────
private Vector2 CalculatePosition(RectTransform target)
{
RectTransform parent = pointer.parent as RectTransform;
if (parent == null) return pointer.anchoredPosition;
Vector3[] corners = new Vector3[4];
target.GetWorldCorners(corners);
Vector3 worldCenter = (corners[0] + corners[2]) * 0.5f;
RectTransformUtility.ScreenPointToLocalPointInRectangle(
parent,
RectTransformUtility.WorldToScreenPoint(null, worldCenter),
null,
out Vector2 localPos
);
return new Vector2(
_baseX.x + offset.x,
localPos.y + offset.y
);
}
private void ShowPointer()
{
if (pointer && !pointer.gameObject.activeSelf)
pointer.gameObject.SetActive(true);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9be1bebe8ec042d49b09c6b154c69c98
timeCreated: 1773315789

View File

@ -61,6 +61,7 @@ namespace RR.Common.UI
/// <summary>
/// Отображает QR-коды из PrizeSystemManager.
/// Создаёт объекты полностью динамически без префаба.
/// Также вызывает выдачу QR-приза через новую систему, если доступна.
/// </summary>
private void DisplayQRCodes()
{

View File

@ -21,6 +21,7 @@ namespace RR.Common.Utility
private readonly object _lock = new object();
private StreamWriter _writer;
private bool _disposed;
private bool _footerWritten;
/// <summary>
/// Создает файловый логгер с указанным путём к файлу.
@ -49,6 +50,9 @@ namespace RR.Common.Utility
// Записываем заголовок
WriteHeader();
// Подписываемся на завершение приложения для гарантированной записи
Application.quitting += OnApplicationQuitting;
}
catch (Exception ex)
{
@ -249,6 +253,37 @@ namespace RR.Common.Utility
#endregion
private void OnApplicationQuitting()
{
WriteFooter();
Dispose();
}
private void WriteFooter()
{
if (_footerWritten || _disposed || _writer == null) return;
lock (_lock)
{
if (_footerWritten) return;
_footerWritten = true;
try
{
_writer.WriteLine();
_writer.WriteLine("================================================================================");
_writer.WriteLine($"Сессия логирования завершена: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
_writer.WriteLine("================================================================================");
_writer.Flush();
}
catch (ObjectDisposedException) { }
catch (Exception ex)
{
Debug.LogError($"[FileLogger] Ошибка при записи footer: {ex.Message}");
}
}
}
/// <summary>
/// Принудительно сбрасывает буфер на диск
/// </summary>
@ -296,7 +331,7 @@ namespace RR.Common.Utility
private void Dispose(bool disposing)
{
if (!disposing || _disposed)
if (_disposed)
return;
lock (_lock)
@ -306,21 +341,16 @@ namespace RR.Common.Utility
try
{
// Гарантированно записываем footer
WriteFooter();
if (disposing)
{
Application.quitting -= OnApplicationQuitting;
}
if (_writer != null)
{
// Закрывающий блок пишем только если поток ещё доступен.
try
{
_writer.WriteLine();
_writer.WriteLine("================================================================================");
_writer.WriteLine($"Сессия логирования завершена: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
_writer.WriteLine("================================================================================");
_writer.Flush();
}
catch (ObjectDisposedException)
{
// Поток уже закрыт извне — это не ошибка завершения.
}
_writer.Dispose();
_writer = null;

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6591511e9fc64464e991b20daad0bace
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,20 @@
1. Поместить RRLauncher.exe в папку Assets\StreamingAssets\
2. Добавить в какой либо скрипт содержимое файла Защитник.txt.
В идеале в bootstrap-script если он реализован
3. Запустить корутину с защитой (пример в файле Вызов.txt)
ВНИМАНИЕ!!!
В алгоритме есть такой костыль, может мешать работе если другое разрешение или несколько мониторов.
Можно пробовать запустить без костыля и дай бог всё будет хорошо
На фишке до какой то поры всё было хорошо, потом сломалось.
else
{
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
Screen.SetResolution(1920, 1080, true);
ClickCenter();
}
Лицензирование производится отдельной программой, которую можно попросить у Бори или Тимура

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: d4caa4d18cab87445ab97773d3a47227
guid: 8eee1e577928330428804446d6aa3197
DefaultImporter:
externalObjects: {}
userData:

Binary file not shown.

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 0ad40d78cfd21f6409db0bd7afe62c44
guid: 97a989dd2fc1dad4587e51145b973ffa
DefaultImporter:
externalObjects: {}
userData:

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
private void Awake()
{
#if !UNITY_EDITOR
StartCoroutine(CheckLicenseCoroutine());
#endif
}

View File

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

View File

@ -0,0 +1,304 @@
#region DEFENCE
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
private const uint WAIT_TIMEOUT = 0x00000102;
private const uint WAIT_OBJECT_0 = 0x00000000;
private System.Collections.IEnumerator CheckLicenseCoroutine()
{
// Ждём один кадр, чтобы Unity завершил инициализацию
yield return null;
bool licenseValid = false;
bool checkCompleted = false;
//bool licenseValid = true;
//bool checkCompleted = true;
string errorMessage = "";
// Запускаем проверку в отдельном потоке (совместимо с IL2CPP)
System.Threading.Thread thread = new System.Threading.Thread(() =>
{
try
{
licenseValid = CheckLicense();
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
checkCompleted = true;
}
});
thread.IsBackground = true;
thread.Start();
float timerest = 30f;
while (!checkCompleted && timerest > 0)
{
timerest -= Time.deltaTime;
yield return null;
}
if (!checkCompleted) licenseValid = false;
if (!licenseValid)
{
yield return new WaitForSeconds(3f);
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
else
{
//exclusive
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
Screen.SetResolution(1920, 1080, true);
ClickCenter();
}
}
private bool CheckLicense()
{
try
{
string launcherName = "RRLauncher.exe";
string launcherPath = null;
// Поиск лаунчера рядом с исполняемым файлом
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
string pathInBase = Path.Combine(baseDirectory, launcherName);
if (File.Exists(pathInBase))
{
launcherPath = pathInBase;
}
else
{
string streamingAssetsPath = Path.Combine(Application.streamingAssetsPath, launcherName);
if (File.Exists(streamingAssetsPath))
{
launcherPath = streamingAssetsPath;
}
else
{
return false;
}
}
try
{
STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
PROCESS_INFORMATION pi;
string workingDir = Path.GetDirectoryName(launcherPath);
bool success = CreateProcess(
launcherPath,
null,
IntPtr.Zero,
IntPtr.Zero,
false,
0,
IntPtr.Zero,
workingDir,
ref si,
out pi);
if (!success) return false;
uint waitResult = WaitForSingleObject(pi.hProcess, 30000);
if (waitResult == WAIT_TIMEOUT)
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return false;
}
// Получение кода возврата
uint exitCode;
if (!GetExitCodeProcess(pi.hProcess, out exitCode))
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return false;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (exitCode == 0)
{
ForceToFront();
return true;
}
return false;
}
catch
{
return false;
}
}
catch (Exception outerEx)
{
Debug.LogError($"[License] CRITICAL ERROR in CheckLicense: {outerEx.Message}");
Debug.LogError($"[License] Stack trace: {outerEx.StackTrace}");
return false;
}
}
#region FORCE TO FRONT
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hwnd);
[DllImport("user32.dll")]
private static extern IntPtr GetActiveWindow();
public void ForceToFront()//Метод который возвращает фокус а окно после открытия защиты
{
IntPtr handle = GetActiveWindow();
SetForegroundWindow(handle);
}
#endregion
#region клик в центр
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
public uint type;
public MOUSEINPUT mi;
}
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
private const uint INPUT_MOUSE = 0;
private const uint MOUSEEVENTF_MOVE = 0x0001;
private const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
private const uint MOUSEEVENTF_LEFTUP = 0x0004;
[DllImport("user32.dll")]
static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
public static void ClickCenter()
{
int screenX = Screen.currentResolution.width / 2;
int screenY = Screen.currentResolution.height / 2;
// WinAPI использует координаты 0–65535
int absoluteX = (int)(screenX * 65535 / Screen.currentResolution.width);
int absoluteY = (int)(screenY * 65535 / Screen.currentResolution.height);
INPUT[] inputs = new INPUT[3];
// движение мыши
inputs[0] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dx = absoluteX,
dy = absoluteY,
dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE,
}
};
// нажатие
inputs[1] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dwFlags = MOUSEEVENTF_LEFTDOWN
}
};
// отпускание
inputs[2] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dwFlags = MOUSEEVENTF_LEFTUP
}
};
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(INPUT)));
}
#endregion
#endregion

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

22
prizes.json Normal file
View File

@ -0,0 +1,22 @@
{
"levels": [
{
"level": "1-1",
"config": {
"type": 2,
"hopperCapsuleCount": 2,
"hopperDelayBetweenCapsules": 1.0,
"prizeStandBoxCount": 5
}
},
{
"level": "1-2",
"config": {
"type": 1,
"hopperCapsuleCount": 1,
"hopperDelayBetweenCapsules": 1.0,
"prizeStandBoxCount": 6
}
}
]
}