Improve VFX, UI overlays, session statistics, COM reliability, audio mixer, and expand architecture guides

Подробный список изменений:
- Перенесены эффекты частиц из C#-кода в готовые префабы-ассеты (Vfx Prefabs) в папке Assets/Resources/Vfx/ с автоматической генерацией материалов в Editor.
- Исправлены ошибки duration на играющих системах частиц и розовые текстуры в GameVfxService.cs.
- Исправлена навигация и фокус кнопок на оверлеях ContinueGameOverlay и PrizeResultOverlay с автоматическим подсвечиванием кнопок лимонно-зеленым цветом при фокусе и проигрыванием звука.
- Добавлена проверка настроек в GameplayHudController.cs для скрытия ContinueGameOverlay, если продолжение игры отключено.
- Расширен сбор статистики: создана структура GameplaySessionRecord, добавлена история до 50 раундов в PrizeSessionStateService и JsonPrizeSessionStateRepository, выведен расчет средних показателей и список 5 последних раундов в сервисной статистике.
- Повышена надежность COM-транспорта: добавлен sleep (80мс) в Dispose для graceful shutdown потоков, реализован цикл переподключения до 10 попыток при обрыве связи.
- Интегрированы настройки громкости с микшером: изменен шаг изменения громкости на 5% (0.05) для удобства, добавлен статус-чек и логи в ApplyServiceVolumes, расширены юнит-тесты на GetFloat параметры.
- Созданы новые архитектурные гайды по аппаратному COM-транспорту и призовой экономике автомата в папке GUIDES/architecture/.
- Оптимизирован и обновлен RAG-индекс проекта.
This commit is contained in:
Данил Ярош 2026-07-03 15:04:08 +03:00
parent 9000a6bc0c
commit 839a68aef1
118 changed files with 27625 additions and 453 deletions

View File

@ -141,7 +141,15 @@ AudioMixerController:
name: MusicVolume
- guid: 189b6be1a4bf3164a90885b0f24a7a48
name: SoundsVolume
m_AudioMixerGroupViews: []
m_AudioMixerGroupViews:
- guids:
- 5164b58c69786c341b1a5415bbcc6718
- 8478690b25035494dadd1a1f37938d91
- 313fac706123ef544a6e182b9f0e9852
- 7fef6e1290001664dac7c593c8fc4894
- 35db7625f6672cb44a6ccabc6e36b6c4
- bddd63f5c1b0be248ba048ea7c6f1235
name: View
m_CurrentViewIndex: 0
m_TargetSnapshot: {fileID: 24500006}
--- !u!243 &24300002

View File

@ -18,57 +18,37 @@ MonoBehaviour:
uiGroup: {fileID: -5027576678584524412, guid: e7f5dba03b1fe564095aec623e122722, type: 2}
gameplayGroup: {fileID: 4708169676529525986, guid: e7f5dba03b1fe564095aec623e122722, type: 2}
ambienceGroup: {fileID: -7217844520931084221, guid: e7f5dba03b1fe564095aec623e122722, type: 2}
musicLoop: {fileID: 0}
musicLoop: {fileID: 8300000, guid: b0296787b59f9f248b8361070a74a2b5, type: 3}
musicLoopVolume: 1
playMusicOnInitialize: 1
defaultMusicCue: 0
musicTracks: []
musicTracks:
- cue: 0
clip: {fileID: 8300000, guid: b0296787b59f9f248b8361070a74a2b5, type: 3}
outputGroupOverride: {fileID: 0}
volume: 1
loop: 1
- cue: 1
clip: {fileID: 8300000, guid: 3a30c29718ace1c468575da20ba204f1, type: 3}
outputGroupOverride: {fileID: 0}
volume: 1
loop: 0
- cue: 1
clip: {fileID: 8300000, guid: ff6aa1560276fae488e89d642928a57a, type: 3}
outputGroupOverride: {fileID: 0}
volume: 1
loop: 0
- cue: 1
clip: {fileID: 8300000, guid: 66f130eb74c4f9249ae760f50057506f, type: 3}
outputGroupOverride: {fileID: 0}
volume: 1
loop: 0
eventBindings:
- eventId: 0
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 2ad9b6d07a8e6214bbe919e2b5d94001, type: 3}
volume: 0.55
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 1
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: b1c90ee21bc444d4fb49b39000584eb0, type: 3}
volume: 0.65
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 2
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 86f4e01db21734942bc827c352d1192d, type: 3}
volume: 0.55
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 3
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 83e35ed99b265944ca0af63349a93b39, type: 3}
volume: 0.7
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 10
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: aa9657a7d0b5f7645bd09cef753a3241, type: 3}
volume: 0.75
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 20
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 46c2c848c1ca5ee4ca5d65e2c53ac8cc, type: 3}
- {fileID: 8300000, guid: fa0b6b9872cd9b84bb24e35b24e57fdf, type: 3}
volume: 0.45
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
@ -76,71 +56,31 @@ MonoBehaviour:
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 803f2112075bf8f43b9932da0b6412f4, type: 3}
- {fileID: 8300000, guid: f59cd1f6b437f7d4f8fa63bad1e466a5, type: 3}
volume: 0.75
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 22
- eventId: 20
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 172d2de1ca63ba742941ed67d82c7a7a, type: 3}
volume: 0.55
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 23
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: b435a43950545be458c5fdca99fbe35d, type: 3}
volume: 0.8
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 24
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: ae1b48318c83bfa40a0c1050ad79b3c3, type: 3}
volume: 0.65
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 25
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 46c2c848c1ca5ee4ca5d65e2c53ac8cc, type: 3}
- {fileID: 8300000, guid: fa0b6b9872cd9b84bb24e35b24e57fdf, type: 3}
volume: 0.35
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 26
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 803f2112075bf8f43b9932da0b6412f4, type: 3}
volume: 0.65
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 27
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: e3dc6a0fbf543b64388f7b60615f4145, type: 3}
- {fileID: 8300000, guid: cfbbacd3ef3f01743aa42aea3cb29270, type: 3}
volume: 0.65
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 28
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 86f4e01db21734942bc827c352d1192d, type: 3}
volume: 0.35
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 30
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 2be8148962585984fa7c7f14ae577839, type: 3}
- {fileID: 8300000, guid: e3dc6a0fbf543b64388f7b60615f4145, type: 3}
volume: 0.55
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
@ -160,78 +100,6 @@ MonoBehaviour:
volume: 0.65
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 33
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 0dd7ddaa5ac08f94ab2a8127bdadfd89, type: 3}
volume: 0.7
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 34
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 12bcd9f3a3e360441922c1b1d580bfd4, type: 3}
volume: 0.75
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 40
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: a64959ada619fd148b353ac9ad0f276d, type: 3}
volume: 0.55
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 41
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: fc76f32ac20220e4fa9947fdda16e80a, type: 3}
volume: 0.55
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 42
channel: 1
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 7933b6e19ab8feb4ea4e905ee9e52c57, type: 3}
volume: 0.55
pitchRange: {x: 0.96, y: 1.04}
spatial: 0
- eventId: 50
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: e3dc6a0fbf543b64388f7b60615f4145, type: 3}
volume: 0.4
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 51
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 46c2c848c1ca5ee4ca5d65e2c53ac8cc, type: 3}
volume: 0.25
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 52
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: 83e35ed99b265944ca0af63349a93b39, type: 3}
volume: 0.65
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 53
channel: 2
outputGroupOverride: {fileID: 0}
clips:
- {fileID: 8300000, guid: e3dc6a0fbf543b64388f7b60615f4145, type: 3}
volume: 0.65
pitchRange: {x: 0.96, y: 1.04}
spatial: 1
- eventId: 54
channel: 2
outputGroupOverride: {fileID: 0}

View File

