Compare commits

...

6 Commits

Author SHA1 Message Date
Savya Bikram Shah
b77976039a built 2026-07-09 15:17:54 +05:45
Savya Bikram Shah
f6f5474366 Merge branch 'work_branch' of https://git.darkmatterproduction.org/savya/Colorbook into savya
# Conflicts:
#	Assets/Darkmatter/Content/Fonts/static/Fredoka-Bold SDF Blue Outline.asset
2026-07-09 10:37:28 +05:45
Savya Bikram Shah
3abf19b5bb minor 2026-07-09 10:34:43 +05:45
Savya Bikram Shah
35e4bf8067 Merge branch 'work_branch' of https://git.darkmatterproduction.org/savya/Colorbook into savya
# Conflicts:
#	Assets/Darkmatter/Content/Fonts/static/Fredoka-Bold SDF Blue Outline.asset
2026-07-07 15:53:34 +05:45
Savya Bikram Shah
df43824fb1 Back button bug fix 2026-07-07 15:53:13 +05:45
Savya Bikram Shah
e167af11fd Fixes optimization and UI/UX updates 2026-07-07 13:05:48 +05:45
38 changed files with 676 additions and 2180 deletions

BIN
.DS_Store vendored

Binary file not shown.

BIN
Assets/.DS_Store vendored

Binary file not shown.

Binary file not shown.

View File

