Bus and crash fixes UX improvements rewarded ads added
This commit is contained in:
@@ -5,6 +5,7 @@ using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core;
|
||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||
using Darkmatter.Core.Contracts.Features.Progression;
|
||||
using Darkmatter.Core.Contracts.Features.SaveGate;
|
||||
using Darkmatter.Core.Contracts.Services.Gallery;
|
||||
using Darkmatter.Core.Data.Signals.Features.Drawing;
|
||||
using Darkmatter.Libs.Observer;
|
||||
@@ -22,6 +23,7 @@ namespace Darkmatter.Features.Artbook
|
||||
private readonly IProgressionSystem _progression;
|
||||
private readonly IDrawingTemplateCatalog _catalog;
|
||||
private readonly IGalleryService _gallery;
|
||||
private readonly IRewardedSaveGate _saveGate;
|
||||
|
||||
private readonly List<ArtbookEntry> _entries = new();
|
||||
private readonly List<Sprite> _ownedSprites = new();
|
||||
@@ -36,13 +38,15 @@ namespace Darkmatter.Features.Artbook
|
||||
IEventBus eventBus,
|
||||
IProgressionSystem progression,
|
||||
IDrawingTemplateCatalog catalog,
|
||||
IGalleryService gallery)
|
||||
IGalleryService gallery,
|
||||
IRewardedSaveGate saveGate)
|
||||
{
|
||||
_view = view;
|
||||
_eventBus = eventBus;
|
||||
_progression = progression;
|
||||
_catalog = catalog;
|
||||
_gallery = gallery;
|
||||
_saveGate = saveGate;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
@@ -151,7 +155,11 @@ namespace Darkmatter.Features.Artbook
|
||||
{
|
||||
if (!entry.HasValue || entry.Value.Thumbnail == null) return;
|
||||
var ct = _cts?.Token ?? CancellationToken.None;
|
||||
// Same kid-friendly prompt + rewarded ad as the gameplay save button.
|
||||
if (!await _saveGate.RequestSaveAsync(ct)) return;
|
||||
await _gallery.SaveImageAsync(entry.Value.Thumbnail, entry.Value.Name, ct);
|
||||
// The art book has no success popup of its own, so use the shared toast.
|
||||
await _saveGate.ShowSavedAsync(ct);
|
||||
}
|
||||
|
||||
private void HandleLeftEditClicked() => OpenForEdit(GetLeftEntry());
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Darkmatter.Features.Capture
|
||||
builder.RegisterInstance(new CaptureConfig(captureScale));
|
||||
builder.Register<ICaptureFeature, CaptureSystem>(Lifetime.Singleton);
|
||||
|
||||
// IRewardedSaveGate is resolved from the root scope (SaveGateModule in Boot).
|
||||
if (captureButtonView != null)
|
||||
builder.RegisterEntryPoint<CaptureButtonPresenter>().WithParameter(captureButtonView);
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.Capture;
|
||||
using Darkmatter.Core.Contracts.Features.SaveGate;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.Capture.UI
|
||||
@@ -9,15 +11,20 @@ namespace Darkmatter.Features.Capture.UI
|
||||
{
|
||||
private readonly CaptureButtonView _view;
|
||||
private readonly ICaptureFeature _capture;
|
||||
private readonly IRewardedSaveGate _saveGate;
|
||||
|
||||
public CaptureButtonPresenter(CaptureButtonView view, ICaptureFeature capture)
|
||||
private CancellationTokenSource _cts;
|
||||
|
||||
public CaptureButtonPresenter(CaptureButtonView view, ICaptureFeature capture, IRewardedSaveGate saveGate)
|
||||
{
|
||||
_view = view;
|
||||
_capture = capture;
|
||||
_saveGate = saveGate;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
_view.OnCaptureClicked += HandleCaptureClicked;
|
||||
}
|
||||
|
||||
@@ -25,11 +32,16 @@ namespace Darkmatter.Features.Capture.UI
|
||||
|
||||
private async UniTaskVoid CaptureAsync()
|
||||
{
|
||||
var ct = _cts?.Token ?? CancellationToken.None;
|
||||
_view.SetInteractable(false);
|
||||
try
|
||||
{
|
||||
await _capture.CapturePngAsync(saveToGallery: true);
|
||||
// Kid-friendly prompt + rewarded ad (shared, cross-scene gate). The save then
|
||||
// publishes the existing signals that drive the "Saved to gallery!" popup.
|
||||
if (await _saveGate.RequestSaveAsync(ct))
|
||||
await _capture.CapturePngAsync(saveToGallery: true, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
finally
|
||||
{
|
||||
_view.SetInteractable(true);
|
||||
@@ -39,6 +51,9 @@ namespace Darkmatter.Features.Capture.UI
|
||||
public void Dispose()
|
||||
{
|
||||
_view.OnCaptureClicked -= HandleCaptureClicked;
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,16 @@ namespace Darkmatter.Features.Coloring.UI
|
||||
rt.localScale = startScale;
|
||||
rt.localRotation = Quaternion.identity;
|
||||
await Tween.Scale(rt, endScale, duration, ease).ToUniTask(cancellationToken: ct);
|
||||
// The view can be destroyed mid-animation (flow advances, Back/Clear, scene teardown).
|
||||
// PrimeTween auto-stops the tween on target death and ToUniTask resolves normally, so
|
||||
// re-check the (Unity fake-null) transform before touching it again — otherwise the
|
||||
// localRotation write below hits a freed native object and throws NullReferenceException.
|
||||
if (rt == null) return;
|
||||
|
||||
await Tween.LocalRotation(rt, new Vector3(0f, 0f, wiggleAngle), wiggleDuration, Ease.InOutSine,
|
||||
cycles: wiggleCycles * 2, cycleMode: CycleMode.Yoyo)
|
||||
.ToUniTask(cancellationToken: ct);
|
||||
if (rt == null) return;
|
||||
rt.localRotation = Quaternion.identity;
|
||||
}
|
||||
|
||||
|
||||
8
Assets/Darkmatter/Code/Features/SaveGate.meta
Normal file
8
Assets/Darkmatter/Code/Features/SaveGate.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6434fcc29f4e4949be4be8115fda2b9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Features.SaveGate",
|
||||
"rootNamespace": "Darkmatter.Features.SaveGate",
|
||||
"references": [
|
||||
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
|
||||
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
|
||||
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
|
||||
"GUID:f51ebe6a0ceec4240a699833d6309b23",
|
||||
"GUID:6055be8ebefd69e48b49212b09b47b2f"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48cf1e0dac50c4952b2f27e17e8a065a
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Features/SaveGate/Installers.meta
Normal file
8
Assets/Darkmatter/Code/Features/SaveGate/Installers.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f38ec6a6d3544172b3fdf20e8d31800
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,31 @@
|
||||
using Darkmatter.Features.SaveGate.Systems;
|
||||
using Darkmatter.Features.SaveGate.UI;
|
||||
using Darkmatter.Libs.Installers;
|
||||
using UnityEngine;
|
||||
using VContainer;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.SaveGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Root module (add to Boot's RootLifetimeScope service modules) so the gate + overlay are
|
||||
/// resolvable and persistent across every scene. If no overlay is assigned it spawns a
|
||||
/// self-building one at runtime — no scene/prefab setup required.
|
||||
/// </summary>
|
||||
public class SaveGateModule : MonoBehaviour, IModule
|
||||
{
|
||||
[SerializeField] private SaveGalleryOverlayView overlayView;
|
||||
[SerializeField, Tooltip("Save anyway when the rewarded ad has no fill or fails to show.")]
|
||||
private bool saveWithoutAdOnFailure = true;
|
||||
|
||||
public void Register(IContainerBuilder builder)
|
||||
{
|
||||
|
||||
// Explicit type so a null overlay still resolves; the gate then lets saving proceed
|
||||
// without a prompt.
|
||||
builder.RegisterEntryPoint<RewardedSaveGate>()
|
||||
.WithParameter(typeof(SaveGalleryOverlayView), overlayView)
|
||||
.WithParameter(new SaveGateConfig(saveWithoutAdOnFailure));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46ff2e3f5fac64fc9a40fe00a37bd9c8
|
||||
61
Assets/Darkmatter/Code/Features/SaveGate/SETUP.md
Normal file
61
Assets/Darkmatter/Code/Features/SaveGate/SETUP.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# SaveGate — cross-scene rewarded-ad save gate (manual Unity wiring)
|
||||
|
||||
Code is complete and compiles. The gate does **not** show its prompt until the overlay is built in
|
||||
**Boot** and assigned to `SaveGateModule`. Until then both callers fall back to saving directly
|
||||
(no prompt), so nothing breaks pre-wiring.
|
||||
|
||||
## What it does
|
||||
|
||||
A single root-scoped gate (`IRewardedSaveGate` → `RewardedSaveGate`) used by **every** save-to-gallery
|
||||
button, in any scene:
|
||||
|
||||
1. Tap save → **kid-friendly prompt** ("Watch a short video to save your picture to the gallery!")
|
||||
with **Watch** / **Cancel**.
|
||||
2. **Cancel** → nothing saved.
|
||||
3. **Watch** → **rewarded ad** (`AdFormat.Rewarded`). Reward earned → caller saves.
|
||||
4. Ad closed early → not saved. No fill / load-or-show error → saved anyway iff
|
||||
**Save Without Ad On Failure** (default ON).
|
||||
|
||||
Callers today:
|
||||
- **Gameplay capture/save button** (`CaptureButtonPresenter`) — gate, then its existing
|
||||
**GallerySaveView** "Saved to gallery!" popup (unchanged).
|
||||
- **Art book** page save buttons (`ArtbookPresenter`) — gate, then the gate's **shared success toast**
|
||||
(the art book has no popup of its own).
|
||||
|
||||
It lives in **Boot** (never unloaded), so the same overlay shows over MainMenu / Colorbook / Gameplay
|
||||
— the loading-screen trick.
|
||||
|
||||
## The overlay prefab (ready-made)
|
||||
|
||||
A complete, wired overlay prefab already exists:
|
||||
|
||||
`Assets/Darkmatter/Content/Colorbook UI/Prefabs/UI/SaveGalleryOverlay.prefab`
|
||||
|
||||
It has its own screen-space Canvas (sort order 5100, 1080×1920 scaler — matches `TutorialOverlayCanvas`),
|
||||
the `SaveGalleryOverlayView` on the root with all six fields wired, and:
|
||||
- **PromptPanel** → dim backdrop + cream Card → Fredoka message, green **Watch video**, grey **Not now**.
|
||||
- **SuccessPanel** (starts inactive) → dark toast → "Saved to gallery!".
|
||||
|
||||
Edit the copy/colors freely; it uses plain colored rects (no sprite art) so a designer can drop in
|
||||
sprites later. To wire it: just drag the prefab into **Boot** (it's a self-contained Canvas, so it
|
||||
works as-is) and assign it to `SaveGateModule` below. Boot is never unloaded, so it persists across
|
||||
every scene. (Building your own overlay also works — put `SaveGalleryOverlayView` on an always-active
|
||||
object and wire promptPanel / watchButton / cancelButton / promptLabel / successPanel / successLabel.)
|
||||
|
||||
## Register in DI (Boot RootLifetimeScope)
|
||||
|
||||
1. Add a **`SaveGateModule`** component in Boot (it's an `IModule`).
|
||||
2. Add it to the **RootLifetimeScope**'s `serviceModules` list (same list that holds the Ad / Loading
|
||||
modules) so it registers at the root.
|
||||
3. On `SaveGateModule`: assign **Overlay View** → the `SaveGalleryOverlayView`; tick
|
||||
**Save Without Ad On Failure** (recommended).
|
||||
|
||||
`IAdService` is already root-scoped (AdServiceModule in Boot); the gate resolves it automatically, and
|
||||
`CaptureButtonPresenter` / `ArtbookPresenter` resolve `IRewardedSaveGate` from the root.
|
||||
|
||||
## Before release
|
||||
|
||||
- `AdUnitCatalogSO` holds AdMob **test** unit IDs (incl. test Rewarded). Swap in the real Rewarded ID.
|
||||
- Child-directed / COPPA app (IDFA off, `TagForChildDirectedTreatment.True`, `MaxAdContentRating.G`
|
||||
already set). Confirm rewarded ads are permitted under the relevant kids / family policies before
|
||||
shipping rewarded-gated saving.
|
||||
7
Assets/Darkmatter/Code/Features/SaveGate/SETUP.md.meta
Normal file
7
Assets/Darkmatter/Code/Features/SaveGate/SETUP.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9db44e977bbde446daa19aa928ee1230
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Features/SaveGate/Systems.meta
Normal file
8
Assets/Darkmatter/Code/Features/SaveGate/Systems.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 010c08604fc7e44ceb9d069874618ce8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Darkmatter.Core.Contracts.Features.SaveGate;
|
||||
using Darkmatter.Core.Contracts.Services.Ads;
|
||||
using Darkmatter.Core.Enums.Services.Ads;
|
||||
using Darkmatter.Features.SaveGate.UI;
|
||||
using VContainer.Unity;
|
||||
|
||||
namespace Darkmatter.Features.SaveGate.Systems
|
||||
{
|
||||
public class RewardedSaveGate : IRewardedSaveGate, IStartable, IDisposable
|
||||
{
|
||||
private readonly SaveGalleryOverlayView _overlay;
|
||||
private readonly IAdService _ads;
|
||||
private readonly SaveGateConfig _config;
|
||||
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _busy;
|
||||
|
||||
public RewardedSaveGate(SaveGalleryOverlayView overlay, IAdService ads, SaveGateConfig config)
|
||||
{
|
||||
_overlay = overlay;
|
||||
_ads = ads;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
// Warm a rewarded ad so the gate is snappy on first save.
|
||||
PrewarmRewardedAsync(_cts.Token).Forget();
|
||||
}
|
||||
|
||||
public async UniTask<bool> RequestSaveAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Overlay not built yet, or no ad service: never block saving.
|
||||
if (_overlay == null || _ads == null) return true;
|
||||
if (_busy) return false; // a prompt is already up; ignore the extra tap
|
||||
|
||||
_busy = true;
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
_cts?.Token ?? CancellationToken.None, cancellationToken);
|
||||
var ct = linked.Token;
|
||||
try
|
||||
{
|
||||
if (!await ShowPromptAsync(ct)) return false; // child tapped Cancel
|
||||
return await TryShowRewardedAsync(ct); // ad closed early / unavailable
|
||||
}
|
||||
catch (OperationCanceledException) { return false; }
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async UniTask ShowSavedAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_overlay == null || !_overlay.HasSuccessPanel) return;
|
||||
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
_cts?.Token ?? CancellationToken.None, cancellationToken);
|
||||
_overlay.ShowSuccess();
|
||||
try
|
||||
{
|
||||
await UniTask.Delay(TimeSpan.FromSeconds(_overlay.SuccessAutoHideSeconds),
|
||||
cancellationToken: linked.Token);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
_overlay.HideSuccess();
|
||||
}
|
||||
|
||||
// Shows the prompt and resolves true on Watch, false on Cancel (or cancellation).
|
||||
private UniTask<bool> ShowPromptAsync(CancellationToken ct)
|
||||
{
|
||||
var tcs = new UniTaskCompletionSource<bool>();
|
||||
CancellationTokenRegistration reg = default;
|
||||
var done = false;
|
||||
|
||||
void Finish(bool result, bool canceled)
|
||||
{
|
||||
if (done) return;
|
||||
done = true;
|
||||
_overlay.OnWatch -= OnWatch;
|
||||
_overlay.OnCancel -= OnCancel;
|
||||
reg.Dispose();
|
||||
_overlay.HidePrompt();
|
||||
if (canceled) tcs.TrySetCanceled(ct);
|
||||
else tcs.TrySetResult(result);
|
||||
}
|
||||
|
||||
void OnWatch() => Finish(true, false);
|
||||
void OnCancel() => Finish(false, false);
|
||||
|
||||
_overlay.OnWatch += OnWatch;
|
||||
_overlay.OnCancel += OnCancel;
|
||||
_overlay.ShowPrompt();
|
||||
reg = ct.Register(() => Finish(false, true));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
// Returns true only when the child earned the reward. Falls back to SaveWithoutAdOnFailure
|
||||
// when the ad can't be shown at all; an ad closed early counts as not rewarded -> no save.
|
||||
private async UniTask<bool> TryShowRewardedAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_ads.IsInitialized) await _ads.InitializeAsync(ct);
|
||||
if (!_ads.IsReady(AdFormat.Rewarded) && !await _ads.LoadAsync(AdFormat.Rewarded, ct))
|
||||
return _config.SaveWithoutAdOnFailure;
|
||||
|
||||
var result = await _ads.ShowAsync(AdFormat.Rewarded, ct);
|
||||
if (result.Rewarded) return true;
|
||||
return result.Shown ? false : _config.SaveWithoutAdOnFailure;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[SaveGate] Rewarded ad gate failed: {ex.Message}");
|
||||
return _config.SaveWithoutAdOnFailure;
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTaskVoid PrewarmRewardedAsync(CancellationToken ct)
|
||||
{
|
||||
if (_ads == null) return;
|
||||
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($"[SaveGate] Rewarded prewarm failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcff7c06c2d77499db81865208f807e7
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Darkmatter.Features.SaveGate
|
||||
{
|
||||
[Serializable]
|
||||
public struct SaveGateConfig
|
||||
{
|
||||
// When the rewarded ad can't be shown (no fill / load or show error), save anyway instead of
|
||||
// blocking the child. A user who opens the ad and closes it early is never rewarded and does
|
||||
// not trigger this fallback.
|
||||
public bool SaveWithoutAdOnFailure { get; }
|
||||
|
||||
public SaveGateConfig(bool saveWithoutAdOnFailure)
|
||||
{
|
||||
SaveWithoutAdOnFailure = saveWithoutAdOnFailure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c7343ebeefe9e4db48c41c617347adae
|
||||
8
Assets/Darkmatter/Code/Features/SaveGate/UI.meta
Normal file
8
Assets/Darkmatter/Code/Features/SaveGate/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1356ea11d3edf4c1293bc8dec5ecdb3a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Darkmatter.Features.SaveGate.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Root-scoped, cross-scene overlay for the save-to-gallery flow: a kid-friendly "watch a video"
|
||||
/// prompt and a "Saved!" toast. Dumb view — it shows/hides panels and raises button events;
|
||||
/// <c>RewardedSaveGate</c> drives the flow. Lives in Boot so it persists over every scene.
|
||||
///
|
||||
/// If the panels aren't wired in the inspector it builds the whole UI in code (see
|
||||
/// <see cref="buildUiAtRuntime"/>), so the feature works with no manual prefab setup. A designer
|
||||
/// can still wire custom panels and the code build is skipped.
|
||||
/// </summary>
|
||||
public class SaveGalleryOverlayView : MonoBehaviour
|
||||
{
|
||||
[Header("Watch-ad prompt")] [SerializeField]
|
||||
private GameObject promptPanel;
|
||||
|
||||
[SerializeField] private Button watchButton;
|
||||
[SerializeField] private Button cancelButton;
|
||||
[SerializeField] private TMP_Text promptLabel;
|
||||
|
||||
[Header("Saved toast")] [SerializeField]
|
||||
private GameObject successPanel;
|
||||
|
||||
[SerializeField] private TMP_Text successLabel;
|
||||
[SerializeField, Min(0f)] private float successAutoHideSeconds = 1.5f;
|
||||
|
||||
public event Action OnWatch;
|
||||
public event Action OnCancel;
|
||||
|
||||
public float SuccessAutoHideSeconds => successAutoHideSeconds;
|
||||
public bool HasSuccessPanel => successPanel != null;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (promptPanel != null) promptPanel.SetActive(false);
|
||||
if (successPanel != null) successPanel.SetActive(false);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (watchButton != null) watchButton.onClick.AddListener(() => OnWatch?.Invoke());
|
||||
if (cancelButton != null) cancelButton.onClick.AddListener(() => OnCancel?.Invoke());
|
||||
}
|
||||
|
||||
public void ShowPrompt()
|
||||
{
|
||||
if (successPanel != null) successPanel.SetActive(false);
|
||||
if (promptPanel != null) promptPanel.SetActive(true);
|
||||
}
|
||||
|
||||
public void HidePrompt()
|
||||
{
|
||||
if (promptPanel != null) promptPanel.SetActive(false);
|
||||
}
|
||||
|
||||
public void ShowSuccess()
|
||||
{
|
||||
if (successPanel != null) successPanel.SetActive(true);
|
||||
}
|
||||
|
||||
public void HideSuccess()
|
||||
{
|
||||
if (successPanel != null) successPanel.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (watchButton != null) watchButton.onClick.RemoveAllListeners();
|
||||
if (cancelButton != null) cancelButton.onClick.RemoveAllListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2688ce9b9d9848719e2ee33cf6eb155
|
||||
@@ -46,6 +46,7 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
private CancellationToken _ct;
|
||||
private bool _completed;
|
||||
private bool _drawingCompleted;
|
||||
private bool _hasColored;
|
||||
private bool _suspended;
|
||||
private Action _reshow;
|
||||
private int _stepIndex;
|
||||
@@ -80,6 +81,9 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
// finds Next early during free-paint. Flag it so the catalog reload that follows isn't
|
||||
// mistaken for a Back-navigation and doesn't restart the tutorial.
|
||||
_navSubs.Add(_bus.Subscribe<DrawingCompletedSignal>(_ => _drawingCompleted = true));
|
||||
// Painting at least one region means the child has grasped the core colour gesture. If
|
||||
// they then go Back, we finish the tutorial instead of restarting it (OnCatalogReadyGlobal).
|
||||
_navSubs.Add(_bus.Subscribe<ColorAppliedSignal>(_ => _hasColored = true));
|
||||
|
||||
StartRun(skipCatalogWait: false);
|
||||
}
|
||||
@@ -93,6 +97,7 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
_gen++;
|
||||
_suspended = false;
|
||||
_drawingCompleted = false;
|
||||
_hasColored = false;
|
||||
_reshow = null;
|
||||
_stepIndex = 0;
|
||||
_overlay.HideInstant();
|
||||
@@ -119,8 +124,19 @@ namespace Darkmatter.Features.Tutorial.Systems
|
||||
// press), whose own completion loads the catalog.
|
||||
private void OnCatalogReadyGlobal(DrawingCatalogReadySignal _)
|
||||
{
|
||||
if (!_completed && !_drawingCompleted && _stepIndex >= 2 && _stepIndex <= 6)
|
||||
StartRun(skipCatalogWait: true);
|
||||
if (_completed || _drawingCompleted || _stepIndex < 2 || _stepIndex > 6) return;
|
||||
|
||||
// Already coloured at least one region before backing out: the child has the core gesture,
|
||||
// so finish the tutorial rather than trapping them in a full restart.
|
||||
if (_hasColored)
|
||||
{
|
||||
_runCts?.Cancel();
|
||||
Complete();
|
||||
_overlay.HideInstant();
|
||||
return;
|
||||
}
|
||||
|
||||
StartRun(skipCatalogWait: true);
|
||||
}
|
||||
|
||||
private void ShowStep(Action show)
|
||||
|
||||
Reference in New Issue
Block a user