@ -486,7 +486,7 @@
"isPartOfComposite": false
},
{
"name": "up",
"name": "Left",
"id": "a198c978-a3d0-4a74-b983-bb738806642f",
"path": "<ComController>/button00",
"interactions": "",
@ -497,7 +497,7 @@
"isPartOfComposite": true
},
{
"name": "down",
"name": "Right",
"id": "a9d0403f-4513-4603-9b49-43958229d392",
"path": "<ComController>/button01",
"interactions": "",
@ -508,7 +508,7 @@
"isPartOfComposite": true
},
{
"name": "left",
"name": "Down",
"id": "250db8f2-7c51-4194-afbb-1de8c14f4111",
"path": "<ComController>/button02",
"interactions": "",
@ -519,7 +519,7 @@
"isPartOfComposite": true
},
{
"name": "right",
"name": "Up",
"id": "686eb2f1-953a-4125-8969-902c3d103faf",
"path": "<ComController>/button03",
"interactions": "",
@ -1130,7 +1130,7 @@
"isPartOfComposite": false
},
{
"name": "up",
"name": "Left",
"id": "620356a3-c4ff-4864-9250-2c7ed7b72b04",
"path": "<ComController>/button00",
"interactions": "",
@ -1141,7 +1141,7 @@
"isPartOfComposite": true
},
{
"name": "down",
"name": "Right",
"id": "f17dd696-874e-48ff-aa99-eb578bd77adc",
"path": "<ComController>/button01",
"interactions": "",
@ -1152,7 +1152,7 @@
"isPartOfComposite": true
},
{
"name": "left",
"name": "Down",
"id": "01e3f407-30b9-4b80-86cb-9ec5fae7aff6",
"path": "<ComController>/button02",
"interactions": "",
@ -1163,7 +1163,7 @@
"isPartOfComposite": true
},
{
"name": "right",
"name": "Up",
"id": "7dd97762-8ade-4ff7-a177-398d6bf65dd9",
"path": "<ComController>/button03",
"interactions": "",
@ -1282,4 +1282,4 @@
]
}
]
}
}

View File

@ -82,6 +82,7 @@ MonoBehaviour:
fallbackDatabase: {fileID: 0}
audioSettings: {fileID: 11400000, guid: b8788b5bf4f92e94f83f651bb5a0a6ee, type: 2}
fileName: service-settings.json
prizeSessionStateFileName: prize-session-state.json
loadImmediately: 1
bindActionsInScene: 1
--- !u!114 &4222904644466244002
@ -98,7 +99,7 @@ MonoBehaviour:
m_EditorClassIdentifier: DanroJump.Runtime::DanroJump.Debugging.RuntimeDebugInstaller
enabledOnStartup: 1
enabledChannels: -1
enabledOutputs: 3
enabledOutputs: -1
fileLoggingEnabled: 0
fileLoggingUsePersistentDataPath: 1
fileLoggingDirectory: Logs

View File

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

View File

@ -0,0 +1,90 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: ParticleDefaultMaterial
m_Shader: {fileID: 4800000, guid: 0406db5a14f94604a8c57ccfbc9f3b46, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords:
- _FLIPBOOKBLENDING_OFF
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendOp: 0
- _CameraFadingEnabled: 0
- _CameraFarFadeDistance: 2
- _CameraNearFadeDistance: 1
- _ColorMode: 0
- _Cull: 2
- _Cutoff: 0.5
- _DistortionBlend: 0.5
- _DistortionEnabled: 0
- _DistortionStrength: 1
- _DistortionStrengthScaled: 0.1
- _DstBlend: 0
- _DstBlendAlpha: 0
- _FlipbookBlending: 0
- _FlipbookMode: 0
- _Mode: 0
- _QueueOffset: 0
- _SoftParticlesEnabled: 0
- _SoftParticlesFarFadeDistance: 1
- _SoftParticlesNearFadeDistance: 0
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _BaseColorAddSubDiff: {r: 0, g: 0, b: 0, a: 0}
- _CameraFadeParams: {r: 0, g: 0, b: 0, a: 0}
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &1502222179824363022
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10

View File

@ -1,8 +1,8 @@
fileFormatVersion: 2
guid: bf7ca407d43a63a4aa7bbee33da26d0f
guid: e261acd8183bb7e489ebd80d5cc9f801
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

8
Assets/SFX/Ambient.meta Normal file
View File

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

8
Assets/SFX/Enemies.meta Normal file
View File

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

BIN
Assets/SFX/Enemies/Crab_Click.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: aeba847740e70ba46b7d3df9513316e1
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Enemies/Enemy_Hit_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 37f7526ec58db8843916100518133785
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Enemies/Knife_Throw_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: f5084f4cf2fabc74fae40a5a9ebc2718
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Enemies/Knife_Throw_2.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: eb475b242c9b6ec448dfec33ec8da9f2
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Enemies/Laser_Fire_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: a0166fbc7106f42498be14d005a200f4
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

8
Assets/SFX/Movement.meta Normal file
View File

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

BIN
Assets/SFX/Movement/Grass_Run_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 144cba17f4a47274d834d5e1fc28acb9
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Movement/Grass_Run_2.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 5f68c3b1c31c833458e189d0b7d622dd
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Movement/Metal_Run_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 44081d4997ebc78418c03a0fa021336d
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Movement/Metal_Run_2.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 64a82f6dc3cfec746bcc23ded9a926ee
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Movement/Metal_Run_3.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 6aedae21e708ea448b1ed0468415c094
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

8
Assets/SFX/Music.meta Normal file
View File

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

BIN
Assets/SFX/Music/music_Level_1.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 3a30c29718ace1c468575da20ba204f1
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Music/music_Level_2.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: ff6aa1560276fae488e89d642928a57a
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Music/music_Level_3.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: 66f130eb74c4f9249ae760f50057506f
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Music/music_MainMenu.mp3 (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: b0296787b59f9f248b8361070a74a2b5
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

8
Assets/SFX/Player.meta Normal file
View File

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

BIN
Assets/SFX/Player/Hurt_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: f59cd1f6b437f7d4f8fa63bad1e466a5
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Player/Hurt_2.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: ce8d9dfcc94fcae46a52bd90ac0b0a19
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Player/Jump_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: fa0b6b9872cd9b84bb24e35b24e57fdf
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Player/Throw_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: a9399f94f3fa20f44b8d76c30deda71c
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

8
Assets/SFX/Props.meta Normal file
View File

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

BIN
Assets/SFX/Props/Checkpoint.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: cfbbacd3ef3f01743aa42aea3cb29270
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Props/Laser_1.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: caba1143532ae5642aa33fa950eb6269
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 1
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/SFX/Props/Pickup_Coin.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,23 @@
fileFormatVersion: 2
guid: d151d983bbf55f7448d1be4e1a2535d0
AudioImporter:
externalObjects: {}
serializedVersion: 8
defaultSettings:
serializedVersion: 2
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
preloadAudioData: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:

8
Assets/SFX/UI.meta Normal file
View File

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

View File

@ -491,6 +491,143 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1743332988
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1743332992}
- component: {fileID: 1743332991}
- component: {fileID: 1743332990}
- component: {fileID: 1743332989}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1743332989
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1743332988}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_VolumeFrameworkUpdateModeOption: 2
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_AllowHDROutput: 1
m_UseScreenCoordOverride: 0
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_TaaSettings:
m_Quality: 3
m_FrameInfluence: 0.1
m_JitterScale: 1
m_MipBias: 0
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
m_Version: 2
--- !u!81 &1743332990
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1743332988}
m_Enabled: 1
--- !u!20 &1743332991
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1743332988}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!4 &1743332992
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1743332988}
serializedVersion: 2
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: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1745121143
GameObject:
m_ObjectHideFlags: 0
@ -647,3 +784,4 @@ SceneRoots:
- {fileID: 1337482712}
- {fileID: 1935555105}
- {fileID: 1503684055}
- {fileID: 1743332992}

View File

@ -1,26 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d6b6fabd3acfe76448ca16ffafdbe9f5, type: 3}
m_Name: MainAudioSettings
m_EditorClassIdentifier: DanroJump.Runtime::DanroJump.Audio.GameAudioSettings
mixer: {fileID: 0}
musicGroup: {fileID: 0}
sfxGroup: {fileID: 0}
uiGroup: {fileID: 0}
gameplayGroup: {fileID: 0}
ambienceGroup: {fileID: 0}
musicLoop: {fileID: 0}
musicLoopVolume: 1
playMusicOnInitialize: 1
defaultMusicCue: 0
musicTracks: []
eventBindings: []

View File