@@ -6,9 +6,12 @@ namespace Darkmatter.Core.Contracts.Features.DrawingCatalog
{
public interface IDrawingTemplateCatalog
{
UniTask FetchAsync();
UniTask FetchAsync();
IReadOnlyList<string> AllTemplateIds { get; }
UniTask<Sprite> GetThumbnailAsync(string id);
// owned = true when the sprite (and its texture) were created for this call from a
// saved thumbnail on disk — the caller must Destroy() both when done. owned = false
// means the template's default thumbnail asset, which must NOT be destroyed.
UniTask<(Sprite sprite, bool owned)> GetThumbnailAsync(string id);
UniTask<IDrawingTemplate> LoadAsync(string id);
void ReleaseAll();
string GetNextTemplate(string currentId);

View File

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

View File

@@ -0,0 +1,48 @@
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Rendering;
using VContainer.Unity;
namespace Darkmatter.Features.AppBoot.Flow
{
// A coloring page is static most of the time; re-rendering it at a locked 60 fps only
// turns battery into heat. Keep the player loop at 60 Hz (input still sampled every
// frame, so the first touch never lands on a skipped frame) but render every other
// frame while nothing has been touched for a few seconds.
public sealed class AdaptiveFrameRate : ITickable
{
private const float FullRateHoldSeconds = 3f;
private const int IdleRenderInterval = 2; // 60 Hz loop -> 30 fps rendering when idle
private float _lastInteractionTime;
public AdaptiveFrameRate()
{
// Treat boot as an interaction so the intro video starts at full rate.
_lastInteractionTime = Time.unscaledTime;
}
public void Tick()
{
if (IsPointerPressed())
_lastInteractionTime = Time.unscaledTime;
var desired = Time.unscaledTime - _lastInteractionTime < FullRateHoldSeconds
? 1
: IdleRenderInterval;
if (OnDemandRendering.renderFrameInterval != desired)
OnDemandRendering.renderFrameInterval = desired;
}
private static bool IsPointerPressed()
{
var touchscreen = Touchscreen.current;
if (touchscreen != null && touchscreen.primaryTouch.press.isPressed)
return true;
var mouse = Mouse.current;
return mouse != null && mouse.leftButton.isPressed;
}
}
}

View File

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

View File

@@ -16,6 +16,7 @@ namespace Darkmatter.Features.AppBoot.Installers
if (sceneRefs != null)
builder.RegisterComponent(sceneRefs);
builder.RegisterEntryPoint<AppBootFlow>();
builder.RegisterEntryPoint<AdaptiveFrameRate>();
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
namespace Darkmatter.Features.Coloring.Systems
@@ -8,6 +9,7 @@ namespace Darkmatter.Features.Coloring.Systems
public sealed class ColoringPinchZoomController : MonoBehaviour
{
private const float MinimumPinchDistance = 8f;
private const float PanZoomEpsilon = 0.0001f;
[SerializeField] private float minZoom = 1f;
[SerializeField] private float maxZoom = 3f;
@@ -22,6 +24,7 @@ namespace Darkmatter.Features.Coloring.Systems
private float _currentZoom = 1f;
private Vector3 _baseScale = Vector3.one;
private Vector2 _baseAnchoredPosition;
private PanDragRelay _panRelay;
public void SetTarget(RectTransform target)
{
@@ -34,6 +37,26 @@ namespace Darkmatter.Features.Coloring.Systems
_baseAnchoredPosition = target != null ? target.anchoredPosition : Vector2.zero;
_currentZoom = 1f;
enabled = _target != null && _parent != null;
_panRelay = null;
if (_target != null)
{
_panRelay = _target.gameObject.GetComponent<PanDragRelay>();
if (_panRelay == null)
_panRelay = _target.gameObject.AddComponent<PanDragRelay>();
_panRelay.Controller = this;
}
UpdatePanRelayState();
}
// While not zoomed the relay is disabled so the event system never resolves it as a
// drag target — sloppy kid taps keep registering as clicks (paints) exactly as before.
// Zoomed in, drags become pans and are suppressed as clicks by uGUI.
private void UpdatePanRelayState()
{
if (_panRelay != null)
_panRelay.enabled = _currentZoom > 1f + PanZoomEpsilon;
}
public void ResetZoom()
@@ -46,6 +69,7 @@ namespace Darkmatter.Features.Coloring.Systems
_currentZoom = 1f;
_isPinching = false;
UpdatePanRelayState();
}
public IDisposable UseNonZoomedView()
@@ -78,6 +102,7 @@ namespace Darkmatter.Features.Coloring.Systems
_isPinching = false;
TryApplyScrollZoom();
TryApplyMousePan();
}
private bool TryApplyPinchZoom()
@@ -111,8 +136,16 @@ namespace Darkmatter.Features.Coloring.Systems
return true;
}
// Anchor the content point that was under the previous midpoint to the current
// midpoint: distance change zooms, midpoint movement pans — one gesture does both.
Vector2 previousMidpoint = (firstTouch.PreviousPosition + secondTouch.PreviousPosition) * 0.5f;
if (!TryGetParentLocalPoint(previousMidpoint, out Vector2 previousMidpointLocal))
{
return true;
}
float targetZoom = Mathf.Clamp(_currentZoom * (distance / previousDistance), minZoom, maxZoom);
ZoomTowards(midpointLocal, targetZoom);
ApplyZoomPan(previousMidpointLocal, midpointLocal, targetZoom);
return true;
}
@@ -152,12 +185,123 @@ namespace Darkmatter.Features.Coloring.Systems
return;
}
Vector2 contentPoint = (parentLocalPoint - _target.anchoredPosition) / _currentZoom;
ApplyZoomPan(parentLocalPoint, parentLocalPoint, targetZoom);
}
private void ApplyZoomPan(Vector2 previousLocalPoint, Vector2 currentLocalPoint, float targetZoom)
{
Vector2 contentPoint = (previousLocalPoint - _target.anchoredPosition) / _currentZoom;
_currentZoom = targetZoom;
_target.localScale = _baseScale * _currentZoom;
_target.anchoredPosition = Mathf.Approximately(_currentZoom, minZoom)
? _baseAnchoredPosition
: parentLocalPoint - contentPoint * _currentZoom;
_target.anchoredPosition = ClampPan(currentLocalPoint - contentPoint * _currentZoom);
UpdatePanRelayState();
}
// Editor/desktop convenience: drag with the right or middle mouse button to pan while zoomed.
private void TryApplyMousePan()
{
Mouse mouse = Mouse.current;
if (mouse == null || (!mouse.rightButton.isPressed && !mouse.middleButton.isPressed))
{
return;
}
if (_currentZoom <= 1f + PanZoomEpsilon)
{
return;
}
Vector2 delta = mouse.delta.ReadValue();
if (delta == Vector2.zero)
{
return;
}
Vector2 screenPosition = mouse.position.ReadValue();
if (!IsInputPointAllowed(screenPosition))
{
return;
}
if (!TryGetParentLocalPoint(screenPosition, out Vector2 currentLocal) ||
!TryGetParentLocalPoint(screenPosition - delta, out Vector2 previousLocal))
{
return;
}
_target.anchoredPosition = ClampPan(_target.anchoredPosition + (currentLocal - previousLocal));
}
// Single-finger (or left-mouse) drag pan, fed by PanDragRelay on the paper root.
// Going through uGUI's drag pipeline matters: once the drag threshold is crossed the
// event system cancels the pending click, so a pan can never paint the region the
// finger ends on — a plain tap still paints.
private void OnPanDrag(PointerEventData eventData, Transform source)
{
if (_target == null || source != _target.transform)
{
return;
}
if (_isPinching || HasMultiTouch())
{
return; // two-finger gesture owns the view
}
if (_currentZoom <= 1f + PanZoomEpsilon)
{
return;
}
if (!TryGetParentLocalPoint(eventData.position, out Vector2 currentLocal) ||
!TryGetParentLocalPoint(eventData.position - eventData.delta, out Vector2 previousLocal))
{
return;
}
_target.anchoredPosition = ClampPan(_target.anchoredPosition + (currentLocal - previousLocal));
}
private static bool HasMultiTouch()
{
Touchscreen touchscreen = Touchscreen.current;
if (touchscreen == null)
{
return false;
}
int count = 0;
foreach (var touchControl in touchscreen.touches)
{
if (touchControl.press.isPressed && ++count > 1)
{
return true;
}
}
return false;
}
// The scaled paper must keep covering its zoom-1 footprint: panning can never expose
// background inside the area the drawing occupies when not zoomed.
private Vector2 ClampPan(Vector2 anchoredPosition)
{
if (_currentZoom <= 1f + PanZoomEpsilon)
{
return _baseAnchoredPosition;
}
Rect rect = _target.rect;
float growth = 1f - _currentZoom; // negative while zoomed in
float minX = _baseAnchoredPosition.x + rect.xMax * _baseScale.x * growth;
float maxX = _baseAnchoredPosition.x + rect.xMin * _baseScale.x * growth;
float minY = _baseAnchoredPosition.y + rect.yMax * _baseScale.y * growth;
float maxY = _baseAnchoredPosition.y + rect.yMin * _baseScale.y * growth;
return new Vector2(
Mathf.Clamp(anchoredPosition.x, minX, maxX),
Mathf.Clamp(anchoredPosition.y, minY, maxY));
}
private bool TryGetParentLocalPoint(Vector2 screenPoint, out Vector2 localPoint)
@@ -233,6 +377,7 @@ namespace Darkmatter.Features.Coloring.Systems
_target.anchoredPosition = state.AnchoredPosition;
_currentZoom = state.CurrentZoom;
_isPinching = false;
UpdatePanRelayState();
}
private readonly struct ViewState
@@ -285,5 +430,25 @@ namespace Darkmatter.Features.Coloring.Systems
Delta = delta;
}
}
// Runtime-attached to the paper root (SetTarget); regions are its children, so drags
// that start on a region bubble up here while taps stay clicks. Implementing the uGUI
// drag interfaces is what makes the event system suppress the region click on drag.
private sealed class PanDragRelay : MonoBehaviour, IBeginDragHandler, IDragHandler
{
internal ColoringPinchZoomController Controller;
public void OnBeginDrag(PointerEventData eventData)
{
}
public void OnDrag(PointerEventData eventData)
{
if (Controller != null)
{
Controller.OnPanDrag(eventData, transform);
}
}
}
}
}

