First pass over MPV for the cement-statue-sticker minigame (#63)

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Reviewed-on: #63
This commit is contained in:
2025-11-24 14:55:45 +00:00
parent e33de5da3d
commit 86c1df55f2
66 changed files with 4455 additions and 413 deletions

View File

@@ -0,0 +1,242 @@
using Core;
using Minigames.StatueDressup.Controllers;
using Minigames.StatueDressup.Data;
using Minigames.StatueDressup.Utils;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace Minigames.StatueDressup.DragDrop
{
/// <summary>
/// Draggable instance of a decoration that can be placed on the statue.
/// Created dynamically when dragging from menu or picking up from statue.
/// Destroyed if dropped outside statue area.
/// </summary>
public class DecorationDraggableInstance : MonoBehaviour
{
[Header("References")]
[SerializeField] private Image decorationImage;
[SerializeField] private CanvasGroup canvasGroup;
private DecorationData _decorationData;
private RectTransform _rectTransform;
private Canvas _canvas;
private RectTransform _statueOutline;
private Transform _statueParent;
private StatueDecorationController _controller;
private AppleHills.Core.Settings.IStatueDressupSettings _settings;
private System.Action _onFinishedCallback;
private bool _isDragging;
private bool _isPlacedOnStatue;
private Vector3 _dragOffset;
// Properties
public DecorationData Data => _decorationData;
public bool IsPlacedOnStatue => _isPlacedOnStatue;
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
_canvas = GetComponentInParent<Canvas>();
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
}
/// <summary>
/// Initialize the draggable instance
/// </summary>
public void Initialize(DecorationData data, RectTransform statueOutline, Transform statueParent,
StatueDecorationController controller, AppleHills.Core.Settings.IStatueDressupSettings settings,
System.Action onFinishedCallback)
{
_decorationData = data;
_statueOutline = statueOutline;
_statueParent = statueParent;
_controller = controller;
_settings = settings;
_onFinishedCallback = onFinishedCallback;
// Set sprite
if (decorationImage != null && data != null && data.DecorationSprite != null)
{
decorationImage.sprite = data.DecorationSprite;
}
// Set authored size
if (_rectTransform != null && data != null)
{
_rectTransform.sizeDelta = data.AuthoredSize;
}
Logging.Debug($"[DecorationDraggableInstance] Initialized: {data?.DecorationName}");
}
/// <summary>
/// Start dragging from icon
/// </summary>
public void StartDragFromIcon(PointerEventData eventData)
{
_isDragging = true;
// Calculate offset from cursor to object center
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint);
_dragOffset = _rectTransform.localPosition - (Vector3)localPoint;
Logging.Debug($"[DecorationDraggableInstance] Started drag from icon: {_decorationData?.DecorationName}");
}
/// <summary>
/// Continue dragging
/// </summary>
public void ContinueDrag(PointerEventData eventData)
{
if (!_isDragging) return;
// Update position to follow cursor
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint);
_rectTransform.localPosition = localPoint + (Vector2)_dragOffset;
}
/// <summary>
/// End drag - check placement
/// </summary>
public void EndDrag(PointerEventData eventData)
{
_isDragging = false;
Logging.Debug($"[DecorationDraggableInstance] Drag ended: {_decorationData?.DecorationName}");
// Check if overlapping with statue
if (IsOverlappingStatue())
{
PlaceOnStatue();
}
else
{
PlayPopOutAndDestroy();
}
}
/// <summary>
/// Check if item overlaps with statue outline
/// </summary>
private bool IsOverlappingStatue()
{
if (_statueOutline == null || _rectTransform == null)
{
Logging.Warning($"[DecorationDraggableInstance] Cannot check overlap - statueOutline or RectTransform is null");
return false;
}
// Get bounds of this item in world space
Rect itemRect = GetWorldRect(_rectTransform);
Rect outlineRect = GetWorldRect(_statueOutline);
// Check for any overlap
bool overlaps = itemRect.Overlaps(outlineRect);
Logging.Debug($"[DecorationDraggableInstance] Overlap check: {_decorationData?.DecorationName}, overlaps={overlaps}");
return overlaps;
}
/// <summary>
/// Get world space rect for a RectTransform
/// </summary>
private Rect GetWorldRect(RectTransform rectTransform)
{
Vector3[] corners = new Vector3[4];
rectTransform.GetWorldCorners(corners);
Vector3 bottomLeft = corners[0];
Vector3 topRight = corners[2];
return new Rect(bottomLeft.x, bottomLeft.y, topRight.x - bottomLeft.x, topRight.y - bottomLeft.y);
}
/// <summary>
/// Place item on statue at current position
/// </summary>
private void PlaceOnStatue()
{
Logging.Debug($"[DecorationDraggableInstance] Placing on statue: {_decorationData?.DecorationName}");
_isPlacedOnStatue = true;
// Move to statue parent if specified
if (_statueParent != null && transform.parent != _statueParent)
{
transform.SetParent(_statueParent, true); // Keep world position
}
// Register with controller
if (_controller != null)
{
_controller.RegisterDecoration(this);
}
// Notify menu controller to hide outline
_onFinishedCallback?.Invoke();
}
/// <summary>
/// Play pop-out animation and destroy
/// </summary>
private void PlayPopOutAndDestroy()
{
Logging.Debug($"[DecorationDraggableInstance] Pop-out and destroy: {_decorationData?.DecorationName}");
// Notify menu controller to hide outline immediately
_onFinishedCallback?.Invoke();
float duration = _settings?.PlacementAnimationDuration ?? 0.3f;
// Play pop-out with fade animation
TweenAnimationUtility.PopOutWithFade(transform, canvasGroup, duration, () =>
{
Destroy(gameObject);
});
}
/// <summary>
/// Allow picking up from statue for repositioning
/// </summary>
public void StartDragFromStatue(Vector3 pointerPosition)
{
if (_controller != null)
{
_controller.UnregisterDecoration(this);
}
_isPlacedOnStatue = false;
_isDragging = true;
// Calculate offset
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_canvas.transform as RectTransform,
pointerPosition,
null,
out Vector2 localPoint);
_dragOffset = _rectTransform.localPosition - (Vector3)localPoint;
Logging.Debug($"[DecorationDraggableInstance] Started drag from statue: {_decorationData?.DecorationName}");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e4659fd035c74a79af0311de9e17f44a
timeCreated: 1763991638

