Compare commits

...

14 Commits

Author SHA1 Message Date
Mausham
435988ca70 added more diagram and animations 2026-06-07 16:58:17 +05:45
Mausham
4eccde41b5 Merge remote-tracking branch 'origin/savya' into work_branch 2026-06-07 16:09:47 +05:45
Mausham
68bea947fe tttt 2026-06-07 16:07:41 +05:45
Savya Bikram Shah
0b9d0ca8f2 fixes 2026-06-07 16:07:30 +05:45
Savya Bikram Shah
521b5c3b7d Intersterial Limit added 2026-06-07 15:57:31 +05:45
Mausham
ec4288e97a Merge remote-tracking branch 'origin/savya' into work_branch 2026-06-07 15:53:45 +05:45
Savya Bikram Shah
d406f41acc Bus and crash fixes UX improvements rewarded ads added 2026-06-07 15:53:16 +05:45
Savya Bikram Shah
144bac177f Ad frequency reduced 2026-06-07 14:17:07 +05:45
Savya Bikram Shah
a07f7618ab Meta and appflyer deeper intregration 2026-06-07 14:12:09 +05:45
Savya Bikram Shah
70b8dd2b95 Child directed made 2026-06-05 18:33:56 +05:45
Savya Bikram Shah
220d651cfb Tutorial fix 2026-06-05 18:33:39 +05:45
Savya Bikram Shah
e27d0e54cb UX updates 2026-06-05 18:00:22 +05:45
Savya Bikram Shah
dee4b004bd Ad units fixed 2026-06-04 13:37:21 +05:45
Savya Bikram Shah
2462d972e3 Ad ids fixed 2026-06-04 13:00:45 +05:45
294 changed files with 17868 additions and 282 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
Assets/.DS_Store vendored

Binary file not shown.

View File

@@ -15,7 +15,7 @@ MonoBehaviour:
m_DefaultGroup: 0e030d5498bfe4ffd8443c796618c539 m_DefaultGroup: 0e030d5498bfe4ffd8443c796618c539
m_currentHash: m_currentHash:
serializedVersion: 2 serializedVersion: 2
Hash: 00000000000000000000000000000000 Hash: f172661451d53007cd560d2db7f013f5
m_OptimizeCatalogSize: 0 m_OptimizeCatalogSize: 0
m_BuildRemoteCatalog: 0 m_BuildRemoteCatalog: 0
m_CatalogRequestsTimeout: 0 m_CatalogRequestsTimeout: 0

Binary file not shown.

View File

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

View File

@@ -0,0 +1,23 @@
using System.Threading;
using Cysharp.Threading.Tasks;
namespace Darkmatter.Core.Contracts.Features.SaveGate
{
/// <summary>
/// Cross-scene gate in front of "save to gallery": shows a kid-friendly prompt and, on confirm,
/// a rewarded ad. Root-scoped so any feature in any scene (capture, art book, …) can reuse it.
/// </summary>
public interface IRewardedSaveGate
{
/// <summary>
/// Shows the prompt and (on Watch) a rewarded ad. Returns true if the caller should save.
/// </summary>
UniTask<bool> RequestSaveAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Briefly shows the shared "Saved to gallery!" confirmation. For callers without their own
/// success popup (e.g. the art book). No-op if the overlay has no success panel wired.
/// </summary>
UniTask ShowSavedAsync(CancellationToken cancellationToken = default);
}
}

View File

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

View File

@@ -1,5 +1,6 @@
using System.Threading; using System.Threading;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using Darkmatter.Core.Enums.Features.Tutorial;
using UnityEngine; using UnityEngine;
namespace Darkmatter.Core.Contracts.Features.Tutorial namespace Darkmatter.Core.Contracts.Features.Tutorial
@@ -18,19 +19,23 @@ namespace Darkmatter.Core.Contracts.Features.Tutorial
/// Spotlight a single tap target. When <paramref name="target"/> is null the dim/hole are /// Spotlight a single tap target. When <paramref name="target"/> is null the dim/hole are
/// skipped and only a centered bubble is shown (a non-blocking hint). /// skipped and only a centered bubble is shown (a non-blocking hint).
/// When <paramref name="blockInput"/> is true every touch outside the hole is swallowed. /// When <paramref name="blockInput"/> is true every touch outside the hole is swallowed.
/// <paramref name="placement"/> overrides where the instruction bubble sits; <paramref name="offset"/>
/// nudges it from that slot (px, +x right / +y up).
/// </summary> /// </summary>
void ShowTap(RectTransform target, string message, bool blockInput); void ShowTap(RectTransform target, string message, bool blockInput, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default);
/// <summary> /// <summary>
/// Spotlight a drag gesture from <paramref name="from"/> to <paramref name="to"/>. Input is /// Spotlight a drag gesture from <paramref name="from"/> to <paramref name="to"/>. Input is
/// never blocked here, so the dragged piece can render above the dim. /// never blocked here, so the dragged piece can render above the dim.
/// <paramref name="placement"/> overrides where the instruction bubble sits; <paramref name="offset"/>
/// nudges it from that slot (px, +x right / +y up).
/// </summary> /// </summary>
void ShowDrag(RectTransform from, RectTransform to, string message); void ShowDrag(RectTransform from, RectTransform to, string message, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default);
/// <summary>Hide everything immediately (used across scene swaps).</summary> /// <summary>Hide everything immediately (used across scene swaps).</summary>
void HideInstant(); void HideInstant();
/// <summary>Show a centered celebratory bubble for a short beat, then hide.</summary> /// <summary>Show a centered celebratory bubble for a short beat, then hide.</summary>
UniTask ShowToastAsync(string message, CancellationToken ct); UniTask ShowToastAsync(string message, CancellationToken ct, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default);
} }
} }

View File

