refactor(engine): split card_plugin runtime code into submodules
Final runtime split for #118: the 2,536-line mod.rs becomes seven focused submodules along existing system boundaries — mod.rs 574 fan-step helpers, CardImageSet, markers, plugin build sync.rs 748 asset loading + card entity lifecycle (spawn/update/position) layout.rs 351 resize snapping, in-place resize, tableau fan spread stock.rs 291 empty-stock recycle hint + count badge highlights.rs 276 hint/right-click highlights, cursor hit-testing labels.rs 208 desktop text labels + Android corner labels anim.rs 200 flip animation, drag shadows Moved items are pub(super); code moved verbatim apart from relocating the two stock colour constants next to their consumers. No behaviour change; all 70 card tests pass unchanged. Refs #118 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
//! Card flip animation and drag shadows.
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use solitaire_core::Card;
|
||||
|
||||
use crate::animation_plugin::EffectiveSlideDuration;
|
||||
use crate::events::{CardFaceRevealedEvent, CardFlippedEvent};
|
||||
use crate::layout::LayoutResource;
|
||||
use crate::resources::DragState;
|
||||
use crate::ui_theme::{
|
||||
CARD_SHADOW_ALPHA_DRAG, CARD_SHADOW_COLOR, CARD_SHADOW_LOCAL_Z,
|
||||
};
|
||||
|
||||
/// Listens for `CardFlippedEvent` and inserts a `CardFlipAnim` on the entity.
|
||||
///
|
||||
/// Skipped when `EffectiveSlideDuration::slide_secs == 0.0` (Instant speed).
|
||||
pub(super) fn start_flip_anim(
|
||||
mut events: MessageReader<CardFlippedEvent>,
|
||||
slide_dur: Option<Res<EffectiveSlideDuration>>,
|
||||
mut commands: Commands,
|
||||
card_entities: Query<(Entity, &CardEntity)>,
|
||||
) {
|
||||
if slide_dur.is_some_and(|d| d.slide_secs == 0.0) {
|
||||
// Instant animation speed — skip the flip effect entirely.
|
||||
events.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
for CardFlippedEvent(flipped_card) in events.read() {
|
||||
for (entity, marker) in &card_entities {
|
||||
if marker.card == *flipped_card {
|
||||
commands.entity(entity).insert(CardFlipAnim {
|
||||
timer: 0.0,
|
||||
phase: FlipPhase::ScalingDown,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Advances `CardFlipAnim` each frame, modifying `Transform::scale.x`.
|
||||
///
|
||||
/// - Phase `ScalingDown`: lerps scale.x from 1.0 → 0.0 over `FLIP_HALF_SECS`.
|
||||
/// - At the midpoint the phase switches to `ScalingUp`, scale.x resets to 0,
|
||||
/// and a `CardFaceRevealedEvent` is fired so audio plays in sync with the reveal.
|
||||
/// - Phase `ScalingUp`: lerps scale.x from 0.0 → 1.0 over `FLIP_HALF_SECS`.
|
||||
/// - When complete the component is removed and scale.x is restored to 1.0.
|
||||
pub(super) fn tick_flip_anim(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
mut anims: Query<(Entity, &CardEntity, &mut Transform, &mut CardFlipAnim)>,
|
||||
mut reveal_events: MessageWriter<CardFaceRevealedEvent>,
|
||||
) {
|
||||
let dt = time.delta_secs();
|
||||
for (entity, card_entity, mut transform, mut anim) in &mut anims {
|
||||
anim.timer += dt;
|
||||
match anim.phase {
|
||||
FlipPhase::ScalingDown => {
|
||||
let t = (anim.timer / FLIP_HALF_SECS).min(1.0);
|
||||
transform.scale.x = 1.0 - t;
|
||||
if t >= 1.0 {
|
||||
anim.phase = FlipPhase::ScalingUp;
|
||||
anim.timer = 0.0;
|
||||
transform.scale.x = 0.0;
|
||||
// Fire the reveal event exactly once, at the phase transition,
|
||||
// so the flip sound is synchronised with the visual face reveal.
|
||||
reveal_events.write(CardFaceRevealedEvent(card_entity.card.clone()));
|
||||
}
|
||||
}
|
||||
FlipPhase::ScalingUp => {
|
||||
let t = (anim.timer / FLIP_HALF_SECS).min(1.0);
|
||||
transform.scale.x = t;
|
||||
if t >= 1.0 {
|
||||
transform.scale.x = 1.0;
|
||||
commands.entity(entity).remove::<CardFlipAnim>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task #38 — Drag-elevation shadow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Maintains a single `ShadowEntity` while cards are being dragged.
|
||||
///
|
||||
/// - If a drag is active, spawns (or repositions) a semi-transparent dark
|
||||
/// sprite behind the top dragged card.
|
||||
/// - If no drag is active, despawns the shadow entity.
|
||||
pub(super) fn update_drag_shadow(
|
||||
mut commands: Commands,
|
||||
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() {
|
||||
// No drag in progress — remove shadow if it exists.
|
||||
if let Some(e) = shadow.take() {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(layout) = layout else { return };
|
||||
let card_w = layout.0.card_size.x;
|
||||
let card_h = layout.0.card_size.y;
|
||||
|
||||
// Find the world position of the first (top) dragged card.
|
||||
let top_pos = drag.cards.first().and_then(|first_card| {
|
||||
card_index
|
||||
.get(first_card)
|
||||
.and_then(|entity| card_entities.get(entity).ok())
|
||||
.map(|(_, t)| t.translation)
|
||||
});
|
||||
|
||||
let Some(top_pos) = top_pos else { return };
|
||||
|
||||
// Shadow is slightly larger, offset behind-and-below, at a z slightly
|
||||
// below the dragged cards.
|
||||
let shadow_pos = top_pos + Vec3::new(-4.0, 4.0, -1.0);
|
||||
|
||||
match *shadow {
|
||||
Some(e) => {
|
||||
// Reposition the existing shadow.
|
||||
commands
|
||||
.entity(e)
|
||||
.insert(Transform::from_translation(shadow_pos));
|
||||
}
|
||||
None => {
|
||||
// Spawn a new shadow sprite. Alpha tracks the per-card
|
||||
// CARD_SHADOW_ALPHA_DRAG token so the Terminal palette's
|
||||
// "no box-shadow" policy disables this stack shadow in
|
||||
// lockstep with the per-card shadows. Re-enabling shadows
|
||||
// is then a one-line change in `ui_theme`, not a hunt
|
||||
// through plugin code.
|
||||
let e = commands
|
||||
.spawn((
|
||||
ShadowEntity,
|
||||
Sprite {
|
||||
color: CARD_SHADOW_COLOR.with_alpha(CARD_SHADOW_ALPHA_DRAG),
|
||||
custom_size: Some(Vec2::new(card_w + 8.0, card_h + 8.0)),
|
||||
..default()
|
||||
},
|
||||
Transform::from_translation(shadow_pos),
|
||||
Visibility::default(),
|
||||
))
|
||||
.id();
|
||||
*shadow = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Snaps every per-card [`CardShadow`] between its idle and lifted tunings
|
||||
/// based on whether the parent [`CardEntity`] is currently in
|
||||
/// [`DragState::cards`]. Runs every frame; the transition is an instant snap
|
||||
/// (no lerp) — the existing shake / settle feedback already handles motion
|
||||
/// at drag-end, so an additional shadow tween would compete with those cues.
|
||||
///
|
||||
/// The shadow size is rebuilt from the parent card's current `Sprite`
|
||||
/// `custom_size` plus the appropriate padding, so the resize handler does
|
||||
/// not need to pre-tune shadow sizes for the drag state — this system fixes
|
||||
/// the geometry within one frame.
|
||||
pub(super) fn update_card_shadows_on_drag(
|
||||
drag: Res<DragState>,
|
||||
cards: Query<(&CardEntity, &Sprite, &Children), Without<CardShadow>>,
|
||||
mut shadows: Query<(&mut Sprite, &mut Transform), With<CardShadow>>,
|
||||
) {
|
||||
let dragged: HashSet<&Card> = drag.cards.iter().collect();
|
||||
|
||||
for (card_entity, card_sprite, children) in cards.iter() {
|
||||
let is_dragged = dragged.contains(&card_entity.card);
|
||||
let (offset, padding, alpha) = card_shadow_params(is_dragged);
|
||||
let Some(card_size) = card_sprite.custom_size else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for child in children.iter() {
|
||||
let Ok((mut shadow_sprite, mut shadow_transform)) = shadows.get_mut(child) else {
|
||||
continue;
|
||||
};
|
||||
shadow_sprite.color = CARD_SHADOW_COLOR.with_alpha(alpha);
|
||||
shadow_sprite.custom_size = Some(card_size + padding);
|
||||
shadow_transform.translation.x = offset.x;
|
||||
shadow_transform.translation.y = offset.y;
|
||||
shadow_transform.translation.z = CARD_SHADOW_LOCAL_Z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task #28 — Hint highlight tick system
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user