Difficulty based sorting and analytics fixes
This commit is contained in:
BIN
Assets/.DS_Store
vendored
BIN
Assets/.DS_Store
vendored
Binary file not shown.
BIN
Assets/AddressableAssetsData/.DS_Store
vendored
Normal file
BIN
Assets/AddressableAssetsData/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/AppsFlyer/.DS_Store
vendored
Normal file
BIN
Assets/AppsFlyer/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/Confetti FX/.DS_Store
vendored
Normal file
BIN
Assets/Confetti FX/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/Darkmatter/.DS_Store
vendored
BIN
Assets/Darkmatter/.DS_Store
vendored
Binary file not shown.
@@ -9,6 +9,7 @@ namespace Darkmatter.Core.Contracts.Features.DrawingCatalog
|
|||||||
{
|
{
|
||||||
string Id { get; }
|
string Id { get; }
|
||||||
string DisplayName { get; }
|
string DisplayName { get; }
|
||||||
|
int Difficulty { get; }
|
||||||
Sprite DefaultThumbnail { get; }
|
Sprite DefaultThumbnail { get; }
|
||||||
GameObject DrawingPrefab { get; }
|
GameObject DrawingPrefab { get; }
|
||||||
GameObject ColoringPrefab { get; }
|
GameObject ColoringPrefab { get; }
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ namespace Darkmatter.Core.Contracts.Services.Analytics
|
|||||||
public const string ShapeAssembled = "shape_assembled";
|
public const string ShapeAssembled = "shape_assembled";
|
||||||
public const string ColorApplied = "color_applied";
|
public const string ColorApplied = "color_applied";
|
||||||
|
|
||||||
// Progression & content (GA4 recommended level_start / level_complete)
|
// Progression & content. level_start is a GA4-recommended games event; level_complete is our
|
||||||
|
// custom completion marker (GA4's own pair would be level_end + success).
|
||||||
public const string LevelStart = "level_start";
|
public const string LevelStart = "level_start";
|
||||||
public const string LevelComplete = "level_complete";
|
public const string LevelComplete = "level_complete";
|
||||||
public const string AllContentCompleted = "all_content_completed";
|
public const string AllContentCompleted = "all_content_completed";
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
namespace Darkmatter.Core.Data.Signals.Features.Capture
|
namespace Darkmatter.Core.Data.Signals.Features.Capture
|
||||||
{
|
{
|
||||||
public record struct GallerySaveCompletedSignal(bool Success);
|
/// <summary>
|
||||||
|
/// TemplateId identifies which drawing was saved; publishers outside gameplay (art book) must set it
|
||||||
|
/// because there is no active drawing for analytics to fall back on there.
|
||||||
|
/// </summary>
|
||||||
|
public record struct GallerySaveCompletedSignal(bool Success, string TemplateId = null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Darkmatter.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The drawing-catalog (colorbook) screen became visible: scene entry (play / back / next) or
|
||||||
|
/// returning from the art book. Distinct from <see cref="OpenColorBookSignal"/>, which is the
|
||||||
|
/// command to show the view and only fires on one navigation path. Analytics keys colorbook_opened
|
||||||
|
/// and abandoned-drawing detection off this.
|
||||||
|
/// </summary>
|
||||||
|
public record struct ColorbookShownSignal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fa0fb32346ff54da3a9bbfec9f763cf2
|
||||||
@@ -12,6 +12,8 @@ namespace Darkmatter.Core.Data.Static.Features.DrawingTemplate
|
|||||||
{
|
{
|
||||||
[SerializeField] private string id;
|
[SerializeField] private string id;
|
||||||
[SerializeField] private string displayName;
|
[SerializeField] private string displayName;
|
||||||
|
[SerializeField, Min(0), Tooltip("0 = auto (pieces + regions). Any positive value overrides the computed difficulty.")]
|
||||||
|
private int difficultyOverride;
|
||||||
[SerializeField] private Sprite defaultThumbnail;
|
[SerializeField] private Sprite defaultThumbnail;
|
||||||
[SerializeField] private GameObject drawingPrefab;
|
[SerializeField] private GameObject drawingPrefab;
|
||||||
[SerializeField] private GameObject coloringPrefab;
|
[SerializeField] private GameObject coloringPrefab;
|
||||||
@@ -22,6 +24,11 @@ namespace Darkmatter.Core.Data.Static.Features.DrawingTemplate
|
|||||||
|
|
||||||
public string Id => id;
|
public string Id => id;
|
||||||
public string DisplayName => displayName;
|
public string DisplayName => displayName;
|
||||||
|
// Workload-based: every shape to place and region to color adds one point.
|
||||||
|
// A positive difficultyOverride replaces the computed value.
|
||||||
|
public int AutoDifficulty => pieces.Count + regions.Count;
|
||||||
|
public int Difficulty => difficultyOverride > 0 ? difficultyOverride : AutoDifficulty;
|
||||||
|
public bool HasDifficultyOverride => difficultyOverride > 0;
|
||||||
public Sprite DefaultThumbnail => defaultThumbnail;
|
public Sprite DefaultThumbnail => defaultThumbnail;
|
||||||
public GameObject DrawingPrefab => drawingPrefab;
|
public GameObject DrawingPrefab => drawingPrefab;
|
||||||
public GameObject ColoringPrefab => coloringPrefab;
|
public GameObject ColoringPrefab => coloringPrefab;
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ namespace Darkmatter.Features.AppBoot.Flow
|
|||||||
|
|
||||||
public async UniTask StartAsync(CancellationToken cancellation = default)
|
public async UniTask StartAsync(CancellationToken cancellation = default)
|
||||||
{
|
{
|
||||||
|
// Entry points run in serviceModules registration order and this module is near the front.
|
||||||
|
// Everything below LoadAsync completes synchronously, so without a real yield the
|
||||||
|
// IntroStartedSignal publish would happen before later-registered subscribers
|
||||||
|
// (e.g. AnalyticsTracker) have run Start() — the event bus has no replay, so they'd miss it.
|
||||||
|
await UniTask.Yield();
|
||||||
|
|
||||||
#if UNITY_ANDROID || UNITY_IOS
|
#if UNITY_ANDROID || UNITY_IOS
|
||||||
Application.targetFrameRate = 60;
|
Application.targetFrameRate = 60;
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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.Features.SaveGate;
|
||||||
using Darkmatter.Core.Contracts.Services.Gallery;
|
using Darkmatter.Core.Contracts.Services.Gallery;
|
||||||
|
using Darkmatter.Core.Data.Signals.Features.Capture;
|
||||||
using Darkmatter.Core.Data.Signals.Features.Drawing;
|
using Darkmatter.Core.Data.Signals.Features.Drawing;
|
||||||
using Darkmatter.Libs.Observer;
|
using Darkmatter.Libs.Observer;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -209,7 +210,20 @@ namespace Darkmatter.Features.Artbook
|
|||||||
texture = await _progression.GetCachedThumbnailAsync(entry.Value.Id);
|
texture = await _progression.GetCachedThumbnailAsync(entry.Value.Id);
|
||||||
if (texture == null) return;
|
if (texture == null) return;
|
||||||
|
|
||||||
|
// Mirror CaptureSystem's pairing: Started before the write, Completed(success) in
|
||||||
|
// finally — art-book saves land in the same gallery_save_* analytics funnel as
|
||||||
|
// gameplay saves.
|
||||||
|
_eventBus.Publish(new GallerySaveStartedSignal());
|
||||||
|
var success = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
await _gallery.SaveImageAsync(texture, entry.Value.Name, ct);
|
await _gallery.SaveImageAsync(texture, entry.Value.Name, ct);
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_eventBus.Publish(new GallerySaveCompletedSignal(success, entry.Value.Id));
|
||||||
|
}
|
||||||
// The art book has no success popup of its own, so use the shared toast.
|
// The art book has no success popup of its own, so use the shared toast.
|
||||||
await _saveGate.ShowSavedAsync(ct);
|
await _saveGate.ShowSavedAsync(ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,12 +60,14 @@ namespace Darkmatter.Features.Capture
|
|||||||
|
|
||||||
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, mipChain: false);
|
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, mipChain: false);
|
||||||
var success = false;
|
var success = false;
|
||||||
|
// Started must pair 1:1 with the Completed publish in finally — a failed LoadImage or a
|
||||||
|
// throw in BuildFileNameAsync would otherwise emit Completed(false) with no Started.
|
||||||
|
_bus.Publish(new GallerySaveStartedSignal());
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (tex.LoadImage(png))
|
if (tex.LoadImage(png))
|
||||||
{
|
{
|
||||||
var fileName = await BuildFileNameAsync();
|
var fileName = await BuildFileNameAsync();
|
||||||
_bus.Publish(new GallerySaveStartedSignal());
|
|
||||||
await _galleryService.SaveImageAsync(tex, fileName, ct);
|
await _galleryService.SaveImageAsync(tex, fileName, ct);
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
@@ -73,7 +75,7 @@ namespace Darkmatter.Features.Capture
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
UnityEngine.Object.Destroy(tex);
|
UnityEngine.Object.Destroy(tex);
|
||||||
_bus.Publish(new GallerySaveCompletedSignal(success));
|
_bus.Publish(new GallerySaveCompletedSignal(success, _progression.LastOpenedTemplateId));
|
||||||
}
|
}
|
||||||
return png;
|
return png;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,13 @@ public class ColorbookFlowController : IAsyncStartable, IDisposable
|
|||||||
// one VContainer cancels on teardown, and the linked _scopeCts may lag the callback order.
|
// one VContainer cancels on teardown, and the linked _scopeCts may lag the callback order.
|
||||||
if (cancellation.IsCancellationRequested || ct.IsCancellationRequested) return;
|
if (cancellation.IsCancellationRequested || ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
if (!_navigatingToGameplay) _loadingScreen.Hide();
|
if (!_navigatingToGameplay)
|
||||||
|
{
|
||||||
|
_loadingScreen.Hide();
|
||||||
|
// Catalog genuinely on screen — not a transit hop toward gameplay (edit/next relays land
|
||||||
|
// in this scene with _navigatingToGameplay already latched and never reveal the catalog).
|
||||||
|
_bus.Publish(new ColorbookShownSignal());
|
||||||
|
}
|
||||||
|
|
||||||
PrewarmInterstitialAdAsync().Forget();
|
PrewarmInterstitialAdAsync().Forget();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ public sealed class DrawingCatalogController : IDrawingCatalogController
|
|||||||
private void Refresh()
|
private void Refresh()
|
||||||
{
|
{
|
||||||
_visible.Clear();
|
_visible.Clear();
|
||||||
|
// Catalog already orders ids by difficulty (easiest first), then id.
|
||||||
_visible.AddRange(_catalog.AllTemplateIds);
|
_visible.AddRange(_catalog.AllTemplateIds);
|
||||||
_visible.Sort(StringComparer.OrdinalIgnoreCase);
|
|
||||||
ListChanged?.Invoke();
|
ListChanged?.Invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,14 @@ namespace Darkmatter.Features.DrawingCatalog
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void HandleOpenColorBookSignal(OpenColorBookSignal signal)
|
private void HandleOpenColorBookSignal(OpenColorBookSignal signal)
|
||||||
|
{
|
||||||
|
ShowCatalog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowCatalog()
|
||||||
{
|
{
|
||||||
_view.Show();
|
_view.Show();
|
||||||
|
_eventBus.Publish(new ColorbookShownSignal());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRightPageBtnClicked()
|
private void OnRightPageBtnClicked()
|
||||||
@@ -84,7 +90,7 @@ namespace Darkmatter.Features.DrawingCatalog
|
|||||||
|
|
||||||
private void OnArtBookBtnClicked()
|
private void OnArtBookBtnClicked()
|
||||||
{
|
{
|
||||||
_eventBus.Publish(new OpenArtBookSignal(()=> _view.Show()));
|
_eventBus.Publish(new OpenArtBookSignal(ShowCatalog));
|
||||||
_view.Hide();
|
_view.Hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ namespace Darkmatter.Features.DrawingTemplates.Editor
|
|||||||
_templates.Clear();
|
_templates.Clear();
|
||||||
_shapes.Clear();
|
_shapes.Clear();
|
||||||
_palettes.Clear();
|
_palettes.Clear();
|
||||||
_templates.AddRange(FindAllOfType<DrawingTemplateSO>());
|
_templates.AddRange(FindAllOfType<DrawingTemplateSO>()
|
||||||
|
.OrderBy(t => t.Difficulty)
|
||||||
|
.ThenBy(t => t.Id, StringComparer.OrdinalIgnoreCase));
|
||||||
_shapes.AddRange(FindAllOfType<ShapeSO>());
|
_shapes.AddRange(FindAllOfType<ShapeSO>());
|
||||||
_palettes.AddRange(FindAllOfType<ColorPaletteSO>());
|
_palettes.AddRange(FindAllOfType<ColorPaletteSO>());
|
||||||
}
|
}
|
||||||
@@ -169,7 +171,7 @@ namespace Darkmatter.Features.DrawingTemplates.Editor
|
|||||||
using (new EditorGUILayout.VerticalScope())
|
using (new EditorGUILayout.VerticalScope())
|
||||||
{
|
{
|
||||||
GUILayout.Label(string.IsNullOrEmpty(t.DisplayName) ? t.name : t.DisplayName, EditorStyles.boldLabel);
|
GUILayout.Label(string.IsNullOrEmpty(t.DisplayName) ? t.name : t.DisplayName, EditorStyles.boldLabel);
|
||||||
GUILayout.Label("id: " + (string.IsNullOrEmpty(t.Id) ? "<unset>" : t.Id), EditorStyles.miniLabel);
|
GUILayout.Label($"id: {(string.IsNullOrEmpty(t.Id) ? "<unset>" : t.Id)} • diff: {t.Difficulty}{(t.HasDifficultyOverride ? "*" : $" ({t.Pieces.Count}p+{t.AuthoredRegions.Count}r)")}", EditorStyles.miniLabel);
|
||||||
}
|
}
|
||||||
if (GUILayout.Button("Select", GUILayout.Width(54)))
|
if (GUILayout.Button("Select", GUILayout.Width(54)))
|
||||||
Select(t);
|
Select(t);
|
||||||
@@ -206,6 +208,13 @@ namespace Darkmatter.Features.DrawingTemplates.Editor
|
|||||||
EditorGUILayout.LabelField("Basics", EditorStyles.boldLabel);
|
EditorGUILayout.LabelField("Basics", EditorStyles.boldLabel);
|
||||||
EditorGUILayout.PropertyField(so.FindProperty("id"), new GUIContent("Id (Addressable Key)"));
|
EditorGUILayout.PropertyField(so.FindProperty("id"), new GUIContent("Id (Addressable Key)"));
|
||||||
EditorGUILayout.PropertyField(so.FindProperty("displayName"));
|
EditorGUILayout.PropertyField(so.FindProperty("displayName"));
|
||||||
|
EditorGUILayout.PropertyField(so.FindProperty("difficultyOverride"),
|
||||||
|
new GUIContent("Difficulty Override", "0 = auto (pieces + regions). Any positive value overrides."));
|
||||||
|
EditorGUILayout.LabelField(
|
||||||
|
new GUIContent("Difficulty (effective)", "Sorts the catalog, lowest first."),
|
||||||
|
new GUIContent(t.HasDifficultyOverride
|
||||||
|
? $"{t.Difficulty} (override; auto would be {t.AutoDifficulty})"
|
||||||
|
: $"{t.Difficulty} (auto: {t.Pieces.Count} pieces + {t.AuthoredRegions.Count} regions)"));
|
||||||
EditorGUILayout.PropertyField(so.FindProperty("defaultThumbnail"));
|
EditorGUILayout.PropertyField(so.FindProperty("defaultThumbnail"));
|
||||||
|
|
||||||
EditorGUILayout.Space(6);
|
EditorGUILayout.Space(6);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
@@ -48,7 +49,7 @@ namespace Darkmatter.Features.DrawingTemplates.Systems
|
|||||||
_byId[t.Id] = t;
|
_byId[t.Id] = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
_allIds.Sort();
|
SortIds();
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,12 +85,22 @@ namespace Darkmatter.Features.DrawingTemplates.Systems
|
|||||||
if (!_allIds.Contains(id))
|
if (!_allIds.Contains(id))
|
||||||
{
|
{
|
||||||
_allIds.Add(id);
|
_allIds.Add(id);
|
||||||
_allIds.Sort();
|
SortIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Easiest drawings first; ties broken by id so the order is stable.
|
||||||
|
private void SortIds()
|
||||||
|
{
|
||||||
|
_allIds.Sort((a, b) =>
|
||||||
|
{
|
||||||
|
var byDifficulty = _byId[a].Difficulty.CompareTo(_byId[b].Difficulty);
|
||||||
|
return byDifficulty != 0 ? byDifficulty : string.Compare(a, b, StringComparison.OrdinalIgnoreCase);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void ReleaseAll()
|
public void ReleaseAll()
|
||||||
{
|
{
|
||||||
if (!_initialized) return;
|
if (!_initialized) return;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
|
using Darkmatter.Core;
|
||||||
using Darkmatter.Core.Contracts.Features.Capture;
|
using Darkmatter.Core.Contracts.Features.Capture;
|
||||||
using Darkmatter.Core.Contracts.Features.Coloring;
|
using Darkmatter.Core.Contracts.Features.Coloring;
|
||||||
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
using Darkmatter.Core.Contracts.Features.DrawingCatalog;
|
||||||
@@ -28,6 +29,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
public sealed class GameplayFlowController : IAsyncStartable, IGameplayFlowController, IDisposable
|
public sealed class GameplayFlowController : IAsyncStartable, IGameplayFlowController, IDisposable
|
||||||
{
|
{
|
||||||
private const int AutosaveDebounceMs = 500;
|
private const int AutosaveDebounceMs = 500;
|
||||||
|
private const float ThumbnailMinIntervalSec = 30f;
|
||||||
|
|
||||||
private readonly IProgressionSystem _progression;
|
private readonly IProgressionSystem _progression;
|
||||||
private readonly IDrawingTemplateCatalog _catalog;
|
private readonly IDrawingTemplateCatalog _catalog;
|
||||||
@@ -44,10 +46,12 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
private string _templateId;
|
private string _templateId;
|
||||||
private DrawingPhase _phase = DrawingPhase.ShapeBuilding;
|
private DrawingPhase _phase = DrawingPhase.ShapeBuilding;
|
||||||
private bool _allContentReported;
|
private bool _allContentReported;
|
||||||
|
private float _lastThumbnailTime = float.NegativeInfinity;
|
||||||
|
|
||||||
private IDisposable _assembledSub;
|
private IDisposable _assembledSub;
|
||||||
private IDisposable _colorAppliedSub;
|
private IDisposable _colorAppliedSub;
|
||||||
private IDisposable _drawingSelectedSub;
|
private IDisposable _drawingSelectedSub;
|
||||||
|
private IDisposable _openColorbookSub;
|
||||||
private CancellationTokenSource _autosaveCts;
|
private CancellationTokenSource _autosaveCts;
|
||||||
private CancellationTokenSource _scopeCts;
|
private CancellationTokenSource _scopeCts;
|
||||||
|
|
||||||
@@ -104,6 +108,10 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
|
_assembledSub = _bus.Subscribe<ShapeAssembledSignal>(OnShapeAssembled);
|
||||||
_colorAppliedSub = _bus.Subscribe<ColorAppliedSignal>(OnColorApplied);
|
_colorAppliedSub = _bus.Subscribe<ColorAppliedSignal>(OnColorApplied);
|
||||||
_drawingSelectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
|
_drawingSelectedSub = _bus.Subscribe<DrawingSelectedSignal>(OnDrawingSelected);
|
||||||
|
// The art book's "colorbook" button publishes OpenColorBookSignal. In the Colorbook
|
||||||
|
// scene DrawingCatalogPresenter shows the catalog; during gameplay that presenter is
|
||||||
|
// dead, so treat the signal as "leave gameplay for the colorbook" (same as back).
|
||||||
|
_openColorbookSub = _bus.Subscribe<OpenColorBookSignal>(OnOpenColorBook);
|
||||||
|
|
||||||
_loadingScreen.SetProgress(1f);
|
_loadingScreen.SetProgress(1f);
|
||||||
if (_phase == DrawingPhase.Coloring)
|
if (_phase == DrawingPhase.Coloring)
|
||||||
@@ -131,8 +139,10 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
|
|
||||||
public async UniTask SaveAsync(CancellationToken ct)
|
public async UniTask SaveAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
await SaveCurrentAsync(ct);
|
// One capture serves both the gallery write and the progress thumbnail — this used to
|
||||||
await _capture.CapturePngAsync(saveToGallery: true, ct);
|
// run the full capture pipeline twice back to back.
|
||||||
|
var png = await _capture.CapturePngAsync(saveToGallery: true, ct);
|
||||||
|
await SaveCurrentAsync(ct, captureThumbnail: false, precapturedThumbnail: png);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async UniTask NextAsync(CancellationToken ct)
|
public async UniTask NextAsync(CancellationToken ct)
|
||||||
@@ -209,6 +219,11 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
DebouncedAutosaveAsync(_autosaveCts.Token).Forget();
|
DebouncedAutosaveAsync(_autosaveCts.Token).Forget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnOpenColorBook(OpenColorBookSignal _)
|
||||||
|
{
|
||||||
|
BackAsync(CancellationToken.None).Forget();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDrawingSelected(DrawingSelectedSignal signal)
|
private void OnDrawingSelected(DrawingSelectedSignal signal)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(signal.TemplateId)) return;
|
if (string.IsNullOrEmpty(signal.TemplateId)) return;
|
||||||
@@ -232,7 +247,13 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await UniTask.Delay(AutosaveDebounceMs, cancellationToken: ct);
|
await UniTask.Delay(AutosaveDebounceMs, cancellationToken: ct);
|
||||||
await SaveCurrentAsync(ct);
|
// Autosaves fire after every paint pause; capturing a thumbnail each time renders the
|
||||||
|
// scene to an RT and PNG-encodes it, which OOM-kills long sessions on low-RAM devices.
|
||||||
|
// Colors always persist; the thumbnail refreshes at most every ThumbnailMinIntervalSec
|
||||||
|
// here and unconditionally on the exit paths (back / next / select-away).
|
||||||
|
bool refreshThumbnail =
|
||||||
|
Time.realtimeSinceStartup - _lastThumbnailTime >= ThumbnailMinIntervalSec;
|
||||||
|
await SaveCurrentAsync(ct, captureThumbnail: refreshThumbnail);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -240,7 +261,8 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async UniTask SaveCurrentAsync(CancellationToken ct)
|
private async UniTask SaveCurrentAsync(CancellationToken ct, bool captureThumbnail = true,
|
||||||
|
byte[] precapturedThumbnail = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_templateId)) return;
|
if (string.IsNullOrEmpty(_templateId)) return;
|
||||||
|
|
||||||
@@ -263,7 +285,10 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
FirstCompletedUtc = existing?.FirstCompletedUtc,
|
FirstCompletedUtc = existing?.FirstCompletedUtc,
|
||||||
};
|
};
|
||||||
|
|
||||||
var thumbnailPng = await _capture.CapturePngAsync(saveToGallery: false, ct);
|
byte[] thumbnailPng = precapturedThumbnail;
|
||||||
|
if (thumbnailPng == null && captureThumbnail)
|
||||||
|
thumbnailPng = await _capture.CapturePngAsync(saveToGallery: false, ct);
|
||||||
|
if (thumbnailPng != null) _lastThumbnailTime = Time.realtimeSinceStartup;
|
||||||
|
|
||||||
await _progression.SaveProgressAsync(progress, thumbnailPng);
|
await _progression.SaveProgressAsync(progress, thumbnailPng);
|
||||||
}
|
}
|
||||||
@@ -284,6 +309,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
|
|||||||
_assembledSub?.Dispose();
|
_assembledSub?.Dispose();
|
||||||
_colorAppliedSub?.Dispose();
|
_colorAppliedSub?.Dispose();
|
||||||
_drawingSelectedSub?.Dispose();
|
_drawingSelectedSub?.Dispose();
|
||||||
|
_openColorbookSub?.Dispose();
|
||||||
_autosaveCts?.Cancel();
|
_autosaveCts?.Cancel();
|
||||||
_autosaveCts?.Dispose();
|
_autosaveCts?.Dispose();
|
||||||
_scopeCts?.Cancel();
|
_scopeCts?.Cancel();
|
||||||
|
|||||||
@@ -56,8 +56,9 @@ namespace Darkmatter.Services.Analytics
|
|||||||
_subs.Add(_bus.Subscribe<IntroCompletedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.IntroCompleted)));
|
_subs.Add(_bus.Subscribe<IntroCompletedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.IntroCompleted)));
|
||||||
_subs.Add(_bus.Subscribe<PlayBtnClickedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.PlayClicked)));
|
_subs.Add(_bus.Subscribe<PlayBtnClickedSignal>(_ => _analytics.LogEvent(AnalyticsEvents.PlayClicked)));
|
||||||
|
|
||||||
// Navigation
|
// Navigation. ColorbookShownSignal (not OpenColorBookSignal, which is the show-view command
|
||||||
_subs.Add(_bus.Subscribe<OpenColorBookSignal>(_ => _analytics.LogEvent(AnalyticsEvents.ColorbookOpened)));
|
// and misses scene-entry paths) fires on every way the catalog becomes visible.
|
||||||
|
_subs.Add(_bus.Subscribe<ColorbookShownSignal>(_ => OnColorbookShown()));
|
||||||
_subs.Add(_bus.Subscribe<OpenArtBookSignal>(_ => _analytics.LogEvent(AnalyticsEvents.ArtbookOpened)));
|
_subs.Add(_bus.Subscribe<OpenArtBookSignal>(_ => _analytics.LogEvent(AnalyticsEvents.ArtbookOpened)));
|
||||||
_subs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => OnReturnToMainMenu()));
|
_subs.Add(_bus.Subscribe<ReturnToMainMenuSignal>(_ => OnReturnToMainMenu()));
|
||||||
|
|
||||||
@@ -90,6 +91,10 @@ namespace Darkmatter.Services.Analytics
|
|||||||
|
|
||||||
private void OnDrawingSelected(string drawingId)
|
private void OnDrawingSelected(string drawingId)
|
||||||
{
|
{
|
||||||
|
// GameplayFlowController relays the selection a second time after the scene swap
|
||||||
|
// (artbook edit path) — same id while already active is that relay, not a new selection.
|
||||||
|
if (drawingId == _activeDrawingId) return;
|
||||||
|
|
||||||
// A new selection while a drawing is still open (uncompleted) = the previous one was abandoned.
|
// A new selection while a drawing is still open (uncompleted) = the previous one was abandoned.
|
||||||
EndActiveDrawingIfAbandoned();
|
EndActiveDrawingIfAbandoned();
|
||||||
|
|
||||||
@@ -156,7 +161,7 @@ namespace Darkmatter.Services.Analytics
|
|||||||
if (dur >= 0f) p[AnalyticsParams.DurationSeconds] = dur;
|
if (dur >= 0f) p[AnalyticsParams.DurationSeconds] = dur;
|
||||||
_analytics.LogEvent(AnalyticsEvents.DrawingCompleted, p);
|
_analytics.LogEvent(AnalyticsEvents.DrawingCompleted, p);
|
||||||
|
|
||||||
// GA4 recommended games event (lights up built-in level funnels).
|
// Companion to GA4's level_start (custom name — GA4's own pair would be level_end).
|
||||||
_analytics.LogEvent(AnalyticsEvents.LevelComplete, new Dictionary<string, object>
|
_analytics.LogEvent(AnalyticsEvents.LevelComplete, new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
[AnalyticsParams.LevelName] = s.TemplateId,
|
[AnalyticsParams.LevelName] = s.TemplateId,
|
||||||
@@ -178,9 +183,11 @@ namespace Darkmatter.Services.Analytics
|
|||||||
private void OnGallerySaveCompleted(GallerySaveCompletedSignal s)
|
private void OnGallerySaveCompleted(GallerySaveCompletedSignal s)
|
||||||
{
|
{
|
||||||
_analytics.LogEvent(AnalyticsEvents.GallerySaveCompleted, AnalyticsParams.Success, s.Success ? "true" : "false");
|
_analytics.LogEvent(AnalyticsEvents.GallerySaveCompleted, AnalyticsParams.Success, s.Success ? "true" : "false");
|
||||||
|
// Prefer the id carried by the signal: art-book saves happen with no active drawing.
|
||||||
|
string drawingId = string.IsNullOrEmpty(s.TemplateId) ? _activeDrawingId ?? string.Empty : s.TemplateId;
|
||||||
_analytics.LogEvent(AnalyticsEvents.DrawingSaved, new Dictionary<string, object>
|
_analytics.LogEvent(AnalyticsEvents.DrawingSaved, new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
[AnalyticsParams.DrawingId] = _activeDrawingId ?? string.Empty,
|
[AnalyticsParams.DrawingId] = drawingId,
|
||||||
[AnalyticsParams.Success] = s.Success ? 1 : 0,
|
[AnalyticsParams.Success] = s.Success ? 1 : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -191,6 +198,15 @@ namespace Darkmatter.Services.Analytics
|
|||||||
_analytics.LogEvent(AnalyticsEvents.MainMenuReturned);
|
_analytics.LogEvent(AnalyticsEvents.MainMenuReturned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnColorbookShown()
|
||||||
|
{
|
||||||
|
// Reaching the catalog with a drawing still open means the player backed out of gameplay
|
||||||
|
// mid-drawing — the abandon moment. (Completion clears the active drawing before the
|
||||||
|
// catalog reloads, so the finish path never lands here with one open.)
|
||||||
|
EndActiveDrawingIfAbandoned();
|
||||||
|
_analytics.LogEvent(AnalyticsEvents.ColorbookOpened);
|
||||||
|
}
|
||||||
|
|
||||||
// Emits drawing_abandoned for an open, uncompleted drawing. Completion clears _activeDrawingId, so
|
// 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.
|
// reaching here with a non-null id means the drawing was left without finishing — the leak signal.
|
||||||
private void EndActiveDrawingIfAbandoned()
|
private void EndActiveDrawingIfAbandoned()
|
||||||
|
|||||||
@@ -68,21 +68,18 @@ namespace Darkmatter.Services.Capture
|
|||||||
|
|
||||||
var prevActive = RenderTexture.active;
|
var prevActive = RenderTexture.active;
|
||||||
RenderTexture.active = rt;
|
RenderTexture.active = rt;
|
||||||
var fullScreen = new Texture2D(sw, sh, TextureFormat.RGBA32, mipChain: false);
|
// Read only the crop region straight off the RT. A full-screen ReadPixels followed by
|
||||||
fullScreen.ReadPixels(new Rect(0, 0, sw, sh), 0, 0);
|
// GetPixels/SetPixels cropping allocated a managed Color[] at 16 bytes/pixel (tens of MB)
|
||||||
fullScreen.Apply();
|
// on every capture — autosave runs this after each paint, OOM-killing low-RAM devices.
|
||||||
|
var cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, mipChain: false);
|
||||||
|
cropped.ReadPixels(new Rect(crop.x, crop.y, cropW, cropH), 0, 0);
|
||||||
|
cropped.Apply();
|
||||||
RenderTexture.active = prevActive;
|
RenderTexture.active = prevActive;
|
||||||
captureViewScope?.Dispose();
|
captureViewScope?.Dispose();
|
||||||
captureViewScope = null;
|
captureViewScope = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cropped = new Texture2D(cropW, cropH, TextureFormat.RGBA32, mipChain: false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
cropped.SetPixels(fullScreen.GetPixels((int)crop.x, (int)crop.y, cropW, cropH));
|
|
||||||
cropped.Apply();
|
|
||||||
|
|
||||||
int targetW = Mathf.Max(1, Mathf.RoundToInt(cropW * scale));
|
int targetW = Mathf.Max(1, Mathf.RoundToInt(cropW * scale));
|
||||||
int targetH = Mathf.Max(1, Mathf.RoundToInt(cropH * scale));
|
int targetH = Mathf.Max(1, Mathf.RoundToInt(cropH * scale));
|
||||||
Texture2D output = cropped;
|
Texture2D output = cropped;
|
||||||
@@ -104,11 +101,6 @@ namespace Darkmatter.Services.Capture
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
|
||||||
Object.Destroy(fullScreen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
paperCanvas.renderMode = prevMode;
|
paperCanvas.renderMode = prevMode;
|
||||||
paperCanvas.worldCamera = prevWorldCam;
|
paperCanvas.worldCamera = prevWorldCam;
|
||||||
|
|||||||
BIN
Assets/Darkmatter/Content/.DS_Store
vendored
BIN
Assets/Darkmatter/Content/.DS_Store
vendored
Binary file not shown.
BIN
Assets/Darkmatter/Content/Colorbook UI/.DS_Store
vendored
BIN
Assets/Darkmatter/Content/Colorbook UI/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
@@ -21,7 +21,7 @@ TextureImporter:
|
|||||||
heightScale: 0.25
|
heightScale: 0.25
|
||||||
normalMapFilter: 0
|
normalMapFilter: 0
|
||||||
flipGreenChannel: 0
|
flipGreenChannel: 0
|
||||||
isReadable: 1
|
isReadable: 0
|
||||||
streamingMipmaps: 0
|
streamingMipmaps: 0
|
||||||
streamingMipmapsPriority: 0
|
streamingMipmapsPriority: 0
|
||||||
vTOnly: 0
|
vTOnly: 0
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ TextureImporter:
|
|||||||
heightScale: 0.25
|
heightScale: 0.25
|
||||||
normalMapFilter: 0
|
normalMapFilter: 0
|
||||||
flipGreenChannel: 0
|
flipGreenChannel: 0
|
||||||
isReadable: 1
|
isReadable: 0
|
||||||
streamingMipmaps: 0
|
streamingMipmaps: 0
|
||||||
streamingMipmapsPriority: 0
|
streamingMipmapsPriority: 0
|
||||||
vTOnly: 0
|
vTOnly: 0
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ TextureImporter:
|
|||||||
heightScale: 0.25
|
heightScale: 0.25
|
||||||
normalMapFilter: 0
|
normalMapFilter: 0
|
||||||
flipGreenChannel: 0
|
flipGreenChannel: 0
|
||||||
isReadable: 1
|
isReadable: 0
|
||||||
streamingMipmaps: 0
|
streamingMipmaps: 0
|
||||||
streamingMipmapsPriority: 0
|
streamingMipmapsPriority: 0
|
||||||
vTOnly: 0
|
vTOnly: 0
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ TextureImporter:
|
|||||||
heightScale: 0.25
|
heightScale: 0.25
|
||||||
normalMapFilter: 0
|
normalMapFilter: 0
|
||||||
flipGreenChannel: 0
|
flipGreenChannel: 0
|
||||||
isReadable: 1
|
isReadable: 0
|
||||||
streamingMipmaps: 0
|
streamingMipmaps: 0
|
||||||
streamingMipmapsPriority: 0
|
streamingMipmapsPriority: 0
|
||||||
vTOnly: 0
|
vTOnly: 0
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ TextureImporter:
|
|||||||
heightScale: 0.25
|
heightScale: 0.25
|
||||||
normalMapFilter: 0
|
normalMapFilter: 0
|
||||||
flipGreenChannel: 0
|
flipGreenChannel: 0
|
||||||
isReadable: 1
|
isReadable: 0
|
||||||
streamingMipmaps: 0
|
streamingMipmaps: 0
|
||||||
streamingMipmapsPriority: 0
|
streamingMipmapsPriority: 0
|
||||||
vTOnly: 0
|
vTOnly: 0
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ TextureImporter:
|
|||||||
heightScale: 0.25
|
heightScale: 0.25
|
||||||
normalMapFilter: 0
|
normalMapFilter: 0
|
||||||
flipGreenChannel: 0
|
flipGreenChannel: 0
|
||||||
isReadable: 1
|
isReadable: 0
|
||||||
streamingMipmaps: 0
|
streamingMipmaps: 0
|
||||||
streamingMipmapsPriority: 0
|
streamingMipmapsPriority: 0
|
||||||
vTOnly: 0
|
vTOnly: 0
|
||||||
|
|||||||
@@ -68,20 +68,7 @@ MonoBehaviour:
|
|||||||
- rid: 570818657416118287
|
- rid: 570818657416118287
|
||||||
- rid: 570818657416118288
|
- rid: 570818657416118288
|
||||||
m_RuntimeSettings:
|
m_RuntimeSettings:
|
||||||
m_List:
|
m_List: []
|
||||||
- rid: 7752762179098771456
|
|
||||||
- rid: 7752762179098771457
|
|
||||||
- rid: 7752762179098771459
|
|
||||||
- rid: 7752762179098771462
|
|
||||||
- rid: 7752762179098771464
|
|
||||||
- rid: 7752762179098771466
|
|
||||||
- rid: 7752762179098771468
|
|
||||||
- rid: 7752762179098771472
|
|
||||||
- rid: 7752762179098771476
|
|
||||||
- rid: 3114554777721110529
|
|
||||||
- rid: 3114554777721110530
|
|
||||||
- rid: 570818657416118282
|
|
||||||
- rid: 570818657416118283
|
|
||||||
m_AssetVersion: 10
|
m_AssetVersion: 10
|
||||||
m_ObsoleteDefaultVolumeProfile: {fileID: 0}
|
m_ObsoleteDefaultVolumeProfile: {fileID: 0}
|
||||||
m_RenderingLayerNames:
|
m_RenderingLayerNames:
|
||||||
|
|||||||
BIN
Assets/FacebookSDK/.DS_Store
vendored
Normal file
BIN
Assets/FacebookSDK/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/Firebase/.DS_Store
vendored
Normal file
BIN
Assets/Firebase/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/Packages/.DS_Store
vendored
Normal file
BIN
Assets/Packages/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/Plugins/.DS_Store
vendored
Normal file
BIN
Assets/Plugins/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/Spine/.DS_Store
vendored
Normal file
BIN
Assets/Spine/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
Assets/TextMesh Pro/.DS_Store
vendored
Normal file
BIN
Assets/TextMesh Pro/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -8,3 +8,13 @@ ScriptedImporter:
|
|||||||
assetBundleName:
|
assetBundleName:
|
||||||
assetBundleVariant:
|
assetBundleVariant:
|
||||||
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
|
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
|
||||||
|
useAsTemplate: 0
|
||||||
|
exposeTemplateAsShader: 0
|
||||||
|
indexedData: {instanceID: 0}
|
||||||
|
template:
|
||||||
|
name:
|
||||||
|
category:
|
||||||
|
description:
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
thumbnail: {instanceID: 0}
|
||||||
|
order: 0
|
||||||
|
|||||||
8
Assets/_Recovery.meta
Normal file
8
Assets/_Recovery.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f8a91ab8cce424696bd7b4cbbac110fe
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
4042
Assets/_Recovery/0.unity
Normal file
4042
Assets/_Recovery/0.unity
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/_Recovery/0.unity.meta
Normal file
7
Assets/_Recovery/0.unity.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 90078c122328b4902a1e67255b30525a
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user