View File

@@ -25,6 +25,12 @@ namespace Darkmatter.Features.DrawingCatalog
private readonly CancellationTokenSource _cts = new();
private IDisposable _openSubscription;
// Sprites/textures decoded from saved thumbnails are created fresh on every refresh
// and are owned by this presenter — without destroying the previous batch each
// Colorbook visit leaks several MB per painted drawing until the OS kills the app.
private readonly List<Sprite> _ownedSprites = new();
private readonly List<Texture2D> _ownedTextures = new();
public DrawingCatalogPresenter(DrawingCatalogView view, IDrawingCatalogController controller,
IDrawingTemplateCatalog catalog, IEventBus eventBus)
{
@@ -109,29 +115,73 @@ namespace Darkmatter.Features.DrawingCatalog
private async UniTask RefreshAsync(CancellationToken ct)
{
var ids = ToFixedRowGridOrder(_controller.VisibleIds);
var vms = new List<CatalogItemVM>(ids.Count);
var newSprites = new List<Sprite>();
var newTextures = new List<Texture2D>();
var applied = false;
_totalPages = (int)Math.Ceiling(ids.Count / (float)ItemPerPage);
_currentPage = 0;
foreach (var id in ids)
try
{
var ids = ToFixedRowGridOrder(_controller.VisibleIds);
var vms = new List<CatalogItemVM>(ids.Count);
_totalPages = (int)Math.Ceiling(ids.Count / (float)ItemPerPage);
_currentPage = 0;
foreach (var id in ids)
{
if (ct.IsCancellationRequested) return;
var (thumb, owned) = await _catalog.GetThumbnailAsync(id);
if (owned)
{
newSprites.Add(thumb);
newTextures.Add(thumb.texture);
}
vms.Add(new CatalogItemVM(id, thumb));
}
if (ct.IsCancellationRequested) return;
var thumb = await _catalog.GetThumbnailAsync(id);
vms.Add(new CatalogItemVM(id, thumb));
_view.ResetScrollPosition();
_view.SetItems(vms);
_view.SetPagination(_currentPage, _totalPages);
// Buttons now reference the new batch; the previous one can go.
DestroyOwnedThumbnails();
_ownedSprites.AddRange(newSprites);
_ownedTextures.AddRange(newTextures);
applied = true;
// Unblock InitializeAsync: items are now on screen, so the loading screen can hide.
_controller.NotifyPopulated();
// Cue the first-run tutorial that the catalog grid is on screen and tappable.
_eventBus.Publish(new DrawingCatalogReadySignal());
}
catch (Exception e)
{
Debug.LogError($"[DrawingCatalog] Failed to refresh catalog: {e}");
}
finally
{
// Aborted refresh (cancelled or threw): don't strand the freshly decoded batch.
if (!applied)
{
foreach (var s in newSprites) UnityEngine.Object.Destroy(s);
foreach (var t in newTextures) UnityEngine.Object.Destroy(t);
}
if (ct.IsCancellationRequested) return;
_view.ResetScrollPosition();
_view.SetItems(vms);
_view.SetPagination(_currentPage, _totalPages);
// Idempotent — always unblock the flow; a stuck loading screen is worse than
// an empty catalog.
_controller.NotifyPopulated();
}
}
// Unblock InitializeAsync: items are now on screen, so the loading screen can hide.
_controller.NotifyPopulated();
// Cue the first-run tutorial that the catalog grid is on screen and tappable.
_eventBus.Publish(new DrawingCatalogReadySignal());
private void DestroyOwnedThumbnails()
{
foreach (var s in _ownedSprites) UnityEngine.Object.Destroy(s);
foreach (var t in _ownedTextures) UnityEngine.Object.Destroy(t);
_ownedSprites.Clear();
_ownedTextures.Clear();
}
private static List<string> ToFixedRowGridOrder(IReadOnlyList<string> ids)
@@ -160,6 +210,7 @@ namespace Darkmatter.Features.DrawingCatalog
_openSubscription?.Dispose();
_cts.Cancel();
_cts.Dispose();
DestroyOwnedThumbnails();
}
}
}

