Basic Setup For Shape Builder
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Darkmatter.Core.Data.Signals.Features.ShapeBuilder
|
||||
{
|
||||
public record struct PieceSnappedSignal(string PieceId);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4a88ca1eb07044daa9fd7a15c3dcc19
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Darkmatter.Core.Data.Signals.Features.ShapeBuilder
|
||||
{
|
||||
public record struct PieceUnsnappedSignal(string PieceId);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d16116e16ea0a40b9afdc4765ba8881c
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52d6fdba64cc3491880636e34ed593d0
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/Darkmatter/Code/Features/ShapeBuilder.meta
Normal file
8
Assets/Darkmatter/Code/Features/ShapeBuilder.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c12a68c64c45647dab002508c06ec8cd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32ae4b246e57f4379baa7b98cffeb8c2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7565e0dc60eca451c9877bd23dddb901
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ca8c3a66565544118d3d52d3922933b
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8f128ba2fede4ab2971c98f4f83819a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6323fd2530234772b15e4b2d169df47
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 679f7a317a1b7445aaa719055bca4a6d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Darkmatter/Code/Features/ShapeBuilder/UI.meta
Normal file
8
Assets/Darkmatter/Code/Features/ShapeBuilder/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cdbdbbcc180884694930e5ce1dbbb7d4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
203
Assets/Darkmatter/Code/Features/ShapeBuilder/UI/ShapePiece.cs
Normal file
203
Assets/Darkmatter/Code/Features/ShapeBuilder/UI/ShapePiece.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 189fa0409d676438abe96e8707e29ad0
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d79b18d536324085b58d842648372a8
|
||||
Reference in New Issue
Block a user