Compare commits

...

16 Commits

Author SHA1 Message Date
Mausham
d700760524 some additional fixes 2026-06-01 14:00:54 +05:45
Mausham
ac0f3a458a added 5 more drawing 2026-06-01 13:29:05 +05:45
Savya Bikram Shah
113b82bd46 fixes 2026-05-29 20:58:40 +05:45
Savya Bikram Shah
3e5ff544bf fixes 2026-05-29 20:31:37 +05:45
Savya Bikram Shah
b74af8867e Minor Fix 2026-05-29 20:04:53 +05:45
Savya Bikram Shah
b669bcc0e0 ads added 2026-05-29 20:02:46 +05:45
Savya Bikram Shah
9b6100b5bb Minor fixes 2026-05-29 19:53:09 +05:45
Savya Bikram Shah
f51286468a fixes 2026-05-29 19:47:45 +05:45
Savya Bikram Shah
8a4a84dadf adservice added 2026-05-29 19:21:13 +05:45
Savya Bikram Shah
9b2e4c915c Merge remote-tracking branch 'origin/work_branch' into savya
# Conflicts:
#	Assets/Darkmatter/Content/Fonts/static/Fredoka-SemiBold SDF.asset
2026-05-29 19:12:35 +05:45
Savya Bikram Shah
84689a6755 audio fixes 2026-05-29 19:12:23 +05:45
Savya Bikram Shah
41c9969996 Audio fixes 2026-05-29 19:07:21 +05:45
Savya Bikram Shah
0b22ed6d09 Music and transparent fixes 2026-05-29 18:42:12 +05:45
Savya Bikram Shah
47fb204446 artbook added to drawing 2026-05-29 18:28:06 +05:45
Savya Bikram Shah
676b389244 Artt book tweaks 2026-05-29 18:20:25 +05:45
Savya Bikram Shah
931515193a Animations added 2026-05-29 18:06:22 +05:45
695 changed files with 28701 additions and 6188 deletions

View File

@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 0e030d5498bfe4ffd8443c796618c539
m_currentHash:
serializedVersion: 2
Hash: fdf5dbef4b3bdd1999753be21e456785
Hash: 00000000000000000000000000000000
m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0
m_CatalogRequestsTimeout: 0
@@ -58,7 +58,7 @@ MonoBehaviour:
m_ContentStateBuildPathProfileVariableName:
m_CustomContentStateBuildPath:
m_ContentStateBuildPath:
m_BuildAddressablesWithPlayerBuild: 0
m_BuildAddressablesWithPlayerBuild: 1
m_overridePlayerVersion: '[UnityEditor.PlayerSettings.bundleVersion]'
m_GroupAssets:
- {fileID: 11400000, guid: ec9d910e81be14a1484f351f20d32f6f, type: 2}

View File

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

View File

@@ -21,6 +21,12 @@ MonoBehaviour:
m_SerializedLabels:
- drawing
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 14477b5d35d0be9439ecb935f1c4e64e
m_Address: Donut
m_ReadOnly: 0
m_SerializedLabels:
- drawing
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 2043f692673a79543afed5cb879f0e04
m_Address: Five
m_ReadOnly: 0
@@ -39,6 +45,24 @@ MonoBehaviour:
m_SerializedLabels:
- drawing
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 4474b93704045a740ba9b8114e8a5238
m_Address: Fish
m_ReadOnly: 0
m_SerializedLabels:
- drawing
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 62ae112e11b695a40b889d773a36f8bd
m_Address: Elephant
m_ReadOnly: 0
m_SerializedLabels:
- drawing
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 6e36ba5d4763c694289c8ca75ee81449
m_Address: Bus
m_ReadOnly: 0
m_SerializedLabels:
- drawing
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: 977dc7dac5ee6b543b8ed47c2299919e
m_Address: Airplane
m_ReadOnly: 0
@@ -63,6 +87,12 @@ MonoBehaviour:
m_SerializedLabels:
- drawing
FlaggedDuringContentUpdateRestriction: 0
- m_GUID: f3200c6715fea4a41bc71c0314b519cd
m_Address: Frog
m_ReadOnly: 0
m_SerializedLabels:
- drawing
FlaggedDuringContentUpdateRestriction: 0
m_ReadOnly: 0
m_Settings: {fileID: 11400000, guid: 4a94ef317c3674edd8270e4ed15031f6, type: 2}
m_SchemaSet:

View File

@@ -0,0 +1,28 @@
%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: 7e3976da977cb49238499ea3b4c237ae, type: 3}
m_Name: ProfileDataSourceSettings
m_EditorClassIdentifier: Unity.Addressables.Editor::UnityEditor.AddressableAssets.Settings.ProfileDataSourceSettings
profileGroupTypes:
- m_GroupTypePrefix: Built-In
m_Variables:
- m_Suffix: BuildPath
m_Value: '[UnityEngine.AddressableAssets.Addressables.BuildPath]/[BuildTarget]'
- m_Suffix: LoadPath
m_Value: '{UnityEngine.AddressableAssets.Addressables.RuntimePath}/[BuildTarget]'
environments: []
currentEnvironment:
id:
projectId:
projectGenesisId:
name:
isDefault: 0

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 550878786d0cb4f48a8afa99c49a5635
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -8,6 +8,5 @@ namespace Darkmatter.Core.Contracts.Features.GameplayFlow
UniTask BackAsync(CancellationToken cancellationToken);
UniTask SaveAsync(CancellationToken cancellationToken);
UniTask NextAsync(CancellationToken cancellationToken);
void OnApplicationPaused();
}
}

View File

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

View File

