Merge remote-tracking branch 'origin/savya' into work_branch

This commit is contained in:
Mausham
2026-06-07 16:09:47 +05:45
6 changed files with 389 additions and 102 deletions

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

@@ -27,12 +27,18 @@ 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 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;
// Coloring book is a child-directed app, so default to true. SetConsent can still // Coloring book is a child-directed app, so default to true. SetConsent can still
// override if a consent flow later supplies a different value. // override if a consent flow later supplies a different value.
@@ -189,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);
@@ -197,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);

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

@@ -759,7 +759,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0} m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0} m_AnchorMax: {x: 0.5, y: 0}
m_AnchoredPosition: {x: 180.83, y: 121} m_AnchoredPosition: {x: 227, y: 189}
m_SizeDelta: {x: 416.99, y: 163} m_SizeDelta: {x: 416.99, y: 163}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &176415042 --- !u!114 &176415042
@@ -1039,7 +1039,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0} m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0} m_AnchorMax: {x: 0.5, y: 0}
m_AnchoredPosition: {x: -188, y: 123} m_AnchoredPosition: {x: -251, y: 191}
m_SizeDelta: {x: 452.9619, y: 188} m_SizeDelta: {x: 452.9619, y: 188}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &281513991 --- !u!114 &281513991
@@ -2411,6 +2411,7 @@ MonoBehaviour:
reloadDelaySeconds: 5 reloadDelaySeconds: 5
reloadMaxAttempts: 6 reloadMaxAttempts: 6
showWatchdogSeconds: 60 showWatchdogSeconds: 60
maxInterstitialsPerSession: 8
--- !u!1 &1173091301 --- !u!1 &1173091301
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -2445,8 +2446,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: 1, y: 1} m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: -27} m_AnchoredPosition: {x: 0, y: -80}
m_SizeDelta: {x: -100, y: 250} m_SizeDelta: {x: -245.3866, y: 250}
m_Pivot: {x: 0.5, y: 1} m_Pivot: {x: 0.5, y: 1}
--- !u!114 &1173091303 --- !u!114 &1173091303
MonoBehaviour: MonoBehaviour:
@@ -3405,7 +3406,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: 812.0266, y: 475.6666} m_SizeDelta: {x: 1258.6208, y: 649.8453}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1770001639 --- !u!114 &1770001639
MonoBehaviour: MonoBehaviour:
@@ -3427,7 +3428,7 @@ MonoBehaviour:
m_OnCullStateChanged: m_OnCullStateChanged:
m_PersistentCalls: m_PersistentCalls:
m_Calls: [] m_Calls: []
m_Sprite: {fileID: 21300000, guid: 9e71c03172e6fbc4695437390c3f102d, type: 3} m_Sprite: {fileID: 21300000, guid: 0495ef6331178714baeafb47f946f3d9, type: 3}
m_Type: 1 m_Type: 1
m_PreserveAspect: 0 m_PreserveAspect: 0
m_FillCenter: 1 m_FillCenter: 1