View File

@@ -53,7 +53,7 @@ namespace Darkmatter.Features.DrawingTemplates.Systems
_initialized = true;
}
public async UniTask<Sprite> GetThumbnailAsync(string id)
public async UniTask<(Sprite sprite, bool owned)> GetThumbnailAsync(string id)
{
if (!_byId.TryGetValue(id, out var t))
throw new KeyNotFoundException($"Template '{id}' not in catalog. Did InitializeAsync run?");
@@ -62,9 +62,9 @@ namespace Darkmatter.Features.DrawingTemplates.Systems
if (savedTex != null)
{
var rect = new Rect(0, 0, savedTex.width, savedTex.height);
return Sprite.Create(savedTex, rect, new Vector2(0.5f, 0.5f));
return (Sprite.Create(savedTex, rect, new Vector2(0.5f, 0.5f)), true);
}
return t.DefaultThumbnail;
return (t.DefaultThumbnail, false);
}
public UniTask<IDrawingTemplate> LoadAsync(string id)

View File

@@ -46,6 +46,7 @@ namespace Darkmatter.Features.GameplayFlow.Systems
private string _templateId;
private DrawingPhase _phase = DrawingPhase.ShapeBuilding;
private bool _allContentReported;
private bool _navigating;
private float _lastThumbnailTime = float.NegativeInfinity;
private IDisposable _assembledSub;
@@ -80,6 +81,27 @@ namespace Darkmatter.Features.GameplayFlow.Systems
}
public async UniTask StartAsync(CancellationToken cancellation)
{
try
{
await StartCoreAsync(cancellation);
}
catch (OperationCanceledException)
{
// Scene torn down mid-init; nothing to recover.
}
catch (Exception e)
{
// Any throw here (missing template id, failed Addressables load, region init)
// would otherwise skip _loadingScreen.Hide() and strand the kid on a frozen
// loader forever. Log it and route back to the Colorbook, whose flow hides the
// loading screen once its catalog is populated.
Debug.LogError($"[GameplayFlow] Failed to start gameplay, returning to Colorbook: {e}");
await RecoverToColorbookAsync();
}
}
private async UniTask StartCoreAsync(CancellationToken cancellation)
{
_scopeCts = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
var ct = _scopeCts.Token;
@@ -127,14 +149,42 @@ namespace Darkmatter.Features.GameplayFlow.Systems
_loadingScreen.Hide();
}
private async UniTask RecoverToColorbookAsync()
{
try
{
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: default);
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: default);
}
catch (Exception e)
{
Debug.LogError($"[GameplayFlow] Recovery to Colorbook failed: {e}");
_loadingScreen.Hide();
}
}
public async UniTask BackAsync(CancellationToken ct)
{
await SaveCurrentAsync(CancellationToken.None);
_loadingScreen.Show();
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: ct);
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: ct);
// A second navigation while one runs (e.g. Back pressed during the completion
// animation inside NextAsync) would interleave two saves and two Colorbook loads,
// and Clear() would rip the celebration apart mid-flight. First navigation wins.
if (_navigating) return;
_navigating = true;
try
{
await SaveCurrentAsync(CancellationToken.None);
_loadingScreen.Show();
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: ct);
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: ct);
}
finally
{
_navigating = false;
}
}
public async UniTask SaveAsync(CancellationToken ct)
@@ -147,48 +197,48 @@ namespace Darkmatter.Features.GameplayFlow.Systems
public async UniTask NextAsync(CancellationToken ct)
{
// Drop any debounced autosave so it can't fire during/after the completion
// animation and capture the animation (or an empty paper) into the thumbnail.
_autosaveCts?.Cancel();
_autosaveCts?.Dispose();
_autosaveCts = null;
await SaveCurrentAsync(ct);
_refs.Confetti.Play();
_sfx.Play(SfxId.FireWorkLaunch);
_sfx.Play(SfxId.LevelComplete);
await _coloring.PlayCompletionAnimationAsync(ct);
_progression.MarkCompleted(_templateId);
var progressAfter = _progression.GetProgress(_templateId);
_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)
if (_navigating) return;
_navigating = true;
try
{
_allContentReported = true;
_bus.Publish(new AllContentCompletedSignal(_progression.CompletedTemplateIds.Count));
}
// Drop any debounced autosave so it can't fire during/after the completion
// animation and capture the animation (or an empty paper) into the thumbnail.
_autosaveCts?.Cancel();
_autosaveCts?.Dispose();
_autosaveCts = null;
var nextId = _catalog.GetNextTemplate(_templateId);
if (string.IsNullOrEmpty(nextId))
{
await SaveCurrentAsync(ct);
_refs.Confetti.Play();
_sfx.Play(SfxId.FireWorkLaunch);
_sfx.Play(SfxId.LevelComplete);
await _coloring.PlayCompletionAnimationAsync(ct);
_progression.MarkCompleted(_templateId);
var progressAfter = _progression.GetProgress(_templateId);
_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);
_loadingScreen.Show();
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: default);
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: default);
return;
if (!string.IsNullOrEmpty(nextId))
_bus.Publish(new DrawingSelectedSignal(nextId));
}
finally
{
_navigating = false;
}
_loadingScreen.Show();
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: default);
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: default);
_bus.Publish(new DrawingSelectedSignal(nextId));
}
@@ -233,13 +283,22 @@ namespace Darkmatter.Features.GameplayFlow.Systems
private async UniTaskVoid EditAsync(string newTemplateId)
{
await SaveCurrentAsync(CancellationToken.None);
_loadingScreen.Show();
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: default);
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: default);
_bus.Publish(new DrawingSelectedSignal(newTemplateId));
if (_navigating) return;
_navigating = true;
try
{
await SaveCurrentAsync(CancellationToken.None);
_loadingScreen.Show();
_shapeBuilder.Clear();
_coloring.Clear();
await _scenes.LoadSceneAsync(nameof(GameScene.Colorbook), progress: null, cancellationToken: default);
await _scenes.UnloadSceneAsync(nameof(GameScene.Gameplay), progress: null, cancellationToken: default);
_bus.Publish(new DrawingSelectedSignal(newTemplateId));
}
finally
{
_navigating = false;
}
}
private async UniTaskVoid DebouncedAutosaveAsync(CancellationToken ct)

