Implement MVP for the statue decoration minigame (#65)

MVP implemented with:
- placing, removing etc. decorations
- saving the state, displaying it on the map, restoring when game restarts
- saving screenshots to folder on device

Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #65
This commit is contained in:
2025-11-27 13:21:22 +00:00
parent 5ad84ca3e8
commit 83aa3d5e6d
71 changed files with 6421 additions and 976 deletions

View File

@@ -0,0 +1,117 @@
using Minigames.StatueDressup.Data;
using UnityEngine;
namespace Minigames.StatueDressup.DragDrop
{
/// <summary>
/// Context object for initializing draggable decorations.
/// Consolidates multiple parameters into a single, cohesive object.
/// </summary>
public class DecorationDragContext
{
/// <summary>
/// The decoration data to display
/// </summary>
public DecorationData Data { get; set; }
/// <summary>
/// The statue outline area for overlap detection
/// </summary>
public RectTransform StatueOutline { get; set; }
/// <summary>
/// Parent transform for decorations placed on statue
/// </summary>
public Transform StatueParent { get; set; }
/// <summary>
/// Parent transform for dragging (usually canvas or draggable container)
/// </summary>
public Transform CanvasParent { get; set; }
/// <summary>
/// Controller for registering/unregistering decorations
/// </summary>
public Controllers.StatueDecorationController Controller { get; set; }
/// <summary>
/// Settings for the minigame
/// </summary>
public AppleHills.Core.Settings.IStatueDressupSettings Settings { get; set; }
/// <summary>
/// Callback when drag operation finishes (success or failure)
/// </summary>
public System.Action OnFinished { get; set; }
/// <summary>
/// Callback to show statue outline during drag
/// </summary>
public System.Action OnShowOutline { get; set; }
/// <summary>
/// Callback to hide statue outline after drag
/// </summary>
public System.Action OnHideOutline { get; set; }
/// <summary>
/// Whether this decoration is being initialized as already placed (from saved state)
/// </summary>
public bool IsPlaced { get; set; }
/// <summary>
/// Create a context for a new decoration being dragged from menu
/// </summary>
public static DecorationDragContext CreateForNewDrag(
DecorationData data,
RectTransform statueOutline,
Transform statueParent,
Controllers.StatueDecorationController controller,
AppleHills.Core.Settings.IStatueDressupSettings settings,
System.Action onFinished,
System.Action onShowOutline,
System.Action onHideOutline)
{
return new DecorationDragContext
{
Data = data,
StatueOutline = statueOutline,
StatueParent = statueParent,
Controller = controller,
Settings = settings,
OnFinished = onFinished,
OnShowOutline = onShowOutline,
OnHideOutline = onHideOutline,
IsPlaced = false
};
}
/// <summary>
/// Create a context for a decoration being loaded from saved state
/// </summary>
public static DecorationDragContext CreateForPlaced(
DecorationData data,
Controllers.StatueDecorationController controller,
AppleHills.Core.Settings.IStatueDressupSettings settings,
RectTransform statueOutline = null,
Transform canvasParent = null,
System.Action onShowOutline = null,
System.Action onHideOutline = null,
System.Action onFinished = null)
{
return new DecorationDragContext
{
Data = data,
StatueOutline = statueOutline,
CanvasParent = canvasParent,
Controller = controller,
Settings = settings,
OnShowOutline = onShowOutline,
OnHideOutline = onHideOutline,
OnFinished = onFinished,
IsPlaced = true
};
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8f2a2c34f6ce482ba117f39cb669e11f
timeCreated: 1764240188

View File

@@ -1,10 +1,11 @@
using Core;
using Minigames.StatueDressup.Controllers;
using Minigames.StatueDressup.Data;
using Minigames.StatueDressup.Utils;
using Minigames.StatueDressup.Events;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Utils;
namespace Minigames.StatueDressup.DragDrop
{
@@ -12,68 +13,140 @@ namespace Minigames.StatueDressup.DragDrop
/// 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.
/// Supports tapping and dragging when placed on statue.
/// </summary>
public class DecorationDraggableInstance : MonoBehaviour
public class DecorationDraggableInstance : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler
{
[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 DecorationData decorationData;
private RectTransform rectTransform;
private Canvas canvas;
private Transform canvasParent; // Parent transform for dragging (usually canvas or draggable container)
private RectTransform statueOutline;
private Transform statueParent;
private StatueDecorationController controller;
private AppleHills.Core.Settings.IStatueDressupSettings settings;
private System.Action onFinishedCallback;
private System.Action onShowOutlineCallback;
private System.Action onHideOutlineCallback;
private bool _isDragging;
private bool _isPlacedOnStatue;
private Vector3 _dragOffset;
private bool isDragging;
private bool isPlacedOnStatue;
private Vector3 dragOffset;
private bool dragStarted; // Track if drag actually started (vs just a click)
// Properties
public DecorationData Data => _decorationData;
public bool IsPlacedOnStatue => _isPlacedOnStatue;
public DecorationData Data => decorationData;
public bool IsPlacedOnStatue => isPlacedOnStatue;
private void Awake()
{
_rectTransform = GetComponent<RectTransform>();
_canvas = GetComponentInParent<Canvas>();
rectTransform = GetComponent<RectTransform>();
canvas = GetComponentInParent<Canvas>();
// Store initial parent for dragging context
if (transform.parent != null)
{
canvasParent = transform.parent;
}
if (canvasGroup == null)
{
canvasGroup = gameObject.AddComponent<CanvasGroup>();
}
// Ensure the decoration image can receive raycasts
if (decorationImage != null)
{
decorationImage.raycastTarget = true;
}
}
/// <summary>
/// Initialize the draggable instance
/// Initialize with context object (preferred method)
/// </summary>
public void Initialize(DecorationData data, RectTransform statueOutline, Transform statueParent,
StatueDecorationController controller, AppleHills.Core.Settings.IStatueDressupSettings settings,
System.Action onFinishedCallback)
public void InitializeWithContext(DecorationDragContext context)
{
_decorationData = data;
_statueOutline = statueOutline;
_statueParent = statueParent;
_controller = controller;
_settings = settings;
_onFinishedCallback = onFinishedCallback;
if (context == null)
{
Logging.Error("[DecorationDraggableInstance] Null context provided!");
return;
}
decorationData = context.Data;
statueOutline = context.StatueOutline;
statueParent = context.StatueParent;
controller = context.Controller;
settings = context.Settings;
onFinishedCallback = context.OnFinished;
onShowOutlineCallback = context.OnShowOutline;
onHideOutlineCallback = context.OnHideOutline;
// Handle placed vs new drag
if (context.IsPlaced)
{
isPlacedOnStatue = true;
isDragging = false;
statueParent = transform.parent; // Already parented to statue
if (context.CanvasParent != null)
{
canvasParent = context.CanvasParent;
}
}
// Set sprite
if (decorationImage != null && data != null && data.DecorationSprite != null)
if (decorationImage != null && context.Data != null && context.Data.DecorationSprite != null)
{
decorationImage.sprite = data.DecorationSprite;
decorationImage.sprite = context.Data.DecorationSprite;
}
// Set authored size
if (_rectTransform != null && data != null)
if (rectTransform != null && context.Data != null)
{
_rectTransform.sizeDelta = data.AuthoredSize;
rectTransform.sizeDelta = context.Data.AuthoredSize;
}
Logging.Debug($"[DecorationDraggableInstance] Initialized: {data?.DecorationName}");
// Make interactive if placed (so it can be picked up)
if (context.IsPlaced && canvasGroup != null)
{
canvasGroup.blocksRaycasts = true;
}
Logging.Debug($"[DecorationDraggableInstance] Initialized with context: {context.Data?.DecorationName}, isPlaced={context.IsPlaced}");
}
/// <summary>
/// Initialize the draggable instance (legacy method - prefer InitializeWithContext)
/// </summary>
[System.Obsolete("Use InitializeWithContext instead")]
public void Initialize(DecorationData data, RectTransform pStatueOutline, Transform pStatueParent,
StatueDecorationController pController, AppleHills.Core.Settings.IStatueDressupSettings pSettings,
System.Action pOnFinishedCallback, System.Action onShowOutline = null, System.Action onHideOutline = null)
{
var context = DecorationDragContext.CreateForNewDrag(
data, pStatueOutline, pStatueParent, pController, pSettings,
pOnFinishedCallback, onShowOutline, onHideOutline
);
InitializeWithContext(context);
}
/// <summary>
/// Initialize as already placed decoration (legacy method - prefer InitializeWithContext)
/// </summary>
[System.Obsolete("Use InitializeWithContext instead")]
public void InitializeAsPlaced(DecorationData data, StatueDecorationController pController,
AppleHills.Core.Settings.IStatueDressupSettings pSettings, RectTransform pStatueOutline = null,
Transform pCanvasParent = null, System.Action onShowOutline = null, System.Action onHideOutline = null,
System.Action onFinished = null)
{
var context = DecorationDragContext.CreateForPlaced(
data, pController, pSettings, pStatueOutline, pCanvasParent,
onShowOutline, onHideOutline, onFinished
);
InitializeWithContext(context);
}
/// <summary>
@@ -81,18 +154,22 @@ namespace Minigames.StatueDressup.DragDrop
/// </summary>
public void StartDragFromIcon(PointerEventData eventData)
{
_isDragging = true;
isDragging = true;
// Broadcast started dragging event (from grid)
var eventDataObj = new DecorationEventData(decorationData, gameObject, transform.position, fromStatue: false);
DecorationEventsManager.BroadcastDecorationStartedDragging(eventDataObj);
// Calculate offset from cursor to object center
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_canvas.transform as RectTransform,
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint);
_dragOffset = _rectTransform.localPosition - (Vector3)localPoint;
dragOffset = rectTransform.localPosition - (Vector3)localPoint;
Logging.Debug($"[DecorationDraggableInstance] Started drag from icon: {_decorationData?.DecorationName}");
Logging.Debug($"[DecorationDraggableInstance] Started drag from icon: {decorationData?.DecorationName}");
}
/// <summary>
@@ -100,16 +177,16 @@ namespace Minigames.StatueDressup.DragDrop
/// </summary>
public void ContinueDrag(PointerEventData eventData)
{
if (!_isDragging) return;
if (!isDragging) return;
// Update position to follow cursor
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_canvas.transform as RectTransform,
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint);
_rectTransform.localPosition = localPoint + (Vector2)_dragOffset;
rectTransform.localPosition = localPoint + (Vector2)dragOffset;
}
/// <summary>
@@ -117,9 +194,13 @@ namespace Minigames.StatueDressup.DragDrop
/// </summary>
public void EndDrag(PointerEventData eventData)
{
_isDragging = false;
isDragging = false;
Logging.Debug($"[DecorationDraggableInstance] Drag ended: {_decorationData?.DecorationName}");
Logging.Debug($"[DecorationDraggableInstance] Drag ended: {decorationData?.DecorationName}");
// Broadcast finished dragging event
var eventDataObj = new DecorationEventData(decorationData, gameObject, transform.position, fromStatue: isPlacedOnStatue);
DecorationEventsManager.BroadcastDecorationFinishedDragging(eventDataObj);
// Check if overlapping with statue
if (IsOverlappingStatue())
@@ -137,20 +218,20 @@ namespace Minigames.StatueDressup.DragDrop
/// </summary>
private bool IsOverlappingStatue()
{
if (_statueOutline == null || _rectTransform == null)
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);
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}");
Logging.Debug($"[DecorationDraggableInstance] Overlap check: {decorationData?.DecorationName}, overlaps={overlaps}");
return overlaps;
}
@@ -158,10 +239,10 @@ namespace Minigames.StatueDressup.DragDrop
/// <summary>
/// Get world space rect for a RectTransform
/// </summary>
private Rect GetWorldRect(RectTransform rectTransform)
private Rect GetWorldRect(RectTransform pRectTransform)
{
Vector3[] corners = new Vector3[4];
rectTransform.GetWorldCorners(corners);
pRectTransform.GetWorldCorners(corners);
Vector3 bottomLeft = corners[0];
Vector3 topRight = corners[2];
@@ -174,24 +255,28 @@ namespace Minigames.StatueDressup.DragDrop
/// </summary>
private void PlaceOnStatue()
{
Logging.Debug($"[DecorationDraggableInstance] Placing on statue: {_decorationData?.DecorationName}");
Logging.Debug($"[DecorationDraggableInstance] Placing on statue: {decorationData?.DecorationName}");
_isPlacedOnStatue = true;
isPlacedOnStatue = true;
// Broadcast dropped on statue event
var eventDataObj = new DecorationEventData(decorationData, gameObject, transform.position, fromStatue: false);
DecorationEventsManager.BroadcastDecorationDroppedOnStatue(eventDataObj);
// Move to statue parent if specified
if (_statueParent != null && transform.parent != _statueParent)
if (statueParent != null && transform.parent != statueParent)
{
transform.SetParent(_statueParent, true); // Keep world position
transform.SetParent(statueParent, true); // Keep world position
}
// Register with controller
if (_controller != null)
if (controller != null)
{
_controller.RegisterDecoration(this);
controller.RegisterDecoration(this);
}
// Notify menu controller to hide outline
_onFinishedCallback?.Invoke();
onFinishedCallback?.Invoke();
}
/// <summary>
@@ -199,16 +284,24 @@ namespace Minigames.StatueDressup.DragDrop
/// </summary>
private void PlayPopOutAndDestroy()
{
Logging.Debug($"[DecorationDraggableInstance] Pop-out and destroy: {_decorationData?.DecorationName}");
Logging.Debug($"[DecorationDraggableInstance] Pop-out and destroy: {decorationData?.DecorationName}");
// Broadcast dropped out event (animation starting)
var eventDataObj = new DecorationEventData(decorationData, gameObject, transform.position, fromStatue: false);
DecorationEventsManager.BroadcastDecorationDroppedOut(eventDataObj);
// Notify menu controller to hide outline immediately
_onFinishedCallback?.Invoke();
onFinishedCallback?.Invoke();
float duration = _settings?.PlacementAnimationDuration ?? 0.3f;
float duration = settings?.PlacementAnimationDuration ?? StatueDressupConstants.DefaultAnimationDuration;
// Play pop-out with fade animation
TweenAnimationUtility.PopOutWithFade(transform, canvasGroup, duration, () =>
{
// Broadcast finished dropping out event (animation complete)
var finalEventData = new DecorationEventData(decorationData, gameObject, transform.position, fromStatue: false);
DecorationEventsManager.BroadcastDecorationFinishedDroppingOut(finalEventData);
Destroy(gameObject);
});
}
@@ -216,27 +309,108 @@ namespace Minigames.StatueDressup.DragDrop
/// <summary>
/// Allow picking up from statue for repositioning
/// </summary>
public void StartDragFromStatue(Vector3 pointerPosition)
public void StartDragFromStatue(PointerEventData eventData)
{
if (_controller != null)
Logging.Debug($"[DecorationDraggableInstance] StartDragFromStatue called for: {decorationData?.DecorationName}");
Logging.Debug($"[DecorationDraggableInstance] Show outline callback is null: {onShowOutlineCallback == null}");
if (controller != null)
{
_controller.UnregisterDecoration(this);
controller.UnregisterDecoration(this);
}
_isPlacedOnStatue = false;
_isDragging = true;
isPlacedOnStatue = false;
isDragging = true;
// Calculate offset
// Broadcast started dragging event (from statue)
var eventDataObj = new DecorationEventData(decorationData, gameObject, transform.position, fromStatue: true);
DecorationEventsManager.BroadcastDecorationStartedDragging(eventDataObj);
// Show statue outline when picking up from statue
if (onShowOutlineCallback != null)
{
Logging.Debug("[DecorationDraggableInstance] Invoking show outline callback");
onShowOutlineCallback.Invoke();
}
else
{
Logging.Warning("[DecorationDraggableInstance] Show outline callback is null - cannot show outline!");
}
// Reparent to canvas for dragging (so coordinates work correctly)
if (canvasParent != null && transform.parent != canvasParent)
{
// Store world position before reparenting
Vector3 worldPos = transform.position;
transform.SetParent(canvasParent, false);
transform.position = worldPos; // Restore world position
}
// Calculate offset using proper camera
RectTransformUtility.ScreenPointToLocalPointInRectangle(
_canvas.transform as RectTransform,
pointerPosition,
null,
canvas.transform as RectTransform,
eventData.position,
eventData.pressEventCamera,
out Vector2 localPoint);
_dragOffset = _rectTransform.localPosition - (Vector3)localPoint;
dragOffset = rectTransform.localPosition - (Vector3)localPoint;
Logging.Debug($"[DecorationDraggableInstance] Started drag from statue: {_decorationData?.DecorationName}");
Logging.Debug($"[DecorationDraggableInstance] Started drag from statue: {decorationData?.DecorationName}");
}
#region Pointer Event Handlers
/// <summary>
/// Handle pointer click - only when placed on statue
/// </summary>
public void OnPointerClick(PointerEventData eventData)
{
// Only handle clicks when placed on statue and not currently dragging
if (!isPlacedOnStatue || dragStarted) return;
Logging.Debug($"[DecorationDraggableInstance] Decoration tapped: {decorationData?.DecorationName}");
// Broadcast tap event
var eventDataObj = new DecorationEventData(decorationData, gameObject, transform.position, fromStatue: true);
DecorationEventsManager.BroadcastDecorationTappedOnStatue(eventDataObj);
// Future: Open detail view, play sound effect, show info popup, etc.
}
/// <summary>
/// Handle drag start - only when placed on statue
/// </summary>
public void OnBeginDrag(PointerEventData eventData)
{
// Only handle drag from statue if already placed
if (!isPlacedOnStatue) return;
dragStarted = true;
StartDragFromStatue(eventData);
}
/// <summary>
/// Handle drag continuation
/// </summary>
public void OnDrag(PointerEventData eventData)
{
if (!isDragging) return;
ContinueDrag(eventData);
}
/// <summary>
/// Handle drag end
/// </summary>
public void OnEndDrag(PointerEventData eventData)
{
if (!isDragging) return;
dragStarted = false;
EndDrag(eventData);
}
#endregion
}
}

View File

@@ -1,5 +1,7 @@
using Core;
using Minigames.StatueDressup.Controllers;
using Minigames.StatueDressup.Data;
using Minigames.StatueDressup.Events;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
@@ -17,7 +19,7 @@ namespace Minigames.StatueDressup.DragDrop
[SerializeField] private Image iconImage;
[SerializeField] private DecorationData decorationData;
private Controllers.DecorationMenuController _menuController;
private DecorationMenuController _menuController;
private DecorationDraggableInstance _activeDraggableInstance;
// Properties
@@ -26,7 +28,7 @@ namespace Minigames.StatueDressup.DragDrop
/// <summary>
/// Initialize the icon with decoration data
/// </summary>
public void Initialize(DecorationData data, Controllers.DecorationMenuController controller)
public void Initialize(DecorationData data, DecorationMenuController controller)
{
decorationData = data;
_menuController = controller;
@@ -46,6 +48,11 @@ namespace Minigames.StatueDressup.DragDrop
if (_activeDraggableInstance == null)
{
Logging.Debug($"[DecorationGridIcon] Item tapped: {decorationData?.DecorationName}");
// Broadcast tapped in grid event
var eventDataObj = new DecorationEventData(decorationData, gameObject, transform.position, fromStatue: false);
DecorationEventsManager.BroadcastDecorationTappedInGrid(eventDataObj);
// Future: Open detail view, preview, etc.
}
}