@ -0,0 +1,244 @@
using System.IO;
using UnityEditor;
using UnityEngine;
namespace DanroJump.Editor
{
public static class VfxGeneratorEditor
{
private const string VfxFolderPath = "Assets/Resources/Vfx";
private const string MaterialPath = VfxFolderPath + "/ParticleDefaultMaterial.mat";
private static readonly string[] PrefabNames = {
"VFX_Jump",
"VFX_CoinCollected",
"VFX_PlayerDeath",
"VFX_PrizeWin",
"VFX_ComError"
};
[InitializeOnLoadMethod]
private static void InitializeOnLoad()
{
// Запускаем генерацию только если хотя бы одного файла не хватает
bool missing = false;
foreach (var name in PrefabNames)
{
if (!File.Exists($"{VfxFolderPath}/{name}.prefab"))
{
missing = true;
break;
}
}
if (missing)
{
GenerateAssets();
}
}
[MenuItem("Tools/Generate VFX Assets")]
public static void GenerateAssets()
{
if (!Directory.Exists(VfxFolderPath))
{
Directory.CreateDirectory(VfxFolderPath);
AssetDatabase.Refresh();
}
var material = GetOrCreateParticleMaterial();
if (material == null)
{
Debug.LogError("[VfxGenerator] Failed to find or create a compatible particle material.");
return;
}
foreach (var name in PrefabNames)
{
var go = new GameObject(name);
var ps = go.AddComponent<ParticleSystem>();
// Настраиваем рендерер и материал
var psr = go.GetComponent<ParticleSystemRenderer>();
if (psr != null)
{
psr.sharedMaterial = material;
}
// Общая базовая настройка
var main = ps.main;
main.loop = false;
main.playOnAwake = false;
var emission = ps.emission;
emission.rateOverTime = 0;
var colorOverLifetime = ps.colorOverLifetime;
colorOverLifetime.enabled = true;
// Специфические настройки для каждого эффекта
switch (name)
{
case "VFX_Jump":
main.startColor = new Color(0.9f, 0.9f, 0.9f, 0.5f);
main.startSize = new ParticleSystem.MinMaxCurve(0.08f, 0.22f);
main.startSpeed = new ParticleSystem.MinMaxCurve(1.5f, 3.5f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.2f, 0.45f);
main.duration = 0.25f;
main.gravityModifier = 0.6f;
var shapeCone = ps.shape;
shapeCone.shapeType = ParticleSystemShapeType.Cone;
shapeCone.angle = 35f;
shapeCone.radius = 0.15f;
shapeCone.rotation = new Vector3(90f, 0f, 0f);
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 12) });
var gradientJump = new Gradient();
gradientJump.SetKeys(
new GradientColorKey[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradientJump;
break;
case "VFX_CoinCollected":
main.startColor = new Color(1.0f, 0.85f, 0.0f, 0.9f);
main.startSize = new ParticleSystem.MinMaxCurve(0.1f, 0.25f);
main.startSpeed = new ParticleSystem.MinMaxCurve(2f, 4f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.3f, 0.6f);
main.duration = 0.3f;
main.gravityModifier = -0.2f;
var shapeSphereCoin = ps.shape;
shapeSphereCoin.shapeType = ParticleSystemShapeType.Sphere;
shapeSphereCoin.radius = 0.1f;
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 18) });
var gradientCoin = new Gradient();
gradientCoin.SetKeys(
new GradientColorKey[] { new GradientColorKey(new Color(1f, 0.9f, 0.2f), 0f), new GradientColorKey(new Color(1f, 0.5f, 0f), 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradientCoin;
break;
case "VFX_PlayerDeath":
main.startColor = new Color(0.8f, 0.1f, 0.1f, 0.8f);
main.startSize = new ParticleSystem.MinMaxCurve(0.15f, 0.35f);
main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 7f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.4f, 0.8f);
main.duration = 0.5f;
main.gravityModifier = 0.8f;
var shapeSphereDeath = ps.shape;
shapeSphereDeath.shapeType = ParticleSystemShapeType.Sphere;
shapeSphereDeath.radius = 0.25f;
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 35) });
var gradientDeath = new Gradient();
gradientDeath.SetKeys(
new GradientColorKey[] { new GradientColorKey(new Color(0.9f, 0.2f, 0.2f), 0f), new GradientColorKey(new Color(0.3f, 0.3f, 0.3f), 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradientDeath;
break;
case "VFX_PrizeWin":
main.startColor = new ParticleSystem.MinMaxGradient(
new Color(0.2f, 0.6f, 1.0f),
new Color(1.0f, 0.2f, 0.6f)
);
main.startSize = new ParticleSystem.MinMaxCurve(0.12f, 0.28f);
main.startSpeed = new ParticleSystem.MinMaxCurve(4f, 8f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.6f, 1.2f);
main.duration = 0.8f;
main.gravityModifier = 0.4f;
var shapeHemiPrize = ps.shape;
shapeHemiPrize.shapeType = ParticleSystemShapeType.Hemisphere;
shapeHemiPrize.radius = 0.3f;
shapeHemiPrize.rotation = new Vector3(-90f, 0f, 0f);
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 50) });
var gradientPrize = new Gradient();
gradientPrize.SetKeys(
new GradientColorKey[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradientPrize;
break;
case "VFX_ComError":
main.startColor = new Color(1.0f, 0.3f, 0.0f, 0.9f);
main.startSize = new ParticleSystem.MinMaxCurve(0.06f, 0.18f);
main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 6f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.25f, 0.5f);
main.duration = 0.4f;
main.gravityModifier = 0.3f;
var shapeConeError = ps.shape;
shapeConeError.shapeType = ParticleSystemShapeType.Cone;
shapeConeError.angle = 60f;
shapeConeError.radius = 0.1f;
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 20) });
var gradientError = new Gradient();
gradientError.SetKeys(
new GradientColorKey[] { new GradientColorKey(new Color(1f, 0.5f, 0f), 0f), new GradientColorKey(new Color(0.8f, 0.1f, 0f), 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradientError;
break;
}
// Сохраняем как префаб
var localPath = $"{VfxFolderPath}/{name}.prefab";
PrefabUtility.SaveAsPrefabAsset(go, localPath);
Object.DestroyImmediate(go);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[VfxGenerator] All particle prefabs generated successfully under Assets/Resources/Vfx!");
}
private static Material GetOrCreateParticleMaterial()
{
var existingMat = AssetDatabase.LoadAssetAtPath<Material>(MaterialPath);
if (existingMat != null)
{
return existingMat;
}
var shader = Shader.Find("Universal Render Pipeline/Particles/2D Unlit");
if (shader == null)
{
shader = Shader.Find("Universal Render Pipeline/Particles/Unlit");
}
if (shader == null)
{
shader = Shader.Find("Sprites/Default");
}
var material = new Material(shader);
var tex = Resources.GetBuiltinResource<Texture2D>("Default-Particle.tif");
if (tex != null)
{
material.mainTexture = tex;
if (material.HasProperty("_BaseMap"))
{
material.SetTexture("_BaseMap", tex);
}
}
AssetDatabase.CreateAsset(material, MaterialPath);
return material;
}
}
}

View File

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

View File

@ -130,13 +130,20 @@ namespace DanroJump.Audio
{
if (settings?.Mixer == null)
{
UnityEngine.Debug.LogWarning("[GameAudioService] Cannot apply volumes: settings or Mixer is null.");
return;
}
var musicVolume = serviceSettings != null ? serviceSettings.GetFloat(ServiceSettingsKeys.MusicVolume) : 1f;
var soundsVolume = serviceSettings != null ? serviceSettings.GetFloat(ServiceSettingsKeys.SoundsVolume) : 1f;
settings.Mixer.SetFloat(MusicVolumeParameter, LinearToDecibels(musicVolume));
settings.Mixer.SetFloat(SoundsVolumeParameter, LinearToDecibels(soundsVolume));
float musicDb = LinearToDecibels(musicVolume);
float soundsDb = LinearToDecibels(soundsVolume);
bool musicOk = settings.Mixer.SetFloat(MusicVolumeParameter, musicDb);
bool soundsOk = settings.Mixer.SetFloat(SoundsVolumeParameter, soundsDb);
UnityEngine.Debug.Log($"[GameAudioService] Applied volumes: Music = {musicVolume * 100:0}% ({musicDb:0.##} dB) [status: {musicOk}], Sounds = {soundsVolume * 100:0}% ({soundsDb:0.##} dB) [status: {soundsOk}]");
}
public static float LinearToDecibels(float value)

View File

@ -292,7 +292,7 @@ namespace DanroJump.Gameplay
: 0;
}
private bool IsContinueEnabled()
public bool IsContinueEnabled()
{
return serviceSettings != null && serviceSettings.GetBool(ServiceSettingsKeys.CanContinueGame);
}

View File

