diff --git a/Assets/Darkmatter/Code/Features/DrawingTemplate/Editor/DrawingTemplateEditorWindow.cs b/Assets/Darkmatter/Code/Features/DrawingTemplate/Editor/DrawingTemplateEditorWindow.cs index cbd616b..8818609 100644 --- a/Assets/Darkmatter/Code/Features/DrawingTemplate/Editor/DrawingTemplateEditorWindow.cs +++ b/Assets/Darkmatter/Code/Features/DrawingTemplate/Editor/DrawingTemplateEditorWindow.cs @@ -328,7 +328,7 @@ namespace Darkmatter.Features.DrawingTemplates.Editor foreach (var m in markers) { if (m.Shape == null) { missing++; continue; } - if (!pieces.Contains(m.Shape)) pieces.Add(m.Shape); + pieces.Add(m.Shape); } Undo.RecordObject(t, "Scan Drawing Prefab"); t.EditorSet(t.Id, t.DisplayName, t.DefaultThumbnail, t.DrawingPrefab, t.ColoringPrefab, diff --git a/Assets/Darkmatter/Code/Features/ShapeBuilder/Commands/SnapPieceCommand.cs b/Assets/Darkmatter/Code/Features/ShapeBuilder/Commands/SnapPieceCommand.cs index 5de1b1e..d5758b2 100644 --- a/Assets/Darkmatter/Code/Features/ShapeBuilder/Commands/SnapPieceCommand.cs +++ b/Assets/Darkmatter/Code/Features/ShapeBuilder/Commands/SnapPieceCommand.cs @@ -12,6 +12,7 @@ namespace Darkmatter.Features.ShapeBuilder.Commands private readonly Quaternion _prevRot; private readonly Transform _prevParent; private readonly int _prevSiblingIndex; + private SlotMarker _snappedSlot; public SnapPieceCommand(ShapePiece piece) { @@ -23,7 +24,12 @@ namespace Darkmatter.Features.ShapeBuilder.Commands _prevRot = Quaternion.identity; } - public void Execute() => _piece.SnapInternal(); + public void Execute() + { + if (_snappedSlot != null) _piece.ReassignActiveSlot(_snappedSlot); + _piece.SnapInternal(); + if (_snappedSlot == null) _snappedSlot = _piece.ActiveSlot; + } public void Undo() => _piece.UnsnapInternal(_prevParent, _prevSiblingIndex, _prevPos, _prevSize, _prevRot); } diff --git a/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/IShapePieceFactory.cs b/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/IShapePieceFactory.cs index 649e8ae..2762b83 100644 --- a/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/IShapePieceFactory.cs +++ b/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/IShapePieceFactory.cs @@ -6,6 +6,6 @@ namespace Darkmatter.Features.ShapeBuilder.Systems { public interface IShapePieceFactory { - ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos); + ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker[] candidateSlots, Vector2 trayPos); } } diff --git a/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/ShapeBuilderController.cs b/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/ShapeBuilderController.cs index 337c8c8..7abbb84 100644 --- a/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/ShapeBuilderController.cs +++ b/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/ShapeBuilderController.cs @@ -117,28 +117,40 @@ namespace Darkmatter.Features.ShapeBuilder.Systems int count, SlotMarker[] slots, float pitch, float trayW) { + var preSnapCounts = new Dictionary(); + if (preSnappedIds != null) + foreach (var id in preSnappedIds) + { + if (string.IsNullOrEmpty(id)) continue; + preSnapCounts[id] = preSnapCounts.GetValueOrDefault(id) + 1; + } + for (int i = 0; i < count; i++) { var shape = template.Pieces[i]; - var slot = FindSlotForShape(slots, shape); - if (slot == null) + var candidates = FindSlotsForShape(slots, shape); + if (candidates.Length == 0) { Debug.LogError($"[ShapeBuilder] No SlotMarker for '{shape.Id}' in '{template.Id}'"); continue; } var trayPos = new Vector2(pitch * (i + 1) - trayW * 0.5f, 0f); - bool preSnapped = preSnappedIds != null && preSnappedIds.Contains(shape.Id); - - var piece = _factory.Create(_piecePrefab, shape, slot, trayPos); + var piece = _factory.Create(_piecePrefab, shape, candidates, trayPos); _pieces.Add(piece); - if (preSnapped) + if (preSnapCounts.TryGetValue(shape.Id, out var remaining) && remaining > 0) { - _undo.Append(new SnapPieceCommand(piece)); - piece.SnapInstantly(); - _snapped++; - _snappedPieceIds.Add(shape.Id); + var unoccupied = FindFirstUnoccupied(candidates); + if (unoccupied != null) + { + _undo.Append(new SnapPieceCommand(piece)); + piece.SnapInstantlyTo(unoccupied); + unoccupied.SetOccupied(true); + _snapped++; + _snappedPieceIds.Add(shape.Id); + preSnapCounts[shape.Id] = remaining - 1; + } } } } @@ -160,10 +172,19 @@ namespace Darkmatter.Features.ShapeBuilder.Systems } } - private static SlotMarker FindSlotForShape(SlotMarker[] slots, ShapeSO shape) + private static SlotMarker[] FindSlotsForShape(SlotMarker[] slots, ShapeSO shape) + { + var list = new List(); + foreach (var s in slots) + if (s != null && s.Shape == shape) + list.Add(s); + return list.ToArray(); + } + + private static SlotMarker FindFirstUnoccupied(SlotMarker[] slots) { foreach (var s in slots) - if (s.Shape == shape) + if (s != null && !s.IsOccupied) return s; return null; } diff --git a/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/ShapePieceFactory.cs b/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/ShapePieceFactory.cs index 49c77e1..4c14063 100644 --- a/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/ShapePieceFactory.cs +++ b/Assets/Darkmatter/Code/Features/ShapeBuilder/Systems/ShapePieceFactory.cs @@ -33,13 +33,13 @@ namespace Darkmatter.Features.ShapeBuilder.Systems _refs = refs; } - public ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker slot, Vector2 trayPos) + public ShapePiece Create(GameObject prefab, ShapeSO shape, SlotMarker[] candidateSlots, Vector2 trayPos) { var go = Object.Instantiate(prefab, _holder.SpawnRoot); go.name = $"Piece_{shape.Id}"; var piece = go.GetComponent(); - piece.Setup(shape, slot, _cfg, _sfx, _bus, _undo, trayPos, _refs.PaperRoot); + piece.Setup(shape, candidateSlots, _cfg, _sfx, _bus, _undo, trayPos, _refs.PaperRoot); return piece; } } diff --git a/Assets/Darkmatter/Code/Features/ShapeBuilder/UI/ShapePiece.cs b/Assets/Darkmatter/Code/Features/ShapeBuilder/UI/ShapePiece.cs index 662b6c4..bdc778c 100644 --- a/Assets/Darkmatter/Code/Features/ShapeBuilder/UI/ShapePiece.cs +++ b/Assets/Darkmatter/Code/Features/ShapeBuilder/UI/ShapePiece.cs @@ -20,7 +20,8 @@ namespace Darkmatter.Features.ShapeBuilder.UI // Bound by Setup private ShapeSO _shape; - private SlotMarker _slot; + private SlotMarker[] _candidateSlots; + private SlotMarker _activeSlot; private ShapeBuilderConfig _cfg; private ISfxPlayer _sfx; private IEventBus _bus; @@ -30,6 +31,9 @@ namespace Darkmatter.Features.ShapeBuilder.UI private RectTransform _dragRoot; private Transform _homeParent; private int _homeSiblingIndex; + private Vector2 _origAnchorMin; + private Vector2 _origAnchorMax; + private Vector2 _origPivot; // Per-drag state private RectTransform _rt; @@ -51,10 +55,13 @@ namespace Darkmatter.Features.ShapeBuilder.UI public int HomeSiblingIndex => _homeSiblingIndex; public Vector2 TrayPosition => _trayPos; public Vector2 TraySize => _traySize; + public SlotMarker ActiveSlot => _activeSlot; + + public void ReassignActiveSlot(SlotMarker slot) => _activeSlot = slot; public void Setup( ShapeSO shape, - SlotMarker slot, + SlotMarker[] candidateSlots, ShapeBuilderConfig cfg, ISfxPlayer sfx, IEventBus bus, @@ -62,9 +69,10 @@ namespace Darkmatter.Features.ShapeBuilder.UI Vector2 trayPos, RectTransform dragRoot) { - _shape = shape; - _slot = slot; - _cfg = cfg; + _shape = shape; + _candidateSlots = candidateSlots; + _activeSlot = null; + _cfg = cfg; _sfx = sfx; _bus = bus; _undo = undo; @@ -74,6 +82,9 @@ namespace Darkmatter.Features.ShapeBuilder.UI _homeParent = RectTransform.parent; _homeSiblingIndex = RectTransform.GetSiblingIndex(); + _origAnchorMin = RectTransform.anchorMin; + _origAnchorMax = RectTransform.anchorMax; + _origPivot = RectTransform.pivot; image.sprite = shape.Sprite; ApplyTrayPose(); @@ -103,17 +114,25 @@ namespace Darkmatter.Features.ShapeBuilder.UI if (_locked) return; var pointerLocal = ScreenToLocal(e.position) + _grabOffset; - bool insidePreview = IsOverSlot(e.position); + var hovered = FindSlotUnder(e.position); + bool insidePreview = hovered != null; if (insidePreview && !_inPreview) { + _activeSlot = hovered; _inPreview = true; _sfx.Play(SfxId.ShapeHover); AnimatePreviewPose(toSlot: true); } + else if (insidePreview && _inPreview && hovered != _activeSlot) + { + _activeSlot = hovered; + AnimatePreviewPose(toSlot: true); + } else if (!insidePreview && _inPreview) { _inPreview = false; + _activeSlot = null; AnimatePreviewPose(toSlot: false); } @@ -125,25 +144,39 @@ namespace Darkmatter.Features.ShapeBuilder.UI { if (_locked) return; - if (IsOverSlot(e.position)) + var target = FindSlotUnder(e.position); + if (target != null) + { + _activeSlot = target; _undo.Push(new SnapPieceCommand(this)); + } else + { + _activeSlot = null; ReturnToTray(); + } } - private bool IsOverSlot(Vector2 screenPos) + private SlotMarker FindSlotUnder(Vector2 screenPos) { - return RectTransformUtility.RectangleContainsScreenPoint( - _slot.RectTransform, screenPos, _eventCam); + if (_candidateSlots == null) return null; + foreach (var s in _candidateSlots) + { + if (s == null) continue; + if (s.IsOccupied && s != _activeSlot) continue; + if (RectTransformUtility.RectangleContainsScreenPoint(s.RectTransform, screenPos, _eventCam)) + return s; + } + return null; } private void AnimatePreviewPose(bool toSlot) { if (_previewSeq.isAlive) _previewSeq.Stop(); - if (toSlot) + if (toSlot && _activeSlot != null) { - var slot = _slot.RectTransform; + var slot = _activeSlot.RectTransform; _previewSeq = Sequence.Create() .Group(Tween.UIAnchoredPosition(RectTransform, SlotPosInDragSpace(), _cfg.SnapDuration, Ease.OutQuad)) .Group(Tween.LocalScale(RectTransform, SlotScaleInDragSpace(), _cfg.SnapDuration, Ease.OutQuad)) @@ -161,7 +194,7 @@ namespace Darkmatter.Features.ShapeBuilder.UI private Vector2 SlotPosInDragSpace() { - Vector3 worldPos = _slot.RectTransform.position; + Vector3 worldPos = _activeSlot.RectTransform.position; Vector3 local = _parentRect.InverseTransformPoint(worldPos); Vector2 parentSize = _parentRect.rect.size; Vector2 anchorRef = (RectTransform.anchorMin - _parentRect.pivot) * parentSize; @@ -177,13 +210,13 @@ namespace Darkmatter.Features.ShapeBuilder.UI private Quaternion SlotRotInDragSpace() { - return Quaternion.Inverse(_parentRect.rotation) * _slot.RectTransform.rotation; + return Quaternion.Inverse(_parentRect.rotation) * _activeSlot.RectTransform.rotation; } private Vector3 SlotScaleInDragSpace() { Vector3 parentLossy = _parentRect.lossyScale; - Vector3 slotLossy = _slot.RectTransform.lossyScale; + Vector3 slotLossy = _activeSlot.RectTransform.lossyScale; return new Vector3( slotLossy.x / Mathf.Max(0.0001f, parentLossy.x), slotLossy.y / Mathf.Max(0.0001f, parentLossy.y), @@ -194,27 +227,49 @@ namespace Darkmatter.Features.ShapeBuilder.UI { StopPreviewTweens(); Lock(); - var slot = _slot.RectTransform; + var slot = _activeSlot.RectTransform; - Tween.Position(RectTransform, slot.position, _cfg.SnapDuration, Ease.OutBack); - Tween.Rotation(RectTransform, slot.rotation, _cfg.SnapDuration, Ease.OutBack); - Tween.LocalScale(RectTransform, slot.localScale, _cfg.SnapDuration, Ease.OutBack); - Tween.UISizeDelta(RectTransform, slot.sizeDelta, _cfg.SnapDuration, Ease.OutBack); + Sequence.Create() + .Group(Tween.Position(RectTransform, slot.position, _cfg.SnapDuration, Ease.OutBack)) + .Group(Tween.Rotation(RectTransform, slot.rotation, _cfg.SnapDuration, Ease.OutBack)) + .Group(Tween.LocalScale(RectTransform, slot.localScale, _cfg.SnapDuration, Ease.OutBack)) + .Group(Tween.UISizeDelta(RectTransform, slot.sizeDelta, _cfg.SnapDuration, Ease.OutBack)) + .ChainCallback(AlignRectToSlot); _sfx.Play(SfxId.ShapeSnap); _bus.Publish(new PieceSnappedSignal(_shape.Id)); } + private void AlignRectToSlot() + { + if (this == null || _activeSlot == null) return; + var rt = RectTransform; + var slot = _activeSlot.RectTransform; + rt.anchorMin = Vector2.zero; + rt.anchorMax = Vector2.one; + rt.pivot = slot.pivot; + rt.anchoredPosition = Vector2.zero; + rt.sizeDelta = Vector2.zero; + rt.localRotation = Quaternion.identity; + rt.localScale = Vector3.one; + } + internal void UnsnapInternal(Transform parent, int siblingIndex, Vector2 pos, Vector2 size, Quaternion rot) { _locked = false; image.raycastTarget = true; + if (_activeSlot != null) _activeSlot.SetOccupied(false); + _activeSlot = null; + Tween.StopAll(onTarget: RectTransform); RectTransform.SetParent(parent, worldPositionStays: false); if (siblingIndex >= 0) RectTransform.SetSiblingIndex(siblingIndex); + RectTransform.anchorMin = _origAnchorMin; + RectTransform.anchorMax = _origAnchorMax; + RectTransform.pivot = _origPivot; RectTransform.anchoredPosition = pos; RectTransform.sizeDelta = size; RectTransform.localRotation = rot; @@ -224,14 +279,11 @@ namespace Darkmatter.Features.ShapeBuilder.UI _bus.Publish(new PieceUnsnappedSignal(_shape.Id)); } - public void SnapInstantly() + public void SnapInstantlyTo(SlotMarker slot) { + _activeSlot = slot; Lock(); - var slot = _slot.RectTransform; - RectTransform.position = slot.position; - RectTransform.rotation = slot.rotation; - RectTransform.localScale = slot.localScale; - RectTransform.sizeDelta = slot.sizeDelta; + AlignRectToSlot(); } private void ReturnToTray() @@ -258,7 +310,11 @@ namespace Darkmatter.Features.ShapeBuilder.UI { _locked = true; image.raycastTarget = false; - RectTransform.SetParent(_slot.RectTransform.parent, worldPositionStays: true); + if (_activeSlot != null) + { + _activeSlot.SetOccupied(true); + RectTransform.SetParent(_activeSlot.RectTransform, worldPositionStays: true); + } } private void ApplyTrayPose() diff --git a/Assets/Darkmatter/Code/Features/ShapeBuilder/UI/SlotMarker.cs b/Assets/Darkmatter/Code/Features/ShapeBuilder/UI/SlotMarker.cs index 6aecf01..076a9d8 100644 --- a/Assets/Darkmatter/Code/Features/ShapeBuilder/UI/SlotMarker.cs +++ b/Assets/Darkmatter/Code/Features/ShapeBuilder/UI/SlotMarker.cs @@ -12,6 +12,9 @@ namespace Darkmatter.Features.ShapeBuilder.UI public ShapeSO Shape => shape; public string SlotId => shape != null ? shape.Id : null; public RectTransform RectTransform => (RectTransform)transform; + public bool IsOccupied { get; private set; } + + public void SetOccupied(bool value) => IsOccupied = value; public void SetOutlineVisible(bool visible) { diff --git a/Assets/Darkmatter/Content/Colorbook UI/Prefabs/SlotMarkers/Square.prefab b/Assets/Darkmatter/Content/Colorbook UI/Prefabs/SlotMarkers/Square.prefab new file mode 100644 index 0000000..7ab94b3 --- /dev/null +++ b/Assets/Darkmatter/Content/Colorbook UI/Prefabs/SlotMarkers/Square.prefab @@ -0,0 +1,92 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &3388453239853753565 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7141417102443485026} + - component: {fileID: 1022797824599580644} + - component: {fileID: 7793443009330116033} + - component: {fileID: 2829602331308694695} + m_Layer: 5 + m_Name: Square + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7141417102443485026 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3388453239853753565} + m_LocalRotation: {x: 0, y: 0, z: 0.7071068, w: 0.7071068} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0.4978243, y: 0.4978243, z: 0.4978243} + m_ConstrainProportionsScale: 1 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 90} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: 170, y: 449} + m_SizeDelta: {x: 375, y: 386} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1022797824599580644 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3388453239853753565} + m_CullTransparentMesh: 1 +--- !u!114 &7793443009330116033 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3388453239853753565} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: c3293fddd457324499e794c843736653, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &2829602331308694695 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3388453239853753565} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5d79b18d536324085b58d842648372a8, type: 3} + m_Name: + m_EditorClassIdentifier: Features.ShapeBuilder::Darkmatter.Features.ShapeBuilder.UI.SlotMarker + shape: {fileID: 11400000, guid: 4a67406eb6fe043628d2b6a4e0c970ba, type: 2} + outline: {fileID: 0} diff --git a/Assets/Darkmatter/Content/Colorbook UI/Prefabs/SlotMarkers/Square.prefab.meta b/Assets/Darkmatter/Content/Colorbook UI/Prefabs/SlotMarkers/Square.prefab.meta new file mode 100644 index 0000000..3d1142d --- /dev/null +++ b/Assets/Darkmatter/Content/Colorbook UI/Prefabs/SlotMarkers/Square.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4d7801ac4730f46b6ba82c8025ebd177 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Darkmatter/Scenes/GamePlay.unity b/Assets/Darkmatter/Scenes/GamePlay.unity index efcaee0..e891c36 100644 --- a/Assets/Darkmatter/Scenes/GamePlay.unity +++ b/Assets/Darkmatter/Scenes/GamePlay.unity @@ -1935,6 +1935,14 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 198479350452278120, guid: 0d57e66d7df915b4aabb02005dd9f8b1, type: 3} + propertyPath: playOnAwake + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 198799323246219528, guid: 0d57e66d7df915b4aabb02005dd9f8b1, type: 3} + propertyPath: playOnAwake + value: 0 + objectReference: {fileID: 0} - target: {fileID: 199137177167099770, guid: 0d57e66d7df915b4aabb02005dd9f8b1, type: 3} propertyPath: m_Enabled value: 0 @@ -3690,8 +3698,8 @@ MonoBehaviour: m_Calls: [] m_text: Artbook m_isRightToLeft: 0 - m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} - m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontAsset: {fileID: 11400000, guid: f282e17aebd2cf547bdab7be22e5c474, type: 2} + m_sharedMaterial: {fileID: 6332886355625335628, guid: f282e17aebd2cf547bdab7be22e5c474, type: 2} m_fontSharedMaterials: [] m_fontMaterial: {fileID: 0} m_fontMaterials: []