@@ -0,0 +1,51 @@
namespace Darkmatter.Core.Contracts.Services.Analytics
{
/// <summary>
/// Canonical analytics event names. Use these constants everywhere — never raw strings — so the
/// Firebase and Facebook sinks stay identical and typo-free. GA4 limit: 500 distinct event names.
/// Names are snake_case and &lt;= 40 chars (Facebook App Events limit). Lives in Core so any feature
/// or service can reference them alongside <see cref="IAnalyticsService"/>.
/// </summary>
public static class AnalyticsEvents
{
// Onboarding / activation funnel
public const string IntroStarted = "intro_started";
public const string IntroCompleted = "intro_completed";
public const string TutorialStarted = "tutorial_started";
public const string TutorialStepCompleted = "tutorial_step_completed";
public const string TutorialCompleted = "tutorial_completed";
public const string PlayClicked = "play_clicked";
public const string FirstDrawingStarted = "first_drawing_started";
// Navigation
public const string ColorbookOpened = "colorbook_opened";
public const string ArtbookOpened = "artbook_opened";
public const string MainMenuReturned = "main_menu_returned";
// Core gameplay loop
public const string DrawingSelected = "drawing_selected";
public const string DrawingStarted = "drawing_started";
public const string DrawingCompleted = "drawing_completed";
public const string DrawingAbandoned = "drawing_abandoned";
public const string DrawingSaved = "drawing_saved";
public const string ShapeBuilderStarted = "shape_builder_started";
public const string ShapeAssembled = "shape_assembled";
public const string ColorApplied = "color_applied";
// Progression & content (GA4 recommended level_start / level_complete)
public const string LevelStart = "level_start";
public const string LevelComplete = "level_complete";
public const string AllContentCompleted = "all_content_completed";
// Gallery capture (pre-existing)
public const string GallerySaveStarted = "gallery_save_started";
public const string GallerySaveCompleted = "gallery_save_completed";
// Monetization (GA4 recommended ad_impression)
public const string AdImpression = "ad_impression";
public const string AdClicked = "ad_clicked";
// Friction
public const string ErrorShown = "error_shown";
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 44ad2e947d64f40a38febad220fa92a1

View File

@@ -0,0 +1,39 @@
namespace Darkmatter.Core.Contracts.Services.Analytics
{
/// <summary>
/// Canonical analytics parameter keys. Pass a consistent set on every gameplay event so reports can
/// answer "which drawing did people quit on", not just "how many quit". Each key used in reports must
/// be registered as a Custom Dimension in the Firebase console. GA4 limit: 25 params per event.
/// GA4-recommended events (level_start/level_complete/ad_impression) reuse GA4's reserved param names
/// (level_name, success, value, currency, ad_platform, ad_format, ad_unit_name) so they're recognised.
/// </summary>
public static class AnalyticsParams
{
// Content identity
public const string DrawingId = "drawing_id";
public const string LevelName = "level_name"; // GA4 reserved (level_start/level_complete)
// Tutorial
public const string StepId = "step_id";
public const string StepIndex = "step_index";
// Gameplay metrics
public const string DurationSeconds = "duration_seconds";
public const string ColorsUsed = "colors_used";
public const string Attempts = "attempts";
public const string ApplyIndex = "apply_index";
public const string CompletionCount = "completion_count";
public const string Success = "success"; // GA4 reserved (level_complete)
// Monetization (GA4 ad_impression reserved names)
public const string Value = "value";
public const string Currency = "currency";
public const string Precision = "precision";
public const string AdPlatform = "ad_platform";
public const string AdFormat = "ad_format";
public const string AdUnitName = "ad_unit_name";
// Friction
public const string ErrorType = "error_type";
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3a7d4e7d6ad7e4ba39f557ddc970890c

View File

@@ -0,0 +1,7 @@
namespace Darkmatter.Core.Data.Signals.Features.AppBoot;
/// <summary>
/// Raised when the intro/logo video begins playing at app boot. Paired with
/// <see cref="IntroCompletedSignal"/> to bound the top of the activation funnel.
/// </summary>
public record struct IntroStartedSignal();

View File

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

View File

@@ -0,0 +1,10 @@
namespace Darkmatter.Core.Data.Signals.Features.Coloring
{
/// <summary>
/// Raised the moment every region of the current drawing has been painted away from its authored
/// (uncoloured) default — i.e. the picture is fully coloured. Published right after the
/// <see cref="ColorAppliedSignal"/> that completes it. Used by the onboarding tutorial to hold the
/// "Tap Next" prompt until the child has coloured the whole picture.
/// </summary>
public record struct AllRegionsColoredSignal;
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 381ea33520a140d8b5639279c5390d2c

View File

@@ -0,0 +1,8 @@
namespace Darkmatter.Core.Data.Signals.Features.GameplayFlow
{
/// <summary>
/// Raised once the player has completed every drawing in the catalog (no content left). Published by
/// the gameplay flow, which owns the catalog + progression; analytics dedupes it to once-ever.
/// </summary>
public record struct AllContentCompletedSignal(int CompletedCount);
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3dc3c138deba5452ebb0d84ad3d4bf44

View File

@@ -6,9 +6,13 @@ namespace Darkmatter.Core.Data.Static.Features.ShapeBuilder
menuName = "Darkmatter/ShapeBuilder/Config")] menuName = "Darkmatter/ShapeBuilder/Config")]
public sealed class ShapeBuilderConfig : ScriptableObject public sealed class ShapeBuilderConfig : ScriptableObject
{ {
[Header("Radii (canvas units; reference resolution 2048x2048)")] // Single catch radius for both preview and snap, expressed as a multiple of
[SerializeField] private float snapRadius = 100f; // EACH slot's own half-diagonal — so a big slot gets a big catch and a small
[SerializeField] private float previewRadius = 200f; // slot a small one, instead of one flat distance for all. 1 = exactly the
// slot's circumscribed circle; larger = more forgiving. Computed in canvas
// reference units, so it feels the same on every screen resolution/aspect.
[Header("Catch radius (multiple of slot half-diagonal)")]
[SerializeField, Range(0.25f, 2.5f)] private float catchRadiusScale = 1.1f;
[Header("Tween durations (seconds)")] [Header("Tween durations (seconds)")]
[SerializeField] private float snapDuration = 0.25f; [SerializeField] private float snapDuration = 0.25f;
@@ -18,16 +22,11 @@ namespace Darkmatter.Core.Data.Static.Features.ShapeBuilder
[SerializeField, Range(1f, 2f)] private float dragScale = 1.15f; [SerializeField, Range(1f, 2f)] private float dragScale = 1.15f;
[SerializeField, Range(0f, 1f)] private float dragAlpha = 0.7f; [SerializeField, Range(0f, 1f)] private float dragAlpha = 0.7f;
[Header("Preview easing")] public float CatchRadiusScale => catchRadiusScale;
[SerializeField] private AnimationCurve previewCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
public float SnapRadius => snapRadius;
public float PreviewRadius => previewRadius;
public float SnapDuration => snapDuration; public float SnapDuration => snapDuration;
public float ReturnDuration => returnDuration; public float ReturnDuration => returnDuration;
public float DragScale => dragScale; public float DragScale => dragScale;
public float DragAlpha => dragAlpha; public float DragAlpha => dragAlpha;
public AnimationCurve PreviewCurve => previewCurve;
public Vector2 DragSizeDelta(ShapeSO shape) => public Vector2 DragSizeDelta(ShapeSO shape) =>
shape != null ? shape.DefaultSizeDelta : new Vector2(256, 256); shape != null ? shape.DefaultSizeDelta : new Vector2(256, 256);

View File

@@ -1,3 +1,4 @@
using Darkmatter.Core.Enums.Features.Tutorial;
using UnityEngine; using UnityEngine;
namespace Darkmatter.Core.Data.Static.Features.Tutorial namespace Darkmatter.Core.Data.Static.Features.Tutorial
@@ -17,9 +18,28 @@ namespace Darkmatter.Core.Data.Static.Features.Tutorial
[SerializeField] private string finishText = "Now finish the puzzle!"; [SerializeField] private string finishText = "Now finish the puzzle!";
[SerializeField] private string colorText = "Choose a color!"; [SerializeField] private string colorText = "Choose a color!";
[SerializeField] private string paintText = "Tap the picture to color it!"; [SerializeField] private string paintText = "Tap the picture to color it!";
[SerializeField] private string finishColoringText = "Color the whole picture!";
[SerializeField] private string nextText = "Tap Next when you're done!"; [SerializeField] private string nextText = "Tap Next when you're done!";
[SerializeField] private string doneText = "Yay! You did it!"; [SerializeField] private string doneText = "Yay! You did it!";
[Header("Bubble placement + offset per step (Auto = smart; offset nudges from the slot in px, +x right / +y up)")]
[SerializeField] private BubblePlacement pickBubble = BubblePlacement.Auto;
[SerializeField] private Vector2 pickBubbleOffset;
[SerializeField] private BubblePlacement dragBubble = BubblePlacement.Auto;
[SerializeField] private Vector2 dragBubbleOffset;
[SerializeField] private BubblePlacement finishBubble = BubblePlacement.Auto;
[SerializeField] private Vector2 finishBubbleOffset;
[SerializeField] private BubblePlacement colorBubble = BubblePlacement.Auto;
[SerializeField] private Vector2 colorBubbleOffset;
[SerializeField] private BubblePlacement paintBubble = BubblePlacement.Auto;
[SerializeField] private Vector2 paintBubbleOffset;
[SerializeField] private BubblePlacement finishColoringBubble = BubblePlacement.Auto;
[SerializeField] private Vector2 finishColoringBubbleOffset;
[SerializeField] private BubblePlacement nextBubble = BubblePlacement.Auto;
[SerializeField] private Vector2 nextBubbleOffset;
[SerializeField] private BubblePlacement doneBubble = BubblePlacement.Auto;
[SerializeField] private Vector2 doneBubbleOffset;
[Header("Watchdog")] [Header("Watchdog")]
[Tooltip("Timeout (s) while waiting for a SYSTEM precondition (scene/catalog/regions ready). If it " + [Tooltip("Timeout (s) while waiting for a SYSTEM precondition (scene/catalog/regions ready). If it " +
"doesn't arrive the tutorial fails open so the player is never trapped.")] "doesn't arrive the tutorial fails open so the player is never trapped.")]
@@ -34,9 +54,28 @@ namespace Darkmatter.Core.Data.Static.Features.Tutorial
public string FinishText => finishText; public string FinishText => finishText;
public string ColorText => colorText; public string ColorText => colorText;
public string PaintText => paintText; public string PaintText => paintText;
public string FinishColoringText => finishColoringText;
public string NextText => nextText; public string NextText => nextText;
public string DoneText => doneText; public string DoneText => doneText;
public BubblePlacement PickBubble => pickBubble;
public BubblePlacement DragBubble => dragBubble;
public BubblePlacement FinishBubble => finishBubble;
public BubblePlacement ColorBubble => colorBubble;
public BubblePlacement PaintBubble => paintBubble;
public BubblePlacement FinishColoringBubble => finishColoringBubble;
public BubblePlacement NextBubble => nextBubble;
public BubblePlacement DoneBubble => doneBubble;
public Vector2 PickBubbleOffset => pickBubbleOffset;
public Vector2 DragBubbleOffset => dragBubbleOffset;
public Vector2 FinishBubbleOffset => finishBubbleOffset;
public Vector2 ColorBubbleOffset => colorBubbleOffset;
public Vector2 PaintBubbleOffset => paintBubbleOffset;
public Vector2 FinishColoringBubbleOffset => finishColoringBubbleOffset;
public Vector2 NextBubbleOffset => nextBubbleOffset;
public Vector2 DoneBubbleOffset => doneBubbleOffset;
public float StepTimeoutSeconds => stepTimeoutSeconds; public float StepTimeoutSeconds => stepTimeoutSeconds;
public float ActionTimeoutSeconds => actionTimeoutSeconds; public float ActionTimeoutSeconds => actionTimeoutSeconds;
} }

View File

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

View File

@@ -0,0 +1,16 @@
namespace Darkmatter.Core.Enums.Features.Tutorial
{
/// <summary>
/// Where the tutorial instruction bubble sits for a given step. <see cref="Auto"/> keeps the
/// smart default (sits opposite the spotlighted target so it never covers it; pinned to a screen
/// edge for the drag step; upper-centre for target-less hints). The other values override that
/// with a fixed vertical slot, horizontally centred.
/// </summary>
public enum BubblePlacement
{
Auto,
Top,
Center,
Bottom,
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 61e68e58d45cf4314886e2a414e3856d

View File

@@ -44,6 +44,7 @@ namespace Darkmatter.Features.AppBoot.Flow
player.loopPointReached += OnDone; player.loopPointReached += OnDone;
player.Play(); player.Play();
_eventBus.Publish(new IntroStartedSignal());
await _sceneService.LoadSceneAsync(nameof(GameScene.MainMenu), null, cancellation); await _sceneService.LoadSceneAsync(nameof(GameScene.MainMenu), null, cancellation);
await tcs.Task.AttachExternalCancellation(cancellation); await tcs.Task.AttachExternalCancellation(cancellation);

View File

@@ -5,6 +5,7 @@ using Cysharp.Threading.Tasks;
using Darkmatter.Core; using Darkmatter.Core;
using Darkmatter.Core.Contracts.Features.DrawingCatalog; using Darkmatter.Core.Contracts.Features.DrawingCatalog;
using Darkmatter.Core.Contracts.Features.Progression; using Darkmatter.Core.Contracts.Features.Progression;
using Darkmatter.Core.Contracts.Features.SaveGate;
using Darkmatter.Core.Contracts.Services.Gallery; using Darkmatter.Core.Contracts.Services.Gallery;
using Darkmatter.Core.Data.Signals.Features.Drawing; using Darkmatter.Core.Data.Signals.Features.Drawing;
using Darkmatter.Libs.Observer; using Darkmatter.Libs.Observer;
@@ -22,6 +23,7 @@ namespace Darkmatter.Features.Artbook
private readonly IProgressionSystem _progression; private readonly IProgressionSystem _progression;
private readonly IDrawingTemplateCatalog _catalog; private readonly IDrawingTemplateCatalog _catalog;
private readonly IGalleryService _gallery; private readonly IGalleryService _gallery;
private readonly IRewardedSaveGate _saveGate;
private readonly List<ArtbookEntry> _entries = new(); private readonly List<ArtbookEntry> _entries = new();
private readonly List<Sprite> _ownedSprites = new(); private readonly List<Sprite> _ownedSprites = new();
@@ -36,13 +38,15 @@ namespace Darkmatter.Features.Artbook
IEventBus eventBus, IEventBus eventBus,
IProgressionSystem progression, IProgressionSystem progression,
IDrawingTemplateCatalog catalog, IDrawingTemplateCatalog catalog,
IGalleryService gallery) IGalleryService gallery,
IRewardedSaveGate saveGate)
{ {
_view = view; _view = view;
_eventBus = eventBus; _eventBus = eventBus;
_progression = progression; _progression = progression;
_catalog = catalog; _catalog = catalog;
_gallery = gallery; _gallery = gallery;
_saveGate = saveGate;
} }
public void Start() public void Start()
@@ -151,7 +155,11 @@ namespace Darkmatter.Features.Artbook
{ {
if (!entry.HasValue || entry.Value.Thumbnail == null) return; if (!entry.HasValue || entry.Value.Thumbnail == null) return;
var ct = _cts?.Token ?? CancellationToken.None; 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); 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()); private void HandleLeftEditClicked() => OpenForEdit(GetLeftEntry());

View File

@@ -18,6 +18,7 @@ namespace Darkmatter.Features.Capture
builder.RegisterInstance(new CaptureConfig(captureScale)); builder.RegisterInstance(new CaptureConfig(captureScale));
builder.Register<ICaptureFeature, CaptureSystem>(Lifetime.Singleton); builder.Register<ICaptureFeature, CaptureSystem>(Lifetime.Singleton);
// IRewardedSaveGate is resolved from the root scope (SaveGateModule in Boot).
if (captureButtonView != null) if (captureButtonView != null)
builder.RegisterEntryPoint<CaptureButtonPresenter>().WithParameter(captureButtonView); builder.RegisterEntryPoint<CaptureButtonPresenter>().WithParameter(captureButtonView);

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Threading;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Features.Capture; using Darkmatter.Core.Contracts.Features.Capture;
using Darkmatter.Core.Contracts.Features.SaveGate;
using VContainer.Unity; using VContainer.Unity;
namespace Darkmatter.Features.Capture.UI namespace Darkmatter.Features.Capture.UI
@@ -9,15 +11,20 @@ namespace Darkmatter.Features.Capture.UI
{ {
private readonly CaptureButtonView _view; private readonly CaptureButtonView _view;
private readonly ICaptureFeature _capture; 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; _view = view;
_capture = capture; _capture = capture;
_saveGate = saveGate;
} }
public void Start() public void Start()
{ {
_cts = new CancellationTokenSource();
_view.OnCaptureClicked += HandleCaptureClicked; _view.OnCaptureClicked += HandleCaptureClicked;
} }
@@ -25,11 +32,16 @@ namespace Darkmatter.Features.Capture.UI
private async UniTaskVoid CaptureAsync() private async UniTaskVoid CaptureAsync()
{ {
var ct = _cts?.Token ?? CancellationToken.None;
_view.SetInteractable(false); _view.SetInteractable(false);
try 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 finally
{ {
_view.SetInteractable(true); _view.SetInteractable(true);
@@ -39,6 +51,9 @@ namespace Darkmatter.Features.Capture.UI
public void Dispose() public void Dispose()
{ {
_view.OnCaptureClicked -= HandleCaptureClicked; _view.OnCaptureClicked -= HandleCaptureClicked;
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
} }
} }
} }

View File

@@ -28,6 +28,7 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
private IDisposable _selectedSub; private IDisposable _selectedSub;
private IDisposable _returnToMainMenuSubscription; private IDisposable _returnToMainMenuSubscription;
private bool _navigatingToGameplay; private bool _navigatingToGameplay;
private int _selectCount;
private CancellationTokenSource _scopeCts; private CancellationTokenSource _scopeCts;
public ColorbookFlowController( public ColorbookFlowController(
@@ -110,9 +111,14 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
_loadingScreen.Show(); _loadingScreen.Show();
_loadingScreen.SetProgress(0f); _loadingScreen.SetProgress(0f);
// Fire the interstitial but never await it: the ad overlays the transition while the level // Frequency cap: show an interstitial on every 2nd level open (2nd, 4th, ...). On skip turns
// loads underneath, so a missed/dropped ad callback can't stall the flow at 0% anymore. // just keep one prewarmed so the next show has an ad ready. Fire-and-forget either way — the
ShowInterstitialAdAsync(ct).Forget(); // ad overlays the transition while the level loads underneath, so a missed/dropped ad callback
// can't stall the flow at 0% anymore.
if (++_selectCount % 2 == 0)
ShowInterstitialAdAsync(ct).Forget();
else
PrewarmInterstitialAdAsync(ct).Forget();
try try
{ {

View File

@@ -79,6 +79,21 @@ public class ColoringController : IColoringController, IDisposable
if (from != color) if (from != color)
_history.Push(new ColorRegionCommand(region, from, color)); _history.Push(new ColorRegionCommand(region, from, color));
_bus.Publish(new ColorAppliedSignal(regionId, color)); _bus.Publish(new ColorAppliedSignal(regionId, color));
if (AllRegionsColored())
_bus.Publish(new AllRegionsColoredSignal());
}
// True once every region has been painted away from its authored (uncoloured) default.
private bool AllRegionsColored()
{
if (_regions.Count == 0) return false;
foreach (var region in _regions)
{
if (region == null) continue;
if (_authoredColors.TryGetValue(region.RegionId, out var authored) && region.Color == authored)
return false;
}
return true;
} }
public IReadOnlyDictionary<string, Color> GetCurrentColors() public IReadOnlyDictionary<string, Color> GetCurrentColors()

View File

@@ -25,10 +25,16 @@ namespace Darkmatter.Features.Coloring.UI
rt.localScale = startScale; rt.localScale = startScale;
rt.localRotation = Quaternion.identity; rt.localRotation = Quaternion.identity;
await Tween.Scale(rt, endScale, duration, ease).ToUniTask(cancellationToken: ct); 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, await Tween.LocalRotation(rt, new Vector3(0f, 0f, wiggleAngle), wiggleDuration, Ease.InOutSine,
cycles: wiggleCycles * 2, cycleMode: CycleMode.Yoyo) cycles: wiggleCycles * 2, cycleMode: CycleMode.Yoyo)
.ToUniTask(cancellationToken: ct); .ToUniTask(cancellationToken: ct);
if (rt == null) return;
rt.localRotation = Quaternion.identity; rt.localRotation = Quaternion.identity;
} }

View File

@@ -43,6 +43,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
private IDrawingTemplate _template; private IDrawingTemplate _template;
private string _templateId; private string _templateId;
private DrawingPhase _phase = DrawingPhase.ShapeBuilding; private DrawingPhase _phase = DrawingPhase.ShapeBuilding;
private bool _allContentReported;
private IDisposable _assembledSub; private IDisposable _assembledSub;
private IDisposable _colorAppliedSub; private IDisposable _colorAppliedSub;
@@ -151,6 +152,16 @@ namespace Darkmatter.Features.GameplayFlow.Systems
var progressAfter = _progression.GetProgress(_templateId); var progressAfter = _progression.GetProgress(_templateId);
_bus.Publish(new DrawingCompletedSignal(_templateId, progressAfter?.completionCount ?? 1)); _bus.Publish(new DrawingCompletedSignal(_templateId, progressAfter?.completionCount ?? 1));
// Player has cleared the whole catalog → surface the "ran out of content" milestone. Guarded
// per session here; analytics dedupes it to once-ever.
if (!_allContentReported &&
_catalog.AllTemplateIds.Count > 0 &&
_progression.CompletedTemplateIds.Count >= _catalog.AllTemplateIds.Count)
{
_allContentReported = true;
_bus.Publish(new AllContentCompletedSignal(_progression.CompletedTemplateIds.Count));
}
var nextId = _catalog.GetNextTemplate(_templateId); var nextId = _catalog.GetNextTemplate(_templateId);
if (string.IsNullOrEmpty(nextId)) if (string.IsNullOrEmpty(nextId))
{ {

View File

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

View File

@@ -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
}

View File

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

View File

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

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 46ff2e3f5fac64fc9a40fe00a37bd9c8

View 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.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9db44e977bbde446daa19aa928ee1230
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -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;
}
}
}