@@ -0,0 +1,26 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Data.Dynamic.Services.Ads;
using Darkmatter.Core.Enums.Services.Ads;
namespace Darkmatter.Core.Contracts.Services.Ads
{
public interface IAdService
{
bool IsInitialized { get; }
event Action<AdFormat, AdLoadState> LoadStateChanged;
UniTask InitializeAsync(CancellationToken cancellationToken);
UniTask<bool> LoadAsync(AdFormat format, CancellationToken cancellationToken);
bool IsReady(AdFormat format);
UniTask<AdShowResult> ShowAsync(AdFormat format, CancellationToken cancellationToken);
UniTask<bool> ShowBannerAsync(BannerSize size, BannerPosition position, CancellationToken cancellationToken);
void HideBanner();
void DestroyBanner();
void SetConsent(bool hasUserConsent, bool isChildDirected);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ffaede2f106c45a68197d70ec929c7de
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
using UnityEngine;
namespace Darkmatter.Core.Contracts.Services.Music
{
public interface IMusicService
{
void Play(AudioClip clip, float volume01 = 1f, bool loop = true);
void Stop(float fadeOutSeconds = 0f);
void SetVolume(float volume01);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
namespace Darkmatter.Core.Data.Dynamic.Services.Ads
{
public readonly struct AdReward
{
public readonly string Type;
public readonly double Amount;
public AdReward(string type, double amount)
{
Type = type;
Amount = amount;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 784af4bbe9df4c25b5d304d8364ade62
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,22 @@
namespace Darkmatter.Core.Data.Dynamic.Services.Ads
{
public readonly struct AdShowResult
{
public readonly bool Shown;
public readonly bool Rewarded;
public readonly AdReward Reward;
public readonly string Error;
public AdShowResult(bool shown, bool rewarded, AdReward reward, string error)
{
Shown = shown;
Rewarded = rewarded;
Reward = reward;
Error = error;
}
public static AdShowResult Success() => new(true, false, default, null);
public static AdShowResult WithReward(AdReward reward) => new(true, true, reward, null);
public static AdShowResult Failure(string error) => new(false, false, default, error);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e2859963572b4173957a583249f5d399
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,4 @@
namespace Darkmatter.Core.Data.Signals.Features.Capture
{
public record struct GallerySaveCompletedSignal(bool Success);
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 04fbdae933fab4011879c232a1041042

View File

@@ -0,0 +1,4 @@
namespace Darkmatter.Core.Data.Signals.Features.Capture
{
public record struct GallerySaveStartedSignal;
}

View File

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

View File

@@ -1,6 +1,7 @@
using System;
using UnityEngine;
namespace Darkmatter.Core
{
public record struct OpenArtBookSignal;
public record struct OpenArtBookSignal(Action OnClose);
}

View File

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

View File

@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using Darkmatter.Core.Enums.Services.Ads;
using UnityEngine;
namespace Darkmatter.Core.Data.Static.Services.Ads
{
[CreateAssetMenu(fileName = "AdUnitCatalog", menuName = "Darkmatter/Ads/Ad Unit Catalog")]
public class AdUnitCatalogSO : ScriptableObject
{
[Serializable]
public class Entry
{
public AdFormat Format;
public string AndroidUnitId;
public string IosUnitId;
}
[Header("App IDs")]
[SerializeField] private string androidAppId;
[SerializeField] private string iosAppId;
[Header("Test Mode")]
[Tooltip("If true, returns Google sample ad unit IDs (safe for development).")]
[SerializeField] private bool useTestUnits = true;
[Tooltip("Device IDs to treat as test devices (hashed IDs from logcat).")]
[SerializeField] private List<string> testDeviceIds = new();
[Header("Production Unit IDs")]
[SerializeField] private List<Entry> entries = new();
public string AndroidAppId => androidAppId;
public string IosAppId => iosAppId;
public bool UseTestUnits => useTestUnits;
public IReadOnlyList<string> TestDeviceIds => testDeviceIds;
public string GetUnitId(AdFormat format, RuntimePlatform platform)
{
if (useTestUnits || Application.isEditor)
{
return GetTestUnitId(format, platform);
}
foreach (var e in entries)
{
if (e == null || e.Format != format) continue;
return platform == RuntimePlatform.IPhonePlayer ? e.IosUnitId : e.AndroidUnitId;
}
return null;
}
private static string GetTestUnitId(AdFormat format, RuntimePlatform platform)
{
bool ios = platform == RuntimePlatform.IPhonePlayer;
return format switch
{
AdFormat.Banner => ios ? "ca-app-pub-3940256099942544/2934735716" : "ca-app-pub-3940256099942544/6300978111",
AdFormat.Interstitial => ios ? "ca-app-pub-3940256099942544/4411468910" : "ca-app-pub-3940256099942544/1033173712",
AdFormat.Rewarded => ios ? "ca-app-pub-3940256099942544/1712485313" : "ca-app-pub-3940256099942544/5224354917",
AdFormat.RewardedInterstitial => ios ? "ca-app-pub-3940256099942544/6978759866" : "ca-app-pub-3940256099942544/5354046379",
AdFormat.AppOpen => ios ? "ca-app-pub-3940256099942544/5575463023" : "ca-app-pub-3940256099942544/9257395921",
_ => null
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e6352f0162df4adf99a5945e25c6bf40
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,11 @@
namespace Darkmatter.Core.Enums.Services.Ads
{
public enum AdFormat
{
Banner,
Interstitial,
Rewarded,
RewardedInterstitial,
AppOpen
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ce1e736d19fc4569af80b06274dc3935
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
namespace Darkmatter.Core.Enums.Services.Ads
{
public enum AdLoadState
{
Idle,
Loading,
Loaded,
Failed
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44f09c3955b34814abfa83407118a6a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace Darkmatter.Core.Enums.Services.Ads
{
public enum BannerPosition
{
Top,
Bottom,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Center
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2223e7f2cedb4f479f7c6722e0ca7fd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace Darkmatter.Core.Enums.Services.Ads
{
public enum BannerSize
{
Banner,
LargeBanner,
MediumRectangle,
FullBanner,
Leaderboard,
SmartBanner,
AnchoredAdaptive
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 570a6e9b47c5451c8ef78aa7a3923ead
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -7,5 +7,9 @@ namespace Darkmatter.Core.Enums.Services.Audio
ShapeSnap = 101,
ShapeReturn = 102,
UiTap = 200,
PlayButtonTap = 201,
LevelComplete = 300,
FireWorkLaunch = 400,
}
}

View File

@@ -26,7 +26,7 @@ namespace Darkmatter.Features.Artbook
private readonly List<ArtbookEntry> _entries = new();
private readonly List<Sprite> _ownedSprites = new();
private readonly List<Texture2D> _ownedTextures = new();
private Action _onClose;
private CancellationTokenSource _cts;
private IDisposable _openSubscription;
private int _currentSpread;
@@ -60,10 +60,12 @@ namespace Darkmatter.Features.Artbook
private void HandleOpenArtbookEvent(OpenArtBookSignal signal)
{
_onClose = null;
_view.Show();
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
_onClose = signal.OnClose;
LoadAndShowAsync(_cts.Token).Forget();
}
@@ -164,7 +166,7 @@ namespace Darkmatter.Features.Artbook
private void HandleBackButtonClicked()
{
_eventBus.Publish(new OpenColorBookSignal());
_onClose?.Invoke();
_view.Hide();
}

View File

@@ -4,6 +4,7 @@
"references": [
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
"GUID:b4c9f7fbf1e144933a1797dc208ece5f",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:f51ebe6a0ceec4240a699833d6309b23"
],

View File

@@ -11,6 +11,7 @@ namespace Darkmatter.Features.Capture
{
[SerializeField, Range(0.1f, 2f)] private float captureScale = 1f;
[SerializeField] private CaptureButtonView captureButtonView;
[SerializeField] private GallerySaveView gallerySaveView;
public void Register(IContainerBuilder builder)
{
@@ -19,6 +20,9 @@ namespace Darkmatter.Features.Capture
if (captureButtonView != null)
builder.RegisterEntryPoint<CaptureButtonPresenter>().WithParameter(captureButtonView);
if (gallerySaveView != null)
builder.RegisterEntryPoint<GallerySavePresenter>().WithParameter(gallerySaveView);
}
}
}

View File

@@ -5,6 +5,8 @@ using Darkmatter.Core.Contracts.Features.Capture;
using Darkmatter.Core.Contracts.Features.GameplayFlow;
using Darkmatter.Core.Contracts.Services.Capture;
using Darkmatter.Core.Contracts.Services.Gallery;
using Darkmatter.Core.Data.Signals.Features.Capture;
using Darkmatter.Libs.Observer;
using UnityEngine;
namespace Darkmatter.Features.Capture
@@ -14,17 +16,20 @@ namespace Darkmatter.Features.Capture
private readonly ICaptureService _captureService;
private readonly IGalleryService _galleryService;
private readonly IGameplaySceneRefs _refs;
private readonly IEventBus _bus;
private readonly CaptureConfig _config;
public CaptureSystem(
ICaptureService captureService,
IGalleryService galleryService,
IGameplaySceneRefs refs,
IEventBus bus,
CaptureConfig config)
{
_captureService = captureService;
_galleryService = galleryService;
_refs = refs;
_bus = bus;
_config = config;
}
@@ -34,15 +39,21 @@ namespace Darkmatter.Features.Capture
if (!saveToGallery || png == null || png.Length == 0) return png;
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, mipChain: false);
var success = false;
try
{
if (tex.LoadImage(png))
{
_bus.Publish(new GallerySaveStartedSignal());
await _galleryService.SaveImageAsync(tex,
$"colorbook_{DateTime.UtcNow:yyyyMMdd_HHmmss}.png", ct);
success = true;
}
}
finally
{
UnityEngine.Object.Destroy(tex);
_bus.Publish(new GallerySaveCompletedSignal(success));
}
return png;
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Data.Signals.Features.Capture;
using Darkmatter.Libs.Observer;
using VContainer.Unity;
namespace Darkmatter.Features.Capture.UI
{
public class GallerySavePresenter : IStartable, IDisposable
{
private readonly GallerySaveView _view;
private readonly IEventBus _bus;
private IDisposable _startedSub;
private IDisposable _completedSub;
private CancellationTokenSource _popupCts;
public GallerySavePresenter(GallerySaveView view, IEventBus bus)
{
_view = view;
_bus = bus;
}
public void Start()
{
_startedSub = _bus.Subscribe<GallerySaveStartedSignal>(OnStarted);
_completedSub = _bus.Subscribe<GallerySaveCompletedSignal>(OnCompleted);
}
private void OnStarted(GallerySaveStartedSignal _)
{
CancelPopup();
_view.ShowSaving();
}
private void OnCompleted(GallerySaveCompletedSignal signal)
{
_view.HideSaving();
if (!signal.Success) return;
CancelPopup();
_popupCts = new CancellationTokenSource();
ShowPopupAsync(_popupCts.Token).Forget();
}
private async UniTaskVoid ShowPopupAsync(CancellationToken ct)
{
_view.ShowSuccess();
try
{
await UniTask.Delay(TimeSpan.FromSeconds(_view.PopupAutoHideSeconds), cancellationToken: ct);
}
catch (OperationCanceledException) { return; }
_view.HideSuccess();
}
private void CancelPopup()
{
_popupCts?.Cancel();
_popupCts?.Dispose();
_popupCts = null;
}
public void Dispose()
{
_startedSub?.Dispose();
_completedSub?.Dispose();
CancelPopup();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 71d8cccc7b3a3436ba83ca3683fbea9f

View File

@@ -0,0 +1,40 @@
using UnityEngine;
namespace Darkmatter.Features.Capture.UI
{
public class GallerySaveView : MonoBehaviour
{
[SerializeField] private GameObject savingIndicator;
[SerializeField] private GameObject successPopup;
[SerializeField, Min(0f)] private float popupAutoHideSeconds = 1.5f;
public float PopupAutoHideSeconds => popupAutoHideSeconds;
private void Awake()
{
if (savingIndicator != null) savingIndicator.SetActive(false);
if (successPopup != null) successPopup.SetActive(false);
}
public void ShowSaving()
{
if (savingIndicator != null) savingIndicator.SetActive(true);
if (successPopup != null) successPopup.SetActive(false);
}
public void HideSaving()
{
if (savingIndicator != null) savingIndicator.SetActive(false);
}
public void ShowSuccess()
{
if (successPopup != null) successPopup.SetActive(true);
}
public void HideSuccess()
{
if (successPopup != null) successPopup.SetActive(false);
}
}
}

View File

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

View File

@@ -6,8 +6,10 @@ using Darkmatter.Core;
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
using Darkmatter.Core.Contracts.Features.Loading;
using Darkmatter.Core.Contracts.Features.Progression;
using Darkmatter.Core.Contracts.Services.Ads;
using Darkmatter.Core.Contracts.Services.Scenes;
using Darkmatter.Core.Data.Signals.Features.Drawing;
using Darkmatter.Core.Enums.Services.Ads;
using Darkmatter.Core.Enums.Services.Scenes;
using Darkmatter.Libs.Observer;
using VContainer.Unity;
@@ -20,33 +22,54 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
private readonly ILoadingScreen _loadingScreen;
private readonly IProgressionSystem _progression;
private readonly ISceneService _scenes;
private readonly IAdService _ads;
private readonly IEventBus _bus;
private IDisposable _selectedSub;
private IDisposable _returnToMainMenuSubscription;
private bool _navigatingToGameplay;
private CancellationTokenSource _scopeCts;
public ColorbookFlowController(
IDrawingCatalogController drawingCatalog,
ILoadingScreen loadingScreen,
IProgressionSystem progression,
ISceneService scenes,
IAdService ads,
IEventBus bus)
{
_drawingCatalog = drawingCatalog;
_loadingScreen = loadingScreen;
_progression = progression;
_scenes = scenes;
_ads = ads;
_bus = bus;
_selectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
}
public async UniTask StartAsync(CancellationToken cancellation = new CancellationToken())
{
_scopeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
_returnToMainMenuSubscription = _bus.Subscribe<ReturnToMainMenuSignal>(OnReturnToMainMenu);
_loadingScreen.SetProgress(1f);
await _drawingCatalog.InitializeAsync(cancellation);
if (!_navigatingToGameplay) _loadingScreen.Hide();
PrewarmRewardedAdAsync(_scopeCts.Token).Forget();
}
private async UniTaskVoid PrewarmRewardedAdAsync(CancellationToken ct)
{
try
{
if (!_ads.IsInitialized) await _ads.InitializeAsync(ct);
if (!_ads.IsReady(AdFormat.Rewarded)) await _ads.LoadAsync(AdFormat.Rewarded, ct);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[ColorbookFlow] Rewarded prewarm failed: {ex.Message}");
}
}
private void OnReturnToMainMenu(ReturnToMainMenuSignal signal)
@@ -66,13 +89,20 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
private void OnDrawingSelected(DrawingSelectedSignal signal)
{
if (_navigatingToGameplay) return;
_navigatingToGameplay = true;
HandleSelectionAsync(signal.TemplateId).Forget();
}
private async UniTaskVoid HandleSelectionAsync(string templateId)
{
var ct = _scopeCts?.Token ?? CancellationToken.None;
_loadingScreen.Show();
_loadingScreen.SetProgress(0f);
await ShowRewardedAdAsync(ct);
var progress = new Progress<float>(p => _loadingScreen.SetProgress(p * 0.5f));
var mappedProgress = new Progress<float>(p => _loadingScreen.SetProgress(0.5f + p * 0.25f));
await _progression.SetLastOpenedAsync(templateId);
@@ -81,9 +111,34 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
cancellationToken: default);
}
private async UniTask ShowRewardedAdAsync(CancellationToken ct)
{
const int InitTimeoutMs = 4000;
try
{
if (!_ads.IsInitialized)
{
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeoutCts.CancelAfter(InitTimeoutMs);
await _ads.InitializeAsync(timeoutCts.Token);
}
if (!_ads.IsReady(AdFormat.Rewarded)) return;
await _ads.ShowAsync(AdFormat.Rewarded, ct);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[ColorbookFlow] Rewarded ad skipped: {ex.Message}");
}
}
public void Dispose()
{
_selectedSub?.Dispose();
_returnToMainMenuSubscription?.Dispose();
_scopeCts?.Cancel();
_scopeCts?.Dispose();
}
}

View File

@@ -31,7 +31,8 @@ public class ColoringController : IColoringController, IDisposable
private GameObject _colorInstance;
private GameObject _colorButtonPrefab;
private GameObject _completionAnimationPrefab;
private GameObject _completionAnimationInstance;
private CompletionAnimationView _completionAnimationView;
private readonly List<ColorRegionView> _regions = new();
private readonly List<ColorButton> _buttons = new();
private readonly Dictionary<string, Color> _authoredColors = new();
@@ -89,17 +90,17 @@ public class ColoringController : IColoringController, IDisposable
public async UniTask PlayCompletionAnimationAsync(CancellationToken ct)
{
if (_completionAnimationPrefab == null) return;
var instance = UnityEngine.Object.Instantiate(_completionAnimationPrefab, _refs.PaperRoot);
if (_completionAnimationInstance == null || _completionAnimationView == null) return;
if (_colorInstance != null) _colorInstance.SetActive(false);
_completionAnimationInstance.SetActive(true);
try
{
var view = instance.GetComponentInChildren<CompletionAnimationView>();
if (view == null) return;
await view.PlayAsync(ct);
await _completionAnimationView.PlayAsync(ct);
}
finally
{
if (instance != null) UnityEngine.Object.Destroy(instance);
if (_completionAnimationInstance != null)
_completionAnimationInstance.SetActive(false);
}
}
@@ -140,7 +141,13 @@ public class ColoringController : IColoringController, IDisposable
_regions.Clear();
_authoredColors.Clear();
_completionAnimationPrefab = null;
if (_completionAnimationInstance != null)
{
UnityEngine.Object.Destroy(_completionAnimationInstance);
_completionAnimationInstance = null;
_completionAnimationView = null;
}
if (_colorInstance != null)
{
@@ -154,7 +161,12 @@ public class ColoringController : IColoringController, IDisposable
private void InitializeColorRegions(IDrawingTemplate template, IReadOnlyDictionary<string, Color> savedColors)
{
_colorInstance = UnityEngine.Object.Instantiate(template.ColoringPrefab, _refs.PaperRoot);
_completionAnimationPrefab = template.CompletionAnimationPrefab;
if (template.CompletionAnimationPrefab != null)
{
_completionAnimationInstance = UnityEngine.Object.Instantiate(template.CompletionAnimationPrefab, _refs.PaperRoot);
_completionAnimationView = _completionAnimationInstance.GetComponentInChildren<CompletionAnimationView>(includeInactive: true);
_completionAnimationInstance.SetActive(false);
}
var views = _colorInstance.GetComponentsInChildren<ColorRegionView>(includeInactive: true);
foreach (var region in views)

View File

@@ -1,4 +1,5 @@
using System;
using Darkmatter.Core;
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
using Darkmatter.Libs.Observer;
using VContainer.Unity;
@@ -22,11 +23,18 @@ namespace Darkmatter.Features.Coloring.UI
{
_view.HideInstant();
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(_ => _view.Show());
_view.OnArtbookClicked += HandleArtbookClicked;
}
private void HandleArtbookClicked()
{
_bus.Publish(new OpenArtBookSignal());
}
public void Dispose()
{
_assembledSub?.Dispose();
_view.OnArtbookClicked -= HandleArtbookClicked;
}
}
}

View File

@@ -1,5 +1,7 @@
using System;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace Darkmatter.Features.Coloring.UI
{
@@ -8,6 +10,7 @@ namespace Darkmatter.Features.Coloring.UI
{
[SerializeField] private RectTransform spawnRoot;
[SerializeField] private RectTransform animatedRoot;
[SerializeField] private Button artbookButton;
[SerializeField] private float showDuration = 0.35f;
[SerializeField] private float hideDuration = 0.25f;
[SerializeField] private Vector2 hiddenOffset = new(1500f, 0f);
@@ -17,9 +20,16 @@ namespace Darkmatter.Features.Coloring.UI
private Sequence _activeSequence;
private bool _refsReady;
public event Action OnArtbookClicked;
public RectTransform SpawnRoot => spawnRoot;
private void Awake() => EnsureRefs();
private void Awake()
{
EnsureRefs();
if (artbookButton != null) artbookButton.onClick.AddListener(HandleArtbookClicked);
}
private void HandleArtbookClicked() => OnArtbookClicked?.Invoke();
private void EnsureRefs()
{
@@ -80,5 +90,10 @@ namespace Darkmatter.Features.Coloring.UI
{
KillActive();
}
private void OnDestroy()
{
if (artbookButton != null) artbookButton.onClick.RemoveListener(HandleArtbookClicked);
}
}
}

View File

@@ -8,17 +8,28 @@ namespace Darkmatter.Features.Coloring.UI
public class CompletionAnimationView : MonoBehaviour
{
[SerializeField] private RectTransform target;
[SerializeField] private float duration = 0.6f;
[SerializeField] private float duration = 1f;
[SerializeField] private Vector3 startScale = Vector3.zero;
[SerializeField] private Vector3 endScale = Vector3.one;
[SerializeField] private Ease ease = Ease.OutBack;
[Header("Wiggle")]
[SerializeField] private float wiggleAngle = 10f;
[SerializeField] private float wiggleDuration = 0.25f;
[SerializeField, Min(1)] private int wiggleCycles = 3;
public async UniTask PlayAsync(CancellationToken ct)
{
var rt = target != null ? target : transform as RectTransform;
if (rt == null) return;
rt.localScale = startScale;
rt.localRotation = Quaternion.identity;
await Tween.Scale(rt, endScale, duration, ease).ToUniTask(cancellationToken: ct);
await Tween.LocalRotation(rt, new Vector3(0f, 0f, wiggleAngle), wiggleDuration, Ease.InOutSine,
cycles: wiggleCycles * 2, cycleMode: CycleMode.Yoyo)
.ToUniTask(cancellationToken: ct);
rt.localRotation = Quaternion.identity;
}
private void OnDestroy()

View File

@@ -6,7 +6,8 @@
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
"GUID:b4c9f7fbf1e144933a1797dc208ece5f",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:f51ebe6a0ceec4240a699833d6309b23"
"GUID:f51ebe6a0ceec4240a699833d6309b23",
"GUID:6055be8ebefd69e48b49212b09b47b2f"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@@ -39,6 +39,8 @@ namespace Darkmatter.Features.DrawingCatalog
_view.OnArtBookClicked += OnArtBookBtnClicked;
_view.OnLeftPageClicked += OnLeftPageBtnClicked;
_view.OnRightPageClicked += OnRightPageBtnClicked;
_view.OnPageChangedByScroll += OnPageChangedByScroll;
_view.OnPageNumberClicked += OnPageNumberClicked;
_controller.ListChanged += OnListChanged;
@@ -68,9 +70,21 @@ namespace Darkmatter.Features.DrawingCatalog
}
}
private void OnPageChangedByScroll(int page)
{
_currentPage = page;
}
private void OnPageNumberClicked(int page)
{
if (page < 0 || page >= _totalPages || page == _currentPage) return;
_currentPage = page;
_view.SetPagination(_currentPage, _totalPages);
}
private void OnArtBookBtnClicked()
{
_eventBus.Publish(new OpenArtBookSignal());
_eventBus.Publish(new OpenArtBookSignal(()=> _view.Show()));
_view.Hide();
}
@@ -116,6 +130,8 @@ namespace Darkmatter.Features.DrawingCatalog
_view.OnArtBookClicked -= OnArtBookBtnClicked;
_view.OnLeftPageClicked -= OnLeftPageBtnClicked;
_view.OnRightPageClicked -= OnRightPageBtnClicked;
_view.OnPageChangedByScroll -= OnPageChangedByScroll;
_view.OnPageNumberClicked -= OnPageNumberClicked;
_openSubscription?.Dispose();
_cts.Cancel();
_cts.Dispose();

View File

@@ -20,15 +20,25 @@ namespace Darkmatter.Features.DrawingCatalog
[SerializeField] private ScrollRect scrollRect;
[SerializeField] private Button leftPageButton;
[SerializeField] private Button rightPageButton;
[Header("Page Numbers")]
[SerializeField] private RectTransform pageNumberContainer;
[SerializeField] private PageNumberButton pageNumberPrefab;
private readonly List<DrawingCatalogButton> _buttons = new();
private readonly List<PageNumberButton> _pageButtons = new();
public event Action<string> OnItemClicked;
public event Action OnBackClicked;
public event Action OnArtBookClicked;
public event Action OnLeftPageClicked;
public event Action OnRightPageClicked;
public event Action<int> OnPageChangedByScroll;
public event Action<int> OnPageNumberClicked;
private Coroutine _sliderCoroutine;
private int _currentPage;
private int _totalPages = 1;
private bool _suppressScrollEvents;
public void Start()
{
@@ -36,6 +46,7 @@ namespace Darkmatter.Features.DrawingCatalog
artBookButton.onClick.AddListener(() => OnArtBookClicked?.Invoke());
leftPageButton.onClick.AddListener(() => OnLeftPageClicked?.Invoke());
rightPageButton.onClick.AddListener(() => OnRightPageClicked?.Invoke());
scrollRect.onValueChanged.AddListener(OnScrollValueChanged);
}
public void Show()
{
@@ -69,8 +80,11 @@ namespace Darkmatter.Features.DrawingCatalog
public void SetPagination(int currentPage, int totalPages)
{
leftPageButton.interactable = currentPage > 0;
rightPageButton.interactable = currentPage < totalPages - 1;
_currentPage = currentPage;
_totalPages = totalPages;
BuildPageNumbers(totalPages);
UpdateArrowButtons();
UpdateActivePageNumber();
if (totalPages <= 1) return;
float pageWidth = scrollRect.viewport.rect.width;
@@ -84,8 +98,65 @@ namespace Darkmatter.Features.DrawingCatalog
_sliderCoroutine = StartCoroutine(SmoothScrollTo(targetPosition));
}
// Keeps the arrow buttons (and the presenter) in sync when the user scrolls manually.
private void OnScrollValueChanged(Vector2 _)
{
if (_suppressScrollEvents || _totalPages <= 1) return;
int page = PageFromScrollPosition();
if (page == _currentPage) return;
_currentPage = page;
UpdateArrowButtons();
UpdateActivePageNumber();
OnPageChangedByScroll?.Invoke(page);
}
private void BuildPageNumbers(int totalPages)
{
if (pageNumberPrefab == null || pageNumberContainer == null) return;
while (_pageButtons.Count < totalPages)
{
var button = Instantiate(pageNumberPrefab, pageNumberContainer);
int pageIndex = _pageButtons.Count;
button.Initialize(pageIndex, () => OnPageNumberClicked?.Invoke(pageIndex));
_pageButtons.Add(button);
}
while (_pageButtons.Count > totalPages)
{
var last = _pageButtons[^1];
_pageButtons.RemoveAt(_pageButtons.Count - 1);
Destroy(last.gameObject);
}
}
private void UpdateActivePageNumber()
{
for (int i = 0; i < _pageButtons.Count; i++)
_pageButtons[i].SetHighlighted(i == _currentPage);
}
private int PageFromScrollPosition()
{
float pageWidth = scrollRect.viewport.rect.width;
float maxScroll = content.rect.width - pageWidth;
if (maxScroll <= 0f) return 0;
int page = Mathf.RoundToInt(scrollRect.horizontalNormalizedPosition * maxScroll / pageWidth);
return Mathf.Clamp(page, 0, _totalPages - 1);
}
private void UpdateArrowButtons()
{
leftPageButton.interactable = _currentPage > 0;
rightPageButton.interactable = _currentPage < _totalPages - 1;
}
private System.Collections.IEnumerator SmoothScrollTo(float targetPosition)
{
_suppressScrollEvents = true;
const float duration = 0.3f;
float elapsed = 0f;
float startPosition = scrollRect.horizontalNormalizedPosition;
@@ -96,12 +167,15 @@ namespace Darkmatter.Features.DrawingCatalog
yield return null;
}
scrollRect.horizontalNormalizedPosition = targetPosition;
_suppressScrollEvents = false;
}
public void ResetScrollPosition()
{
_suppressScrollEvents = true;
scrollRect.horizontalNormalizedPosition = 0f;
_suppressScrollEvents = false;
}
}
}

View File

@@ -0,0 +1,47 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Darkmatter.Features.DrawingCatalog
{
public class PageNumberButton : MonoBehaviour
{
[SerializeField] private TMP_Text label;
[SerializeField] private Button button;
[Header("Highlight")]
[Tooltip("Image faded for unselected pages. Use the button's color block (below) to match the disabled look.")]
[SerializeField] private Image image;
[Tooltip("When on, unselected pages use the button's Disabled Color and selected use Normal Color. " +
"When off, only the alpha below is applied.")]
[SerializeField] private bool useButtonColors = true;
[Range(0f, 1f)]
[SerializeField] private float unselectedAlpha = 0.5f;
public int PageIndex { get; private set; }
public void Initialize(int pageIndex, UnityEngine.Events.UnityAction onClick)
{
PageIndex = pageIndex;
if (label != null) label.text = (pageIndex + 1).ToString();
button.onClick.RemoveAllListeners();
button.onClick.AddListener(onClick);
}
public void SetHighlighted(bool isActive)
{
if (image == null) return;
if (useButtonColors)
{
var colors = button.colors;
image.color = isActive ? colors.normalColor : colors.disabledColor;
return;
}
var c = image.color;
c.a = isActive ? 1f : unselectedAlpha;
image.color = c;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 19495f2daba89ce44abe376a40ca313d

View File

@@ -9,13 +9,14 @@ using Darkmatter.Core.Contracts.Features.GameplayFlow;
using Darkmatter.Core.Contracts.Features.Loading;
using Darkmatter.Core.Contracts.Features.Progression;
using Darkmatter.Core.Contracts.Features.ShapeBuilder;
using Darkmatter.Core.Contracts.Services.Gallery;
using Darkmatter.Core.Contracts.Services.Audio;
using Darkmatter.Core.Contracts.Services.Scenes;
using Darkmatter.Core.Data.Dynamic.Features.Progression;
using Darkmatter.Core.Data.Signals.Features.Coloring;
using Darkmatter.Core.Data.Signals.Features.Drawing;
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
using Darkmatter.Core.Enums.Features.Progression;
using Darkmatter.Core.Enums.Services.Audio;
using Darkmatter.Core.Enums.Services.Scenes;
using Darkmatter.Libs.Observer;
using UnityEngine;
@@ -35,6 +36,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
private readonly ICaptureFeature _capture;
private readonly ILoadingScreen _loadingScreen;
private readonly IGameplaySceneRefs _refs;
private readonly ISfxPlayer _sfx;
private readonly IEventBus _bus;
private IDrawingTemplate _template;
@@ -43,6 +45,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
private IDisposable _assembledSub;
private IDisposable _colorAppliedSub;
private IDisposable _drawingSelectedSub;
private CancellationTokenSource _autosaveCts;
private CancellationTokenSource _scopeCts;
@@ -55,6 +58,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
ICaptureFeature capture,
ILoadingScreen loadingScreen,
IGameplaySceneRefs refs,
ISfxPlayer sfx,
IEventBus bus)
{
_progression = progression;
@@ -65,6 +69,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_capture = capture;
_loadingScreen = loadingScreen;
_refs = refs;
_sfx = sfx;
_bus = bus;
}
@@ -89,9 +94,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
_colorAppliedSub = _bus.Subscribe<ColorAppliedSignal>(OnColorApplied);
Application.quitting += OnAppQuitting;
Application.focusChanged += OnAppFocusChanged;
_drawingSelectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
_loadingScreen.SetProgress(1f);
if (_phase == DrawingPhase.Coloring)
@@ -127,6 +130,8 @@ namespace Darkmatter.Features.GameplayFlow.Systems
{
await SaveCurrentAsync(ct);
_refs.Confetti.Play();
_sfx.Play(SfxId.FireWorkLaunch);
_sfx.Play(SfxId.LevelComplete);
await _coloring.PlayCompletionAnimationAsync(ct);
_progression.MarkCompleted(_templateId);
@@ -149,17 +154,6 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_bus.Publish(new DrawingSelectedSignal(nextId));
}
public void OnApplicationPaused()
{
SaveCurrentAsync(CancellationToken.None).Forget();
}
private void OnAppQuitting() => OnApplicationPaused();
private void OnAppFocusChanged(bool focused)
{
if (!focused) OnApplicationPaused();
}
private void OnShapeAssembled(ShapeAssembledSignal signal)
{
@@ -188,6 +182,24 @@ namespace Darkmatter.Features.GameplayFlow.Systems
DebouncedAutosaveAsync(_autosaveCts.Token).Forget();
}
private void OnDrawingSelected(DrawingSelectedSignal signal)
{
if (string.IsNullOrEmpty(signal.TemplateId)) return;
if (signal.TemplateId == _templateId) return;
EditAsync(signal.TemplateId).Forget();
}
private async UniTaskVoid EditAsync(string newTemplateId)
{
await SaveCurrentAsync(CancellationToken.None);
_loadingScreen.Show();
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: default);
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: default);
_bus.Publish(new DrawingSelectedSignal(newTemplateId));
}
private async UniTaskVoid DebouncedAutosaveAsync(CancellationToken ct)
{
try
@@ -241,11 +253,10 @@ namespace Darkmatter.Features.GameplayFlow.Systems
public void Dispose()
{
Application.quitting -= OnAppQuitting;
Application.focusChanged -= OnAppFocusChanged;
_assembledSub?.Dispose();
_colorAppliedSub?.Dispose();
_drawingSelectedSub?.Dispose();
_autosaveCts?.Cancel();
_autosaveCts?.Dispose();
_scopeCts?.Cancel();

View File

@@ -1,8 +1,9 @@
using System;
using Darkmatter.Core.Contracts.Services.Audio;
using Darkmatter.Core.Data.Signals.Features.AppBoot;
using Darkmatter.Core.Data.Signals.Features.MainMenu;
using Darkmatter.Core.Enums.Services.Audio;
using Darkmatter.Libs.Observer;
using UnityEngine;
using VContainer.Unity;
namespace Darkmatter.Features.Mainmenu
@@ -11,17 +12,19 @@ namespace Darkmatter.Features.Mainmenu
{
private readonly IEventBus _eventBus;
private readonly MainMenuView _view;
private readonly ISfxPlayer _sfxPlayer;
public MainMenuPresenter(MainMenuView view, IEventBus eventBus)
public MainMenuPresenter(MainMenuView view, IEventBus eventBus, ISfxPlayer sfxPlayer)
{
_view = view;
_eventBus = eventBus;
_sfxPlayer = sfxPlayer;
}
public void Start()
{
_view.OnPlayBtnPressedEvent += OnPlayBtnPressed;
_view.OnPlayBtnClickedEvent += OnPlayBtnClicked;
_eventBus.Subscribe<IntroCompletedSignal>(OnIntroComplete);
}
@@ -31,6 +34,11 @@ namespace Darkmatter.Features.Mainmenu
_view.PlayMascotIntro();
}
private void OnPlayBtnPressed()
{
_sfxPlayer.Play(SfxId.PlayButtonTap);
}
private void OnPlayBtnClicked()
{
_eventBus.Publish(new PlayBtnClickedSignal());
@@ -38,6 +46,7 @@ namespace Darkmatter.Features.Mainmenu
public void Dispose()
{
_view.OnPlayBtnPressedEvent -= OnPlayBtnPressed;
_view.OnPlayBtnClickedEvent -= OnPlayBtnClicked;
}
}

View File

@@ -10,7 +10,6 @@ namespace Darkmatter.Features.Mainmenu
public class MainMenuView : MonoBehaviour
{
[Header("UI Elements")]
//[SerializeField] private Button playBtn;
[SerializeField] private PlayButtonView playBtn;
[SerializeField] private SkeletonGraphic mascotSkeletonGraphic;
@@ -20,12 +19,14 @@ namespace Darkmatter.Features.Mainmenu
[SerializeField] private string helloAnimation = "hello";
[SerializeField] private float helloInterval = 5f;
public event Action OnPlayBtnPressedEvent;
public event Action OnPlayBtnClickedEvent;
private CancellationTokenSource helloCts;
private void Start()
{
playBtn.OnPlayBtnPressedEvent += () => OnPlayBtnPressedEvent?.Invoke();
playBtn.OnPlayBtnClickedEvent += () => OnPlayBtnClickedEvent?.Invoke();
}

View File

@@ -12,6 +12,7 @@ namespace Darkmatter.Features.Mainmenu
private Animator playBtnAnimator;
[SerializeField] private ParticleSystem playBtnParticle;
public event Action OnPlayBtnPressedEvent;
public event Action OnPlayBtnClickedEvent;
private void Awake()
@@ -29,6 +30,7 @@ namespace Darkmatter.Features.Mainmenu
{
playBtn.interactable = false;
playBtnAnimator.enabled = false;
OnPlayBtnPressedEvent?.Invoke();
PlayBtnSequenceAsync(this.GetCancellationTokenOnDestroy()).Forget();
}

View File

@@ -1,4 +1,5 @@
using System;
using Darkmatter.Core;
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
using Darkmatter.Libs.Observer;
using VContainer.Unity;
@@ -24,12 +25,19 @@ namespace Darkmatter.Features.ShapeBuilder.UI
_view.HideInstant();
_startedSub = _bus.Subscribe<ShapeBuilderStartedSignal>(_ => _view.Show());
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(_ => _view.Hide());
_view.OnArtbookClicked += HandleArtbookClicked;
}
private void HandleArtbookClicked()
{
_bus.Publish(new OpenArtBookSignal());
}
public void Dispose()
{
_startedSub?.Dispose();
_assembledSub?.Dispose();
_view.OnArtbookClicked -= HandleArtbookClicked;
}
}
}

View File

@@ -1,5 +1,8 @@
using System;
using Darkmatter.Features.ShapeBuilder.Systems;
using PrimeTween;
using UnityEngine;
using UnityEngine.UI;
namespace Darkmatter.Features.ShapeBuilder.UI
{
@@ -8,6 +11,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
{
[SerializeField] private RectTransform spawnRoot;
[SerializeField] private RectTransform animatedRoot;
[SerializeField] private Button artbookButton;
[SerializeField] private float showDuration = 0.35f;
[SerializeField] private float hideDuration = 0.25f;
[SerializeField] private Vector2 hiddenOffset = new(1500f, 0f);
@@ -15,6 +19,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
private CanvasGroup _canvasGroup;
private Vector2 _shownAnchoredPos;
private Sequence _activeSequence;
public event Action OnArtbookClicked;
public RectTransform SpawnRoot => spawnRoot;
public float SpawnWidth => spawnRoot.rect.width;
@@ -24,6 +29,12 @@ namespace Darkmatter.Features.ShapeBuilder.UI
_canvasGroup = GetComponent<CanvasGroup>();
if (animatedRoot == null) animatedRoot = (RectTransform)transform;
_shownAnchoredPos = animatedRoot.anchoredPosition;
artbookButton.onClick.AddListener(HandleArtbookClicked);
}
private void HandleArtbookClicked()
{
OnArtbookClicked?.Invoke();
}
public Sequence Show()
@@ -73,5 +84,10 @@ namespace Darkmatter.Features.ShapeBuilder.UI
{
KillActive();
}
private void OnDestroy()
{
artbookButton.onClick.RemoveListener(HandleArtbookClicked);
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
using Darkmatter.Core.Contracts.Services.Ads;
using Darkmatter.Core.Data.Static.Services.Ads;
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
using VContainer.Unity;
namespace Darkmatter.Services.Ads
{
public class AdServiceModule : MonoBehaviour, IModule
{
[SerializeField] private AdUnitCatalogSO adUnitCatalog;
[SerializeField] private AdMobAdService adService;
public void Register(IContainerBuilder builder)
{
if (adUnitCatalog != null)
builder.RegisterComponent(adUnitCatalog);
var resolved = adService;
if (resolved == null) resolved = GetComponent<AdMobAdService>();
if (resolved == null) resolved = GetComponentInChildren<AdMobAdService>(includeInactive: true);
if (resolved == null) resolved = FindFirstObjectByType<AdMobAdService>(FindObjectsInactive.Include);
if (resolved != null)
builder.RegisterComponent<IAdService>(resolved);
else
Debug.LogError("[AdServiceModule] No AdMobAdService component found. Assign 'adService' field or add AdMobAdService MonoBehaviour to scene.");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a17cb41935543a6b4f67be8f398d774
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,25 @@
{
"name": "Services.Ads",
"rootNamespace": "Darkmatter.Services.Ads",
"references": [
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
"GUID:f51ebe6a0ceec4240a699833d6309b23",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.google.ads.mobile",
"expression": "1.0.0",
"define": "GOOGLE_MOBILE_ADS"
}
],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 662a8e56a1724c84ad8c10211211f0d5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,535 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Services.Ads;
using Darkmatter.Core.Data.Dynamic.Services.Ads;
using Darkmatter.Core.Data.Static.Services.Ads;
using Darkmatter.Core.Enums.Services.Ads;
using UnityEngine;
using AdFormat = Darkmatter.Core.Enums.Services.Ads.AdFormat;
#if GOOGLE_MOBILE_ADS
using GoogleMobileAds.Api;
#endif
namespace Darkmatter.Services.Ads
{
public class AdMobAdService : MonoBehaviour, IAdService
{
[SerializeField] private AdUnitCatalogSO catalog;
[Tooltip("Auto-reload after dismiss/failure for non-banner formats.")]
[SerializeField] private bool autoReload = true;
[Tooltip("Seconds between auto-reload retries on failure.")]
[SerializeField, Min(1f)] private float reloadDelaySeconds = 5f;
public bool IsInitialized => _initialized;
public event Action<AdFormat, AdLoadState> LoadStateChanged;
private bool _initialized;
private bool _hasUserConsent = true;
private bool _isChildDirected;
private CancellationTokenSource _lifetimeCts;
private readonly Dictionary<AdFormat, AdLoadState> _states = new();
#if GOOGLE_MOBILE_ADS
private InterstitialAd _interstitial;
private RewardedAd _rewarded;
private RewardedInterstitialAd _rewardedInterstitial;
private AppOpenAd _appOpen;
private BannerView _banner;
#endif
private void Awake()
{
_lifetimeCts = new CancellationTokenSource();
foreach (AdFormat fmt in Enum.GetValues(typeof(AdFormat)))
_states[fmt] = AdLoadState.Idle;
}
private void OnDestroy()
{
_lifetimeCts?.Cancel();
_lifetimeCts?.Dispose();
_lifetimeCts = null;
#if GOOGLE_MOBILE_ADS
_interstitial?.Destroy();
_rewarded?.Destroy();
_rewardedInterstitial?.Destroy();
_appOpen?.Destroy();
_banner?.Destroy();
#endif
}
public async UniTask InitializeAsync(CancellationToken cancellationToken)
{
if (_initialized) return;
if (catalog == null)
{
Debug.LogError("[AdMobAdService] No AdUnitCatalogSO assigned.");
return;
}
#if GOOGLE_MOBILE_ADS
ApplyRequestConfiguration();
var tcs = new UniTaskCompletionSource<bool>();
MobileAds.Initialize(_ => tcs.TrySetResult(true));
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
await tcs.Task;
}
_initialized = true;
Debug.Log("[AdMobAdService] Initialized.");
#else
await UniTask.CompletedTask;
_initialized = true;
Debug.LogWarning("[AdMobAdService] GOOGLE_MOBILE_ADS not defined. Service will no-op. Add scripting define after importing Google Mobile Ads SDK.");
#endif
}
public async UniTask<bool> LoadAsync(AdFormat format, CancellationToken cancellationToken)
{
if (!_initialized)
{
Debug.LogWarning("[AdMobAdService] Load called before init.");
return false;
}
#if GOOGLE_MOBILE_ADS
string unitId = catalog.GetUnitId(format, Application.platform);
if (string.IsNullOrEmpty(unitId))
{
Debug.LogWarning($"[AdMobAdService] No unit ID for {format} on {Application.platform}.");
return false;
}
SetState(format, AdLoadState.Loading);
var request = new AdRequest();
switch (format)
{
case AdFormat.Interstitial:
return await LoadInterstitialAsync(unitId, request, cancellationToken);
case AdFormat.Rewarded:
return await LoadRewardedAsync(unitId, request, cancellationToken);
case AdFormat.RewardedInterstitial:
return await LoadRewardedInterstitialAsync(unitId, request, cancellationToken);
case AdFormat.AppOpen:
return await LoadAppOpenAsync(unitId, request, cancellationToken);
case AdFormat.Banner:
Debug.LogWarning("[AdMobAdService] Use ShowBannerAsync for banners.");
SetState(format, AdLoadState.Failed);
return false;
}
return false;
#else
await UniTask.CompletedTask;
SetState(format, AdLoadState.Failed);
return false;
#endif
}
public bool IsReady(AdFormat format)
{
if (!_initialized) return false;
#if GOOGLE_MOBILE_ADS
return format switch
{
AdFormat.Interstitial => _interstitial != null && _interstitial.CanShowAd(),
AdFormat.Rewarded => _rewarded != null && _rewarded.CanShowAd(),
AdFormat.RewardedInterstitial => _rewardedInterstitial != null && _rewardedInterstitial.CanShowAd(),
AdFormat.AppOpen => _appOpen != null && _appOpen.CanShowAd(),
_ => false
};
#else
return false;
#endif
}
public async UniTask<AdShowResult> ShowAsync(AdFormat format, CancellationToken cancellationToken)
{
if (!_initialized) return AdShowResult.Failure("Not initialized.");
#if GOOGLE_MOBILE_ADS
if (!IsReady(format))
{
bool loaded = await LoadAsync(format, cancellationToken);
if (!loaded) return AdShowResult.Failure($"{format} failed to load.");
}
switch (format)
{
case AdFormat.Interstitial: return await ShowInterstitialAsync(cancellationToken);
case AdFormat.Rewarded: return await ShowRewardedAsync(cancellationToken);
case AdFormat.RewardedInterstitial: return await ShowRewardedInterstitialAsync(cancellationToken);
case AdFormat.AppOpen: return await ShowAppOpenAsync(cancellationToken);
}
return AdShowResult.Failure($"{format} not supported by ShowAsync.");
#else
await UniTask.CompletedTask;
return AdShowResult.Failure("GOOGLE_MOBILE_ADS not defined.");
#endif
}
public async UniTask<bool> ShowBannerAsync(BannerSize size, BannerPosition position, CancellationToken cancellationToken)
{
if (!_initialized) return false;
#if GOOGLE_MOBILE_ADS
string unitId = catalog.GetUnitId(AdFormat.Banner, Application.platform);
if (string.IsNullOrEmpty(unitId)) return false;
_banner?.Destroy();
_banner = new BannerView(unitId, MapBannerSize(size), MapBannerPosition(position));
var tcs = new UniTaskCompletionSource<bool>();
_banner.OnBannerAdLoaded += () => tcs.TrySetResult(true);
_banner.OnBannerAdLoadFailed += err =>
{
Debug.LogWarning($"[AdMobAdService] Banner load failed: {err}");
tcs.TrySetResult(false);
};
SetState(AdFormat.Banner, AdLoadState.Loading);
_banner.LoadAd(new AdRequest());
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.Banner, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
#else
await UniTask.CompletedTask;
return false;
#endif
}
public void HideBanner()
{
#if GOOGLE_MOBILE_ADS
_banner?.Hide();
#endif
}
public void DestroyBanner()
{
#if GOOGLE_MOBILE_ADS
_banner?.Destroy();
_banner = null;
SetState(AdFormat.Banner, AdLoadState.Idle);
#endif
}
public void SetConsent(bool hasUserConsent, bool isChildDirected)
{
_hasUserConsent = hasUserConsent;
_isChildDirected = isChildDirected;
#if GOOGLE_MOBILE_ADS
if (_initialized) ApplyRequestConfiguration();
#endif
}
#if GOOGLE_MOBILE_ADS
private void ApplyRequestConfiguration()
{
var config = new RequestConfiguration
{
TagForChildDirectedTreatment = _isChildDirected
? TagForChildDirectedTreatment.True
: TagForChildDirectedTreatment.Unspecified,
TagForUnderAgeOfConsent = _hasUserConsent
? TagForUnderAgeOfConsent.Unspecified
: TagForUnderAgeOfConsent.True
};
if (catalog.TestDeviceIds is { Count: > 0 })
{
config.TestDeviceIds = new List<string>(catalog.TestDeviceIds);
}
MobileAds.SetRequestConfiguration(config);
}
private async UniTask<bool> LoadInterstitialAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<bool>();
InterstitialAd.Load(unitId, request, (ad, error) =>
{
if (error != null || ad == null)
{
Debug.LogWarning($"[AdMobAdService] Interstitial load failed: {error}");
tcs.TrySetResult(false);
return;
}
_interstitial?.Destroy();
_interstitial = ad;
WireFullScreenEvents(ad, AdFormat.Interstitial);
tcs.TrySetResult(true);
});
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.Interstitial, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
}
private async UniTask<bool> LoadRewardedAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<bool>();
RewardedAd.Load(unitId, request, (ad, error) =>
{
if (error != null || ad == null)
{
Debug.LogWarning($"[AdMobAdService] Rewarded load failed: {error}");
tcs.TrySetResult(false);
return;
}
_rewarded?.Destroy();
_rewarded = ad;
WireFullScreenEvents(ad, AdFormat.Rewarded);
tcs.TrySetResult(true);
});
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.Rewarded, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
}
private async UniTask<bool> LoadRewardedInterstitialAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<bool>();
RewardedInterstitialAd.Load(unitId, request, (ad, error) =>
{
if (error != null || ad == null)
{
Debug.LogWarning($"[AdMobAdService] RewardedInterstitial load failed: {error}");
tcs.TrySetResult(false);
return;
}
_rewardedInterstitial?.Destroy();
_rewardedInterstitial = ad;
WireFullScreenEvents(ad, AdFormat.RewardedInterstitial);
tcs.TrySetResult(true);
});
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.RewardedInterstitial, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
}
private async UniTask<bool> LoadAppOpenAsync(string unitId, AdRequest request, CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<bool>();
AppOpenAd.Load(unitId, request, (ad, error) =>
{
if (error != null || ad == null)
{
Debug.LogWarning($"[AdMobAdService] AppOpen load failed: {error}");
tcs.TrySetResult(false);
return;
}
_appOpen?.Destroy();
_appOpen = ad;
WireFullScreenEvents(ad, AdFormat.AppOpen);
tcs.TrySetResult(true);
});
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
{
bool ok = await tcs.Task;
SetState(AdFormat.AppOpen, ok ? AdLoadState.Loaded : AdLoadState.Failed);
return ok;
}
}
private async UniTask<AdShowResult> ShowInterstitialAsync(CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<AdShowResult>();
Action onClosed = () => tcs.TrySetResult(AdShowResult.Success());
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
_interstitial.OnAdFullScreenContentClosed += onClosed;
_interstitial.OnAdFullScreenContentFailed += onFailed;
_interstitial.Show();
try
{
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
return await tcs.Task;
}
finally
{
if (_interstitial != null)
{
_interstitial.OnAdFullScreenContentClosed -= onClosed;
_interstitial.OnAdFullScreenContentFailed -= onFailed;
}
ScheduleReload(AdFormat.Interstitial);
}
}
private async UniTask<AdShowResult> ShowRewardedAsync(CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<AdShowResult>();
bool earned = false;
AdReward reward = default;
Action onClosed = () => tcs.TrySetResult(earned ? AdShowResult.WithReward(reward) : AdShowResult.Success());
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
_rewarded.OnAdFullScreenContentClosed += onClosed;
_rewarded.OnAdFullScreenContentFailed += onFailed;
_rewarded.Show(r =>
{
earned = true;
reward = new AdReward(r.Type, r.Amount);
});
try
{
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
return await tcs.Task;
}
finally
{
if (_rewarded != null)
{
_rewarded.OnAdFullScreenContentClosed -= onClosed;
_rewarded.OnAdFullScreenContentFailed -= onFailed;
}
ScheduleReload(AdFormat.Rewarded);
}
}
private async UniTask<AdShowResult> ShowRewardedInterstitialAsync(CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<AdShowResult>();
bool earned = false;
AdReward reward = default;
Action onClosed = () => tcs.TrySetResult(earned ? AdShowResult.WithReward(reward) : AdShowResult.Success());
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
_rewardedInterstitial.OnAdFullScreenContentClosed += onClosed;
_rewardedInterstitial.OnAdFullScreenContentFailed += onFailed;
_rewardedInterstitial.Show(r =>
{
earned = true;
reward = new AdReward(r.Type, r.Amount);
});
try
{
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
return await tcs.Task;
}
finally
{
if (_rewardedInterstitial != null)
{
_rewardedInterstitial.OnAdFullScreenContentClosed -= onClosed;
_rewardedInterstitial.OnAdFullScreenContentFailed -= onFailed;
}
ScheduleReload(AdFormat.RewardedInterstitial);
}
}
private async UniTask<AdShowResult> ShowAppOpenAsync(CancellationToken cancellationToken)
{
var tcs = new UniTaskCompletionSource<AdShowResult>();
Action onClosed = () => tcs.TrySetResult(AdShowResult.Success());
Action<AdError> onFailed = err => tcs.TrySetResult(AdShowResult.Failure(err?.GetMessage()));
_appOpen.OnAdFullScreenContentClosed += onClosed;
_appOpen.OnAdFullScreenContentFailed += onFailed;
_appOpen.Show();
try
{
using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
return await tcs.Task;
}
finally
{
if (_appOpen != null)
{
_appOpen.OnAdFullScreenContentClosed -= onClosed;
_appOpen.OnAdFullScreenContentFailed -= onFailed;
}
ScheduleReload(AdFormat.AppOpen);
}
}
private void WireFullScreenEvents(InterstitialAd ad, AdFormat format) =>
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
private void WireFullScreenEvents(RewardedAd ad, AdFormat format) =>
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
private void WireFullScreenEvents(RewardedInterstitialAd ad, AdFormat format) =>
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
private void WireFullScreenEvents(AppOpenAd ad, AdFormat format) =>
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
private void ScheduleReload(AdFormat format)
{
if (!autoReload || _lifetimeCts == null) return;
ReloadAfterDelayAsync(format, _lifetimeCts.Token).Forget();
}
private async UniTaskVoid ReloadAfterDelayAsync(AdFormat format, CancellationToken cancellationToken)
{
try
{
await UniTask.Delay(TimeSpan.FromSeconds(reloadDelaySeconds), cancellationToken: cancellationToken);
await LoadAsync(format, cancellationToken);
}
catch (OperationCanceledException) { }
}
private static AdSize MapBannerSize(BannerSize size) => size switch
{
BannerSize.Banner => AdSize.Banner,
BannerSize.LargeBanner => AdSize.LargeBanner,
BannerSize.MediumRectangle => AdSize.MediumRectangle,
BannerSize.FullBanner => AdSize.IABBanner,
BannerSize.Leaderboard => AdSize.Leaderboard,
BannerSize.SmartBanner => AdSize.SmartBanner,
BannerSize.AnchoredAdaptive => AdSize.GetCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(AdSize.FullWidth),
_ => AdSize.Banner
};
private static AdPosition MapBannerPosition(BannerPosition position) => position switch
{
BannerPosition.Top => AdPosition.Top,
BannerPosition.Bottom => AdPosition.Bottom,
BannerPosition.TopLeft => AdPosition.TopLeft,
BannerPosition.TopRight => AdPosition.TopRight,
BannerPosition.BottomLeft => AdPosition.BottomLeft,
BannerPosition.BottomRight => AdPosition.BottomRight,
BannerPosition.Center => AdPosition.Center,
_ => AdPosition.Bottom
};
#endif
private void SetState(AdFormat format, AdLoadState state)
{
_states[format] = state;
LoadStateChanged?.Invoke(format, state);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 018ba83ad51c4d9a89a904ba3d21ead2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -10,12 +10,24 @@ namespace Darkmatter.Services.Audio
public class AudioServiceModule : MonoBehaviour, IModule
{
[SerializeField] private SfxCatalogSO sfxCatalog;
[SerializeField] private AudioService audioService;
public void Register(IContainerBuilder builder)
{
if (sfxCatalog != null)
builder.RegisterComponent(sfxCatalog);
builder.Register<IAudioService, AudioService>(Lifetime.Singleton);
if (sfxCatalog == null)
{
Debug.LogError("[AudioServiceModule] SfxCatalogSO not assigned.");
return;
}
builder.RegisterComponent(sfxCatalog);
if (audioService == null)
{
Debug.LogError("[AudioServiceModule] AudioService not assigned. Assign the scene component to the 'audioService' field.");
return;
}
builder.RegisterComponent<IAudioService>(audioService);
builder.Register<ISfxPlayer, SfxPlayer>(Lifetime.Singleton);
}
}

View File

@@ -72,8 +72,18 @@ namespace Darkmatter.Services.Audio
private Dictionary<AudioChannel, AudioMixerGroup> _mixerGroups;
private float[] _currentWeights;
private bool _initialized;
private void Awake()
{
InitializeAsync(default).Forget();
}
public UniTask InitializeAsync(CancellationToken cancellationToken)
{
if (_initialized) return UniTask.CompletedTask;
_initialized = true;
if (sourceRoot == null) sourceRoot = transform;
_lifetimeCts = new CancellationTokenSource();
@@ -336,8 +346,11 @@ namespace Darkmatter.Services.Audio
source.loop = request.PlayMode == AudioPlayMode.Loop;
source.transform.localPosition = Vector3.zero;
source.enabled = true;
if (!source.gameObject.activeInHierarchy) source.gameObject.SetActive(true);
source.Play();
Debug.Log($"[AudioService] Play clip='{(request.Clip != null ? request.Clip.name : "null")}' ch={request.Channel} vol={source.volume:F2} mixer={(source.outputAudioMixerGroup != null ? source.outputAudioMixerGroup.name : "none")} listener={(AudioListener.volume)} muted={AudioListener.pause} isPlaying={source.isPlaying}");
if (IsChannelPaused(request.Channel))
{
source.Pause();

View File

@@ -1,73 +1,115 @@
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Services.Camera;
using Darkmatter.Core.Contracts.Services.Capture;
using UnityEngine;
using CameraType = Darkmatter.Core.Enums.Services.Camera.CameraType;
namespace Darkmatter.Services.Capture
{
public class CaptureService : ICaptureService
{
private readonly ICameraService _cameraService;
public CaptureService(ICameraService cameraService) => _cameraService = cameraService;
public async UniTask<byte[]> CapturePngAsync(GameObject captureObject, float scale,
CancellationToken cancellationToken = default)
{
if (captureObject == null) return null;
var paperRT = captureObject.transform as RectTransform;
var paperCanvas = GetRootCanvas(captureObject);
var cam = _cameraService.GetCamera(CameraType.CaptureCamera);
if (paperCanvas == null || paperRT == null || cam == null) return null;
int sw = Screen.width;
int sh = Screen.height;
var disabledCanvases = DisableOtherRootCanvases(paperCanvas);
var cam = Camera.main;
CameraClearFlags prevFlags = default;
Color prevBg = default;
if (cam != null)
{
prevFlags = cam.clearFlags;
prevBg = cam.backgroundColor;
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0f, 0f, 0f, 0f);
}
var disabledGraphics = HideNonPaperGraphics(paperRT);
await UniTask.WaitForEndOfFrame(cancellationToken);
Rect crop = ComputeCropRect(captureObject, sw, sh);
int cropW = Mathf.Max(1, (int)crop.width);
int cropH = Mathf.Max(1, (int)crop.height);
var prevFlags = cam.clearFlags;
var prevBg = cam.backgroundColor;
var prevTarget = cam.targetTexture;
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0f, 0f, 0f, 0f);
var rt = RenderTexture.GetTemporary(sw, sh, 24, RenderTextureFormat.ARGB32);
cam.targetTexture = rt;
var prevMode = paperCanvas.renderMode;
var prevWorldCam = paperCanvas.worldCamera;
var prevPlaneDist = paperCanvas.planeDistance;
paperCanvas.renderMode = RenderMode.ScreenSpaceCamera;
paperCanvas.worldCamera = cam;
paperCanvas.planeDistance = Mathf.Max(0.5f, (cam.nearClipPlane + cam.farClipPlane) * 0.5f);
var fullScreen = ScreenCapture.CaptureScreenshotAsTexture();
try
{
Rect crop = ComputeCropRect(captureObject, fullScreen.width, fullScreen.height);
int cropW = Mathf.Max(1, (int)crop.width);
int cropH = Mathf.Max(1, (int)crop.height);
await UniTask.WaitForEndOfFrame(cancellationToken);
Canvas.ForceUpdateCanvases();
cam.Render();
var prevActive = RenderTexture.active;
RenderTexture.active = rt;
var fullScreen = new Texture2D(sw, sh, TextureFormat.RGBA32, mipChain: false);
fullScreen.ReadPixels(new Rect(0, 0, sw, sh), 0, 0);
fullScreen.Apply();
RenderTexture.active = prevActive;
var cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, mipChain: false);
try
{
cropped.SetPixels(fullScreen.GetPixels((int)crop.x, (int)crop.y, cropW, cropH));
cropped.Apply();
int targetW = Mathf.Max(1, Mathf.RoundToInt(cropW * scale));
int targetH = Mathf.Max(1, Mathf.RoundToInt(cropH * scale));
Texture2D output = cropped;
if (output.width != targetW || output.height != targetH)
output = Resize(cropped, targetW, targetH);
var cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, mipChain: false);
try
{
return output.EncodeToPNG();
cropped.SetPixels(fullScreen.GetPixels((int)crop.x, (int)crop.y, cropW, cropH));
cropped.Apply();
int targetW = Mathf.Max(1, Mathf.RoundToInt(cropW * scale));
int targetH = Mathf.Max(1, Mathf.RoundToInt(cropH * scale));
Texture2D output = cropped;
if (output.width != targetW || output.height != targetH)
output = Resize(cropped, targetW, targetH);
try
{
return output.EncodeToPNG();
}
finally
{
if (output != cropped) Object.Destroy(output);
}
}
finally
{
if (output != cropped) Object.Destroy(output);
Object.Destroy(cropped);
}
}
finally
{
Object.Destroy(cropped);
Object.Destroy(fullScreen);
}
}
finally
{
Object.Destroy(fullScreen);
foreach (var c in disabledCanvases) if (c != null) c.enabled = true;
if (cam != null)
{
cam.clearFlags = prevFlags;
cam.backgroundColor = prevBg;
}
paperCanvas.renderMode = prevMode;
paperCanvas.worldCamera = prevWorldCam;
paperCanvas.planeDistance = prevPlaneDist;
cam.targetTexture = prevTarget;
cam.clearFlags = prevFlags;
cam.backgroundColor = prevBg;
RenderTexture.ReleaseTemporary(rt);
foreach (var g in disabledGraphics)
if (g != null)
g.enabled = true;
foreach (var c in disabledCanvases)
if (c != null)
c.enabled = true;
}
}
@@ -88,6 +130,23 @@ namespace Darkmatter.Services.Capture
c.enabled = false;
disabled.Add(c);
}
return disabled;
}
private static List<UnityEngine.UI.Graphic> HideNonPaperGraphics(Transform paper)
{
var disabled = new List<UnityEngine.UI.Graphic>();
if (paper == null) return disabled;
var all = Object.FindObjectsByType<UnityEngine.UI.Graphic>(FindObjectsSortMode.None);
foreach (var g in all)
{
if (g == null || !g.enabled) continue;
if (g.transform.IsChildOf(paper)) continue;
g.enabled = false;
disabled.Add(g);
}
return disabled;
}
@@ -98,7 +157,6 @@ namespace Darkmatter.Services.Capture
var corners = new Vector3[4];
rt.GetWorldCorners(corners);
// ScreenSpaceOverlay canvas: world corners are already in screen pixels.
float minX = Mathf.Clamp(corners[0].x, 0, screenW);
float minY = Mathf.Clamp(corners[0].y, 0, screenH);
float maxX = Mathf.Clamp(corners[2].x, 0, screenW);

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
using Darkmatter.Core.Contracts.Services.Music;
using Darkmatter.Libs.Installers;
using Darkmatter.Services.Music.Systems;
using UnityEngine;
using UnityEngine.Audio;
using VContainer;
using VContainer.Unity;
namespace Darkmatter.Services.Music.Installers
{
public class MusicServiceModule : MonoBehaviour, IModule
{
[SerializeField] private AudioClip defaultTrack;
[SerializeField, Range(0f, 1f)] private float defaultVolume = 0.7f;
[SerializeField, Min(0f)] private float crossFadeSeconds = 0.4f;
[SerializeField] private AudioMixerGroup mixerGroup;
public void Register(IContainerBuilder builder)
{
builder.RegisterInstance(new MusicConfig(defaultTrack, defaultVolume, crossFadeSeconds, mixerGroup));
builder.RegisterEntryPoint<MusicService>(Lifetime.Singleton).As<IMusicService>();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 47cce31d8e31341a0ae46684e87cbd95

View File

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

View File

@@ -0,0 +1,23 @@
using System;
using UnityEngine;
using UnityEngine.Audio;
namespace Darkmatter.Services.Music.Systems
{
[Serializable]
public readonly struct MusicConfig
{
public AudioClip DefaultTrack { get; }
public float DefaultVolume { get; }
public float CrossFadeSeconds { get; }
public AudioMixerGroup MixerGroup { get; }
public MusicConfig(AudioClip defaultTrack, float defaultVolume, float crossFadeSeconds, AudioMixerGroup mixerGroup)
{
DefaultTrack = defaultTrack;
DefaultVolume = defaultVolume;
CrossFadeSeconds = crossFadeSeconds;
MixerGroup = mixerGroup;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4c15bf1ed26cf4c8f8339b22cb57efc6

View File

@@ -0,0 +1,146 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Services.Music;
using Darkmatter.Core.Data.Signals.Features.AppBoot;
using Darkmatter.Libs.Observer;
using UnityEngine;
using VContainer.Unity;
namespace Darkmatter.Services.Music.Systems
{
public class MusicService : IMusicService, IStartable, IDisposable
{
private readonly IEventBus _bus;
private readonly MusicConfig _config;
private GameObject _host;
private AudioSource _source;
private IDisposable _introSub;
private CancellationTokenSource _fadeCts;
public MusicService(IEventBus bus, MusicConfig config)
{
_bus = bus;
_config = config;
}
public void Start()
{
_host = new GameObject("[MusicService]");
UnityEngine.Object.DontDestroyOnLoad(_host);
_source = _host.AddComponent<AudioSource>();
_source.playOnAwake = false;
_source.loop = true;
_source.spatialBlend = 0f;
_source.volume = _config.DefaultVolume;
if (_config.MixerGroup != null) _source.outputAudioMixerGroup = _config.MixerGroup;
_introSub = _bus.Subscribe<IntroCompletedSignal>(OnIntroCompleted);
}
private void OnIntroCompleted(IntroCompletedSignal _)
{
if (_config.DefaultTrack != null && (_source == null || !_source.isPlaying))
Play(_config.DefaultTrack, _config.DefaultVolume, loop: true);
}
public void Play(AudioClip clip, float volume01 = 1f, bool loop = true)
{
if (clip == null || _source == null) return;
CancelFade();
if (_source.isPlaying && _config.CrossFadeSeconds > 0f)
{
_fadeCts = new CancellationTokenSource();
CrossFadeAsync(clip, Mathf.Clamp01(volume01), loop, _fadeCts.Token).Forget();
return;
}
_source.clip = clip;
_source.loop = loop;
_source.volume = Mathf.Clamp01(volume01);
_source.Play();
}
public void Stop(float fadeOutSeconds = 0f)
{
if (_source == null) return;
CancelFade();
if (fadeOutSeconds <= 0f) { _source.Stop(); return; }
_fadeCts = new CancellationTokenSource();
FadeOutAsync(fadeOutSeconds, _fadeCts.Token).Forget();
}
public void SetVolume(float volume01)
{
if (_source == null) return;
_source.volume = Mathf.Clamp01(volume01);
}
private async UniTaskVoid CrossFadeAsync(AudioClip nextClip, float targetVolume, bool loop, CancellationToken ct)
{
try
{
float start = _source.volume;
float t = 0f;
float dur = _config.CrossFadeSeconds;
while (t < dur)
{
ct.ThrowIfCancellationRequested();
t += Time.unscaledDeltaTime;
_source.volume = Mathf.Lerp(start, 0f, t / dur);
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}
_source.Stop();
_source.clip = nextClip;
_source.loop = loop;
_source.volume = 0f;
_source.Play();
t = 0f;
while (t < dur)
{
ct.ThrowIfCancellationRequested();
t += Time.unscaledDeltaTime;
_source.volume = Mathf.Lerp(0f, targetVolume, t / dur);
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}
_source.volume = targetVolume;
}
catch (OperationCanceledException) { }
}
private async UniTaskVoid FadeOutAsync(float seconds, CancellationToken ct)
{
try
{
float start = _source.volume;
float t = 0f;
while (t < seconds)
{
ct.ThrowIfCancellationRequested();
t += Time.unscaledDeltaTime;
_source.volume = Mathf.Lerp(start, 0f, t / seconds);
await UniTask.Yield(PlayerLoopTiming.Update, ct);
}
_source.Stop();
_source.volume = start;
}
catch (OperationCanceledException) { }
}
private void CancelFade()
{
_fadeCts?.Cancel();
_fadeCts?.Dispose();
_fadeCts = null;
}
public void Dispose()
{
CancelFade();
_introSub?.Dispose();
if (_host != null) UnityEngine.Object.Destroy(_host);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8b4c07c26f9a44409bd43d2777b9951d

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
Congrats! Now you have the FREE CASUAL GAME SFX
This is the FREE SFX solucion for your casual games!
*designed by Matheus Klein.
------>>>>> Do you want a GIFT? <<<<<------------
Subscribe here and download it:
https://lp.labmdk.com/free-gift
------>>>>> <<<<<------------
Leave for us your Review!
------>>>>> <<<<<------------
Check It out:
--> Casual-pack-SFX
https://assetstore.unity.com/packages/audio/sound-fx/casual-pack-sfx-192848?_ga=2.42343461.1670194729.1622553216-1748727751.1620872230
--> Casual-pack- MUSIC
https://assetstore.unity.com/packages/audio/music/casual-pack-music-193478?_ga=2.42343461.1670194729.1622553216-1748727751.1620872230
--> Casual-pack-COMPLETE
https://assetstore.unity.com/packages/audio/music/casual-pack-complete-196775
------>>>>> <<<<<------------
--> We created this product with enthusiasm and dedication. Enjoy it!
--> If you have any problem or if you are searching for customized assets
feel free to contact us:
- www.labmdk.com
- contato@labmdk.com
-->For more, follow us:
@matheus_klein.mk
@3dk_art
---> Looking for an original soundtrack for your custom project?
What can I offer?
Composing exclusive music, soundtrack, filmscore for film, video game, trailers, apps and advertisement.
Any music genre: cinematic, orchestral, Rock, metal, jazz, hip-hop, EDM, ambient, 8 bit etc.
Realistic sound. I work with virtual instruments and some real instruments.
We're looking forward to working with you again.

View File

@@ -0,0 +1,15 @@
fileFormatVersion: 2
guid: 8dc69ef1d5b95dd48b0e5599b3de71d0
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 197054
packageName: FREE CASUAL PACK SFX
packageVersion: 2.0
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/(README) WELCOME_FREE CASUAL PACK
SFX.txt
uploadId: 550051

View File

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

View File

@@ -0,0 +1,29 @@
fileFormatVersion: 2
guid: cbd5ee476a488444aaeb06b7e1fb04e0
AudioImporter:
externalObjects: {}
serializedVersion: 6
defaultSettings:
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
preloadAudioData: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 197054
packageName: FREE CASUAL PACK SFX
packageVersion: 2.0
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/10 - BONUS/Casual loop 1.wav
uploadId: 550051

View File

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

View File

@@ -0,0 +1,29 @@
fileFormatVersion: 2
guid: 36a97d152a07e9f489c73b24e2870cf0
AudioImporter:
externalObjects: {}
serializedVersion: 6
defaultSettings:
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
preloadAudioData: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 197054
packageName: FREE CASUAL PACK SFX
packageVersion: 2.0
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/CAMERA CLICKS/CAMERA CLICK 1.wav
uploadId: 550051

View File

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

View File

@@ -0,0 +1,29 @@
fileFormatVersion: 2
guid: 4b5dbd3527221224caf2fde9d72cddb7
AudioImporter:
externalObjects: {}
serializedVersion: 6
defaultSettings:
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
preloadAudioData: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 197054
packageName: FREE CASUAL PACK SFX
packageVersion: 2.0
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/CARDS/CARDS 1.wav
uploadId: 550051

View File

@@ -0,0 +1,29 @@
fileFormatVersion: 2
guid: 4f3fe7a1f92f1e94db61865f48491057
AudioImporter:
externalObjects: {}
serializedVersion: 6
defaultSettings:
loadType: 0
sampleRateSetting: 0
sampleRateOverride: 44100
compressionFormat: 1
quality: 1
conversionMode: 0
platformSettingOverrides: {}
forceToMono: 0
normalize: 1
preloadAudioData: 1
loadInBackground: 0
ambisonic: 0
3D: 1
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 197054
packageName: FREE CASUAL PACK SFX
packageVersion: 2.0
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/CARDS/CARDS 2.wav
uploadId: 550051

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 2a78ea6479568174996fe4ab6d489c16
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 197054
packageName: FREE CASUAL PACK SFX
packageVersion: 2.0
assetPath: Assets/1.1 - FREE CASUAL PACK SFX/CASUAL GAME SFX FREE_2.0.pdf
uploadId: 550051

View File

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

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