View File

@@ -13,6 +13,7 @@ namespace Darkmatter.Features.Mainmenu
private readonly IEventBus _eventBus;
private readonly MainMenuView _view;
private readonly ISfxPlayer _sfxPlayer;
private IDisposable _introSubscription;
public MainMenuPresenter(MainMenuView view, IEventBus eventBus, ISfxPlayer sfxPlayer)
@@ -26,7 +27,7 @@ namespace Darkmatter.Features.Mainmenu
{
_view.OnPlayBtnPressedEvent += OnPlayBtnPressed;
_view.OnPlayBtnClickedEvent += OnPlayBtnClicked;
_eventBus.Subscribe<IntroCompletedSignal>(OnIntroComplete);
_introSubscription = _eventBus.Subscribe<IntroCompletedSignal>(OnIntroComplete);
}
private void OnIntroComplete(IntroCompletedSignal signal)
@@ -48,6 +49,7 @@ namespace Darkmatter.Features.Mainmenu
{
_view.OnPlayBtnPressedEvent -= OnPlayBtnPressed;
_view.OnPlayBtnClickedEvent -= OnPlayBtnClicked;
_introSubscription?.Dispose();
}
}
}

View File

@@ -86,16 +86,27 @@ public sealed class ProgressionRepository
{
if (string.IsNullOrEmpty(templateId) || png == null || png.Length == 0) return;
Directory.CreateDirectory(ThumbnailDirectory());
var path = ThumbnailPath(templateId);
var tmp = path + ".tmp";
await UniTask.RunOnThreadPool(() =>
// Never let thumbnail IO (disk full, permission churn) throw into the save path:
// saves run before every Back/Next navigation, and an unhandled IOException there
// aborts the scene transition and traps the user in gameplay. Losing a thumbnail
// is fine — the colors themselves persist via PlayerPrefs.
try
{
File.WriteAllBytes(tmp, png);
if (File.Exists(path)) File.Delete(path);
File.Move(tmp, path);
});
Directory.CreateDirectory(ThumbnailDirectory());
var path = ThumbnailPath(templateId);
var tmp = path + ".tmp";
await UniTask.RunOnThreadPool(() =>
{
File.WriteAllBytes(tmp, png);
if (File.Exists(path)) File.Delete(path);
File.Move(tmp, path);
});
}
catch (Exception e)
{
Debug.LogWarning($"[Progression] Failed writing thumbnail '{templateId}': {e.Message}");
}
}
public async UniTask<Texture2D> LoadThumbnailAsync(string templateId)