View File

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

View File

@@ -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;
}
}
}

View File

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

View File

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

View File

@@ -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();
}
}
}

View File

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

View File

@@ -107,31 +107,32 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
_bus.Publish(new ShapeBuilderStartedSignal(template.Id)); _bus.Publish(new ShapeBuilderStartedSignal(template.Id));
var colorByShapeId = BuildColorMap(template, slots); // Distinct color per element — every piece and slot gets its own color,
// even when two pieces share the same shape. Hues are stepped by the
// golden ratio so successive colors land far apart on the wheel.
float hue = UnityEngine.Random.value;
foreach (var s in slots) foreach (var s in slots)
if (s != null && s.Shape != null && colorByShapeId.TryGetValue(s.Shape.Id, out var c)) if (s != null)
s.SetColor(c); s.SetColor(NextDistinctColor(ref hue));
CreateShapePieceInstances(template, preSnappedIds, count, slots, colorByShapeId); CreateShapePieceInstances(template, preSnappedIds, count, slots, ref hue);
CheckIfShapeAssembled(); CheckIfShapeAssembled();
} }
private static Dictionary<string, Color> BuildColorMap(IDrawingTemplate template, SlotMarker[] slots) // Conjugate of the golden ratio; adding it (mod 1) to a hue each step
// produces a maximally-spread, low-collision sequence of distinct colors.
private const float GoldenHueStep = 0.61803398875f;
private static Color NextDistinctColor(ref float hue)
{ {
var map = new Dictionary<string, Color>(); hue = Mathf.Repeat(hue + GoldenHueStep, 1f);
foreach (var p in template.Pieces) return Color.HSVToRGB(hue, 0.7f, 0.95f);
if (p != null && !string.IsNullOrEmpty(p.Id) && !map.ContainsKey(p.Id))
map[p.Id] = Color.HSVToRGB(UnityEngine.Random.value, 0.7f, 0.95f);
foreach (var s in slots)
if (s != null && s.Shape != null && !string.IsNullOrEmpty(s.Shape.Id) && !map.ContainsKey(s.Shape.Id))
map[s.Shape.Id] = Color.HSVToRGB(UnityEngine.Random.value, 0.7f, 0.95f);
return map;
} }
private void CreateShapePieceInstances(IDrawingTemplate template, IReadOnlyCollection<string> preSnappedIds, private void CreateShapePieceInstances(IDrawingTemplate template, IReadOnlyCollection<string> preSnappedIds,
int count, int count,
SlotMarker[] slots, Dictionary<string, Color> colorByShapeId) SlotMarker[] slots, ref float hue)
{ {
var preSnapCounts = new Dictionary<string, int>(); var preSnapCounts = new Dictionary<string, int>();
if (preSnappedIds != null) if (preSnappedIds != null)
@@ -152,8 +153,7 @@ namespace Darkmatter.Features.ShapeBuilder.Systems
} }
var piece = _factory.Create(_piecePrefab, shape, candidates, Vector2.zero); var piece = _factory.Create(_piecePrefab, shape, candidates, Vector2.zero);
if (colorByShapeId != null && colorByShapeId.TryGetValue(shape.Id, out var c)) piece.SetColor(NextDistinctColor(ref hue));
piece.SetColor(c);
_pieces.Add(piece); _pieces.Add(piece);
if (preSnapCounts.TryGetValue(shape.Id, out var remaining) && remaining > 0) if (preSnapCounts.TryGetValue(shape.Id, out var remaining) && remaining > 0)

View File

@@ -97,7 +97,10 @@ namespace Darkmatter.Features.ShapeBuilder.UI
_bus = bus; _bus = bus;
_undo = undo; _undo = undo;
_trayPos = trayPos; _trayPos = trayPos;
_traySize = shape.DefaultSizeDelta; // Keep the piece at the prefab Image's authored size. Sourcing this from
// shape.DefaultSizeDelta let each ShapeSO override the image's default size;
// the prefab (with preserveAspect) is the single source of truth now.
_traySize = RectTransform.sizeDelta;
_dragRoot = dragRoot; _dragRoot = dragRoot;
_homeParent = RectTransform.parent; _homeParent = RectTransform.parent;
@@ -151,7 +154,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI
if (_locked) return; if (_locked) return;
var pointerLocal = ScreenToLocal(e.position) + _grabOffset; var pointerLocal = ScreenToLocal(e.position) + _grabOffset;
var hovered = FindSlotUnder(e.position); var hovered = FindSlotForCatch(e.position);
bool insidePreview = hovered != null; bool insidePreview = hovered != null;
if (insidePreview && !_inPreview) if (insidePreview && !_inPreview)
@@ -187,7 +190,11 @@ namespace Darkmatter.Features.ShapeBuilder.UI
SetAlpha(_dragOrigAlpha); SetAlpha(_dragOrigAlpha);
var target = FindSlotUnder(e.position); // If a slot is already previewing, releasing commits to it. Otherwise catch
// a quick drop with no prior preview using the same per-slot radius.
var target = _inPreview && _activeSlot != null
? _activeSlot
: FindSlotForCatch(e.position);
if (target != null) if (target != null)
{ {
_activeSlot = target; _activeSlot = target;
@@ -200,17 +207,45 @@ namespace Darkmatter.Features.ShapeBuilder.UI
} }
} }
private SlotMarker FindSlotUnder(Vector2 screenPos) // Nearest slot whose per-slot catch circle contains the pointer. Each slot's
// radius is derived from its own size (see SlotCatchRadius), so big slots catch
// from farther than small ones. All distances are in PaperRoot local space —
// ScreenPointToLocalPointInRectangle and InverseTransformVector both strip the
// CanvasScaler factor, so the catch feels identical on every screen resolution.
private SlotMarker FindSlotForCatch(Vector2 screenPos)
{ {
if (_candidateSlots == null) return null; if (_candidateSlots == null) return null;
Vector2 pointerLocal = ScreenToLocal(screenPos);
SlotMarker best = null;
float bestSqr = float.MaxValue;
foreach (var s in _candidateSlots) foreach (var s in _candidateSlots)
{ {
if (s == null) continue; if (s == null) continue;
if (s.IsOccupied && s != _activeSlot) continue; if (s.IsOccupied && s != _activeSlot) continue;
if (RectTransformUtility.RectangleContainsScreenPoint(s.RectTransform, screenPos, _eventCam)) var srt = s.RectTransform;
return s; Vector2 slotLocal = _parentRect.InverseTransformPoint(srt.position);
float sqr = (slotLocal - pointerLocal).sqrMagnitude;
float radius = SlotCatchRadius(srt);
if (sqr <= radius * radius && sqr < bestSqr)
{
bestSqr = sqr;
best = s;
}
} }
return null; return best;
}
// Catch radius for one slot, in PaperRoot local units: half the slot's diagonal
// (its size mapped into parent space, so any slot rotation/mirror/scale is
// accounted for) times the config multiplier. Scales per slot — not one flat
// distance for all.
private float SlotCatchRadius(RectTransform slot)
{
Vector2 size = slot.rect.size;
Vector2 prX = _parentRect.InverseTransformVector(slot.TransformVector(new Vector3(size.x, 0f, 0f)));
Vector2 prY = _parentRect.InverseTransformVector(slot.TransformVector(new Vector3(0f, size.y, 0f)));
float halfDiagonal = 0.5f * new Vector2(prX.magnitude, prY.magnitude).magnitude;
return halfDiagonal * _cfg.CatchRadiusScale;
} }
private void AnimatePreviewPose(bool toSlot) private void AnimatePreviewPose(bool toSlot)

View File