@ -1948,6 +1948,9 @@ namespace DanroJump.Hardware.Com
Send("2130", true);
}
// Даем фоновому потоку чтения время выйти из ReadLine по таймауту (который равен 50мс)
System.Threading.Thread.Sleep(80);
ClosePort();
localCts?.Dispose();
localCts = null;
@ -2022,9 +2025,8 @@ namespace DanroJump.Hardware.Com
}
catch (Exception exception) when (exception is IOException || exception is InvalidOperationException)
{
LogWarning($"{PortName} disconnected: {exception.Message}");
NotifyDisconnected();
break;
LogWarning($"{PortName} connection lost: {exception.Message}. Reconnecting...");
await ReconnectAsync(token);
}
catch (Exception exception)
{
@ -2313,21 +2315,44 @@ namespace DanroJump.Hardware.Com
LastInputBits = DefaultInputBits;
ClosePort();
await UniTask.Delay(500, DelayType.Realtime, PlayerLoopTiming.Update, token);
await UniTask.SwitchToThreadPool();
if (!OpenPort())
int retryCount = 0;
const int maxRetries = 10;
while (!token.IsCancellationRequested && retryCount < maxRetries)
{
NotifyDisconnected();
return;
await UniTask.Delay(1000, DelayType.Realtime, PlayerLoopTiming.Update, token);
if (token.IsCancellationRequested)
{
return;
}
await UniTask.SwitchToThreadPool();
if (token.IsCancellationRequested)
{
return;
}
if (OpenPort())
{
LogWarning($"{PortName} reconnected successfully after {retryCount + 1} retries.");
if (useEncryptedHandshake)
{
await RunHandshakeAsync(token);
}
else
{
MarkOperational();
}
return;
}
retryCount++;
LogWarning($"{PortName} reconnect attempt {retryCount} failed. Retrying...");
}
if (useEncryptedHandshake)
if (!token.IsCancellationRequested)
{
await RunHandshakeAsync(token);
}
else
{
MarkOperational();
NotifyDisconnected();
}
}

View File

@ -0,0 +1,37 @@
using System;
namespace DanroJump.Prizes
{
/// <summary>
/// Запись о сыгранной игровой сессии для эксплуатационной истории.
/// </summary>
[Serializable]
public sealed class GameplaySessionRecord
{
public GameplaySessionRecord(
string sessionId,
int score,
int coins,
float maxHeight,
int deathCount,
PrizeRewardKind rewardKind,
DateTime timestamp)
{
SessionId = sessionId ?? string.Empty;
Score = score;
Coins = coins;
MaxHeight = maxHeight;
DeathCount = deathCount;
RewardKind = rewardKind;
Timestamp = timestamp;
}
public string SessionId { get; }
public int Score { get; }
public int Coins { get; }
public float MaxHeight { get; }
public int DeathCount { get; }
public PrizeRewardKind RewardKind { get; }
public DateTime Timestamp { get; }
}
}

View File

@ -12,6 +12,7 @@ namespace DanroJump.Prizes
PrizeSessionRecord ActiveSession { get; }
IReadOnlyList<PrizeSessionRecord> History { get; }
IReadOnlyList<GameplaySessionRecord> GameplayHistory { get; }
int StartedCount { get; }
int CompletedCount { get; }
int FailedCount { get; }
@ -29,7 +30,14 @@ namespace DanroJump.Prizes
bool MarkActiveSessionShown();
bool CompleteActiveSession(string note = null);
bool FailActiveSession(string reason);
void RecordGameplaySession(int score, int coins);
void RecordGameplaySession(
string sessionId,
int score,
int coins,
float maxHeight,
int deathCount,
PrizeRewardKind rewardKind
);
void RecordQrShow();
void RecordComError();
void RecordPhysicalDispense(bool success);

View File

@ -65,6 +65,27 @@ namespace DanroJump.Prizes
}
}
var gameplayHistory = new List<GameplaySessionRecord>();
if (data.gameplayHistory != null)
{
foreach (var entry in data.gameplayHistory)
{
if (entry == null)
{
continue;
}
gameplayHistory.Add(new GameplaySessionRecord(
entry.sessionId,
entry.score,
entry.coins,
entry.maxHeight,
entry.deathCount,
entry.rewardKind,
FromUtcTicks(entry.timestampTicks)));
}
}
snapshot = new PrizeSessionStateSnapshot(
history,
data.issuedSessionIds ?? new List<string>(),
@ -74,6 +95,7 @@ namespace DanroJump.Prizes
data.failedCount,
data.duplicateBlockedCount,
data.fallbackSessionCounter,
gameplayHistory,
data.totalGamesPlayed,
data.totalScore,
data.totalCoins,
@ -153,6 +175,28 @@ namespace DanroJump.Prizes
});
}
if (snapshot.GameplayHistory != null)
{
foreach (var record in snapshot.GameplayHistory)
{
if (record == null)
{
continue;
}
data.gameplayHistory.Add(new PrizeSessionStateSaveData.GameplayEntry
{
sessionId = record.SessionId,
score = record.Score,
coins = record.Coins,
maxHeight = record.MaxHeight,
deathCount = record.DeathCount,
rewardKind = record.RewardKind,
timestampTicks = record.Timestamp.Ticks
});
}
}
WriteAllTextSafely(Path, JsonUtility.ToJson(data, true));
return true;
}
@ -248,6 +292,7 @@ namespace DanroJump.Prizes
public int comErrors;
public List<string> issuedSessionIds = new();
public List<Entry> history = new();
public List<GameplayEntry> gameplayHistory = new();
[Serializable]
public sealed class Entry
@ -263,6 +308,18 @@ namespace DanroJump.Prizes
public long updatedAtUtcTicks;
public string note = string.Empty;
}
[Serializable]
public sealed class GameplayEntry
{
public string sessionId = string.Empty;
public int score;
public int coins;
public float maxHeight;
public int deathCount;
public PrizeRewardKind rewardKind;
public long timestampTicks;
}
}
}
}

View File