View File

@@ -15,7 +15,23 @@ namespace Darkmatter.Libs.Observer
if (_map.TryGetValue(typeof(T), out var handlerObj))
{
var action = (Action<T>)handlerObj;
action?.Invoke(evt);
if (action == null) return;
// Invoke handlers one by one: a multicast invoke would let one throwing
// subscriber silently starve every later subscriber and rethrow into the
// publisher (paint / scene-transition code), turning a local bug into a
// stuck flow.
foreach (var handler in action.GetInvocationList())
{
try
{
((Action<T>)handler).Invoke(evt);
}
catch (Exception e)
{
UnityEngine.Debug.LogException(e);
}
}
}
}

View File

@@ -350,7 +350,9 @@ namespace Darkmatter.Services.Audio
if (!source.gameObject.activeInHierarchy) source.gameObject.SetActive(true);
source.Play();
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.Log($"[AudioService] Play clip='{(request.Clip != null ? request.Clip.name : "null")}' ch={request.Channel} vol={source.volume:F2} mixer={(source.outputAudioMixerGroup != null ? source.outputAudioMixerGroup.name : "none")} listener={(AudioListener.volume)} muted={AudioListener.pause} isPlaying={source.isPlaying}");
#endif
if (IsChannelPaused(request.Channel))
{
source.Pause();

View File

@@ -72,14 +72,14 @@ namespace Darkmatter.Services.Capture
// GetPixels/SetPixels cropping allocated a managed Color[] at 16 bytes/pixel (tens of MB)
// 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;
captureViewScope?.Dispose();
captureViewScope = null;
try
{
cropped.ReadPixels(new Rect(crop.x, crop.y, cropW, cropH), 0, 0);
cropped.Apply();
RenderTexture.active = prevActive;
captureViewScope?.Dispose();
captureViewScope = null;
int targetW = Mathf.Max(1, Mathf.RoundToInt(cropW * scale));
int targetH = Mathf.Max(1, Mathf.RoundToInt(cropH * scale));
Texture2D output = cropped;
@@ -97,17 +97,29 @@ namespace Darkmatter.Services.Capture
}
finally
{
RenderTexture.active = prevActive;
Object.Destroy(cropped);
}
}
finally
{
paperCanvas.renderMode = prevMode;
paperCanvas.worldCamera = prevWorldCam;
paperCanvas.planeDistance = prevPlaneDist;
cam.targetTexture = prevTarget;
cam.clearFlags = prevFlags;
cam.backgroundColor = prevBg;
// The awaits above give scene teardown (autosave racing an unload) a chance to
// destroy the canvas or camera; touching them then would throw from finally and
// skip the rest of the restore.
if (paperCanvas != null)
{
paperCanvas.renderMode = prevMode;
paperCanvas.worldCamera = prevWorldCam;
paperCanvas.planeDistance = prevPlaneDist;
}
if (cam != null)
{
cam.targetTexture = prevTarget;
cam.clearFlags = prevFlags;
cam.backgroundColor = prevBg;
}
RenderTexture.ReleaseTemporary(rt);
if (disabledGraphics != null)
foreach (var g in disabledGraphics)

View File

@@ -118,8 +118,17 @@ namespace Darkmatter.Services.Gallery
GL.PopMatrix();
var output = new Texture2D(bgW, bgH, TextureFormat.RGBA32, mipChain: false);
output.ReadPixels(new Rect(0, 0, bgW, bgH), 0, 0);
output.Apply();
try
{
output.ReadPixels(new Rect(0, 0, bgW, bgH), 0, 0);
output.Apply();
}
catch
{
UnityEngine.Object.Destroy(output);
throw;
}
return output;
}
finally