@@ -11,6 +11,7 @@ using Darkmatter.Core.Data.Signals.Features.GameplayFlow; // DrawingCompletedSig
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder; // ShapeBuilderStarted/PieceSnapped/ShapeAssembled using Darkmatter.Core.Data.Signals.Features.ShapeBuilder; // ShapeBuilderStarted/PieceSnapped/ShapeAssembled
using Darkmatter.Core.Data.Signals.Features.Tutorial; using Darkmatter.Core.Data.Signals.Features.Tutorial;
using Darkmatter.Core.Data.Static.Features.Tutorial; using Darkmatter.Core.Data.Static.Features.Tutorial;
using Darkmatter.Core.Enums.Features.Tutorial; // BubblePlacement
using Darkmatter.Features.Coloring.UI; // ColorButton, ColorRegionView using Darkmatter.Features.Coloring.UI; // ColorButton, ColorRegionView
using Darkmatter.Features.DrawingCatalog; // DrawingCatalogButton using Darkmatter.Features.DrawingCatalog; // DrawingCatalogButton
using Darkmatter.Features.GameplayFlow.UI; // NextButtonView using Darkmatter.Features.GameplayFlow.UI; // NextButtonView
@@ -44,6 +45,8 @@ namespace Darkmatter.Features.Tutorial.Systems
private CancellationTokenSource _runCts; private CancellationTokenSource _runCts;
private CancellationToken _ct; private CancellationToken _ct;
private bool _completed; private bool _completed;
private bool _drawingCompleted;
private bool _hasColored;
private bool _suspended; private bool _suspended;
private Action _reshow; private Action _reshow;
private int _stepIndex; private int _stepIndex;
@@ -74,6 +77,13 @@ namespace Darkmatter.Features.Tutorial.Systems
_navSubs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => Suspend())); _navSubs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => Suspend()));
_navSubs.Add(_bus.Subscribe<OpenColorBookSignal>(_ => Resume())); _navSubs.Add(_bus.Subscribe<OpenColorBookSignal>(_ => Resume()));
_navSubs.Add(_bus.Subscribe<DrawingCatalogReadySignal>(OnCatalogReadyGlobal)); _navSubs.Add(_bus.Subscribe<DrawingCatalogReadySignal>(OnCatalogReadyGlobal));
// The drawing being completed (Next pressed) is the run's true end — even if the child
// 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); StartRun(skipCatalogWait: false);
} }
@@ -86,6 +96,8 @@ namespace Darkmatter.Features.Tutorial.Systems
_ct = _runCts.Token; _ct = _runCts.Token;
_gen++; _gen++;
_suspended = false; _suspended = false;
_drawingCompleted = false;
_hasColored = false;
_reshow = null; _reshow = null;
_stepIndex = 0; _stepIndex = 0;
_overlay.HideInstant(); _overlay.HideInstant();
@@ -108,12 +120,23 @@ namespace Darkmatter.Features.Tutorial.Systems
} }
// The catalog re-appeared while we were mid-gameplay -> the player went Back. Restart from // The catalog re-appeared while we were mid-gameplay -> the player went Back. Restart from
// step 1 (the catalog is already on screen, so skip the wait). Excludes step 6+, whose own // step 1 (the catalog is already on screen, so skip the wait). Excludes step 7+ (the Next
// completion loads the catalog. // press), whose own completion loads the catalog.
private void OnCatalogReadyGlobal(DrawingCatalogReadySignal _) private void OnCatalogReadyGlobal(DrawingCatalogReadySignal _)
{ {
if (!_completed && _stepIndex >= 2 && _stepIndex <= 5) if (_completed || _drawingCompleted || _stepIndex < 2 || _stepIndex > 6) return;
StartRun(skipCatalogWait: true);
// 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) private void ShowStep(Action show)
@@ -154,7 +177,7 @@ namespace Darkmatter.Features.Tutorial.Systems
if (!await WaitForSignalAsync<DrawingCatalogReadySignal>()) return; if (!await WaitForSignalAsync<DrawingCatalogReadySignal>()) return;
} }
await UniTask.NextFrame(_ct); await UniTask.NextFrame(_ct);
ShowStep(() => ShowBlockingTap(FindFirstCatalogCell(), _config.PickText)); ShowStep(() => ShowBlockingTap(FindFirstCatalogCell(), _config.PickText, _config.PickBubble, _config.PickBubbleOffset));
if (!await WaitForActionAsync<DrawingSelectedSignal>()) return; if (!await WaitForActionAsync<DrawingSelectedSignal>()) return;
EndStep("pick", 1); EndStep("pick", 1);
@@ -165,7 +188,7 @@ namespace Darkmatter.Features.Tutorial.Systems
ShowStep(() => ShowStep(() =>
{ {
var (pieceRect, slotRect) = FindFirstPieceAndSlot(); var (pieceRect, slotRect) = FindFirstPieceAndSlot();
if (pieceRect != null) _overlay.ShowDrag(pieceRect, slotRect, _config.DragText); if (pieceRect != null) _overlay.ShowDrag(pieceRect, slotRect, _config.DragText, _config.DragBubble, _config.DragBubbleOffset);
else Debug.LogWarning("[Tutorial] No draggable piece found for the drag step."); else Debug.LogWarning("[Tutorial] No draggable piece found for the drag step.");
}); });
if (!await WaitForActionAsync<PieceSnappedSignal>()) return; // any snap teaches the gesture if (!await WaitForActionAsync<PieceSnappedSignal>()) return; // any snap teaches the gesture
@@ -173,7 +196,7 @@ namespace Darkmatter.Features.Tutorial.Systems
// Step 3 — finish the rest of the puzzle freely (non-blocking hint). // Step 3 — finish the rest of the puzzle freely (non-blocking hint).
_stepIndex = 3; _stepIndex = 3;
ShowStep(() => _overlay.ShowTap(null, _config.FinishText, blockInput: false)); ShowStep(() => _overlay.ShowTap(null, _config.FinishText, blockInput: false, _config.FinishBubble, _config.FinishBubbleOffset));
if (!await WaitForActionAsync<ShapeAssembledSignal>()) return; if (!await WaitForActionAsync<ShapeAssembledSignal>()) return;
EndStep("finish", 3); EndStep("finish", 3);
@@ -181,32 +204,55 @@ namespace Darkmatter.Features.Tutorial.Systems
_stepIndex = 4; _stepIndex = 4;
if (!await WaitForSignalAsync<RegionsInitializedSignal>()) return; if (!await WaitForSignalAsync<RegionsInitializedSignal>()) return;
await UniTask.NextFrame(_ct); await UniTask.NextFrame(_ct);
ShowStep(() => ShowBlockingTap(FindFirstColorButton(), _config.ColorText)); ShowStep(() => ShowBlockingTap(FindFirstColorButton(), _config.ColorText, _config.ColorBubble, _config.ColorBubbleOffset));
if (!await WaitForActionAsync<ColorSelectedSignal>()) return; if (!await WaitForActionAsync<ColorSelectedSignal>()) return;
EndStep("color", 4); EndStep("color", 4);
// Step 5 — paint a region. // Step 5 — paint the first region (teaches the tap by spotlighting one region).
_stepIndex = 5; _stepIndex = 5;
ShowStep(() => ShowBlockingTap(FindLargestRegion(), _config.PaintText)); // Watch full-colour completion across both this taught tap and the free-paint step that
// follows. A one-region drawing is finished by this very tap, so its signal would fire
// before step 6 could subscribe — the flag lets us skip the free-paint step instead of
// stranding the child on a hint with nothing left to colour.
var fullyColored = false;
using var fullColorSub = _bus.Subscribe<AllRegionsColoredSignal>(_ => fullyColored = true);
ShowStep(() => ShowBlockingTap(FindLargestRegion(), _config.PaintText, _config.PaintBubble, _config.PaintBubbleOffset));
if (!await WaitForActionAsync<ColorAppliedSignal>()) return; if (!await WaitForActionAsync<ColorAppliedSignal>()) return;
EndStep("paint", 5); EndStep("paint", 5);
// Step 6 — press Next. // Step 6 — colour the rest of the picture freely (non-blocking hint). Hold here until
// every region is filled, so the "Tap Next" prompt only appears once colouring is done.
// The child can still reach the live Next button during this open step; if they finish
// the drawing early that way, take it as done rather than waiting on a fill that can't come.
_stepIndex = 6; _stepIndex = 6;
await UniTask.NextFrame(_ct); if (!fullyColored)
ShowStep(() => ShowBlockingTap(FindNextButton(), _config.NextText)); {
if (!await WaitForActionAsync<DrawingCompletedSignal>()) return; ShowStep(() => _overlay.ShowTap(null, _config.FinishColoringText, blockInput: false, _config.FinishColoringBubble, _config.FinishColoringBubbleOffset));
EndStep("next", 6); await UniTask.WhenAny(
WaitForActionAsync<AllRegionsColoredSignal>(),
WaitForActionAsync<DrawingCompletedSignal>());
}
EndStep("finishColoring", 6);
// Step 7 — celebrate. // Step 7 — press Next (skipped if the drawing was already completed early).
_stepIndex = 7; if (!_drawingCompleted)
await _overlay.ShowToastAsync(_config.DoneText, _ct); {
_stepIndex = 7;
await UniTask.NextFrame(_ct);
ShowStep(() => ShowBlockingTap(FindNextButton(), _config.NextText, _config.NextBubble, _config.NextBubbleOffset));
if (!await WaitForActionAsync<DrawingCompletedSignal>()) return;
EndStep("next", 7);
}
// Step 8 — celebrate.
_stepIndex = 8;
await _overlay.ShowToastAsync(_config.DoneText, _ct, _config.DoneBubble, _config.DoneBubbleOffset);
} }
private void ShowBlockingTap(RectTransform target, string message) private void ShowBlockingTap(RectTransform target, string message, BubblePlacement placement, Vector2 offset)
{ {
if (target == null) Debug.LogWarning($"[Tutorial] No target found for step: \"{message}\""); if (target == null) Debug.LogWarning($"[Tutorial] No target found for step: \"{message}\"");
_overlay.ShowTap(target, message, blockInput: target != null); _overlay.ShowTap(target, message, blockInput: target != null, placement, offset);
} }
private void EndStep(string id, int index) private void EndStep(string id, int index)
@@ -283,13 +329,16 @@ namespace Darkmatter.Features.Tutorial.Systems
private static (RectTransform piece, RectTransform slot) FindFirstPieceAndSlot() private static (RectTransform piece, RectTransform slot) FindFirstPieceAndSlot()
{ {
// Pieces spawn in data order under one SpawnRoot, so sibling 0 is the top of the tray.
// FindObjectsByType returns undefined order — pick the lowest sibling index, not [0].
var pieces = UnityEngine.Object.FindObjectsByType<ShapePiece>(FindObjectsSortMode.None); var pieces = UnityEngine.Object.FindObjectsByType<ShapePiece>(FindObjectsSortMode.None);
ShapePiece piece = null; ShapePiece piece = null;
var bestIndex = int.MaxValue;
foreach (var p in pieces) foreach (var p in pieces)
{ {
if (p == null || p.IsLocked || !p.gameObject.activeInHierarchy) continue; if (p == null || p.IsLocked || !p.gameObject.activeInHierarchy) continue;
piece = p; var index = p.transform.GetSiblingIndex();
break; if (index < bestIndex) { bestIndex = index; piece = p; }
} }
if (piece == null) return (null, null); if (piece == null) return (null, null);

View File

@@ -2,6 +2,7 @@ using System;
using System.Threading; using System.Threading;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Features.Tutorial; using Darkmatter.Core.Contracts.Features.Tutorial;
using Darkmatter.Core.Enums.Features.Tutorial;
using PrimeTween; using PrimeTween;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
@@ -45,6 +46,8 @@ namespace Darkmatter.Features.Tutorial.UI
[SerializeField] private float fadeDuration = 0.25f; [SerializeField] private float fadeDuration = 0.25f;
[SerializeField] private float toastSeconds = 1.6f; [SerializeField] private float toastSeconds = 1.6f;
[SerializeField] private float bubbleGap = 60f; [SerializeField] private float bubbleGap = 60f;
[Tooltip("Margin (px) from the top/bottom edge for the Top/Bottom bubble placement presets.")]
[SerializeField] private float bubbleEdgeMargin = 150f;
[SerializeField] private float haloPulseScale = 1.18f; [SerializeField] private float haloPulseScale = 1.18f;
[SerializeField] private float pulseDuration = 0.6f; [SerializeField] private float pulseDuration = 0.6f;
[SerializeField] private float dragHandDuration = 1.1f; [SerializeField] private float dragHandDuration = 1.1f;
@@ -60,6 +63,8 @@ namespace Darkmatter.Features.Tutorial.UI
private RectTransform _area; private RectTransform _area;
private Camera _overlayCam; private Camera _overlayCam;
private Mode _mode = Mode.Hidden; private Mode _mode = Mode.Hidden;
private BubblePlacement _placement = BubblePlacement.Auto;
private Vector2 _bubbleOffset;
private RectTransform _target; // tap target private RectTransform _target; // tap target
private RectTransform _dragFrom; // the piece (drag start) private RectTransform _dragFrom; // the piece (drag start)
private RectTransform _dragTo; // the slot (drag destination) private RectTransform _dragTo; // the slot (drag destination)
@@ -91,11 +96,13 @@ namespace Darkmatter.Features.Tutorial.UI
// ── ITutorialOverlay ───────────────────────────────────────────────── // ── ITutorialOverlay ─────────────────────────────────────────────────
public void ShowTap(RectTransform target, string message, bool blockInput) public void ShowTap(RectTransform target, string message, bool blockInput, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default)
{ {
CacheRefs(); CacheRefs();
KillAnims(); KillAnims();
SetText(message); SetText(message);
_placement = placement;
_bubbleOffset = offset;
_dragFrom = null; _dragFrom = null;
_dragTo = null; _dragTo = null;
@@ -122,11 +129,13 @@ namespace Darkmatter.Features.Tutorial.UI
FadeInQuick(); FadeInQuick();
} }
public void ShowDrag(RectTransform from, RectTransform to, string message) public void ShowDrag(RectTransform from, RectTransform to, string message, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default)
{ {
CacheRefs(); CacheRefs();
KillAnims(); KillAnims();
SetText(message); SetText(message);
_placement = placement;
_bubbleOffset = offset;
// No dim and no blocking — the piece lives under the overlay and must stay visible and // No dim and no blocking — the piece lives under the overlay and must stay visible and
// draggable. Halo + hand travel together along the piece -> slot path, recomputed live. // draggable. Halo + hand travel together along the piece -> slot path, recomputed live.
@@ -153,11 +162,13 @@ namespace Darkmatter.Features.Tutorial.UI
FadeInQuick(); FadeInQuick();
} }
public async UniTask ShowToastAsync(string message, CancellationToken ct) public async UniTask ShowToastAsync(string message, CancellationToken ct, BubblePlacement placement = BubblePlacement.Auto, Vector2 offset = default)
{ {
CacheRefs(); CacheRefs();
KillAnims(); KillAnims();
SetText(message); SetText(message);
_placement = placement;
_bubbleOffset = offset;
_mode = Mode.Centered; _mode = Mode.Centered;
_target = null; _target = null;
@@ -238,8 +249,10 @@ namespace Darkmatter.Features.Tutorial.UI
if (hand != null) if (hand != null)
hand.anchoredPosition = centerLocal + new Vector2(0f, -radiusLocal * 0.5f); hand.anchoredPosition = centerLocal + new Vector2(0f, -radiusLocal * 0.5f);
PositionBubble(centerLocal.y, centerLocal.y + radiusLocal, centerLocal.y - radiusLocal, if (_placement == BubblePlacement.Auto)
_area.rect.height * 0.5f); PositionBubble(centerLocal.y, centerLocal.y + radiusLocal, centerLocal.y - radiusLocal);
else
PositionBubblePreset(_placement);
} }
// Drag: halo + hand glide piece -> slot, recomputed live so it tracks the real positions. // Drag: halo + hand glide piece -> slot, recomputed live so it tracks the real positions.
@@ -283,27 +296,59 @@ namespace Darkmatter.Features.Tutorial.UI
// The drag path spans the play area, so pin the bubble to a screen edge instead of hugging // The drag path spans the play area, so pin the bubble to a screen edge instead of hugging
// the piece — keeps it off the shapes. // the piece — keeps it off the shapes.
PositionBubbleAtEdge(dragBubbleAtTop, dragBubbleEdgeMargin); if (_placement == BubblePlacement.Auto)
PositionBubbleAtEdge(dragBubbleAtTop, dragBubbleEdgeMargin);
else
PositionBubblePreset(_placement);
} }
private void LayoutCentered() private void LayoutCentered()
{ {
if (bubbleRoot == null) return; if (bubbleRoot == null) return;
if (_placement != BubblePlacement.Auto) { PositionBubblePreset(_placement); return; }
float halfH = _area.rect.height * 0.5f; float halfH = _area.rect.height * 0.5f;
float bubbleHalf = bubbleRoot.rect.height * 0.5f; SetBubbleAnchored(new Vector2(0f, halfH * 0.45f));
bubbleRoot.anchoredPosition =
new Vector2(0f, Mathf.Clamp(halfH * 0.45f, -halfH + bubbleHalf, halfH - bubbleHalf));
} }
private void PositionBubble(float holeCenterY, float holeTopY, float holeBottomY, float halfH) // Applies the per-step offset to a computed base position and clamps so the bubble always
// stays fully on-screen, then commits it. Every bubble-positioning path routes through here.
private void SetBubbleAnchored(Vector2 baseLocal)
{
if (bubbleRoot == null) return;
float halfW = _area.rect.width * 0.5f;
float halfH = _area.rect.height * 0.5f;
float bubbleHalfW = bubbleRoot.rect.width * 0.5f;
float bubbleHalfH = bubbleRoot.rect.height * 0.5f;
Vector2 p = baseLocal + _bubbleOffset;
p.x = Mathf.Clamp(p.x, -halfW + bubbleHalfW, halfW - bubbleHalfW);
p.y = Mathf.Clamp(p.y, -halfH + bubbleHalfH, halfH - bubbleHalfH);
bubbleRoot.anchoredPosition = p;
}
private void PositionBubble(float holeCenterY, float holeTopY, float holeBottomY)
{ {
if (bubbleRoot == null) return; if (bubbleRoot == null) return;
float bubbleHalf = bubbleRoot.rect.height * 0.5f; float bubbleHalf = bubbleRoot.rect.height * 0.5f;
float y = holeCenterY <= 0f float y = holeCenterY <= 0f
? holeTopY + bubbleGap + bubbleHalf ? holeTopY + bubbleGap + bubbleHalf
: holeBottomY - bubbleGap - bubbleHalf; : holeBottomY - bubbleGap - bubbleHalf;
y = Mathf.Clamp(y, -halfH + bubbleHalf, halfH - bubbleHalf); SetBubbleAnchored(new Vector2(0f, y));
bubbleRoot.anchoredPosition = new Vector2(0f, y); }
// Fixed-slot placement chosen per step (Top/Center/Bottom). Horizontally centred; clamped so
// the bubble always stays fully on-screen.
private void PositionBubblePreset(BubblePlacement placement)
{
if (bubbleRoot == null) return;
float halfH = _area.rect.height * 0.5f;
float bubbleHalf = bubbleRoot.rect.height * 0.5f;
float y = placement switch
{
BubblePlacement.Top => halfH - bubbleHalf - bubbleEdgeMargin,
BubblePlacement.Bottom => -halfH + bubbleHalf + bubbleEdgeMargin,
_ => 0f, // Center
};
SetBubbleAnchored(new Vector2(0f, y));
} }
// Pins the bubble to the top or bottom edge (used for the drag step, whose target spans the // Pins the bubble to the top or bottom edge (used for the drag step, whose target spans the
@@ -314,8 +359,7 @@ namespace Darkmatter.Features.Tutorial.UI
float halfH = _area.rect.height * 0.5f; float halfH = _area.rect.height * 0.5f;
float bubbleHalf = bubbleRoot.rect.height * 0.5f; float bubbleHalf = bubbleRoot.rect.height * 0.5f;
float y = top ? halfH - bubbleHalf - margin : -halfH + bubbleHalf + margin; float y = top ? halfH - bubbleHalf - margin : -halfH + bubbleHalf + margin;
y = Mathf.Clamp(y, -halfH + bubbleHalf, halfH - bubbleHalf); SetBubbleAnchored(new Vector2(0f, y));
bubbleRoot.anchoredPosition = new Vector2(0f, y);
} }
// ── Coordinate conversion (camera-agnostic) ────────────────────────── // ── Coordinate conversion (camera-agnostic) ──────────────────────────
@@ -405,6 +449,8 @@ namespace Darkmatter.Features.Tutorial.UI
private void ApplyHiddenState() private void ApplyHiddenState()
{ {
_mode = Mode.Hidden; _mode = Mode.Hidden;
_placement = BubblePlacement.Auto;
_bubbleOffset = Vector2.zero;
_target = null; _target = null;
_dragFrom = null; _dragFrom = null;
_dragTo = null; _dragTo = null;

View File

@@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.Threading; using System.Threading;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Services.Ads; using Darkmatter.Core.Contracts.Services.Ads;
using Darkmatter.Core.Contracts.Services.Analytics;
using Darkmatter.Core.Data.Dynamic.Services.Ads; using Darkmatter.Core.Data.Dynamic.Services.Ads;
using Darkmatter.Core.Data.Static.Services.Ads; using Darkmatter.Core.Data.Static.Services.Ads;
using Darkmatter.Core.Enums.Services.Ads; using Darkmatter.Core.Enums.Services.Ads;
using UnityEngine; using UnityEngine;
using VContainer;
using AdFormat = Darkmatter.Core.Enums.Services.Ads.AdFormat; using AdFormat = Darkmatter.Core.Enums.Services.Ads.AdFormat;
#if GOOGLE_MOBILE_ADS #if GOOGLE_MOBILE_ADS
using GoogleMobileAds.Api; using GoogleMobileAds.Api;
@@ -25,13 +27,22 @@ namespace Darkmatter.Services.Ads
[SerializeField, Min(1)] private int reloadMaxAttempts = 6; [SerializeField, Min(1)] private int reloadMaxAttempts = 6;
[Tooltip("Hard fallback (seconds) to recover a full-screen show if AdMob never raises its close callback. Android focus-return recovery usually fires far sooner; this cap covers iOS/edge cases. Must exceed max plausible ad length so a real ad is never cut short.")] [Tooltip("Hard fallback (seconds) to recover a full-screen show if AdMob never raises its close callback. Android focus-return recovery usually fires far sooner; this cap covers iOS/edge cases. Must exceed max plausible ad length so a real ad is never cut short.")]
[SerializeField, Min(15f)] private float showWatchdogSeconds = 60f; [SerializeField, Min(15f)] private float showWatchdogSeconds = 60f;
[Tooltip("Max interstitials shown per app session (run). Once reached, ShowAsync(Interstitial) no-ops until the app restarts. Counts only ads actually shown, not failed/skipped attempts. 0 disables interstitials.")]
[SerializeField, Min(0)] private int maxInterstitialsPerSession = 8;
public bool IsInitialized => _initialized; public bool IsInitialized => _initialized;
public event Action<AdFormat, AdLoadState> LoadStateChanged; public event Action<AdFormat, AdLoadState> LoadStateChanged;
private IAnalyticsService _analytics;
private bool _initialized; private bool _initialized;
// Per-session interstitial counter. The service is the app-lifetime singleton (it retains
// loaded ads across scene swaps), so this survives Colorbook<->Gameplay transitions and only
// resets on app restart — i.e. a true per-session cap.
private int _interstitialsShownThisSession;
private bool _hasUserConsent = true; private bool _hasUserConsent = true;
private bool _isChildDirected; // Coloring book is a child-directed app, so default to true. SetConsent can still
// override if a consent flow later supplies a different value.
private bool _isChildDirected = true;
private CancellationTokenSource _lifetimeCts; private CancellationTokenSource _lifetimeCts;
// App interruption state, fed by the Unity lifecycle messages below. A full-screen ad // App interruption state, fed by the Unity lifecycle messages below. A full-screen ad
@@ -51,6 +62,12 @@ namespace Darkmatter.Services.Ads
private BannerView _banner; private BannerView _banner;
#endif #endif
// Method injection: AdMobAdService is a scene MonoBehaviour registered via RegisterComponent, so
// VContainer injects here rather than through a constructor. IAnalyticsService is the Root-scoped
// composite (Firebase + Facebook).
[Inject]
public void Construct(IAnalyticsService analytics) => _analytics = analytics;
private void Awake() private void Awake()
{ {
_lifetimeCts = new CancellationTokenSource(); _lifetimeCts = new CancellationTokenSource();
@@ -178,6 +195,11 @@ namespace Darkmatter.Services.Ads
if (!_initialized) return AdShowResult.Failure("Not initialized."); if (!_initialized) return AdShowResult.Failure("Not initialized.");
#if GOOGLE_MOBILE_ADS #if GOOGLE_MOBILE_ADS
// Per-session interstitial cap. Check before load so a capped show wastes no fill. Skip
// is silent (Failure, Shown=false); the fire-and-forget caller just keeps playing.
if (format == AdFormat.Interstitial && _interstitialsShownThisSession >= maxInterstitialsPerSession)
return AdShowResult.Failure("Session interstitial cap reached.");
if (!IsReady(format)) if (!IsReady(format))
{ {
bool loaded = await LoadAsync(format, cancellationToken); bool loaded = await LoadAsync(format, cancellationToken);
@@ -186,7 +208,12 @@ namespace Darkmatter.Services.Ads
switch (format) switch (format)
{ {
case AdFormat.Interstitial: return await ShowInterstitialAsync(cancellationToken); case AdFormat.Interstitial:
{
var result = await ShowInterstitialAsync(cancellationToken);
if (result.Shown) _interstitialsShownThisSession++; // count real shows only
return result;
}
case AdFormat.Rewarded: return await ShowRewardedAsync(cancellationToken); case AdFormat.Rewarded: return await ShowRewardedAsync(cancellationToken);
case AdFormat.RewardedInterstitial: return await ShowRewardedInterstitialAsync(cancellationToken); case AdFormat.RewardedInterstitial: return await ShowRewardedInterstitialAsync(cancellationToken);
case AdFormat.AppOpen: return await ShowAppOpenAsync(cancellationToken); case AdFormat.AppOpen: return await ShowAppOpenAsync(cancellationToken);
@@ -208,6 +235,8 @@ namespace Darkmatter.Services.Ads
_banner?.Destroy(); _banner?.Destroy();
_banner = new BannerView(unitId, MapBannerSize(size), MapBannerPosition(position)); _banner = new BannerView(unitId, MapBannerSize(size), MapBannerPosition(position));
_banner.OnAdPaid += v => LogAdImpression(AdFormat.Banner, v);
_banner.OnAdClicked += () => LogAdClicked(AdFormat.Banner);
var tcs = new UniTaskCompletionSource<bool>(); var tcs = new UniTaskCompletionSource<bool>();
_banner.OnBannerAdLoaded += () => tcs.TrySetResult(true); _banner.OnBannerAdLoaded += () => tcs.TrySetResult(true);
@@ -267,7 +296,9 @@ namespace Darkmatter.Services.Ads
: TagForChildDirectedTreatment.Unspecified, : TagForChildDirectedTreatment.Unspecified,
TagForUnderAgeOfConsent = _hasUserConsent TagForUnderAgeOfConsent = _hasUserConsent
? TagForUnderAgeOfConsent.Unspecified ? TagForUnderAgeOfConsent.Unspecified
: TagForUnderAgeOfConsent.True : TagForUnderAgeOfConsent.True,
// Child-directed app: cap served ads at G-rated content.
MaxAdContentRating = MaxAdContentRating.G
}; };
if (catalog.TestDeviceIds is { Count: > 0 }) if (catalog.TestDeviceIds is { Count: > 0 })
@@ -556,17 +587,59 @@ namespace Darkmatter.Services.Ads
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
} }
private void WireFullScreenEvents(InterstitialAd ad, AdFormat format) => private void WireFullScreenEvents(InterstitialAd ad, AdFormat format)
{
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle); ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
ad.OnAdPaid += v => LogAdImpression(format, v);
ad.OnAdClicked += () => LogAdClicked(format);
}
private void WireFullScreenEvents(RewardedAd ad, AdFormat format) => private void WireFullScreenEvents(RewardedAd ad, AdFormat format)
{
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle); ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
ad.OnAdPaid += v => LogAdImpression(format, v);
ad.OnAdClicked += () => LogAdClicked(format);
}
private void WireFullScreenEvents(RewardedInterstitialAd ad, AdFormat format) => private void WireFullScreenEvents(RewardedInterstitialAd ad, AdFormat format)
{
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle); ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
ad.OnAdPaid += v => LogAdImpression(format, v);
ad.OnAdClicked += () => LogAdClicked(format);
}
private void WireFullScreenEvents(AppOpenAd ad, AdFormat format) => private void WireFullScreenEvents(AppOpenAd ad, AdFormat format)
{
ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle); ad.OnAdFullScreenContentClosed += () => SetState(format, AdLoadState.Idle);
ad.OnAdPaid += v => LogAdImpression(format, v);
ad.OnAdClicked += () => LogAdClicked(format);
}
// Ad revenue: GA4-recommended ad_impression carries value+currency from AdMob's paid callback —
// this is what surfaces ad revenue in Firebase/GA4. Ad callbacks fire on the Unity main thread
// (RaiseAdEventsOnUnityMainThread = true), so logging straight to analytics is safe.
private void LogAdImpression(AdFormat format, AdValue value)
{
_analytics?.LogEvent(AnalyticsEvents.AdImpression, new Dictionary<string, object>
{
[AnalyticsParams.AdPlatform] = "AdMob",
[AnalyticsParams.AdFormat] = format.ToString(),
[AnalyticsParams.AdUnitName] = catalog.GetUnitId(format, Application.platform) ?? string.Empty,
[AnalyticsParams.Value] = value.Value / 1_000_000.0, // AdValue.Value is micros
[AnalyticsParams.Currency] = value.CurrencyCode ?? string.Empty,
[AnalyticsParams.Precision] = value.Precision.ToString(),
});
}
private void LogAdClicked(AdFormat format)
{
_analytics?.LogEvent(AnalyticsEvents.AdClicked, new Dictionary<string, object>
{
[AnalyticsParams.AdPlatform] = "AdMob",
[AnalyticsParams.AdFormat] = format.ToString(),
[AnalyticsParams.AdUnitName] = catalog.GetUnitId(format, Application.platform) ?? string.Empty,
});
}
private void ScheduleReload(AdFormat format) private void ScheduleReload(AdFormat format)
{ {

View File

@@ -10,8 +10,17 @@ namespace Darkmatter.Services.Analytics
{ {
public void Register(IContainerBuilder builder) public void Register(IContainerBuilder builder)
{ {
builder.RegisterEntryPoint<FirebaseAnalyticsSystem>().As<IAnalyticsService>(); // Firebase + Facebook are sinks (registered AsSelf so the composite can inject them);
// CompositeAnalyticsService is the single IAnalyticsService the rest of the app consumes.
builder.RegisterEntryPoint<FirebaseAnalyticsSystem>().AsSelf();
builder.RegisterEntryPoint<FacebookAnalyticsSystem>().AsSelf();
builder.Register<CompositeAnalyticsService>(Lifetime.Singleton).As<IAnalyticsService>();
builder.RegisterEntryPoint<AnalyticsTracker>(); builder.RegisterEntryPoint<AnalyticsTracker>();
builder.RegisterEntryPoint<ErrorAnalyticsTracker>();
// Feeds the FCM token to AppsFlyer for uninstall measurement (Android-only).
builder.RegisterEntryPoint<AppsFlyerUninstallTracker>();
} }
} }
} }