@ -39,12 +39,20 @@ namespace DanroJump.Prizes
public PrizeFlowResult StartPrizeFlow(GameplaySession session)
{
LastResult = EvaluateCompletedSession(session);
if (session != null && sessionState != null)
{
sessionState.RecordGameplaySession(session.Score, session.Coins);
sessionState.RecordGameplaySession(
session.SessionId,
session.Score,
session.Coins,
session.MaxRunHeight,
session.DeathCount,
LastResult.RewardKind
);
}
LastResult = EvaluateCompletedSession(session);
if (LastResult.HasPrize && sessionState != null &&
!sessionState.TryBeginSession(LastResult, out _))
{

View File

@ -11,8 +11,10 @@ namespace DanroJump.Prizes
public sealed class PrizeSessionStateService : IPrizeSessionStateService
{
private const string RecoveryFailureNote = "Сессия восстановлена после перезапуска до завершения выдачи.";
private const int MaxHistorySize = 50;
private readonly List<PrizeSessionRecord> history = new();
private readonly List<GameplaySessionRecord> gameplayHistory = new();
private readonly HashSet<string> issuedSessionIds = new(StringComparer.Ordinal);
private readonly IPrizeSessionStateRepository repository;
private int fallbackSessionCounter;
@ -38,6 +40,7 @@ namespace DanroJump.Prizes
public PrizeSessionRecord ActiveSession { get; private set; }
public IReadOnlyList<PrizeSessionRecord> History => history;
public IReadOnlyList<GameplaySessionRecord> GameplayHistory => gameplayHistory;
public int StartedCount { get; private set; }
public int CompletedCount { get; private set; }
public int FailedCount { get; private set; }
@ -128,8 +131,30 @@ namespace DanroJump.Prizes
return $"Игр: {TotalGamesPlayed}. Очков всего: {TotalScore}. Монет всего: {TotalCoins}. QR показов: {QrShows}. Физич. выдач: {PhysicalDispenses} (ошибок: {PhysicalDispenseFailures}). COM-ошибок: {ComErrors}. Призовые сессии: начато {StartedCount}, завершено {CompletedCount}, ошибок {FailedCount}, повторов заблокировано {DuplicateBlockedCount}. {activeText} {lastText}";
}
public void RecordGameplaySession(int score, int coins)
public void RecordGameplaySession(
string sessionId,
int score,
int coins,
float maxHeight,
int deathCount,
PrizeRewardKind rewardKind)
{
var record = new GameplaySessionRecord(
sessionId,
score,
coins,
maxHeight,
deathCount,
rewardKind,
DateTime.UtcNow
);
gameplayHistory.Add(record);
if (gameplayHistory.Count > MaxHistorySize)
{
gameplayHistory.RemoveAt(0);
}
TotalGamesPlayed++;
TotalScore += score;
TotalCoins += coins;
@ -200,8 +225,20 @@ namespace DanroJump.Prizes
}
history.Clear();
gameplayHistory.Clear();
issuedSessionIds.Clear();
if (snapshot.GameplayHistory != null)
{
foreach (var record in snapshot.GameplayHistory)
{
if (record != null)
{
gameplayHistory.Add(record);
}
}
}
foreach (var record in snapshot.History)
{
if (record != null)
@ -261,6 +298,7 @@ namespace DanroJump.Prizes
FailedCount,
DuplicateBlockedCount,
fallbackSessionCounter,
gameplayHistory.ToArray(),
TotalGamesPlayed,
TotalScore,
TotalCoins,

View File

@ -18,6 +18,7 @@ namespace DanroJump.Prizes
int failedCount,
int duplicateBlockedCount,
int fallbackSessionCounter,
IReadOnlyList<GameplaySessionRecord> gameplayHistory = null,
int totalGamesPlayed = 0,
int totalScore = 0,
int totalCoins = 0,
@ -34,6 +35,7 @@ namespace DanroJump.Prizes
FailedCount = Math.Max(0, failedCount);
DuplicateBlockedCount = Math.Max(0, duplicateBlockedCount);
FallbackSessionCounter = Math.Max(0, fallbackSessionCounter);
GameplayHistory = gameplayHistory?.Where(static record => record != null).ToArray() ?? Array.Empty<GameplaySessionRecord>();
TotalGamesPlayed = Math.Max(0, totalGamesPlayed);
TotalScore = Math.Max(0, totalScore);
TotalCoins = Math.Max(0, totalCoins);
@ -44,6 +46,7 @@ namespace DanroJump.Prizes
}
public IReadOnlyList<PrizeSessionRecord> History { get; }
public IReadOnlyList<GameplaySessionRecord> GameplayHistory { get; }
public IReadOnlyCollection<string> IssuedSessionIds { get; }
public string ActiveSessionId { get; }
public int StartedCount { get; }

View File

@ -5,9 +5,9 @@ using DanroJump.SceneFlow;
namespace DanroJump.Settings
{
/// <summary>
/// MonoBehaviour-фасад для вызова действий сервиса настроек из UI/Unity Events.
/// MonoBehaviour-фасад для вызова действий приложения из UI/Unity Events.
/// </summary>
public sealed class ServiceSettingsActions : MonoBehaviour
public sealed class AppUIFacade : MonoBehaviour
{
private IServiceSettingsService settings;
private ISceneFlowService sceneFlow;
@ -105,7 +105,7 @@ namespace DanroJump.Settings
{
if (sceneFlow == null)
{
Debug.LogWarning($"{nameof(ServiceSettingsActions)} requires {nameof(ISceneFlowService)} from DI.", this);
Debug.LogWarning($"{nameof(AppUIFacade)} requires {nameof(ISceneFlowService)} from DI.", this);
}
return sceneFlow;

View File

@ -1,4 +1,6 @@
using System;
using DanroJump.Prizes;
using DanroJump.UI.MainMenu;
namespace DanroJump.Settings
{
@ -36,6 +38,10 @@ namespace DanroJump.Settings
"Номер поддержки",
"Открыто служебное окно номера поддержки автомата.",
ServiceSettingsKeys.SupportNumber),
ServiceSettingsKeys.SettingsPinButton => OpenSettingEditor(
"PIN-код",
"Открыто служебное окно изменения PIN-кода.",
ServiceSettingsKeys.SettingsPin),
ServiceSettingsKeys.CurrencyButton => OpenSettingEditor(
"Валюта баланса",
"Открыто служебное окно валюты денежного баланса.",
@ -46,11 +52,11 @@ namespace DanroJump.Settings
"Проверка hopper/выдачи"),
ServiceSettingsKeys.LevelsPrizesButton => ServiceSettingsActionResult.Success(
"Настройки призов",
ServiceSettingsPrizeSummaryBuilder.Build(settings),
PrizeInfoFormatter.Build(settings),
ServiceSettingsKeys.PrizeLevels),
ServiceSettingsKeys.QrSettingsButton => ServiceSettingsActionResult.Success(
"Настройки QR",
ServiceSettingsQrSummaryBuilder.Build(settings),
QrInfoFormatter.Build(settings),
ServiceSettingsKeys.QrPrizeTextSettings),
ServiceSettingsKeys.StandSettingsButton => ServiceSettingsActionResult.Success(
"Настройки витрины",

View File

@ -29,15 +29,33 @@ namespace DanroJump.Settings
$"- [{record.StartedAtUtc:yyyy-MM-dd HH:mm:ss}] {FormatReward(record.Result.RewardKind)} ({record.Result.Score}очк) -> {FormatStatus(record.Status)}{(string.IsNullOrWhiteSpace(record.Note) ? "" : ": " + record.Note)}"))
: "История призов пуста.";
var gameplayHistory = prizeSessionState.GameplayHistory;
var avgScore = gameplayHistory != null && gameplayHistory.Count > 0
? gameplayHistory.Average(r => r.Score)
: 0f;
var avgCoins = gameplayHistory != null && gameplayHistory.Count > 0
? gameplayHistory.Average(r => r.Coins)
: 0f;
var avgHeight = gameplayHistory != null && gameplayHistory.Count > 0
? gameplayHistory.Average(r => r.MaxHeight)
: 0f;
var recentGameplay = gameplayHistory != null && gameplayHistory.Count > 0
? string.Join("\n", gameplayHistory.Skip(Math.Max(0, gameplayHistory.Count - 5)).Select(record =>
$"- [{record.Timestamp:yyyy-MM-dd HH:mm:ss}] очков: {record.Score}, монет: {record.Coins}, высота: {record.MaxHeight:F1}м, смертей: {record.DeathCount}, приз: {FormatReward(record.RewardKind)}"))
: "История игр пуста.";
prizeSummary = $"\n\n--- Эксплуатационная статистика ---\n" +
$"Всего сыграно игр: {prizeSessionState.TotalGamesPlayed}\n" +
$"Набрано очков всего: {prizeSessionState.TotalScore}\n" +
$"Собрано монет всего: {prizeSessionState.TotalCoins}\n" +
$"Средние показатели (последние сессии): очков {avgScore:F1}, монет {avgCoins:F1}, высота {avgHeight:F1}м\n" +
$"Показов QR-кодов: {prizeSessionState.QrShows}\n" +
$"Успешных физ. выдач: {prizeSessionState.PhysicalDispenses}\n" +
$"Сбоев физ. выдач: {prizeSessionState.PhysicalDispenseFailures}\n" +
$"Сбоев связи по COM: {prizeSessionState.ComErrors}\n" +
$"Призовые сессии: начато {prizeSessionState.StartedCount}, завершено {prizeSessionState.CompletedCount}, сбоев {prizeSessionState.FailedCount}, повторов заблокировано {prizeSessionState.DuplicateBlockedCount}\n" +
$"\n--- Последние Игры (до 5) ---\n{recentGameplay}\n" +
$"\n--- Последние Призовые сессии (до 5) ---\n{recentHistory}";
}
else

View File

@ -8,8 +8,9 @@ namespace DanroJump.Settings
{
/// <summary>
/// ProjectContext/SceneContext installer сервисных настроек.
/// ProjectContext/SceneContext installer глобальных сервисов.
/// </summary>
public sealed class ServiceSettingsInstaller : MonoInstaller
public sealed class AppServicesInstaller : MonoInstaller
{
[SerializeField] private string databaseAddress = ServiceSettingsAddressKeys.Database;
[SerializeField] private ServiceSettingsDatabase fallbackDatabase;
@ -29,7 +30,7 @@ namespace DanroJump.Settings
if (database == null)
{
Debug.LogError($"{nameof(ServiceSettingsInstaller)} could not load {nameof(ServiceSettingsDatabase)}.", this);
Debug.LogError($"{nameof(AppServicesInstaller)} could not load {nameof(ServiceSettingsDatabase)}.", this);
databaseProvider.Dispose();
Container.BindInterfacesAndSelfTo<SceneFlowService>().AsSingle();
return;
@ -59,7 +60,7 @@ namespace DanroJump.Settings
}
Container.BindInterfacesAndSelfTo<GameAudioService>().AsSingle().NonLazy();
Container.BindInterfacesTo<ServiceSettingsSceneInjector>().AsSingle().WithArguments(bindActionsInScene).NonLazy();
Container.BindInterfacesTo<AppUIFacadeInjector>().AsSingle().WithArguments(bindActionsInScene).NonLazy();
}
}
}

View File

@ -7,17 +7,17 @@ using Zenject;
namespace DanroJump.Settings
{
/// <summary>
/// Инжектит сценовые ServiceSettingsActions из ProjectContext без прямого глобального поиска объектов.
/// Инжектит сценовые AppUIFacade из ProjectContext без прямого глобального поиска объектов.
/// </summary>
public sealed class ServiceSettingsSceneInjector : IInitializable, IDisposable
public sealed class AppUIFacadeInjector : IInitializable, IDisposable
{
private readonly DiContainer container;
private readonly bool injectActionsInScene;
private readonly List<GameObject> rootObjects = new();
private readonly List<ServiceSettingsActions> actions = new();
private readonly List<AppUIFacade> actions = new();
private readonly HashSet<EntityId> injectedEntityIds = new();
public ServiceSettingsSceneInjector(DiContainer container, bool injectActionsInScene)
public AppUIFacadeInjector(DiContainer container, bool injectActionsInScene)
{
this.container = container;
this.injectActionsInScene = injectActionsInScene;

View File

@ -36,8 +36,12 @@ namespace DanroJump.Settings
public const string CanContinueGame = "gameplay.canContinueGame";
public const string InactivityTimeout = "gameplay.inactivityTimeout";
public const string GameDifficulty = "gameplay.gameDifficulty";
public const string RequireSettingsPin = "security.requirePin";
public const string SettingsPin = "security.pin";
public const string StatisticsButton = "btn.statistics";
public const string SettingsPinButton = "btn.settingsPin";
public const string PhoneNumberButton = "btn.phoneNumber";
public const string BigPrizeButton = "btn.bigPrize";
public const string SupportNumberButton = "btn.supportNumber";

View File

@ -1,6 +1,7 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
using DanroJump.Audio;
namespace DanroJump.UI.Gameplay
{
@ -16,15 +17,13 @@ namespace DanroJump.UI.Gameplay
private readonly Label detailsLabel;
private readonly Button continueButton;
private readonly Button menuButton;
private readonly IGameAudioService audioService;
private float inputEnabledAtTime = float.NegativeInfinity;
public ContinueGameOverlayWindow(VisualElement root)
public ContinueGameOverlayWindow(VisualElement root, IGameAudioService audioService = null)
{
this.root = root;
if (this.root != null)
{
this.root.focusable = true;
}
this.audioService = audioService;
titleLabel = root?.Q<Label>("ContinueGameTitle");
detailsLabel = root?.Q<Label>("ContinueGameDetails");
@ -34,11 +33,15 @@ namespace DanroJump.UI.Gameplay
if (continueButton != null)
{
continueButton.clicked += HandleContinueClicked;
continueButton.RegisterCallback<FocusInEvent>(HandleButtonFocusIn);
continueButton.RegisterCallback<FocusOutEvent>(HandleButtonFocusOut);
}
if (menuButton != null)
{
menuButton.clicked += HandleMenuClicked;
menuButton.RegisterCallback<FocusInEvent>(HandleButtonFocusIn);
menuButton.RegisterCallback<FocusOutEvent>(HandleButtonFocusOut);
}
}
@ -78,7 +81,20 @@ namespace DanroJump.UI.Gameplay
root.SetEnabled(true);
IsOpen = true;
inputEnabledAtTime = Time.unscaledTime + OpenInputGuardSeconds;
root.Focus();
FocusDefaultButton();
}
private void FocusDefaultButton()
{
if (continueButton != null && continueButton.enabledSelf)
{
continueButton.Focus();
}
else if (menuButton != null)
{
menuButton.Focus();
}
}
public void Close()
@ -98,11 +114,35 @@ namespace DanroJump.UI.Gameplay
if (continueButton != null)
{
continueButton.clicked -= HandleContinueClicked;
continueButton.UnregisterCallback<FocusInEvent>(HandleButtonFocusIn);
continueButton.UnregisterCallback<FocusOutEvent>(HandleButtonFocusOut);
}
if (menuButton != null)
{
menuButton.clicked -= HandleMenuClicked;
menuButton.UnregisterCallback<FocusInEvent>(HandleButtonFocusIn);
menuButton.UnregisterCallback<FocusOutEvent>(HandleButtonFocusOut);
}
}
private static readonly Color FocusedButtonTint = new Color32(0xCE, 0xFF, 0x1A, 0xFF);
private static readonly Color NormalButtonTint = Color.white;
private void HandleButtonFocusIn(FocusInEvent evt)
{
if (evt.currentTarget is Button button)
{
audioService?.Play(GameAudioEvent.UiNavigate);
button.style.unityBackgroundImageTintColor = new StyleColor(FocusedButtonTint);
}
}
private void HandleButtonFocusOut(FocusOutEvent evt)
{
if (evt.currentTarget is Button button)
{
button.style.unityBackgroundImageTintColor = new StyleColor(NormalButtonTint);
}
}

View File

@ -203,7 +203,7 @@ namespace DanroJump.UI.Gameplay
prizeResultOverlay?.Dispose();
prizeOverlayRoot = overlayRoot;
prizeResultOverlay = new PrizeResultOverlayWindow(overlayRoot);
prizeResultOverlay = new PrizeResultOverlayWindow(overlayRoot, audioService);
prizeResultOverlay.ReturnRequested += HandlePrizeReturnRequested;
}
@ -223,7 +223,7 @@ namespace DanroJump.UI.Gameplay
continueGameOverlay?.Dispose();
continueOverlayRoot = overlayRoot;
continueGameOverlay = new ContinueGameOverlayWindow(overlayRoot);
continueGameOverlay = new ContinueGameOverlayWindow(overlayRoot, audioService);
continueGameOverlay.ContinueRequested += HandleContinueRequested;
continueGameOverlay.MenuRequested += HandleContinueMenuRequested;
}
@ -476,6 +476,12 @@ namespace DanroJump.UI.Gameplay
return;
}
if (!session.IsContinueEnabled())
{
continueGameOverlay?.Close();
return;
}
EnsureDocument();
BindContinueOverlay();

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using DanroJump.Hardware.Com;
using DanroJump.Prizes;
using DanroJump.Prizes.Qr;
using DanroJump.Audio;
using UnityEngine;
using UnityEngine.UIElements;
@ -25,15 +26,13 @@ namespace DanroJump.UI.Gameplay
private readonly Label qrMessageLabel;
private readonly VisualElement prizeStandContainer;
private readonly Button returnButton;
private readonly IGameAudioService audioService;
private float inputEnabledAtTime = float.NegativeInfinity;
public PrizeResultOverlayWindow(VisualElement root)
public PrizeResultOverlayWindow(VisualElement root, IGameAudioService audioService = null)
{
this.root = root;
if (this.root != null)
{
this.root.focusable = true;
}
this.audioService = audioService;
titleLabel = root?.Q<Label>("PrizeResultTitle");
subtitleLabel = root?.Q<Label>("PrizeResultSubtitle");
@ -57,6 +56,8 @@ namespace DanroJump.UI.Gameplay
if (returnButton != null)
{
returnButton.clicked += HandleReturnClicked;
returnButton.RegisterCallback<FocusInEvent>(HandleButtonFocusIn);
returnButton.RegisterCallback<FocusOutEvent>(HandleButtonFocusOut);
}
}
@ -99,7 +100,11 @@ namespace DanroJump.UI.Gameplay
root.SetEnabled(true);
IsOpen = true;
inputEnabledAtTime = Time.unscaledTime + OpenInputGuardSeconds;
root.Focus();
if (returnButton != null)
{
returnButton.Focus();
}
}
public void Close()
@ -196,6 +201,28 @@ namespace DanroJump.UI.Gameplay
if (returnButton != null)
{
returnButton.clicked -= HandleReturnClicked;
returnButton.UnregisterCallback<FocusInEvent>(HandleButtonFocusIn);
returnButton.UnregisterCallback<FocusOutEvent>(HandleButtonFocusOut);
}
}
private static readonly Color FocusedButtonTint = new Color32(0xCE, 0xFF, 0x1A, 0xFF);
private static readonly Color NormalButtonTint = Color.white;
private void HandleButtonFocusIn(FocusInEvent evt)
{
if (evt.currentTarget is Button button)
{
audioService?.Play(GameAudioEvent.UiNavigate);
button.style.unityBackgroundImageTintColor = new StyleColor(FocusedButtonTint);
}
}
private void HandleButtonFocusOut(FocusOutEvent evt)
{
if (evt.currentTarget is Button button)
{
button.style.unityBackgroundImageTintColor = new StyleColor(NormalButtonTint);
}
}

