Files
zfxaction25/cardrow.md
2026-04-19 00:43:27 +02:00

191 lines
10 KiB
Markdown

# Card Row UX (target design)
This document describes the intended behavior for the next CardRow/CardControl UX iteration (presorted hand, external-drag box select, unified program row).
## Sources
- `DonkeysAndDroids.Godot/CardRow.cs`
- `DonkeysAndDroids.Godot/CardControl.cs`
- `DonkeysAndDroids.Godot/card_row.tscn`
- `DonkeysAndDroids.Godot/program.tscn`
- `DonkeysAndDroids.Godot/ProgramScreen.cs`
- `DonkeysAndDroids.Godot/improve.tscn`
- `DonkeysAndDroids.Godot/Improve.cs`
- `DonkeysAndDroids.Godot/draw_glitch.tscn`
- `DonkeysAndDroids.Godot/DrawGlitch.cs`
- `DonkeysAndDroids.Godot/gamble.tscn`
- `DonkeysAndDroids.Godot/Gamble.cs`
- `DonkeysAndDroids.Godot/buffer_overflow.tscn`
- `DonkeysAndDroids.Godot/BufferOverflow.cs`
## Component model
- CardRow is the container that owns CardControls, maintains OrderedCards, layout, and drag/drop reordering. It exposes signals `SelectionChanged` and `OrderChanged`.
- CardControl is the per-card UI. It handles hover/selection/drag visuals and emits `CardDroppedOn` and `CardSelectionChanged` signals.
## Card states and visuals
State triggers and visuals (applies when not Disabled unless noted):
- Idle: default; scale (1,1), pivot center, modulate (1,1,1), z-index 0.
- Hovered: mouse enters card while no GUI drag is active and (CanSelect or CanDrag) is true; scale (1.25,1.25), modulate (1.25,1.25,1.25), z-index 1.
- Selected: `Selected == true`; pivot Y moves to bottom edge, scale (1.25,1.25). Tooltip panel is updated via `Tooltip.Instance.Describe`.
- Drop target (before/after): during a valid drag over this card, `m_DropBefore` set based on cursor X; pivot X snaps to left or right edge, scale (1.25,1.25), alpha set to 0.5.
- Dragging (source card): after drag starts, card is hidden by setting scale X to 0; hover and drop indicators do not apply.
State precedence (in `UpdateState`):
1) Dragging overrides all other visuals.
2) Drop target overrides pivot X and alpha (pivot Y from Selected may remain).
3) Hover overrides scale/modulate.
4) Selected sets pivot Y and scale.
All pivot/scale/modulate changes are tweened with short spring transitions.
## Selection behavior
- Select toggle: left mouse press + release on a card toggles `Selected` if `CanSelect` and not Disabled.
- External-drag box select: if the left mouse button is pressed outside any CardRow (not on a card/row) and the mouse is dragged, any hovered card in the first CardRow entered is force-selected (no toggle off) until release. This is additive unless `SupportsMultiSelect` is false (in which case the most recently hovered card remains selected).
- Clicking a selected card again deselects it (subject to capacity rules when deselecting; see Program screen).
- SupportsMultiSelect:
- If false, selecting a card automatically deselects any other selected cards in that row.
- If true, multiple cards can be toggled independently.
- SelectionContext is global: selecting a card in any row clears selection in the previously active row. Multi-row selection is not possible.
- `SelectionChanged` emits an array of selected CardControls from the row. The array order is not guaranteed (dictionary order).
- Tooltip:
- Hover uses Godot `TooltipText` set to card name + description.
- Selection additionally calls `Tooltip.Instance.Describe(card)`.
## Drag and drop behavior
### Drag start
- Valid only when `CanDrag` is true and the card is not Disabled.
- Drag payload is the original CardControl; drag preview is a duplicated CardControl configured with the same card and row.
- The source card becomes visually hidden (scale X = 0) until drag ends.
### Multi-drag selection
- `SupportsMultiDrag` must be true.
- If multiple cards are selected and the dragged card is among them, all selected cards are dragged together.
- If multiple cards are selected but the dragged card is not selected, only the dragged card moves.
- If only one card is selected, only that card moves.
- Dragged card order is preserved as the order they appear in `OrderedCards`.
### Drop targets
- Row background (`CardRow._DropData`) inserts at an index computed from cursor X relative to existing card positions.
- Card target (`CardControl._DropData`) inserts before/after the target card based on cursor X < card width/2.
### Valid drop checks
A drop is valid only if all are true:
- Target row is not Disabled.
- Target row `CardsCanDrag == true`.
- Drag payload is a CardControl currently in a CardRow.
- If source row != target row, capacity check passes:
`targetRow.OccupiedSpace + sum(dragged.OccupiedSpace) <= targetRow.MaxCards`.
Additional invalid cases:
- Dropping a card onto itself (source == target) is ignored.
- Dropping onto a card that is part of the dragged set is ignored.
- If the dragged card cannot be resolved in its source row (no card id match), the drop is ignored.
### Move results
- Reorder within the same row:
- Cards are removed and reinserted at the adjusted index (accounts for removed cards before the insert point).
- Selection state is preserved because existing CardControls are reused.
- `OrderChanged` is emitted on that row.
- Move across rows:
- Cards are removed from the source list and inserted into the target list.
- New CardControls are created in the target row; selection state resets for moved cards.
- `OrderChanged` is emitted on the target row only.
- All moves animate via tweened layout. During drag-triggered moves, new cards start at their target positions (no center-entry animation).
## Row-to-row interaction rules (valid and invalid)
- Selecting a card in Row B always clears selection in Row A (global SelectionContext).
- External-drag box selection locks onto the first row entered; hovered cards in other rows are ignored until mouse release.
- Dragging from Row A to Row B is only possible if both rows have `CardsCanDrag = true` and are not Disabled.
- Dropping on a row with `CardsCanDrag = false` is invalid (no acceptance, no drop indicator).
- Dropping into a row that would exceed `MaxCards` is invalid (drop rejected).
- Dragging within a row is always allowed when the row is draggable, regardless of capacity.
- Dragging while any GUI drag is active disables hover visuals on other cards (no hover scaling).
## Layout and ordering
- Card positions are computed from `CardSize`, `CardSpacing`, and the row `Size`.
- Non-wrap rows center the line of cards; spacing shrinks (can be negative) when there is insufficient width, causing overlap.
- Wrap rows (`WrapCards = true`) break onto additional rows when the next card would overflow width and set `CustomMinimumSize` to fit.
- `GetSortedCards` sorts by:
1) Rarity
2) Card Id (ECard enum)
3) Modifier count
4) CardId (unique Guid)
## Use cases by screen
### Program row (Program screen, unified hand/tape)
Files: `DonkeysAndDroids.Godot/program.tscn`, `DonkeysAndDroids.Godot/ProgramScreen.cs`.
Row node: single CardRow replacing the split `%Hand`/`%Tape`.
Configuration:
- CardSize 80x100 (from `card_row.tscn`).
- CardSpacing 2.
- AnimationDuration 0.5.
- CardsCanDrag true, CardsCanSelect true.
- SupportsMultiSelect true, SupportsMultiDrag true.
- `MaxCards` is not used to enforce tape/hand capacity; capacity is enforced by selection rules.
Behavior:
- The row contains all cards from `CoreLoop.Hand` and `CoreLoop.Tape`.
- Initial order on entry: `CoreLoop.Tape` (in tape order) followed by `GetSortedCards(CoreLoop.Hand)` (hand pre-sorted).
- Initial selection: cards from `CoreLoop.Tape` are Selected; cards from `CoreLoop.Hand` are Deselected.
- Selection defines location:
- Selected cards count as Tape.
- Deselected cards count as Hand.
- Tape order is the order of selected cards in the row; hand order is the order of deselected cards in the row.
- Program row order is authoritative; results must not reorder existing cards. `HandResult`/`TapeResult` are treated as membership sets (selection + add/remove only).
- New cards not already in the row are appended after existing cards (tape cards in result order, then hand cards in result order).
- Dragging reorders the row only (no row-to-row drag). Multi-drag still reorders within the row.
- Selecting a card is only allowed if the resulting tape `OccupiedSpace` is `<= TapeLength`.
- Deselecting a card is only allowed if the resulting hand `OccupiedSpace` is `<= HandSize`.
- Discard operates on the selected set (selected cards can be discarded).
- Sort button re-sorts only the deselected (hand) cards and keeps selected (tape) cards fixed in place.
### Shop (Improve screen)
Files: `DonkeysAndDroids.Godot/improve.tscn`, `DonkeysAndDroids.Godot/Improve.cs`.
Row node: `%Shop` (CardRow instance).
Configuration:
- CardSize 90x112.
- CardSpacing 5.
- CardsCanDrag false.
- CardsCanSelect true.
- SupportsMultiSelect true.
- SupportsMultiDrag false (default).
Behavior:
- Multiple cards can be selected for purchase; Buy button cost is the sum of selected cards.
- Selection is the only interaction; drag and drop are disabled.
- Selection persists within the row until cleared by clicking again or selecting another row.
Invalid interactions:
- Dragging from or dropping onto the shop row is not possible (CardsCanDrag false).
- Selecting cards in the deck preview clears shop selection.
### Deck preview (Improve + DrawGlitch screens)
Files: `DonkeysAndDroids.Godot/improve.tscn`, `DonkeysAndDroids.Godot/draw_glitch.tscn`,
`DonkeysAndDroids.Godot/Improve.cs`, `DonkeysAndDroids.Godot/DrawGlitch.cs`.
Row nodes: `%Deck` in both scenes.
Configuration (Improve deck):
- CardsCanDrag false.
- WrapCards true.
- CardSpacing 15.
- CardsCanSelect true (default), SupportsMultiSelect false.
Configuration (DrawGlitch deck):
- CardsCanDrag false.
- WrapCards true.
- CardSpacing 10.
- CardsCanSelect true (default), SupportsMultiSelect false.
Behavior:
- Deck order is always the sorted order from `GetSortedCards`.
- Cards can be hovered and selected (single selection only), primarily for tooltip/inspect.
- No drag/drop or reordering.
Invalid interactions:
- Dragging from or dropping onto deck preview rows is not possible.
- Selecting a card here clears any selection in other rows.
### Variants using the same CardRow mechanics (for completeness)
- Gamble (`DonkeysAndDroids.Godot/gamble.tscn`): `%Patches` row uses CardsCanDrag false and single selection to pick exactly one card to add.
- BufferOverflow (`DonkeysAndDroids.Godot/buffer_overflow.tscn`): `%Hand` row uses CardsCanDrag false and single selection to pick exactly one card to remove.