View File

@@ -2,14 +2,12 @@
"name": "Services.Analytics", "name": "Services.Analytics",
"rootNamespace": "Darkmatter.Services.Analytics", "rootNamespace": "Darkmatter.Services.Analytics",
"references": [ "references": [
"GUID:bd7ea2d41bfe64d229c22616f66e20f7",
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1", "GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
"GUID:f51ebe6a0ceec4240a699833d6309b23", "GUID:f51ebe6a0ceec4240a699833d6309b23",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc", "GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:f8c64bb88d959406689053ae3f31183d",
"GUID:a0b1547602fc44f6da0a5e755ab3a7ef",
"GUID:6a0a834eb41764f12ba55c3fb04a40cb", "GUID:6a0a834eb41764f12ba55c3fb04a40cb",
"GUID:b4c9f7fbf1e144933a1797dc208ece5f" "GUID:b4c9f7fbf1e144933a1797dc208ece5f",
"GUID:2a37df438292d4903b4e5159c5de3bf9"
], ],
"includePlatforms": [], "includePlatforms": [],
"excludePlatforms": [], "excludePlatforms": [],
@@ -20,4 +18,4 @@
"defineConstraints": [], "defineConstraints": [],
"versionDefines": [], "versionDefines": [],
"noEngineReferences": false "noEngineReferences": false
} }