View File

@ -138,6 +138,7 @@ namespace DanroJump.UI.MainMenu
RegisterCreditCallbacks();
RegisterInputActions();
RefreshStaticLabels();
audioService?.PlayMusic(GameMusicCue.Menu);
FocusDefaultButtonNextFrameAsync(this.GetCancellationTokenOnDestroy()).Forget(Debug.LogException);
}
@ -276,7 +277,8 @@ namespace DanroJump.UI.MainMenu
var currentValue = serviceSettingsService.GetString(targetDefinition.Key);
var profile = ResolveTextInputProfile(targetDefinition.Key);
serviceTextInputModal.SetContext(
targetDefinition,
result.Title,
targetDefinition.DisplayName,
currentValue,
profile,
value =>
@ -379,6 +381,7 @@ namespace DanroJump.UI.MainMenu
{
return settingKey switch
{
ServiceSettingsKeys.SettingsPin => new ArcadeStringInputProfile("0123456789", 4),
ServiceSettingsKeys.SupportNumber => ArcadeStringInputProfile.Phone(),
ServiceSettingsKeys.PrizePhoneNumber => ArcadeStringInputProfile.Phone(),
ServiceSettingsKeys.Currency => ArcadeStringInputProfile.ShortText(maxLength: 10),
@ -925,7 +928,14 @@ namespace DanroJump.UI.MainMenu
audioService?.Play(GameAudioEvent.UiSubmit);
ResolveServiceSettingsIfNeeded();
if (serviceTextInputModal != null && serviceTextInputModal.Root != null)
bool requirePin = serviceSettings == null || serviceSettings.GetBool(ServiceSettingsKeys.RequireSettingsPin);
string correctPin = serviceSettings != null ? serviceSettings.GetString(ServiceSettingsKeys.SettingsPin) : "1234";
if (string.IsNullOrEmpty(correctPin))
{
correctPin = "1234";
}
if (requirePin && serviceTextInputModal != null && serviceTextInputModal.Root != null)
{
var profile = new ArcadeStringInputProfile("0123456789", 4);
serviceTextInputModal.SetContext(
@ -935,7 +945,7 @@ namespace DanroJump.UI.MainMenu
profile,
pin =>
{
if (pin == "1234")
if (pin == correctPin)
{
if (settingsModal != null && settingsModal.Root != null)
{

View File

@ -1,11 +1,12 @@
using DanroJump.Prizes;
using DanroJump.Settings;
namespace DanroJump.Settings
namespace DanroJump.UI.MainMenu
{
/// <summary>
/// Формирует операторскую сводку призовых уровней и legacy-флагов.
/// </summary>
internal static class ServiceSettingsPrizeSummaryBuilder
internal static class PrizeInfoFormatter
{
public static string Build(IReadOnlyServiceSettings settings)
{

View File

@ -1,11 +1,12 @@
using DanroJump.Prizes.Qr;
using DanroJump.Settings;
namespace DanroJump.Settings
namespace DanroJump.UI.MainMenu
{
/// <summary>
/// Формирует операторскую сводку QR-выдачи и коротких текстов призов.
/// Формирует операторскую сводку по конфигурации QR-кодов.
/// </summary>
internal static class ServiceSettingsQrSummaryBuilder
internal static class QrInfoFormatter
{
public static string Build(IReadOnlyServiceSettings settings)
{

View File

@ -403,12 +403,19 @@ namespace DanroJump.UI.MainMenu
: FormatAction(state.SelectedAction);
}
RefreshActionButton(confirmButton, ArcadeStringInputAction.Confirm);
RefreshActionButton(deleteButton, ArcadeStringInputAction.Delete);
RefreshActionButton(cancelButton, ArcadeStringInputAction.Cancel);
bool isAnyActionSelected = false;
RefreshActionButton(confirmButton, ArcadeStringInputAction.Confirm, ref isAnyActionSelected);
RefreshActionButton(deleteButton, ArcadeStringInputAction.Delete, ref isAnyActionSelected);
RefreshActionButton(cancelButton, ArcadeStringInputAction.Cancel, ref isAnyActionSelected);
if (!isAnyActionSelected && Root != null)
{
Root.Focus();
}
}
private void RefreshActionButton(Button button, ArcadeStringInputAction action)
private void RefreshActionButton(Button button, ArcadeStringInputAction action, ref bool isAnyActionSelected)
{
if (button == null)
{
@ -418,6 +425,8 @@ namespace DanroJump.UI.MainMenu
if (state.IsActionSelected(action))
{
button.AddToClassList(ActiveActionClass);
button.Focus();
isAnyActionSelected = true;
return;
}

View File

@ -3,209 +3,51 @@ using UnityEngine;
namespace DanroJump.Vfx
{
/// <summary>
/// Служба визуальных эффектов, программно создающая и настраивающая системы частиц Unity.
/// Позволяет избежать зависимости от префабов в редакторе и обеспечивает высокую переносимость.
/// Служба визуальных эффектов, загружающая и инстанцирующая префабы систем частиц из Resources/Vfx/.
/// </summary>
public sealed class GameVfxService : IVfxService
{
public void SpawnJumpEffect(Vector3 position)
{
var go = new GameObject("VFX_Jump");
go.transform.position = position;
var ps = go.AddComponent<ParticleSystem>();
var main = ps.main;
main.startColor = new Color(0.9f, 0.9f, 0.9f, 0.5f);
main.startSize = new ParticleSystem.MinMaxCurve(0.08f, 0.22f);
main.startSpeed = new ParticleSystem.MinMaxCurve(1.5f, 3.5f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.2f, 0.45f);
main.duration = 0.25f;
main.loop = false;
main.gravityModifier = 0.6f;
var shape = ps.shape;
shape.shapeType = ParticleSystemShapeType.Cone;
shape.angle = 35f;
shape.radius = 0.15f;
shape.rotation = new Vector3(90f, 0f, 0f); // Направление вниз под ноги
var emission = ps.emission;
emission.rateOverTime = 0;
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 12) });
var colorOverLifetime = ps.colorOverLifetime;
colorOverLifetime.enabled = true;
var gradient = new Gradient();
gradient.SetKeys(
new GradientColorKey[] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.6f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradient;
ps.Play();
if (Application.isPlaying)
{
Object.Destroy(go, main.duration + main.startLifetime.constantMax);
}
SpawnVfx("VFX_Jump", position);
}
public void SpawnCoinCollectedEffect(Vector3 position)
{
var go = new GameObject("VFX_CoinCollected");
go.transform.position = position;
var ps = go.AddComponent<ParticleSystem>();
var main = ps.main;
main.startColor = new Color(1.0f, 0.85f, 0.0f, 0.9f); // Золотой цвет
main.startSize = new ParticleSystem.MinMaxCurve(0.1f, 0.25f);
main.startSpeed = new ParticleSystem.MinMaxCurve(2f, 4f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.3f, 0.6f);
main.duration = 0.3f;
main.loop = false;
main.gravityModifier = -0.2f; // Легкий подъем вверх
var shape = ps.shape;
shape.shapeType = ParticleSystemShapeType.Sphere;
shape.radius = 0.1f;
var emission = ps.emission;
emission.rateOverTime = 0;
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 18) });
var colorOverLifetime = ps.colorOverLifetime;
colorOverLifetime.enabled = true;
var gradient = new Gradient();
gradient.SetKeys(
new GradientColorKey[] { new GradientColorKey(new Color(1f, 0.9f, 0.2f), 0f), new GradientColorKey(new Color(1f, 0.5f, 0f), 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradient;
ps.Play();
if (Application.isPlaying)
{
Object.Destroy(go, main.duration + main.startLifetime.constantMax);
}
SpawnVfx("VFX_CoinCollected", position);
}
public void SpawnPlayerDeathEffect(Vector3 position)
{
var go = new GameObject("VFX_PlayerDeath");
go.transform.position = position;
var ps = go.AddComponent<ParticleSystem>();
var main = ps.main;
main.startColor = new Color(0.8f, 0.1f, 0.1f, 0.8f); // Красный / серый взрыв
main.startSize = new ParticleSystem.MinMaxCurve(0.15f, 0.35f);
main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 7f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.4f, 0.8f);
main.duration = 0.5f;
main.loop = false;
main.gravityModifier = 0.8f;
var shape = ps.shape;
shape.shapeType = ParticleSystemShapeType.Sphere;
shape.radius = 0.25f;
var emission = ps.emission;
emission.rateOverTime = 0;
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 35) });
var colorOverLifetime = ps.colorOverLifetime;
colorOverLifetime.enabled = true;
var gradient = new Gradient();
gradient.SetKeys(
new GradientColorKey[] { new GradientColorKey(new Color(0.9f, 0.2f, 0.2f), 0f), new GradientColorKey(new Color(0.3f, 0.3f, 0.3f), 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradient;
ps.Play();
if (Application.isPlaying)
{
Object.Destroy(go, main.duration + main.startLifetime.constantMax);
}
SpawnVfx("VFX_PlayerDeath", position);
}
public void SpawnPrizeEffect(Vector3 position)
{
var go = new GameObject("VFX_PrizeWin");
go.transform.position = position;
var ps = go.AddComponent<ParticleSystem>();
var main = ps.main;
main.startColor = Color.white; // Конфетти будут разных цветов
main.startSize = new ParticleSystem.MinMaxCurve(0.12f, 0.28f);
main.startSpeed = new ParticleSystem.MinMaxCurve(4f, 8f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.6f, 1.2f);
main.duration = 0.8f;
main.loop = false;
main.gravityModifier = 0.4f;
var shape = ps.shape;
shape.shapeType = ParticleSystemShapeType.Hemisphere; // Разлет вверх полусферой
shape.radius = 0.3f;
shape.rotation = new Vector3(-90f, 0f, 0f); // Поворот вверх
var emission = ps.emission;
emission.rateOverTime = 0;
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 50) });
// Добавим смену цветов
var colorBySpeed = ps.colorBySpeed; // Используем градиент для создания разноцветных конфетти
var colorOverLifetime = ps.colorOverLifetime;
colorOverLifetime.enabled = true;
// Настроим случайный цвет при рождении через градиент с множеством ключей или Random Between Two Colors
main.startColor = new ParticleSystem.MinMaxGradient(
new Color(0.2f, 0.6f, 1.0f), // Голубой
new Color(1.0f, 0.2f, 0.6f) // Розовый
);
ps.Play();
if (Application.isPlaying)
{
Object.Destroy(go, main.duration + main.startLifetime.constantMax);
}
SpawnVfx("VFX_PrizeWin", position);
}
public void SpawnComErrorEffect(Vector3 position)
{
var go = new GameObject("VFX_ComError");
go.transform.position = position;
var ps = go.AddComponent<ParticleSystem>();
SpawnVfx("VFX_ComError", position);
}
var main = ps.main;
main.startColor = new Color(1.0f, 0.3f, 0.0f, 0.9f); // Оранжевые тревожные искры
main.startSize = new ParticleSystem.MinMaxCurve(0.06f, 0.18f);
main.startSpeed = new ParticleSystem.MinMaxCurve(3f, 6f);
main.startLifetime = new ParticleSystem.MinMaxCurve(0.25f, 0.5f);
main.duration = 0.4f;
main.loop = false;
main.gravityModifier = 0.3f;
var shape = ps.shape;
shape.shapeType = ParticleSystemShapeType.Cone;
shape.angle = 60f;
shape.radius = 0.1f;
var emission = ps.emission;
emission.rateOverTime = 0;
emission.SetBursts(new ParticleSystem.Burst[] { new ParticleSystem.Burst(0f, 20) });
var colorOverLifetime = ps.colorOverLifetime;
colorOverLifetime.enabled = true;
var gradient = new Gradient();
gradient.SetKeys(
new GradientColorKey[] { new GradientColorKey(new Color(1f, 0.5f, 0f), 0f), new GradientColorKey(new Color(0.8f, 0.1f, 0f), 1f) },
new GradientAlphaKey[] { new GradientAlphaKey(0.9f, 0f), new GradientAlphaKey(0f, 1f) }
);
colorOverLifetime.color = gradient;
private void SpawnVfx(string prefabName, Vector3 position)
{
var prefab = Resources.Load<ParticleSystem>($"Vfx/{prefabName}");
if (prefab == null)
{
Debug.LogWarning($"[GameVfxService] Prefab Resources/Vfx/{prefabName} not found. Attempting to generate default assets.");
return;
}
var ps = Object.Instantiate(prefab, position, Quaternion.identity);
ps.Play();
if (Application.isPlaying)
{
Object.Destroy(go, main.duration + main.startLifetime.constantMax);
var main = ps.main;
Object.Destroy(ps.gameObject, main.duration + main.startLifetime.constantMax);
}
}
}

View File

@ -32,7 +32,7 @@ MonoBehaviour:
intStep: 1
minFloat: 0
maxFloat: 1
floatStep: 0.01
floatStep: 0.05
displayFloatAsPercentage: 1
options: []
buttonText:
@ -59,7 +59,7 @@ MonoBehaviour:
intStep: 1
minFloat: 0
maxFloat: 1
floatStep: 0.01
floatStep: 0.05
displayFloatAsPercentage: 1
options: []
buttonText:
@ -985,3 +985,80 @@ MonoBehaviour:
buttonData:
buttonColor: {r: 0.22352943, g: 0.9803922, b: 1, a: 1}
buttonSprite: {fileID: 0}
- key: security.requirePin
displayName: "Запрос PIN-кода"
description: "Требовать ввод PIN-кода для входа в меню настроек"
group: "Безопасность"
shownInServiceMenu: 1
exposedForExternalChanges: 0
valueType: 0
displayMode: 1
defaultBool: 1
defaultInt: 0
defaultFloat: 1
defaultString:
defaultOptionIndex: 0
minInt: 0
maxInt: 1
intStep: 1
minFloat: 0
maxFloat: 1
floatStep: 0.1
displayFloatAsPercentage: 0
options: []
buttonText:
buttonData:
buttonColor: {r: 0, g: 0, b: 0, a: 0}
buttonSprite: {fileID: 0}
- key: security.pin
displayName: "PIN-код"
description: "PIN-код для доступа к меню настроек (4 цифры)"
group: "Безопасность"
shownInServiceMenu: 0
exposedForExternalChanges: 0
valueType: 3
displayMode: 4
defaultBool: 1
defaultInt: 0
defaultFloat: 1
defaultString: "1234"
defaultOptionIndex: 0
minInt: 0
maxInt: 10
intStep: 1
minFloat: 0
maxFloat: 1
floatStep: 0.1
displayFloatAsPercentage: 0
options: []
buttonText:
buttonData:
buttonColor: {r: 0, g: 0, b: 0, a: 0}
buttonSprite: {fileID: 0}
- key: btn.settingsPin
displayName: "PIN-код"
description: "PIN-код для доступа к меню настроек (4 цифры)"
group: "Безопасность"
shownInServiceMenu: 1
exposedForExternalChanges: 0
valueType: 4
displayMode: 5
defaultBool: 1
defaultInt: 0
defaultFloat: 1
defaultString:
defaultOptionIndex: 0
minInt: 0
maxInt: 10
intStep: 1
minFloat: 0
maxFloat: 1
floatStep: 0.1
displayFloatAsPercentage: 0
options: []
buttonText:
buttonData:
buttonColor: {r: 0, g: 0, b: 0, a: 0}
buttonSprite: {fileID: 0}

View File

@ -9,7 +9,7 @@ using Zenject;
namespace DanroJump.Tests.EditMode
{
public sealed class ServiceSettingsSceneInjectorTests
public sealed class AppUIFacadeInjectorTests
{
private readonly List<GameObject> createdObjects = new();
@ -31,12 +31,12 @@ namespace DanroJump.Tests.EditMode
public void Initialize_InjectsActionsAlreadyPresentInLoadedScene()
{
var gameObject = CreateGameObject("Service Settings Actions");
var actions = gameObject.AddComponent<ServiceSettingsActions>();
var actions = gameObject.AddComponent<AppUIFacade>();
var sceneFlow = new RecordingSceneFlowService();
var container = new DiContainer();
container.Bind<IServiceSettingsService>().FromInstance(new NoopServiceSettingsService()).AsSingle();
container.Bind<ISceneFlowService>().FromInstance(sceneFlow).AsSingle();
var injector = new ServiceSettingsSceneInjector(container, true);
var injector = new AppUIFacadeInjector(container, true);
injector.Initialize();
actions.LoadScene("Game");
@ -51,15 +51,15 @@ namespace DanroJump.Tests.EditMode
public void Initialize_DoesNothingWhenSceneActionInjectionIsDisabled()
{
var gameObject = CreateGameObject("Disabled Service Settings Actions");
var actions = gameObject.AddComponent<ServiceSettingsActions>();
var actions = gameObject.AddComponent<AppUIFacade>();
var sceneFlow = new RecordingSceneFlowService();
var container = new DiContainer();
container.Bind<IServiceSettingsService>().FromInstance(new NoopServiceSettingsService()).AsSingle();
container.Bind<ISceneFlowService>().FromInstance(sceneFlow).AsSingle();
var injector = new ServiceSettingsSceneInjector(container, false);
var injector = new AppUIFacadeInjector(container, false);
injector.Initialize();
LogAssert.Expect(LogType.Warning, "ServiceSettingsActions requires ISceneFlowService from DI.");
LogAssert.Expect(LogType.Warning, "AppUIFacade requires ISceneFlowService from DI.");
actions.LoadMainMenu();
injector.Dispose();

Some files were not shown because too many files have changed in this diff Show More