Files
TheriapolisV3/Theriapolis.Game/CodexUI/Drag/DragDropController.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

100 lines
3.5 KiB
C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Theriapolis.Game.CodexUI.Core;
namespace Theriapolis.Game.CodexUI.Drag;
/// <summary>
/// Screen-level coordinator for click-drag interactions. Source widgets call
/// <see cref="BeginDrag"/> on left-mouse-down within their bounds and supply
/// (a) an arbitrary payload object — game-specific state, e.g. <c>StatPoolPayload</c> —
/// and (b) a ghost callback that paints a small follow-the-cursor visual.
///
/// Drop targets register via <see cref="RegisterTarget"/>; on left-mouse-up
/// the controller hit-tests in registration order and fires <see cref="OnDrop"/>
/// with the matching target's id. Pressing <see cref="Keys.Escape"/> mid-drag
/// cancels and fires <see cref="OnCancel"/>.
///
/// One-controller-per-screen; concrete screens own the instance and pass it
/// down to widgets that participate in drag-drop.
/// </summary>
public sealed class DragDropController
{
public bool IsDragging => _payload is not null;
public object? Payload => _payload;
public Point CursorPosition { get; private set; }
private object? _payload;
private System.Action<SpriteBatch, Point>? _ghost;
private readonly System.Collections.Generic.List<DropTarget> _targets = new();
public event System.Action<object, string>? OnDrop; // (payload, targetId)
public event System.Action<object>? OnCancel;
public event System.Action<object, Point>? OnDropAnywhere; // fired when drop lands outside any registered target
public void BeginDrag(object payload, System.Action<SpriteBatch, Point> ghost)
{
_payload = payload;
_ghost = ghost;
}
public void RegisterTarget(string id, Rectangle bounds)
=> _targets.Add(new DropTarget(id, bounds));
public void ClearTargets() => _targets.Clear();
public void Update(GameTime gt, CodexInput input)
{
CursorPosition = input.MousePosition;
if (!IsDragging) { _targets.Clear(); return; }
if (input.KeyJustPressed(Keys.Escape))
{
OnCancel?.Invoke(_payload!);
_payload = null;
_ghost = null;
_targets.Clear();
return;
}
if (input.LeftJustReleased)
{
string? hit = null;
foreach (var t in _targets)
if (t.Bounds.Contains(input.MousePosition)) { hit = t.Id; break; }
if (hit is not null) OnDrop?.Invoke(_payload!, hit);
else OnDropAnywhere?.Invoke(_payload!, input.MousePosition);
_payload = null;
_ghost = null;
_targets.Clear();
}
}
public void Draw(SpriteBatch sb)
{
if (_payload is null || _ghost is null) return;
_ghost(sb, CursorPosition);
}
private readonly struct DropTarget
{
public readonly string Id;
public readonly Rectangle Bounds;
public DropTarget(string id, Rectangle bounds) { Id = id; Bounds = bounds; }
}
}
/// <summary>
/// Payload type for the stat-assignment drag-drop dance. Mirrors the React
/// design's <c>{from, value, idx, ability}</c> object so behavior ports verbatim.
/// </summary>
public sealed class StatPoolPayload
{
public required string Source { get; init; } // "pool" or "slot"
public required int Value { get; init; }
public int? PoolIdx { get; init; } // index in pool list when Source == "pool"
public Theriapolis.Core.Rules.Stats.AbilityId? Ability { get; init; } // when Source == "slot"
}