View File

@@ -4,22 +4,43 @@ using Darkmatter.Core;
using Darkmatter.Core.Contracts.Services.Analytics; using Darkmatter.Core.Contracts.Services.Analytics;
using Darkmatter.Core.Data.Signals.Features.AppBoot; using Darkmatter.Core.Data.Signals.Features.AppBoot;
using Darkmatter.Core.Data.Signals.Features.Capture; using Darkmatter.Core.Data.Signals.Features.Capture;
using Darkmatter.Core.Data.Signals.Features.Coloring;
using Darkmatter.Core.Data.Signals.Features.Drawing; using Darkmatter.Core.Data.Signals.Features.Drawing;
using Darkmatter.Core.Data.Signals.Features.GameplayFlow; using Darkmatter.Core.Data.Signals.Features.GameplayFlow;
using Darkmatter.Core.Data.Signals.Features.MainMenu; using Darkmatter.Core.Data.Signals.Features.MainMenu;
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder; using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
using Darkmatter.Core.Data.Signals.Features.Tutorial; using Darkmatter.Core.Data.Signals.Features.Tutorial;
using Darkmatter.Libs.Observer; using Darkmatter.Libs.Observer;
using UnityEngine;
using VContainer.Unity; using VContainer.Unity;
namespace Darkmatter.Services.Analytics namespace Darkmatter.Services.Analytics
{ {
/// <summary>
/// Bridges gameplay signals to analytics events. Holds light per-drawing state (start time, distinct
/// colors, shape attempts) so completion/abandon events carry duration_seconds, colors_used and
/// attempts — the params that turn "they quit" into "they quit on drawing X after N seconds".
///
/// Logs once to <see cref="IAnalyticsService"/> (the composite), which fans out to Firebase + Facebook.
/// </summary>
public sealed class AnalyticsTracker : IStartable, IDisposable public sealed class AnalyticsTracker : IStartable, IDisposable
{ {
private const int ColorApplySampleEvery = 25; // color_applied is high-frequency → sample
private const string FirstDrawingKey = "analytics_first_drawing_started";
private const string AllContentKey = "analytics_all_content_completed";
private readonly IEventBus _bus; private readonly IEventBus _bus;
private readonly IAnalyticsService _analytics; private readonly IAnalyticsService _analytics;
private readonly List<IDisposable> _subs = new(); private readonly List<IDisposable> _subs = new();
// Per-drawing aggregation state.
private string _activeDrawingId;
private float _drawingStartTime = -1f;
private readonly HashSet<int> _colors = new();
private int _colorApplyCount;
private int _shapeAttempts;
private bool _firstDrawingTracked;
public AnalyticsTracker(IEventBus bus, IAnalyticsService analytics) public AnalyticsTracker(IEventBus bus, IAnalyticsService analytics)
{ {
_bus = bus; _bus = bus;
@@ -28,40 +49,180 @@ namespace Darkmatter.Services.Analytics
public void Start() public void Start()
{ {
_subs.Add(_bus.Subscribe<IntroCompletedSignal>(_ => _analytics.LogEvent("intro_completed"))); _firstDrawingTracked = PlayerPrefs.GetInt(FirstDrawingKey, 0) == 1;
_subs.Add(_bus.Subscribe<PlayBtnClickedSignal>(_ => _analytics.LogEvent("play_clicked")));
_subs.Add(_bus.Subscribe<OpenColorBookSignal>(_ => _analytics.LogEvent("colorbook_opened")));
_subs.Add(_bus.Subscribe<OpenArtBookSignal>(_ => _analytics.LogEvent("artbook_opened")));
_subs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => _analytics.LogEvent("main_menu_returned")));
_subs.Add(_bus.Subscribe<DrawingSelectedSignal>(s => // Onboarding / activation funnel
_analytics.LogEvent("drawing_selected", "template_id", s.TemplateId))); _subs.Add(_bus.Subscribe<IntroStartedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.IntroStarted)));
_subs.Add(_bus.Subscribe<IntroCompletedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.IntroCompleted)));
_subs.Add(_bus.Subscribe<PlayBtnClickedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.PlayClicked)));
_subs.Add(_bus.Subscribe<ShapeBuilderStartedSignal>(s => // Navigation
_analytics.LogEvent("shape_builder_started", "template_id", s.TemplateId))); _subs.Add(_bus.Subscribe<OpenColorBookSignal>(_ => _analytics.LogEvent(AnalyticsEvents.ColorbookOpened)));
_subs.Add(_bus.Subscribe<OpenArtBookSignal>(_ => _analytics.LogEvent(AnalyticsEvents.ArtbookOpened)));
_subs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => OnReturnToMainMenu()));
_subs.Add(_bus.Subscribe<ShapeAssembledSignal>(s => // Core gameplay loop
_analytics.LogEvent("shape_assembled", "template_id", s.TemplateId))); _subs.Add(_bus.Subscribe<DrawingSelectedSignal>(s => OnDrawingSelected(s.TemplateId)));
_subs.Add(_bus.Subscribe<ColorAppliedSignal>(OnColorApplied));
_subs.Add(_bus.Subscribe<ShapeBuilderStartedSignal>(s => OnShapeBuilderStarted(s.TemplateId)));
_subs.Add(_bus.Subscribe<PieceSnappedSignal>(_ => _shapeAttempts++));
_subs.Add(_bus.Subscribe<PieceUnsnappedSignal>(_ => _shapeAttempts++));
_subs.Add(_bus.Subscribe<ShapeAssembledSignal>(s => OnShapeAssembled(s.TemplateId)));
_subs.Add(_bus.Subscribe<DrawingCompletedSignal>(OnDrawingCompleted));
_subs.Add(_bus.Subscribe<DrawingCompletedSignal>(s => _analytics.LogEvent("drawing_completed", // Progression & content
_subs.Add(_bus.Subscribe<AllContentCompletedSignal>(OnAllContentCompleted));
// Capture / save
_subs.Add(_bus.Subscribe<GallerySaveStartedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.GallerySaveStarted)));
_subs.Add(_bus.Subscribe<GallerySaveCompletedSignal>(OnGallerySaveCompleted));
// Tutorial
_subs.Add(_bus.Subscribe<TutorialStartedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.TutorialStarted)));
_subs.Add(_bus.Subscribe<TutorialStepCompletedSignal>(s => _analytics.LogEvent(AnalyticsEvents.TutorialStepCompleted,
new Dictionary<string, object> new Dictionary<string, object>
{ {
["template_id"] = s.TemplateId, [AnalyticsParams.StepId] = s.StepId,
["completion_count"] = s.CompletionCount, [AnalyticsParams.StepIndex] = s.StepIndex,
}))); })));
_subs.Add(_bus.Subscribe<TutorialCompletedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.TutorialCompleted)));
}
_subs.Add(_bus.Subscribe<GallerySaveStartedSignal>(_ => _analytics.LogEvent("gallery_save_started"))); private void OnDrawingSelected(string drawingId)
_subs.Add(_bus.Subscribe<GallerySaveCompletedSignal>(s => {
_analytics.LogEvent("gallery_save_completed", "success", s.Success ? "true" : "false"))); // A new selection while a drawing is still open (uncompleted) = the previous one was abandoned.
EndActiveDrawingIfAbandoned();
_subs.Add(_bus.Subscribe<TutorialStartedSignal>(_ => _analytics.LogEvent("tutorial_started"))); _activeDrawingId = drawingId;
_subs.Add(_bus.Subscribe<TutorialStepCompletedSignal>(s => _analytics.LogEvent("tutorial_step_completed", _drawingStartTime = Time.realtimeSinceStartup;
new Dictionary<string, object> _colors.Clear();
_colorApplyCount = 0;
_shapeAttempts = 0;
_analytics.LogEvent(AnalyticsEvents.DrawingSelected, AnalyticsParams.DrawingId, drawingId);
_analytics.LogEvent(AnalyticsEvents.DrawingStarted, AnalyticsParams.DrawingId, drawingId);
_analytics.LogEvent(AnalyticsEvents.LevelStart, AnalyticsParams.LevelName, drawingId);
if (!_firstDrawingTracked)
{
_firstDrawingTracked = true;
PlayerPrefs.SetInt(FirstDrawingKey, 1);
PlayerPrefs.Save();
_analytics.LogEvent(AnalyticsEvents.FirstDrawingStarted, AnalyticsParams.DrawingId, drawingId);
}
}
private void OnColorApplied(ColorAppliedSignal s)
{
_colorApplyCount++;
Color32 c = s.Color;
_colors.Add((c.r << 24) | (c.g << 16) | (c.b << 8) | c.a);
// High-frequency event: emit only the first apply and every Nth thereafter.
if (_colorApplyCount == 1 || _colorApplyCount % ColorApplySampleEvery == 0)
{
_analytics.LogEvent(AnalyticsEvents.ColorApplied, new Dictionary<string, object>
{ {
["step_id"] = s.StepId, [AnalyticsParams.DrawingId] = _activeDrawingId ?? string.Empty,
["step_index"] = s.StepIndex, [AnalyticsParams.ApplyIndex] = _colorApplyCount,
}))); });
_subs.Add(_bus.Subscribe<TutorialCompletedSignal>(_ => _analytics.LogEvent("tutorial_completed"))); }
}
private void OnShapeBuilderStarted(string drawingId)
{
_shapeAttempts = 0;
_analytics.LogEvent(AnalyticsEvents.ShapeBuilderStarted, AnalyticsParams.DrawingId, drawingId);
}
private void OnShapeAssembled(string drawingId)
{
_analytics.LogEvent(AnalyticsEvents.ShapeAssembled, new Dictionary<string, object>
{
[AnalyticsParams.DrawingId] = drawingId,
[AnalyticsParams.Attempts] = _shapeAttempts,
});
}
private void OnDrawingCompleted(DrawingCompletedSignal s)
{
var p = new Dictionary<string, object>
{
[AnalyticsParams.DrawingId] = s.TemplateId,
[AnalyticsParams.CompletionCount] = s.CompletionCount,
[AnalyticsParams.ColorsUsed] = _colors.Count,
};
float dur = ElapsedDrawingSeconds();
if (dur >= 0f) p[AnalyticsParams.DurationSeconds] = dur;
_analytics.LogEvent(AnalyticsEvents.DrawingCompleted, p);
// GA4 recommended games event (lights up built-in level funnels).
_analytics.LogEvent(AnalyticsEvents.LevelComplete, new Dictionary<string, object>
{
[AnalyticsParams.LevelName] = s.TemplateId,
[AnalyticsParams.Success] = 1,
});
ClearActiveDrawing();
}
private void OnAllContentCompleted(AllContentCompletedSignal s)
{
if (PlayerPrefs.GetInt(AllContentKey, 0) == 1) return; // once ever
PlayerPrefs.SetInt(AllContentKey, 1);
PlayerPrefs.Save();
_analytics.LogEvent(AnalyticsEvents.AllContentCompleted,
AnalyticsParams.CompletionCount, s.CompletedCount.ToString());
}
private void OnGallerySaveCompleted(GallerySaveCompletedSignal s)
{
_analytics.LogEvent(AnalyticsEvents.GallerySaveCompleted, AnalyticsParams.Success, s.Success ? "true" : "false");
_analytics.LogEvent(AnalyticsEvents.DrawingSaved, new Dictionary<string, object>
{
[AnalyticsParams.DrawingId] = _activeDrawingId ?? string.Empty,
[AnalyticsParams.Success] = s.Success ? 1 : 0,
});
}
private void OnReturnToMainMenu()
{
EndActiveDrawingIfAbandoned();
_analytics.LogEvent(AnalyticsEvents.MainMenuReturned);
}
// Emits drawing_abandoned for an open, uncompleted drawing. Completion clears _activeDrawingId, so
// reaching here with a non-null id means the drawing was left without finishing — the leak signal.
private void EndActiveDrawingIfAbandoned()
{
if (_activeDrawingId == null) return;
var p = new Dictionary<string, object>
{
[AnalyticsParams.DrawingId] = _activeDrawingId,
[AnalyticsParams.ColorsUsed] = _colors.Count,
};
float dur = ElapsedDrawingSeconds();
if (dur >= 0f) p[AnalyticsParams.DurationSeconds] = dur;
_analytics.LogEvent(AnalyticsEvents.DrawingAbandoned, p);
ClearActiveDrawing();
}
private float ElapsedDrawingSeconds()
{
if (_drawingStartTime < 0f) return -1f;
float d = Time.realtimeSinceStartup - _drawingStartTime;
return d >= 0f ? d : -1f; // guard against realtime baseline reset across an app relaunch
}
private void ClearActiveDrawing()
{
_activeDrawingId = null;
_drawingStartTime = -1f;
_colors.Clear();
_colorApplyCount = 0;
_shapeAttempts = 0;
} }
public void Dispose() public void Dispose()

View File

