b451f83174
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>
100 lines
3.5 KiB
C#
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"
|
|
}
|