View File

@@ -0,0 +1,100 @@
using Core;
using Minigames.StatueDressup.Data;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace Minigames.StatueDressup.DragDrop
{
/// <summary>
/// Static grid icon for decorations in the menu.
/// Handles tap and drag initiation, but doesn't move itself.
/// Spawns a draggable instance when drag starts.
/// </summary>
public class DecorationGridIcon : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[Header("References")]
[SerializeField] private Image iconImage;
[SerializeField] private DecorationData decorationData;
private Controllers.DecorationMenuController _menuController;
private DecorationDraggableInstance _activeDraggableInstance;
// Properties
public DecorationData Data => decorationData;
/// <summary>
/// Initialize the icon with decoration data
/// </summary>
public void Initialize(DecorationData data, Controllers.DecorationMenuController controller)
{
decorationData = data;
_menuController = controller;
if (iconImage != null && data != null && data.DecorationSprite != null)
{
iconImage.sprite = data.DecorationSprite;
}
}
/// <summary>
/// Handle tap/click on icon
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
// Only process clicks if we're not dragging
if (_activeDraggableInstance == null)
{
Logging.Debug($"[DecorationGridIcon] Item tapped: {decorationData?.DecorationName}");
// Future: Open detail view, preview, etc.
}
}
/// <summary>
/// Handle drag start - spawn draggable instance
/// </summary>
public void OnBeginDrag(PointerEventData eventData)
{
if (_menuController == null || decorationData == null)
{
Logging.Warning("[DecorationGridIcon] Cannot start drag - missing controller or data");
return;
}
Logging.Debug($"[DecorationGridIcon] Starting drag for: {decorationData.DecorationName}");
// Spawn draggable instance at cursor position
_activeDraggableInstance = _menuController.SpawnDraggableInstance(decorationData, eventData.position);
// Start the drag on the spawned instance
if (_activeDraggableInstance != null)
{
_activeDraggableInstance.StartDragFromIcon(eventData);
}
}
/// <summary>
/// Forward drag events to the active draggable instance
/// </summary>
public void OnDrag(PointerEventData eventData)
{
if (_activeDraggableInstance != null)
{
_activeDraggableInstance.ContinueDrag(eventData);
}
}
/// <summary>
/// Forward drag end to the active draggable instance
/// </summary>
public void OnEndDrag(PointerEventData eventData)
{
if (_activeDraggableInstance != null)
{
_activeDraggableInstance.EndDrag(eventData);
_activeDraggableInstance = null;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9c806d80a321498c9f33f13d7a31065c
timeCreated: 1763991611