@@ -0,0 +1,66 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VContainer.Unity;
#if UNITY_ANDROID
using AppsFlyerSDK;
using Firebase;
using Firebase.Messaging;
#endif
namespace Darkmatter.Services.Analytics
{
/// <summary>
/// Forwards the Firebase Cloud Messaging registration token to AppsFlyer so it can measure uninstalls.
/// Android-only: AppsFlyer's server-uninstall measurement keys off the FCM token. iOS uninstall uses the
/// APNs device token via a different AppsFlyer API and is intentionally not wired here, so FCM is not
/// initialised on iOS.
///
/// AppsFlyer itself is started in the Boot scene (AppsFlyerObjectScript); this only feeds it the token.
/// </summary>
public class AppsFlyerUninstallTracker : IAsyncStartable, IDisposable
{
public async UniTask StartAsync(CancellationToken cancellation = default)
{
#if UNITY_ANDROID
DependencyStatus status;
try
{
status = await FirebaseApp.CheckAndFixDependenciesAsync().AsUniTask();
}
catch (Exception e)
{
Debug.LogError($"[AppsFlyerUninstall] Firebase init failed: {e}");
return;
}
if (status != DependencyStatus.Available)
{
Debug.LogError($"[AppsFlyerUninstall] Firebase deps unavailable: {status}");
return;
}
// Subscribing triggers FCM registration; the current token is delivered here on every refresh.
FirebaseMessaging.TokenReceived += OnTokenReceived;
#else
await UniTask.CompletedTask;
#endif
}
#if UNITY_ANDROID
private static void OnTokenReceived(object sender, TokenReceivedEventArgs e)
{
try { AppsFlyer.updateServerUninstallToken(e.Token); }
catch (Exception ex) { Debug.LogError($"[AppsFlyerUninstall] updateServerUninstallToken failed: {ex}"); }
}
#endif
public void Dispose()
{
#if UNITY_ANDROID
FirebaseMessaging.TokenReceived -= OnTokenReceived;
#endif
}
}
}

View File

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

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using Darkmatter.Core.Contracts.Services.Analytics;
using UnityEngine;
namespace Darkmatter.Services.Analytics
{
/// <summary>
/// The single <see cref="IAnalyticsService"/> the app consumes. Fans every call out to all sinks
/// (Firebase + Facebook) so call sites log once and both backends receive it. A throwing sink is
/// isolated so it can't break the others. To add a sink: add a constructor arg and include it below.
/// </summary>
public sealed class CompositeAnalyticsService : IAnalyticsService
{
private readonly IAnalyticsService[] _sinks;
public CompositeAnalyticsService(FirebaseAnalyticsSystem firebase, FacebookAnalyticsSystem facebook)
{
_sinks = new IAnalyticsService[] { firebase, facebook };
}
public void LogEvent(string name) =>
ForEach(s => s.LogEvent(name));
public void LogEvent(string name, string paramName, string paramValue) =>
ForEach(s => s.LogEvent(name, paramName, paramValue));
public void LogEvent(string name, IReadOnlyDictionary<string, object> parameters) =>
ForEach(s => s.LogEvent(name, parameters));
public void SetUserProperty(string name, string value) =>
ForEach(s => s.SetUserProperty(name, value));
private void ForEach(Action<IAnalyticsService> action)
{
foreach (var sink in _sinks)
{
try { action(sink); }
catch (Exception e) { Debug.LogError($"[Analytics] Sink {sink.GetType().Name} failed: {e}"); }
}
}
}
}

View File

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

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using Darkmatter.Core.Contracts.Services.Analytics;
using UnityEngine;
using VContainer.Unity;
namespace Darkmatter.Services.Analytics
{
/// <summary>
/// Forwards Unity error/exception logs to analytics as a sampled <c>error_shown</c> event so
/// crash-adjacent friction surfaces in funnels. Capped per session and deduped by error_type to avoid
/// event floods / GA4 quota burn. No message text or PII is sent — only the leading token of the
/// condition (e.g. "NullReferenceException"). Distinct from Crashlytics (which logs full crashes).
/// </summary>
public sealed class ErrorAnalyticsTracker : IStartable, IDisposable
{
private const int MaxPerSession = 25;
private readonly IAnalyticsService _analytics;
private readonly HashSet<string> _seen = new();
private int _count;
public ErrorAnalyticsTracker(IAnalyticsService analytics) => _analytics = analytics;
public void Start() => Application.logMessageReceived += OnLog;
public void Dispose() => Application.logMessageReceived -= OnLog;
private void OnLog(string condition, string stackTrace, LogType type)
{
if (type != LogType.Error && type != LogType.Exception) return;
if (_count >= MaxPerSession) return;
string errorType = ExtractType(condition);
if (!_seen.Add(errorType)) return; // one per distinct type per session
_count++;
_analytics.LogEvent(AnalyticsEvents.ErrorShown, AnalyticsParams.ErrorType, errorType);
}
// Leading token only — keeps cardinality low and avoids leaking message contents / PII.
private static string ExtractType(string condition)
{
if (string.IsNullOrEmpty(condition)) return "unknown";
int colon = condition.IndexOf(':');
string head = (colon > 0 ? condition.Substring(0, colon) : condition).Trim();
int space = head.IndexOf(' ');
if (space > 0) head = head.Substring(0, space);
return head.Length > 40 ? head.Substring(0, 40) : head;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 05e0b4bc8382a40c0a78b122ac37a9fa

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using Darkmatter.Core.Contracts.Services.Analytics;
using Facebook.Unity;
using UnityEngine;
using VContainer.Unity;
namespace Darkmatter.Services.Analytics
{
/// <summary>
/// Facebook App Events sink. Mirrors <see cref="FirebaseAnalyticsSystem"/>'s readiness queue: events
/// logged before FB.Init completes are buffered and flushed on init.
///
/// Compliance: child-directed app — advertiser-ID collection is OFF (FacebookSettings), so App Events
/// flow without IDFA. Do NOT enable advertiser tracking / ATT here. See memory child-directed-coppa.
/// </summary>
public class FacebookAnalyticsSystem : IAnalyticsService, IStartable
{
private bool _ready;
private readonly Queue<Action> _pending = new();
public void Start()
{
if (FB.IsInitialized) OnInitialized();
else FB.Init(OnInitialized);
}
private void OnInitialized()
{
FB.ActivateApp();
_ready = true;
while (_pending.Count > 0)
{
try { _pending.Dequeue().Invoke(); }
catch (Exception e) { Debug.LogError($"[FacebookAnalytics] Queued event failed: {e}"); }
}
}
public void LogEvent(string name) =>
Run(() => FB.LogAppEvent(MapName(name)));
public void LogEvent(string name, string paramName, string paramValue) =>
Run(() => FB.LogAppEvent(MapName(name), null,
new Dictionary<string, object> { [paramName] = paramValue ?? string.Empty }));
public void LogEvent(string name, IReadOnlyDictionary<string, object> parameters)
{
if (parameters == null || parameters.Count == 0)
{
LogEvent(name);
return;
}
var dict = new Dictionary<string, object>(parameters.Count);
float? valueToSum = null;
foreach (var kv in parameters)
{
// GA4 ad_impression carries revenue in "value"; surface it to FB as the summable value.
if (kv.Key == AnalyticsParams.Value && TryToFloat(kv.Value, out var v)) valueToSum = v;
dict[kv.Key] = ToFbValue(kv.Value);
}
Run(() => FB.LogAppEvent(MapName(name), valueToSum, dict));
}
// Facebook App Events has no user-property concept.
public void SetUserProperty(string name, string value) { }
private void Run(Action action)
{
if (_ready)
{
try { action(); }
catch (Exception e) { Debug.LogError($"[FacebookAnalytics] Event failed: {e}"); }
}
else _pending.Enqueue(action);
}
// Map our snake_case names to Facebook standard events where one exists; else log as custom.
// (This SDK's AppEventName has no Ad* constants, so ad_impression / ad_clicked stay custom.)
private static string MapName(string name) => name switch
{
AnalyticsEvents.TutorialCompleted => AppEventName.CompletedTutorial,
AnalyticsEvents.LevelComplete => AppEventName.AchievedLevel,
_ => name
};
private static object ToFbValue(object value) => value switch
{
null => string.Empty,
bool b => b ? 1 : 0,
string s => s,
int or long or float or double => value,
_ => value.ToString()
};
private static bool TryToFloat(object value, out float result)
{
switch (value)
{
case float f: result = f; return true;
case double d: result = (float)d; return true;
case int i: result = i; return true;
case long l: result = l; return true;
default: result = 0f; return false;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 196c5d190005d45f885236d8765813d1

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@@ -0,0 +1,156 @@
fileFormatVersion: 2
guid: e7748c89fd816f04689143ae3922ed82
TextureImporter:
internalIDToNameTable:
- first:
213: 6248555910068260328
second: Gemini_Generated_Image_fqdfbxfqdfbxfqdf__1_-removebg-preview_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: Gemini_Generated_Image_fqdfbxfqdfbxfqdf__1_-removebg-preview_0
rect:
serializedVersion: 2
x: 71
y: 53
width: 357
height: 388
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 8ed49c27e7457b650800000000000000
internalID: 6248555910068260328
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
Gemini_Generated_Image_fqdfbxfqdfbxfqdf__1_-removebg-preview_0: 6248555910068260328
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -0,0 +1,156 @@
fileFormatVersion: 2
guid: 1cab925f0053c824f83ae1aae48a61d9
TextureImporter:
internalIDToNameTable:
- first:
213: 629112722899171841
second: Gemini_Generated_Image_fqdfbxfqdfbxfqdf-removebg-preview_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: Gemini_Generated_Image_fqdfbxfqdfbxfqdf-removebg-preview_0
rect:
serializedVersion: 2
x: 100
y: 57
width: 293
height: 387
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 10a751430be0bb800800000000000000
internalID: 629112722899171841
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
Gemini_Generated_Image_fqdfbxfqdfbxfqdf-removebg-preview_0: 629112722899171841
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -0,0 +1,208 @@
fileFormatVersion: 2
guid: 1809a484a1d2b86458674647a5aab85b
TextureImporter:
internalIDToNameTable:
- first:
213: 288685977361415653
second: Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_0
- first:
213: 750885019931044399
second: Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_1
- first:
213: -8179473444649422271
second: Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_2
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_0
rect:
serializedVersion: 2
x: 216
y: 425
width: 31
height: 31
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 5ed734ced5e910400800000000000000
internalID: 288685977361415653
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_1
rect:
serializedVersion: 2
x: 238
y: 345
width: 116
height: 98
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: f2ae62082fdab6a00800000000000000
internalID: 750885019931044399
vertices: []
indices:
edges: []
weights: []
- serializedVersion: 2
name: Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_2
rect:
serializedVersion: 2
x: 112
y: 153
width: 289
height: 212
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 1423ee12d3cac7e80800000000000000
internalID: -8179473444649422271
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable:
Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_0: 288685977361415653
Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_1: 750885019931044399
Gemini_Generated_Image_fqdfbxfqdfbxfqdf__2_-removebg-preview_2: -8179473444649422271
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@@ -91,7 +91,7 @@ MonoBehaviour:
target: {fileID: 8820358277770550893} target: {fileID: 8820358277770550893}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 1.3, y: 1.3, z: 1.3}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -91,7 +91,7 @@ MonoBehaviour:
target: {fileID: 6959668731956208313} target: {fileID: 6959668731956208313}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 0.6, y: 0.6, z: 0.6}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -0,0 +1,143 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &911665673896949303
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3751946808055176737}
- component: {fileID: 9029580750236182666}
- component: {fileID: 4079314869276449126}
- component: {fileID: 3450054360618172263}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3751946808055176737
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.6729, y: 0.6729, z: 0.6729}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 4495824370100226873}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 2039, y: 1141}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9029580750236182666
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_CullTransparentMesh: 1
--- !u!114 &4079314869276449126
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: e7748c89fd816f04689143ae3922ed82, type: 3}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &3450054360618172263
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01fc8d24088db42b3a76d40120f81a9c, type: 3}
m_Name:
m_EditorClassIdentifier: Features.Coloring::Darkmatter.Features.Coloring.UI.CompletionAnimationView
target: {fileID: 3751946808055176737}
duration: 1
startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1.3, y: 1.3, z: 1.3}
ease: 27
wiggleAngle: 10
wiggleDuration: 0.25
wiggleCycles: 3
--- !u!1 &4542850601508665442
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4495824370100226873}
- component: {fileID: 4370133920526401211}
m_Layer: 5
m_Name: Boat Animation
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4495824370100226873
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4542850601508665442}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3751946808055176737}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4370133920526401211
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4542850601508665442}
m_CullTransparentMesh: 1

View File

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

View File

@@ -136,7 +136,7 @@ MonoBehaviour:
target: {fileID: 4536272910939100773} target: {fileID: 4536272910939100773}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 0.85, y: 0.85, z: 0.85}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -91,7 +91,7 @@ MonoBehaviour:
target: {fileID: 5687255240744794981} target: {fileID: 5687255240744794981}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 1.5, y: 1.5, z: 1.5}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -136,7 +136,7 @@ MonoBehaviour:
target: {fileID: 2116642338194493525} target: {fileID: 2116642338194493525}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 1.5, y: 1.5, z: 1.5}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -91,7 +91,7 @@ MonoBehaviour:
target: {fileID: 8489464462539967032} target: {fileID: 8489464462539967032}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 0.67, y: 0.67, z: 0.67}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -136,7 +136,7 @@ MonoBehaviour:
target: {fileID: 7378745237972353107} target: {fileID: 7378745237972353107}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 0.8, y: 0.8, z: 0.8}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -91,7 +91,7 @@ MonoBehaviour:
target: {fileID: 3689421456007431995} target: {fileID: 3689421456007431995}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 0.82, y: 0.82, z: 0.82}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -136,7 +136,7 @@ MonoBehaviour:
target: {fileID: 6962455140498089637} target: {fileID: 6962455140498089637}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 1.2, y: 1.2, z: 1.2}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -136,7 +136,7 @@ MonoBehaviour:
target: {fileID: 1348893645794610921} target: {fileID: 1348893645794610921}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 1.3, y: 1.3, z: 1.3}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -136,7 +136,7 @@ MonoBehaviour:
target: {fileID: 241661359568125684} target: {fileID: 241661359568125684}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 1.2, y: 1.2, z: 1.2}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -136,7 +136,7 @@ MonoBehaviour:
target: {fileID: 1915196982905270959} target: {fileID: 1915196982905270959}
duration: 1 duration: 1
startScale: {x: 0, y: 0, z: 0} startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1, y: 1, z: 1} endScale: {x: 0.7, y: 0.7, z: 0.7}
ease: 27 ease: 27
wiggleAngle: 10 wiggleAngle: 10
wiggleDuration: 0.25 wiggleDuration: 0.25