Binary file not shown.

Binary file not shown.

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

View File

@@ -23,7 +23,7 @@ MonoBehaviour:
m_RequireOpaqueTexture: 0
m_OpaqueDownsampling: 1
m_SupportsTerrainHoles: 1
m_SupportsHDR: 1
m_SupportsHDR: 0
m_HDRColorBufferPrecision: 0
m_MSAA: 1
m_RenderScale: 1
@@ -42,7 +42,7 @@ MonoBehaviour:
m_SupportProbeVolumeScenarioBlending: 0
m_ProbeVolumeSHBands: 1
m_MainLightRenderingMode: 1
m_MainLightShadowsSupported: 1
m_MainLightShadowsSupported: 0
m_MainLightShadowmapResolution: 2048
m_AdditionalLightsRenderingMode: 1
m_AdditionalLightsPerObjectLimit: 4
@@ -99,7 +99,7 @@ MonoBehaviour:
obsoleteHasProbeVolumes:
m_Keys: []
m_Values:
m_PrefilteringModeMainLightShadows: 4
m_PrefilteringModeMainLightShadows: 0
m_PrefilteringModeAdditionalLight: 4
m_PrefilteringModeAdditionalLightShadows: 0
m_PrefilterXRKeywords: 1

View File

@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?><resources><string name="com.crashlytics.android.build_id" translatable="false">8fc48ed6-41de-4dcf-a980-9aa3316fad11</string></resources>
<?xml version="1.0" encoding="utf-8"?><resources><string name="com.crashlytics.android.build_id" translatable="false">902fe897-8c3c-4178-afb3-844ca2e340bb</string></resources>

