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

This commit is contained in:
Mausham
2026-05-28 13:39:01 +05:45
39 changed files with 1092 additions and 266 deletions

View File

@@ -10,8 +10,8 @@ namespace Darkmatter.Core.Data.Dynamic.Features.Progression
{
public string templateId;
public DrawingPhase phase;
public List<string> SnappedPieces;
public List<RegionColorEntry> RegionColors;
public List<string> snappedPieces;
public List<RegionColorEntry> regionColors;
public bool hasThumbnail;
public bool hasBeenCompleted;
@@ -26,8 +26,8 @@ namespace Darkmatter.Core.Data.Dynamic.Features.Progression
{
this.templateId = templateId;
this.phase = phase;
SnappedPieces = snappedPieces;
RegionColors = regionColors;
this.snappedPieces = snappedPieces;
this.regionColors = regionColors;
this.hasThumbnail = hasThumbnail;
this.hasBeenCompleted = hasBeenCompleted;
this.completionCount = completionCount;

View File

@@ -1,4 +1,5 @@
using System;
using UnityEngine;
namespace Darkmatter.Core.Data.Dynamic.Features.Progression
{
@@ -6,6 +7,6 @@ namespace Darkmatter.Core.Data.Dynamic.Features.Progression
public struct RegionColorEntry
{
public string regionId;
public float r, g, b, a;
public Color color;
}
}

View File

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

View File

@@ -0,0 +1,6 @@
using UnityEngine;
namespace Darkmatter.Core.Data.Signals.Features.Coloring
{
public record struct ColorAppliedSignal(string RegionId, Color Color);
}

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
namespace Darkmatter.Core.Data.Signals.Features.ShapeBuilder
{
public record struct PieceSnappedSignal(string PieceId);
}

View File

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

View File

@@ -0,0 +1,4 @@
namespace Darkmatter.Core.Data.Signals.Features.ShapeBuilder
{
public record struct PieceUnsnappedSignal(string PieceId);
}

View File

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

View File

@@ -0,0 +1,6 @@
using UnityEngine;
namespace Darkmatter.Core.Data.Signals.Features.ShapeBuilder
{
public record struct ShapeAssembledSignal(string TemplateId);
}

View File

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

View File

@@ -0,0 +1,30 @@
using UnityEngine;
namespace Darkmatter.Core.Data.Static.Features.ShapeBuilder
{
[CreateAssetMenu(fileName = "ShapeBuilderConfig",
menuName = "Darkmatter/ShapeBuilder/Config")]
public sealed class ShapeBuilderConfig : ScriptableObject
{
[Header("Radii (canvas units; reference resolution 2048x2048)")]
[SerializeField] private float snapRadius = 100f;
[SerializeField] private float snapGraceMultiplier = 1.5f;
[SerializeField] private float previewRadius = 200f;
[Header("Tween durations (seconds)")]
[SerializeField] private float snapDuration = 0.25f;
[SerializeField] private float returnDuration = 0.25f;
[Header("Preview easing")]
[SerializeField] private AnimationCurve previewCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
public float SnapRadius => snapRadius;
public float PreviewRadius => previewRadius;
public float SnapDuration => snapDuration;
public float ReturnDuration => returnDuration;
public AnimationCurve PreviewCurve => previewCurve;
public Vector2 DragSizeDelta(ShapeSO shape) =>
shape != null ? shape.DefaultSizeDelta : new Vector2(256, 256);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 52d6fdba64cc3491880636e34ed593d0

View File

@@ -7,5 +7,6 @@ namespace Darkmatter.Core.Data.Static.Features.ShapeBuilder
{
[field: SerializeField] public string Id { get; private set; }
[field: SerializeField] public Sprite Sprite { get; private set; }
[field: SerializeField] public Vector2 DefaultSizeDelta { get; private set; } = new(256, 256);
}
}

View File

@@ -3,14 +3,8 @@ namespace Darkmatter.Core.Enums.Services.Audio
public enum SfxId
{
None = 0,
WiperUp = 100,
WiperDown = 101,
BlinkerTick = 200,
GearShift = 300,
ReverseBeep = 400,
ShapeHover = 100,
ShapeSnap = 101,
ShapeReturn = 102,
}
}

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
using Darkmatter.Core.Contracts.Features.History;
using Darkmatter.Features.ShapeBuilder.UI;
using UnityEngine;
namespace Darkmatter.Features.ShapeBuilder.Commands
{
/// <summary>
/// Undoable snap. Captures the piece's pre-snap pose + parent at ctor time;
/// Execute drives the normal snap tween; Undo restores the pre-snap state
/// and re-enables drag.
/// </summary>
internal sealed class SnapPieceCommand : ICommand
{
private readonly ShapePiece _piece;
private readonly Vector2 _prevPos;
private readonly Vector2 _prevSize;
private readonly Quaternion _prevRot;
private readonly Transform _prevParent;
public SnapPieceCommand(ShapePiece piece)
{
_piece = piece;
var rt = piece.RectTransform;
_prevPos = rt.anchoredPosition;
_prevSize = rt.sizeDelta;
_prevRot = rt.localRotation;
_prevParent = rt.parent;
}
public void Execute() => _piece.SnapInternal();
public void Undo() => _piece.UnsnapInternal(_prevParent, _prevPos, _prevSize, _prevRot);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7565e0dc60eca451c9877bd23dddb901

View File

@@ -0,0 +1,22 @@
{
"name": "Features.ShapeBuilder",
"rootNamespace": "Darkmatter.Features.ShapeBuilder",
"references": [
"GUID:6a0a834eb41764f12ba55c3fb04a40cb",
"GUID:c1c03c0e5b2f4412b9f2be1c20d6a9b1",
"GUID:c176ee863a5e74e88a6517f9f102cf92",
"GUID:b4c9f7fbf1e144933a1797dc208ece5f",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:f51ebe6a0ceec4240a699833d6309b23",
"GUID:80ecb87cae9c44d19824e70ea7229748"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
using Darkmatter.Core.Data.Static.Features.ShapeBuilder;
using Darkmatter.Libs.Installers;
using UnityEngine;
using VContainer;
namespace Darkmatter.Features.ShapeBuilder.Installers
{
public class ShapeBuilderFeatureModule : MonoBehaviour, IModule
{
[SerializeField] private ShapeBuilderConfig config;
public void Register(IContainerBuilder builder)
{
if (config != null)
builder.RegisterInstance(config);
// ShapePiece instances are MonoBehaviours instantiated by the
// ShapeBuilderController; nothing to register here for them.
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,203 @@
using Darkmatter.Core.Contracts.Features.History;
using Darkmatter.Core.Contracts.Services.Audio;
using Darkmatter.Core.Data.Signals.Features.ShapeBuilder;
using Darkmatter.Core.Data.Static.Features.ShapeBuilder;
using Darkmatter.Core.Enums.Services.Audio;
using Darkmatter.Features.ShapeBuilder.Commands;
using Darkmatter.Libs.Observer;
using PrimeTween;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace Darkmatter.Features.ShapeBuilder.UI
{
/// <summary>
/// Single-MB shape piece. Handles drag, reactive preview lerp, snap,
/// and return-to-tray. Snap is wrapped in a <see cref="SnapPieceCommand"/>
/// pushed onto the shared <see cref="IUndoStack"/> so it participates in
/// the same undo/redo flow as coloring.
/// </summary>
[RequireComponent(typeof(RectTransform))]
public sealed class ShapePiece : MonoBehaviour,
IBeginDragHandler, IDragHandler, IEndDragHandler
{
[SerializeField] private Image image;
// Bound by Setup
private ShapeSO _shape;
private SlotMarker _slot;
private ShapeBuilderConfig _cfg;
private ISfxPlayer _sfx;
private IEventBus _bus;
private IUndoStack _undo;
private Vector2 _trayPos;
private Vector2 _traySize;
// Per-drag state
private RectTransform _rt;
private RectTransform _parentRect;
private Camera _eventCam;
private Vector2 _grabOffset;
private bool _locked;
private bool _inPreview;
public ShapeSO Shape => _shape;
public string PieceId => _shape != null ? _shape.Id : null;
public bool IsLocked => _locked;
public RectTransform RectTransform => _rt != null ? _rt : (_rt = (RectTransform)transform);
public void Setup(
ShapeSO shape,
SlotMarker slot,
ShapeBuilderConfig cfg,
ISfxPlayer sfx,
IEventBus bus,
IUndoStack undo,
Vector2 trayPos,
bool preSnapped)
{
_shape = shape;
_slot = slot;
_cfg = cfg;
_sfx = sfx;
_bus = bus;
_undo = undo;
_trayPos = trayPos;
_traySize = shape.DefaultSizeDelta;
image.sprite = shape.Sprite;
ApplyTrayPose();
if (preSnapped) SnapInstantly();
}
public void OnBeginDrag(PointerEventData e)
{
if (_locked) return;
_parentRect = (RectTransform)RectTransform.parent;
_eventCam = e.pressEventCamera;
_grabOffset = RectTransform.anchoredPosition - ScreenToLocal(e.position);
_inPreview = false;
}
public void OnDrag(PointerEventData e)
{
if (_locked) return;
var pointerLocal = ScreenToLocal(e.position) + _grabOffset;
var slotPos = _slot.RectTransform.anchoredPosition;
float dist = Vector2.Distance(pointerLocal, slotPos);
if (dist <= _cfg.PreviewRadius)
{
if (!_inPreview)
{
_inPreview = true;
_sfx.Play(SfxId.ShapeHover);
}
ApplyPreviewLerp(pointerLocal, dist);
}
else
{
_inPreview = false;
RectTransform.anchoredPosition = pointerLocal;
RectTransform.sizeDelta = _traySize;
RectTransform.localRotation = Quaternion.identity;
}
}
public void OnEndDrag(PointerEventData e)
{
if (_locked) return;
float dist = Vector2.Distance(
RectTransform.anchoredPosition,
_slot.RectTransform.anchoredPosition);
if (dist <= _cfg.SnapRadius)
_undo.Push(new SnapPieceCommand(this)); // Push calls Execute → SnapInternal
else
ReturnToTray();
}
private void ApplyPreviewLerp(Vector2 pointerLocal, float dist)
{
float t = Mathf.Clamp01(1f - dist / _cfg.PreviewRadius);
if (_cfg.PreviewCurve != null) t = _cfg.PreviewCurve.Evaluate(t);
var slot = _slot.RectTransform;
RectTransform.anchoredPosition = Vector2.Lerp(pointerLocal, slot.anchoredPosition, t);
RectTransform.sizeDelta = Vector2.Lerp(_traySize, slot.sizeDelta, t);
RectTransform.localRotation = Quaternion.Slerp(Quaternion.identity, slot.localRotation, t);
}
// ---- Command-driven snap / un-snap ----
internal void SnapInternal()
{
Lock();
var slot = _slot.RectTransform;
Tween.UIAnchoredPosition(RectTransform, slot.anchoredPosition, _cfg.SnapDuration, Ease.OutBack);
Tween.UISizeDelta (RectTransform, slot.sizeDelta, _cfg.SnapDuration, Ease.OutBack);
Tween.LocalRotation (RectTransform, slot.localRotation, _cfg.SnapDuration, Ease.OutBack);
_sfx.Play(SfxId.ShapeSnap);
_bus.Publish(new PieceSnappedSignal(_shape.Id));
}
internal void UnsnapInternal(Transform parent, Vector2 pos, Vector2 size, Quaternion rot)
{
_locked = false;
image.raycastTarget = true;
RectTransform.SetParent(parent, worldPositionStays: false);
Tween.UIAnchoredPosition(RectTransform, pos, _cfg.ReturnDuration, Ease.OutQuad);
Tween.UISizeDelta (RectTransform, size, _cfg.ReturnDuration, Ease.OutQuad);
Tween.LocalRotation (RectTransform, rot, _cfg.ReturnDuration, Ease.OutQuad);
_sfx.Play(SfxId.ShapeReturn);
_bus.Publish(new PieceUnsnappedSignal(_shape.Id));
}
private void SnapInstantly()
{
Lock();
var slot = _slot.RectTransform;
RectTransform.anchoredPosition = slot.anchoredPosition;
RectTransform.sizeDelta = slot.sizeDelta;
RectTransform.localRotation = slot.localRotation;
}
private void ReturnToTray()
{
_sfx.Play(SfxId.ShapeReturn);
Sequence.Create()
.Group(Tween.UIAnchoredPosition(RectTransform, _trayPos, _cfg.ReturnDuration, Ease.OutQuad))
.Group(Tween.UISizeDelta (RectTransform, _traySize, _cfg.ReturnDuration, Ease.OutQuad))
.Group(Tween.LocalRotation (RectTransform, Quaternion.identity, _cfg.ReturnDuration, Ease.OutQuad));
}
private void Lock()
{
_locked = true;
image.raycastTarget = false;
RectTransform.SetParent(_slot.RectTransform.parent, worldPositionStays: false);
}
private void ApplyTrayPose()
{
RectTransform.anchoredPosition = _trayPos;
RectTransform.sizeDelta = _traySize;
RectTransform.localRotation = Quaternion.identity;
}
private Vector2 ScreenToLocal(Vector2 screenPos)
{
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_parentRect, screenPos, _eventCam, out var local);
return local;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 189fa0409d676438abe96e8707e29ad0

View File

@@ -0,0 +1,21 @@
using Darkmatter.Core.Data.Static.Features.ShapeBuilder;
using UnityEngine;
using UnityEngine.UI;
namespace Darkmatter.Features.ShapeBuilder.UI
{
public sealed class SlotMarker : MonoBehaviour
{
[SerializeField] private ShapeSO shape;
[SerializeField] private Image outline;
public ShapeSO Shape => shape;
public string SlotId => shape != null ? shape.Id : null;
public RectTransform RectTransform => (RectTransform)transform;
public void SetOutlineVisible(bool visible)
{
if (outline != null) outline.enabled = visible;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5d79b18d536324085b58d842648372a8