perf(engine): route remaining drag card→entity lookups through CardEntityIndex
Replace O(n) `Query::iter().find()` card scans with O(1) `CardEntityIndex`
lookups in the mouse and touch drag pipelines (`follow_drag`, `end_drag`,
`touch_follow_drag`, `touch_end_drag`) and `update_drag_shadow` — 7 sites
across 5 systems. Each ran per dragged card per frame during a drag.
`InputPlugin` now defensively `init_resource::<CardEntityIndex>()` (idempotent;
`CardPlugin` still owns and rebuilds it) so the plugin is self-sufficient in
tests. The lone remaining card-keyed `.find` is a `#[cfg(test)]` world-query
helper, which is the correct pattern there.
Completes the CardEntityIndex migration started in ef1efdc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1487,6 +1487,7 @@ fn update_drag_shadow(
|
||||
drag: Res<DragState>,
|
||||
layout: Option<Res<LayoutResource>>,
|
||||
card_entities: Query<(&CardEntity, &Transform)>,
|
||||
card_index: Res<CardEntityIndex>,
|
||||
mut shadow: Local<Option<Entity>>,
|
||||
) {
|
||||
if drag.is_idle() {
|
||||
@@ -1503,9 +1504,9 @@ fn update_drag_shadow(
|
||||
|
||||
// Find the world position of the first (top) dragged card.
|
||||
let top_pos = drag.cards.first().and_then(|first_card| {
|
||||
card_entities
|
||||
.iter()
|
||||
.find(|(marker, _)| marker.card == *first_card)
|
||||
card_index
|
||||
.get(first_card)
|
||||
.and_then(|entity| card_entities.get(entity).ok())
|
||||
.map(|(_, t)| t.translation)
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,9 @@ use solitaire_core::game_state::GameState;
|
||||
use crate::auto_complete_plugin::AutoCompleteState;
|
||||
use crate::card_animation::tuning::AnimationTuning;
|
||||
use crate::card_animation::{CardAnimation, MotionCurve};
|
||||
use crate::card_plugin::{CardEntity, HintHighlight, HintHighlightTimer, STACK_FAN_FRAC};
|
||||
use crate::card_plugin::{
|
||||
CardEntity, CardEntityIndex, HintHighlight, HintHighlightTimer, STACK_FAN_FRAC,
|
||||
};
|
||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||
use crate::events::{
|
||||
DrawRequestEvent, ForfeitRequestEvent, HintVisualEvent, InfoToastEvent, MoveRejectedEvent,
|
||||
@@ -121,6 +123,10 @@ impl Plugin for InputPlugin {
|
||||
.init_resource::<HintSolverConfig>()
|
||||
.init_resource::<crate::pending_hint::PendingHintTask>()
|
||||
.init_resource::<GameInputConsumedResource>()
|
||||
// The drag systems resolve cards via `CardEntityIndex`; `CardPlugin`
|
||||
// owns and rebuilds it, but init here too so `InputPlugin` is
|
||||
// self-sufficient in tests (idempotent if already registered).
|
||||
.init_resource::<CardEntityIndex>()
|
||||
.add_message::<StartZenRequestEvent>()
|
||||
.add_message::<InfoToastEvent>()
|
||||
.add_message::<ForfeitRequestEvent>()
|
||||
@@ -674,6 +680,7 @@ fn follow_drag(
|
||||
layout: Option<Res<LayoutResource>>,
|
||||
tuning: Res<AnimationTuning>,
|
||||
mut card_transforms: Query<(&CardEntity, &mut Transform, &mut Sprite)>,
|
||||
card_index: Res<CardEntityIndex>,
|
||||
) {
|
||||
// Skip if idle or if a touch drag is running.
|
||||
if drag.is_idle() || drag.active_touch_id.is_some() {
|
||||
@@ -704,9 +711,8 @@ fn follow_drag(
|
||||
// Elevate cards: push to DRAG_Z and dim slightly so the board
|
||||
// beneath stays readable.
|
||||
for (i, card) in drag.cards.iter().enumerate() {
|
||||
if let Some((_, mut transform, mut sprite)) = card_transforms
|
||||
.iter_mut()
|
||||
.find(|(ce, _, _)| ce.card == *card)
|
||||
if let Some(entity) = card_index.get(card)
|
||||
&& let Ok((_, mut transform, mut sprite)) = card_transforms.get_mut(entity)
|
||||
{
|
||||
transform.translation.z = dragged_card_z(i);
|
||||
sprite.color.set_alpha(0.85);
|
||||
@@ -719,9 +725,8 @@ fn follow_drag(
|
||||
let fan = -layout.0.card_size.y * layout.0.tableau_fan_frac;
|
||||
|
||||
for (i, card) in drag.cards.iter().enumerate() {
|
||||
if let Some((_, mut transform, _)) = card_transforms
|
||||
.iter_mut()
|
||||
.find(|(ce, _, _)| ce.card == *card)
|
||||
if let Some(entity) = card_index.get(card)
|
||||
&& let Ok((_, mut transform, _)) = card_transforms.get_mut(entity)
|
||||
{
|
||||
transform.translation.x = bottom_pos.x;
|
||||
transform.translation.y = bottom_pos.y + fan * i as f32;
|
||||
@@ -743,6 +748,7 @@ fn end_drag(
|
||||
mut changed: MessageWriter<StateChangedEvent>,
|
||||
mut commands: Commands,
|
||||
card_entities: Query<(Entity, &CardEntity, &Transform)>,
|
||||
card_index: Res<CardEntityIndex>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
drag.clear();
|
||||
@@ -830,9 +836,8 @@ fn end_drag(
|
||||
continue;
|
||||
};
|
||||
let target_pos = card_position(&game.0, &layout.0, &origin, stack_index);
|
||||
if let Some((entity, _, transform)) = card_entities
|
||||
.iter()
|
||||
.find(|(_, ce, _)| ce.card == *card)
|
||||
if let Some(entity) = card_index.get(card)
|
||||
&& let Ok((_, _, transform)) = card_entities.get(entity)
|
||||
{
|
||||
let drag_pos = transform.translation.truncate();
|
||||
let drag_z = transform.translation.z;
|
||||
@@ -930,6 +935,7 @@ fn touch_follow_drag(
|
||||
layout: Option<Res<LayoutResource>>,
|
||||
tuning: Res<AnimationTuning>,
|
||||
mut card_transforms: Query<(&CardEntity, &mut Transform, &mut Sprite)>,
|
||||
card_index: Res<CardEntityIndex>,
|
||||
) {
|
||||
let Some(active_id) = drag.active_touch_id else {
|
||||
return; // Mouse drag or idle.
|
||||
@@ -957,9 +963,8 @@ fn touch_follow_drag(
|
||||
drag.committed = true;
|
||||
|
||||
for (i, card) in drag.cards.iter().enumerate() {
|
||||
if let Some((_, mut transform, mut sprite)) = card_transforms
|
||||
.iter_mut()
|
||||
.find(|(ce, _, _)| ce.card == *card)
|
||||
if let Some(entity) = card_index.get(card)
|
||||
&& let Ok((_, mut transform, mut sprite)) = card_transforms.get_mut(entity)
|
||||
{
|
||||
transform.translation.z = dragged_card_z(i);
|
||||
sprite.color.set_alpha(0.85);
|
||||
@@ -971,9 +976,8 @@ fn touch_follow_drag(
|
||||
let fan = -layout.0.card_size.y * layout.0.tableau_fan_frac;
|
||||
|
||||
for (i, card) in drag.cards.iter().enumerate() {
|
||||
if let Some((_, mut transform, _)) = card_transforms
|
||||
.iter_mut()
|
||||
.find(|(ce, _, _)| ce.card == *card)
|
||||
if let Some(entity) = card_index.get(card)
|
||||
&& let Ok((_, mut transform, _)) = card_transforms.get_mut(entity)
|
||||
{
|
||||
transform.translation.x = bottom_pos.x;
|
||||
transform.translation.y = bottom_pos.y + fan * i as f32;
|
||||
@@ -998,6 +1002,7 @@ fn touch_end_drag(
|
||||
mut changed: MessageWriter<StateChangedEvent>,
|
||||
mut commands: Commands,
|
||||
card_entities: Query<(Entity, &CardEntity, &Transform)>,
|
||||
card_index: Res<CardEntityIndex>,
|
||||
) {
|
||||
let Some(active_id) = drag.active_touch_id else {
|
||||
return; // Mouse drag or idle.
|
||||
@@ -1070,9 +1075,8 @@ fn touch_end_drag(
|
||||
continue;
|
||||
};
|
||||
let target_pos = card_position(&game.0, &layout.0, &origin, stack_index);
|
||||
if let Some((entity, _, transform)) = card_entities
|
||||
.iter()
|
||||
.find(|(_, ce, _)| ce.card == *card)
|
||||
if let Some(entity) = card_index.get(card)
|
||||
&& let Ok((_, _, transform)) = card_entities.get(entity)
|
||||
{
|
||||
let drag_pos = transform.translation.truncate();
|
||||
let drag_z = transform.translation.z;
|
||||
|
||||
Reference in New Issue
Block a user