View File

@@ -0,0 +1,6 @@
package com.google.android.play.core.missingsplits;
import android.app.Application;
public class MissingSplitsManagingApplication extends Application {
}

View File

@@ -0,0 +1,26 @@
fileFormatVersion: 2
guid: 23c90ef77b914a42a239857d3e9386d0
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Android:
enabled: 1
settings: {}
Any:
enabled: 0
settings: {}
Editor:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,70 @@
apply plugin: 'com.android.application'
apply from: 'setupSymbols.gradle'
apply from: '../shared/keepUnitySymbols.gradle'
apply from: '../shared/common.gradle'
dependencies {
implementation project(':unityLibrary')
}
android {
namespace "**NAMESPACE**"
ndkPath "**NDKPATH**"
ndkVersion "**NDKVERSION**"
compileSdk **APIVERSION**
buildToolsVersion "**BUILDTOOLS**"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
defaultConfig {
minSdk **MINSDK**
targetSdk **TARGETSDK**
applicationId '**APPLICATIONID**'
ndk {
abiFilters **ABIFILTERS**
debugSymbolLevel **DEBUGSYMBOLLEVEL**
}
versionCode **VERSIONCODE**
versionName '**VERSIONNAME**'
}
androidResources {
noCompress = **BUILTIN_NOCOMPRESS** + unityStreamingAssets.tokenize(', ')
ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~"
}**SIGN**
lint {
abortOnError false
}
buildTypes {
debug {
minifyEnabled **MINIFY_DEBUG**
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')**SIGNCONFIG**
}
release {
minifyEnabled **MINIFY_RELEASE**
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')**SIGNCONFIG**
}
}**PACKAGING****PLAY_ASSET_PACKS****SPLITS**
**BUILT_APK_LOCATION**
bundle {
language {
enableSplit false
}
density {
enableSplit false
}
abi {
enableSplit false
}
texture {
enableSplit false
}
}
**GOOGLE_PLAY_DEPENDENCIES**
}**SPLITS_VERSION_CODE****LAUNCHER_SOURCE_BUILD_SETUP**

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 93290f6ffc554b9589f42bc71d39e40b
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -13,4 +13,4 @@ MonoBehaviour:
m_Name: SmartlookSettings
m_EditorClassIdentifier:
ProjectKey: 3bcfb098c5c4f6f1f48d8bbef4c0c94d7c2f9bc9
FPS: 30
FPS: 2

View File

@@ -11,7 +11,7 @@ PlayerSettings:
defaultScreenOrientation: 4
targetDevice: 2
useOnDemandResources: 0
accelerometerFrequency: 60
accelerometerFrequency: 0
companyName: Darkmatter Game Production
productName: Kidsage Colorbook
defaultCursor: {fileID: 0}
@@ -148,7 +148,7 @@ PlayerSettings:
loadStoreDebugModeEnabled: 0
visionOSBundleVersion: 1.0
tvOSBundleVersion: 1.0
bundleVersion: 2.0
bundleVersion: 2.2
preloadedAssets:
- {fileID: 11400000, guid: cc969dbecb228fa49b16da9273753a8f, type: 2}
- {fileID: -944628639613478452, guid: 2bcd2660ca9b64942af0de543d8d7100, type: 3}
@@ -180,7 +180,7 @@ PlayerSettings:
iPhone: 1
tvOS: 0
overrideDefaultApplicationIdentifier: 1
AndroidBundleVersionCode: 9
AndroidBundleVersionCode: 12
AndroidMinSdkVersion: 26
AndroidTargetSdkVersion: 0
AndroidPreferredInstallLocation: 1
@@ -268,7 +268,7 @@ PlayerSettings:
useCustomMainManifest: 1
useCustomLauncherManifest: 0
useCustomMainGradleTemplate: 1
useCustomLauncherGradleManifest: 0
useCustomLauncherGradleManifest: 1
useCustomBaseGradleTemplate: 0
useCustomGradlePropertiesTemplate: 1
useCustomGradleSettingsTemplate: 1