View File

@@ -0,0 +1,143 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &911665673896949303
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3751946808055176737}
- component: {fileID: 9029580750236182666}
- component: {fileID: 4079314869276449126}
- component: {fileID: 3450054360618172263}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3751946808055176737
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.6729, y: 0.6729, z: 0.6729}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 4495824370100226873}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 2039, y: 1141}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9029580750236182666
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_CullTransparentMesh: 1
--- !u!114 &4079314869276449126
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 1cab925f0053c824f83ae1aae48a61d9, type: 3}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &3450054360618172263
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01fc8d24088db42b3a76d40120f81a9c, type: 3}
m_Name:
m_EditorClassIdentifier: Features.Coloring::Darkmatter.Features.Coloring.UI.CompletionAnimationView
target: {fileID: 3751946808055176737}
duration: 1
startScale: {x: 0, y: 0, z: 0}
endScale: {x: 1.5, y: 1.5, z: 1.5}
ease: 27
wiggleAngle: 10
wiggleDuration: 0.25
wiggleCycles: 3
--- !u!1 &4542850601508665442
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4495824370100226873}
- component: {fileID: 4370133920526401211}
m_Layer: 5
m_Name: Kite Animation
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4495824370100226873
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4542850601508665442}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3751946808055176737}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4370133920526401211
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4542850601508665442}
m_CullTransparentMesh: 1

View File

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

View File

@@ -0,0 +1,143 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &911665673896949303
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3751946808055176737}
- component: {fileID: 9029580750236182666}
- component: {fileID: 4079314869276449126}
- component: {fileID: 3450054360618172263}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3751946808055176737
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.6729, y: 0.6729, z: 0.6729}
m_ConstrainProportionsScale: 1
m_Children: []
m_Father: {fileID: 4495824370100226873}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -121}
m_SizeDelta: {x: 2039, y: 1141}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9029580750236182666
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_CullTransparentMesh: 1
--- !u!114 &4079314869276449126
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 1809a484a1d2b86458674647a5aab85b, type: 3}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &3450054360618172263
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 911665673896949303}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 01fc8d24088db42b3a76d40120f81a9c, type: 3}
m_Name:
m_EditorClassIdentifier: Features.Coloring::Darkmatter.Features.Coloring.UI.CompletionAnimationView
target: {fileID: 3751946808055176737}
duration: 1
startScale: {x: 0, y: 0, z: 0}
endScale: {x: 2, y: 2, z: 2}
ease: 27
wiggleAngle: 10
wiggleDuration: 0.25
wiggleCycles: 3
--- !u!1 &4542850601508665442
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4495824370100226873}
- component: {fileID: 4370133920526401211}
m_Layer: 5
m_Name: Train Animation
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4495824370100226873
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4542850601508665442}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3751946808055176737}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4370133920526401211
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4542850601508665442}
m_CullTransparentMesh: 1

View File

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

View File

@@ -330,15 +330,15 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 0.24355799 value: 0.4092992
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 0.24355799 value: 0.4092992
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
value: 0.24355799 value: 0.4092992
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalPosition.x propertyPath: m_LocalPosition.x
@@ -370,11 +370,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: -381.463 value: -404.24997
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: 20.077026 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
@@ -562,15 +562,15 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 0.24355799 value: 0.4092992
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 0.24355799 value: 0.4092992
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
value: 0.24355799 value: 0.4092992
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalPosition.x propertyPath: m_LocalPosition.x
@@ -602,11 +602,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: -204.58456 value: -107
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: 20.077026 value: -0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x

View File

@@ -27,7 +27,7 @@ RectTransform:
m_GameObject: {fileID: 4769907632149696896} m_GameObject: {fileID: 4769907632149696896}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 3, y: 3, z: 3} m_LocalScale: {x: 3.5, y: 3.3, z: 3}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 6264741870445479171} m_Father: {fileID: 6264741870445479171}
@@ -170,11 +170,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 3 value: 3.5
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 3 value: 3.3
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
@@ -210,11 +210,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: -259.30493 value: -302.52243
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: 460.50226 value: 506.55237
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
@@ -296,11 +296,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 3 value: 3.5
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 3 value: 3.3
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
@@ -336,11 +336,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: -205.8421 value: -240.14912
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: 184.36838 value: 202.80527
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
@@ -414,11 +414,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 3 value: 3.5
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 3 value: 3.3
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
@@ -454,11 +454,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: 6.6078157 value: 7.709134
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: -186.51785 value: -205.16962
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
@@ -524,11 +524,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3} - target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 3 value: 3.4124076
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3} - target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 3 value: 3.3875923
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3} - target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
@@ -564,11 +564,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3} - target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: 320.40002 value: 373.8
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3} - target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: -148.79999 value: -163.67998
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3} - target: {fileID: 592763083234151009, guid: f734f1a4f3d05fb4680f42933ad2a434, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
@@ -638,11 +638,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 3 value: 3.5
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 3 value: 3.3
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
@@ -678,11 +678,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: -251.99991 value: -293.99976
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: 480.3 value: 528.3301
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3} - target: {fileID: 3648889831887995107, guid: ed3abc5b1c6bc43938850705ab3e4d4b, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
@@ -752,11 +752,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 3 value: 3.4326606
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 3 value: 3.3673391
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
@@ -792,11 +792,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: -140.40002 value: -163.80006
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: 498.90015 value: 548.79016
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3} - target: {fileID: 4772353642633016153, guid: 483b170ade7354414929ed6252fa552e, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
@@ -878,11 +878,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 3 value: 3.5
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 3 value: 3.3
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
@@ -918,11 +918,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: 235.2001 value: 274.40002
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: -478.7433 value: -526.61755
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x
@@ -996,11 +996,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.x propertyPath: m_LocalScale.x
value: 3 value: 3.5
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.y propertyPath: m_LocalScale.y
value: 3 value: 3.3
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalScale.z propertyPath: m_LocalScale.z
@@ -1036,11 +1036,11 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: -229.06802 value: -267.246
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: -478.7433 value: -526.61755
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3} - target: {fileID: 4433096382570807284, guid: 70aa2daf45f0649c2823d1930e906f59, type: 3}
propertyPath: m_LocalEulerAnglesHint.x propertyPath: m_LocalEulerAnglesHint.x

View File

@@ -12,6 +12,7 @@ GameObject:
- component: {fileID: 501846013816502432} - component: {fileID: 501846013816502432}
- component: {fileID: 8665832532240036901} - component: {fileID: 8665832532240036901}
- component: {fileID: 8513459380562180911} - component: {fileID: 8513459380562180911}
- component: {fileID: 7638529631096518376}
m_Layer: 5 m_Layer: 5
m_Name: Recatngle m_Name: Recatngle
m_TagString: Untagged m_TagString: Untagged
@@ -90,3 +91,18 @@ MonoBehaviour:
m_EditorClassIdentifier: Features.ShapeBuilder::Darkmatter.Features.ShapeBuilder.UI.SlotMarker m_EditorClassIdentifier: Features.ShapeBuilder::Darkmatter.Features.ShapeBuilder.UI.SlotMarker
shape: {fileID: 11400000, guid: 4a67406eb6fe043628d2b6a4e0c970ba, type: 2} shape: {fileID: 11400000, guid: 4a67406eb6fe043628d2b6a4e0c970ba, type: 2}
outline: {fileID: 0} outline: {fileID: 0}
--- !u!114 &7638529631096518376
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4279528263583881541}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e19747de3f5aca642ab2be37e372fb86, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Outline
m_EffectColor: {r: 0, g: 0, b: 0, a: 0.5}
m_EffectDistance: {x: 2.5, y: -2.5}
m_UseGraphicAlpha: 1

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -75,8 +75,8 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_UiScaleMode: 1 m_UiScaleMode: 1
m_ReferencePixelsPerUnit: 100 m_ReferencePixelsPerUnit: 100
m_ScaleFactor: 1 m_ScaleFactor: 1
@@ -98,8 +98,8 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_IgnoreReversedGraphics: 1 m_IgnoreReversedGraphics: 1
m_BlockingObjects: 0 m_BlockingObjects: 0
m_BlockingMask: m_BlockingMask:
@@ -127,8 +127,8 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 97ce2c486cc8541d1ab1d83fda7f8eda, type: 3} m_Script: {fileID: 11500000, guid: 97ce2c486cc8541d1ab1d83fda7f8eda, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
overlayView: {fileID: 100106} overlayView: {fileID: 100106}
config: {fileID: 11400000, guid: 71357eb1222bb4151ab4e5697a1decd3, type: 2} config: {fileID: 11400000, guid: 71357eb1222bb4151ab4e5697a1decd3, type: 2}
--- !u!1 &100110 --- !u!1 &100110
@@ -182,8 +182,8 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4ecf127118699405ebcd0e0712d7373d, type: 3} m_Script: {fileID: 11500000, guid: 4ecf127118699405ebcd0e0712d7373d, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
canvas: {fileID: 100102} canvas: {fileID: 100102}
rootGroup: {fileID: 100105} rootGroup: {fileID: 100105}
cutout: {fileID: 100204} cutout: {fileID: 100204}
@@ -198,6 +198,9 @@ MonoBehaviour:
haloPulseScale: 1.18 haloPulseScale: 1.18
pulseDuration: 0.6 pulseDuration: 0.6
dragHandDuration: 1.1 dragHandDuration: 1.1
dragHandTipOffset: {x: 0, y: 70}
dragBubbleAtTop: 1
dragBubbleEdgeMargin: 150
--- !u!225 &6008915593776814208 --- !u!225 &6008915593776814208
CanvasGroup: CanvasGroup:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -266,8 +269,8 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 0.72} m_Color: {r: 0, g: 0, b: 0, a: 0.72}
m_RaycastTarget: 1 m_RaycastTarget: 1
@@ -296,8 +299,8 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 29c836b3d2ed343e6a810f6e7548f487, type: 3} m_Script: {fileID: 11500000, guid: 29c836b3d2ed343e6a810f6e7548f487, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
cutoutShader: {fileID: 4800000, guid: 57d1ed1c62afa45db97a7c8e9ace795c, type: 3} cutoutShader: {fileID: 4800000, guid: 57d1ed1c62afa45db97a7c8e9ace795c, type: 3}
dimImage: {fileID: 100203} dimImage: {fileID: 100203}
--- !u!1 &100600 --- !u!1 &100600
@@ -352,11 +355,11 @@ MonoBehaviour:
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100600} m_GameObject: {fileID: 100600}
m_Enabled: 1 m_Enabled: 0
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0.9} m_Color: {r: 1, g: 1, b: 1, a: 0.9}
m_RaycastTarget: 0 m_RaycastTarget: 0
@@ -410,7 +413,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 130, y: 130} m_SizeDelta: {x: 200, y: 200}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &100702 --- !u!222 &100702
CanvasRenderer: CanvasRenderer:
@@ -430,8 +433,8 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1} m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 0 m_RaycastTarget: 0
@@ -465,7 +468,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 1 m_IsActive: 0
--- !u!224 &100801 --- !u!224 &100801
RectTransform: RectTransform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -524,7 +527,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 0} m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1} m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 91} m_SizeDelta: {x: -235, y: 91}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &100902 --- !u!222 &100902
CanvasRenderer: CanvasRenderer:
@@ -544,8 +547,8 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0.96} m_Color: {r: 1, g: 1, b: 1, a: 0.96}
m_RaycastTarget: 0 m_RaycastTarget: 0
@@ -574,7 +577,7 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
m_HorizontalFit: 2 m_HorizontalFit: 2
m_VerticalFit: 0 m_VerticalFit: 0
@@ -588,7 +591,7 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
m_Padding: m_Padding:
m_Left: 100 m_Left: 100
@@ -639,8 +642,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1} m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 463.445, y: -95.50001} m_AnchoredPosition: {x: 207.45999, y: -95.50001}
m_SizeDelta: {x: 0, y: 0} m_SizeDelta: {x: 214.92, y: 72.61}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &101002 --- !u!222 &101002
CanvasRenderer: CanvasRenderer:
@@ -660,7 +663,7 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
m_Material: {fileID: 0} m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1} m_Color: {r: 1, g: 1, b: 1, a: 1}
@@ -752,7 +755,7 @@ MonoBehaviour:
m_Enabled: 1 m_Enabled: 1
m_EditorHideFlags: 0 m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3e37e2b5f1962004c8f10779a2fcf23a, type: 3} m_Script: {fileID: 11500000, guid: 3e37e2b5f1962004c8f10779a2fcf23a, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: uLayout::Poke.UI.LayoutText m_EditorClassIdentifier: uLayout::Poke.UI.LayoutText
m_log: 0 m_log: 0
m_ignoreLayout: 0 m_ignoreLayout: 0

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

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