feat(engine): add CardPlugin with procedural card rendering
Each card is a parent Sprite (white for face-up, blue for face-down) with a Text2d child showing rank+suit (e.g. "AH", "10C", "KS"). Hearts and diamonds render red; clubs and spades black. Face-down labels are hidden. Tableau cards fan downward; other piles stack at the same position with a small z-offset. Sync runs in PostStartup for the initial deal and in Update after every StateChangedEvent. To avoid a one-frame lag, downstream plugins run after the new GameMutation system set. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use solitaire_engine::{GamePlugin, TablePlugin};
|
use solitaire_engine::{CardPlugin, GamePlugin, TablePlugin};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
@@ -15,5 +15,6 @@ fn main() {
|
|||||||
)
|
)
|
||||||
.add_plugins(GamePlugin)
|
.add_plugins(GamePlugin)
|
||||||
.add_plugins(TablePlugin)
|
.add_plugins(TablePlugin)
|
||||||
|
.add_plugins(CardPlugin)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,433 @@
|
|||||||
|
//! Procedural card rendering.
|
||||||
|
//!
|
||||||
|
//! Each card is a parent entity with a coloured body `Sprite` and a child
|
||||||
|
//! `Text2d` showing rank+suit. Entities are synced with `GameStateResource`
|
||||||
|
//! on every `StateChangedEvent`: missing cards are spawned, present cards
|
||||||
|
//! are repositioned/updated in place, and stale cards are despawned.
|
||||||
|
//!
|
||||||
|
//! Phase 3 uses ASCII rank letters ("A", "2"…"10", "J", "Q", "K") and ASCII
|
||||||
|
//! suit letters ("C", "D", "H", "S") so rendering does not depend on the
|
||||||
|
//! bundled font carrying Unicode suit glyphs. When real card art lands in a
|
||||||
|
//! later phase, this plugin is replaced — the `CardEntity` marker and the
|
||||||
|
//! "sync on StateChangedEvent" contract stay the same.
|
||||||
|
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use bevy::color::Color;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use solitaire_core::card::{Card, Rank, Suit};
|
||||||
|
use solitaire_core::game_state::GameState;
|
||||||
|
use solitaire_core::pile::PileType;
|
||||||
|
|
||||||
|
use crate::events::StateChangedEvent;
|
||||||
|
use crate::game_plugin::GameMutation;
|
||||||
|
use crate::layout::{Layout, LayoutResource};
|
||||||
|
use crate::resources::GameStateResource;
|
||||||
|
|
||||||
|
/// Fraction of card height used as vertical offset between stacked tableau cards.
|
||||||
|
const TABLEAU_FAN_FRAC: f32 = 0.25;
|
||||||
|
|
||||||
|
/// Fraction of card height used as a tiny offset between stacked cards in
|
||||||
|
/// non-tableau piles, so stacking is visible.
|
||||||
|
const STACK_FAN_FRAC: f32 = 0.003;
|
||||||
|
|
||||||
|
/// Font size as a fraction of card width.
|
||||||
|
const FONT_SIZE_FRAC: f32 = 0.28;
|
||||||
|
|
||||||
|
const CARD_FACE_COLOUR: Color = Color::srgb(0.98, 0.98, 0.95);
|
||||||
|
const CARD_BACK_COLOUR: Color = Color::srgb(0.15, 0.30, 0.55);
|
||||||
|
const RED_SUIT_COLOUR: Color = Color::srgb(0.78, 0.12, 0.15);
|
||||||
|
const BLACK_SUIT_COLOUR: Color = Color::srgb(0.08, 0.08, 0.08);
|
||||||
|
|
||||||
|
/// Marker component linking a Bevy entity to a `solitaire_core::Card::id`.
|
||||||
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
|
pub struct CardEntity {
|
||||||
|
pub card_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marker for the text child inside a card entity.
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct CardLabel;
|
||||||
|
|
||||||
|
/// Renders cards by reading `GameStateResource` on `StateChangedEvent`.
|
||||||
|
pub struct CardPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CardPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
// PostStartup ensures TablePlugin's Startup system has inserted
|
||||||
|
// LayoutResource before we try to read it.
|
||||||
|
app.add_systems(PostStartup, sync_cards_startup)
|
||||||
|
.add_systems(Update, sync_cards_on_change.after(GameMutation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the initial deal. Runs in `PostStartup`, so all `Startup` systems
|
||||||
|
/// (including `TablePlugin::setup_table` which inserts `LayoutResource`)
|
||||||
|
/// have already completed.
|
||||||
|
fn sync_cards_startup(
|
||||||
|
commands: Commands,
|
||||||
|
game: Res<GameStateResource>,
|
||||||
|
layout: Option<Res<LayoutResource>>,
|
||||||
|
entities: Query<(Entity, &CardEntity)>,
|
||||||
|
) {
|
||||||
|
if let Some(layout) = layout {
|
||||||
|
sync_cards(commands, &game.0, &layout.0, &entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_cards_on_change(
|
||||||
|
mut events: EventReader<StateChangedEvent>,
|
||||||
|
commands: Commands,
|
||||||
|
game: Res<GameStateResource>,
|
||||||
|
layout: Option<Res<LayoutResource>>,
|
||||||
|
entities: Query<(Entity, &CardEntity)>,
|
||||||
|
) {
|
||||||
|
if events.read().next().is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(layout) = layout {
|
||||||
|
sync_cards(commands, &game.0, &layout.0, &entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_cards(
|
||||||
|
mut commands: Commands,
|
||||||
|
game: &GameState,
|
||||||
|
layout: &Layout,
|
||||||
|
entities: &Query<(Entity, &CardEntity)>,
|
||||||
|
) {
|
||||||
|
let positions = card_positions(game, layout);
|
||||||
|
|
||||||
|
// Map card_id -> Entity for in-place updates.
|
||||||
|
let mut existing: HashMap<u32, Entity> = HashMap::new();
|
||||||
|
for (entity, marker) in entities.iter() {
|
||||||
|
existing.insert(marker.card_id, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
let live_ids: HashSet<u32> = positions.iter().map(|(c, _, _)| c.id).collect();
|
||||||
|
|
||||||
|
// Despawn any entity whose card is no longer tracked.
|
||||||
|
for (card_id, entity) in &existing {
|
||||||
|
if !live_ids.contains(card_id) {
|
||||||
|
commands.entity(*entity).despawn_recursive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each card in the current state: spawn or update its entity.
|
||||||
|
for (card, position, z) in positions {
|
||||||
|
match existing.get(&card.id) {
|
||||||
|
Some(&entity) => update_card_entity(&mut commands, entity, &card, position, z, layout),
|
||||||
|
None => spawn_card_entity(&mut commands, &card, position, z, layout),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an ordered vec of (card, position, z) for every card in the game.
|
||||||
|
fn card_positions(game: &GameState, layout: &Layout) -> Vec<(Card, Vec2, f32)> {
|
||||||
|
let mut out: Vec<(Card, Vec2, f32)> = Vec::with_capacity(52);
|
||||||
|
let piles = [
|
||||||
|
PileType::Stock,
|
||||||
|
PileType::Waste,
|
||||||
|
PileType::Foundation(Suit::Clubs),
|
||||||
|
PileType::Foundation(Suit::Diamonds),
|
||||||
|
PileType::Foundation(Suit::Hearts),
|
||||||
|
PileType::Foundation(Suit::Spades),
|
||||||
|
PileType::Tableau(0),
|
||||||
|
PileType::Tableau(1),
|
||||||
|
PileType::Tableau(2),
|
||||||
|
PileType::Tableau(3),
|
||||||
|
PileType::Tableau(4),
|
||||||
|
PileType::Tableau(5),
|
||||||
|
PileType::Tableau(6),
|
||||||
|
];
|
||||||
|
|
||||||
|
for pile_type in piles {
|
||||||
|
let Some(base) = layout.pile_positions.get(&pile_type) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(pile) = game.piles.get(&pile_type) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let is_tableau = matches!(pile_type, PileType::Tableau(_));
|
||||||
|
let fan_y = if is_tableau {
|
||||||
|
-layout.card_size.y * TABLEAU_FAN_FRAC
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i, card) in pile.cards.iter().enumerate() {
|
||||||
|
let pos = Vec2::new(base.x, base.y + fan_y * i as f32);
|
||||||
|
let z = 1.0 + (i as f32) * STACK_FAN_FRAC;
|
||||||
|
out.push((card.clone(), pos, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_card_entity(commands: &mut Commands, card: &Card, pos: Vec2, z: f32, layout: &Layout) {
|
||||||
|
let body_colour = if card.face_up {
|
||||||
|
CARD_FACE_COLOUR
|
||||||
|
} else {
|
||||||
|
CARD_BACK_COLOUR
|
||||||
|
};
|
||||||
|
|
||||||
|
commands
|
||||||
|
.spawn((
|
||||||
|
CardEntity { card_id: card.id },
|
||||||
|
Sprite {
|
||||||
|
color: body_colour,
|
||||||
|
custom_size: Some(layout.card_size),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_xyz(pos.x, pos.y, z),
|
||||||
|
Visibility::default(),
|
||||||
|
))
|
||||||
|
.with_children(|b| {
|
||||||
|
b.spawn((
|
||||||
|
CardLabel,
|
||||||
|
Text2d::new(label_for(card)),
|
||||||
|
TextFont {
|
||||||
|
font_size: layout.card_size.x * FONT_SIZE_FRAC,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(text_colour(card)),
|
||||||
|
// Above the card body on z so it doesn't get occluded by the
|
||||||
|
// parent sprite in back-to-front rendering.
|
||||||
|
Transform::from_xyz(0.0, 0.0, 0.01),
|
||||||
|
label_visibility(card),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_card_entity(
|
||||||
|
commands: &mut Commands,
|
||||||
|
entity: Entity,
|
||||||
|
card: &Card,
|
||||||
|
pos: Vec2,
|
||||||
|
z: f32,
|
||||||
|
layout: &Layout,
|
||||||
|
) {
|
||||||
|
let body_colour = if card.face_up {
|
||||||
|
CARD_FACE_COLOUR
|
||||||
|
} else {
|
||||||
|
CARD_BACK_COLOUR
|
||||||
|
};
|
||||||
|
|
||||||
|
commands.entity(entity).insert((
|
||||||
|
Sprite {
|
||||||
|
color: body_colour,
|
||||||
|
custom_size: Some(layout.card_size),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
Transform::from_xyz(pos.x, pos.y, z),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Despawn the old label child and respawn a fresh one, so rank/suit/
|
||||||
|
// colour/visibility all stay in sync with the card's current state.
|
||||||
|
commands.entity(entity).despawn_descendants();
|
||||||
|
commands.entity(entity).with_children(|b| {
|
||||||
|
b.spawn((
|
||||||
|
CardLabel,
|
||||||
|
Text2d::new(label_for(card)),
|
||||||
|
TextFont {
|
||||||
|
font_size: layout.card_size.x * FONT_SIZE_FRAC,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TextColor(text_colour(card)),
|
||||||
|
Transform::from_xyz(0.0, 0.0, 0.01),
|
||||||
|
label_visibility(card),
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label_for(card: &Card) -> String {
|
||||||
|
let rank = match card.rank {
|
||||||
|
Rank::Ace => "A",
|
||||||
|
Rank::Two => "2",
|
||||||
|
Rank::Three => "3",
|
||||||
|
Rank::Four => "4",
|
||||||
|
Rank::Five => "5",
|
||||||
|
Rank::Six => "6",
|
||||||
|
Rank::Seven => "7",
|
||||||
|
Rank::Eight => "8",
|
||||||
|
Rank::Nine => "9",
|
||||||
|
Rank::Ten => "10",
|
||||||
|
Rank::Jack => "J",
|
||||||
|
Rank::Queen => "Q",
|
||||||
|
Rank::King => "K",
|
||||||
|
};
|
||||||
|
let suit = match card.suit {
|
||||||
|
Suit::Clubs => "C",
|
||||||
|
Suit::Diamonds => "D",
|
||||||
|
Suit::Hearts => "H",
|
||||||
|
Suit::Spades => "S",
|
||||||
|
};
|
||||||
|
format!("{rank}{suit}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_colour(card: &Card) -> Color {
|
||||||
|
if card.suit.is_red() {
|
||||||
|
RED_SUIT_COLOUR
|
||||||
|
} else {
|
||||||
|
BLACK_SUIT_COLOUR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label_visibility(card: &Card) -> Visibility {
|
||||||
|
if card.face_up {
|
||||||
|
Visibility::Inherited
|
||||||
|
} else {
|
||||||
|
Visibility::Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::game_plugin::GamePlugin;
|
||||||
|
use crate::table_plugin::TablePlugin;
|
||||||
|
|
||||||
|
fn app() -> App {
|
||||||
|
let mut app = App::new();
|
||||||
|
app.add_plugins(MinimalPlugins)
|
||||||
|
.add_plugins(GamePlugin)
|
||||||
|
.add_plugins(TablePlugin)
|
||||||
|
.add_plugins(CardPlugin);
|
||||||
|
app.update();
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn label_for_ace_of_hearts_is_ah() {
|
||||||
|
let c = Card {
|
||||||
|
id: 0,
|
||||||
|
suit: Suit::Hearts,
|
||||||
|
rank: Rank::Ace,
|
||||||
|
face_up: true,
|
||||||
|
};
|
||||||
|
assert_eq!(label_for(&c), "AH");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn label_for_ten_of_clubs_is_10c() {
|
||||||
|
let c = Card {
|
||||||
|
id: 0,
|
||||||
|
suit: Suit::Clubs,
|
||||||
|
rank: Rank::Ten,
|
||||||
|
face_up: true,
|
||||||
|
};
|
||||||
|
assert_eq!(label_for(&c), "10C");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_colour_is_red_for_hearts_and_diamonds() {
|
||||||
|
let h = Card {
|
||||||
|
id: 0,
|
||||||
|
suit: Suit::Hearts,
|
||||||
|
rank: Rank::Ace,
|
||||||
|
face_up: true,
|
||||||
|
};
|
||||||
|
let d = Card {
|
||||||
|
id: 0,
|
||||||
|
suit: Suit::Diamonds,
|
||||||
|
rank: Rank::Ace,
|
||||||
|
face_up: true,
|
||||||
|
};
|
||||||
|
assert_eq!(text_colour(&h), RED_SUIT_COLOUR);
|
||||||
|
assert_eq!(text_colour(&d), RED_SUIT_COLOUR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_colour_is_black_for_clubs_and_spades() {
|
||||||
|
let c = Card {
|
||||||
|
id: 0,
|
||||||
|
suit: Suit::Clubs,
|
||||||
|
rank: Rank::Ace,
|
||||||
|
face_up: true,
|
||||||
|
};
|
||||||
|
let s = Card {
|
||||||
|
id: 0,
|
||||||
|
suit: Suit::Spades,
|
||||||
|
rank: Rank::Ace,
|
||||||
|
face_up: true,
|
||||||
|
};
|
||||||
|
assert_eq!(text_colour(&c), BLACK_SUIT_COLOUR);
|
||||||
|
assert_eq!(text_colour(&s), BLACK_SUIT_COLOUR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn card_plugin_spawns_all_52_cards() {
|
||||||
|
let mut app = app();
|
||||||
|
let count = app
|
||||||
|
.world_mut()
|
||||||
|
.query::<&CardEntity>()
|
||||||
|
.iter(app.world())
|
||||||
|
.count();
|
||||||
|
assert_eq!(count, 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tableau_face_down_cards_start_hidden_label() {
|
||||||
|
let mut app = app();
|
||||||
|
// Every tableau column except column 0 has face-down cards. Count
|
||||||
|
// CardLabels with Visibility::Hidden — should equal 0+1+2+3+4+5+6 = 21
|
||||||
|
// (every tableau card except the top of each column is face-down).
|
||||||
|
let hidden_count = app
|
||||||
|
.world_mut()
|
||||||
|
.query::<(&CardLabel, &Visibility)>()
|
||||||
|
.iter(app.world())
|
||||||
|
.filter(|(_, v)| matches!(v, Visibility::Hidden))
|
||||||
|
.count();
|
||||||
|
// 21 tableau face-down + 24 stock face-down = 45.
|
||||||
|
assert_eq!(hidden_count, 45);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_changed_event_triggers_resync() {
|
||||||
|
let mut app = app();
|
||||||
|
// Trigger a draw, which moves a card from stock to waste and should
|
||||||
|
// flip it face-up. Count visible labels after.
|
||||||
|
app.world_mut().send_event(crate::events::DrawRequestEvent);
|
||||||
|
app.update();
|
||||||
|
// Now 1 card in waste (face-up), 23 in stock (face-down). So 24
|
||||||
|
// hidden labels total in stock, plus 21 in tableau = 44.
|
||||||
|
let hidden_count = app
|
||||||
|
.world_mut()
|
||||||
|
.query::<(&CardLabel, &Visibility)>()
|
||||||
|
.iter(app.world())
|
||||||
|
.filter(|(_, v)| matches!(v, Visibility::Hidden))
|
||||||
|
.count();
|
||||||
|
assert_eq!(hidden_count, 44);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn card_positions_includes_all_52_cards() {
|
||||||
|
let g = GameState::new(42, solitaire_core::game_state::DrawMode::DrawOne);
|
||||||
|
let layout =
|
||||||
|
crate::layout::compute_layout(Vec2::new(1280.0, 800.0));
|
||||||
|
let positions = card_positions(&g, &layout);
|
||||||
|
assert_eq!(positions.len(), 52);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn card_positions_tableau_cards_are_fanned_downward() {
|
||||||
|
let g = GameState::new(42, solitaire_core::game_state::DrawMode::DrawOne);
|
||||||
|
let layout =
|
||||||
|
crate::layout::compute_layout(Vec2::new(1280.0, 800.0));
|
||||||
|
let positions = card_positions(&g, &layout);
|
||||||
|
|
||||||
|
// Collect positions for Tableau(6) (should have 7 cards).
|
||||||
|
let tableau_6_base = layout.pile_positions[&PileType::Tableau(6)];
|
||||||
|
let mut ys: Vec<f32> = positions
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, pos, _)| (pos.x - tableau_6_base.x).abs() < 1e-3)
|
||||||
|
.map(|(_, pos, _)| pos.y)
|
||||||
|
.collect();
|
||||||
|
ys.sort_by(|a, b| b.partial_cmp(a).unwrap());
|
||||||
|
assert_eq!(ys.len(), 7);
|
||||||
|
// Every subsequent card should be strictly lower.
|
||||||
|
for w in ys.windows(2) {
|
||||||
|
assert!(w[0] > w[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,12 @@ use crate::events::{
|
|||||||
};
|
};
|
||||||
use crate::resources::{DragState, GameStateResource, SyncStatusResource};
|
use crate::resources::{DragState, GameStateResource, SyncStatusResource};
|
||||||
|
|
||||||
|
/// System set for `GamePlugin`'s state-mutating systems. Downstream plugins
|
||||||
|
/// that read the resulting `StateChangedEvent` should schedule themselves
|
||||||
|
/// `.after(GameMutation)` so updates propagate within a single frame.
|
||||||
|
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct GameMutation;
|
||||||
|
|
||||||
/// Registers game resources, events, and the systems that route user intent
|
/// Registers game resources, events, and the systems that route user intent
|
||||||
/// (events) into mutations on `GameState`.
|
/// (events) into mutations on `GameState`.
|
||||||
pub struct GamePlugin;
|
pub struct GamePlugin;
|
||||||
@@ -39,7 +45,8 @@ impl Plugin for GamePlugin {
|
|||||||
handle_move,
|
handle_move,
|
||||||
handle_undo,
|
handle_undo,
|
||||||
)
|
)
|
||||||
.chain(),
|
.chain()
|
||||||
|
.in_set(GameMutation),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,19 @@
|
|||||||
//! Additional plugins (`TablePlugin`, `CardPlugin`, `InputPlugin`,
|
//! Additional plugins (`TablePlugin`, `CardPlugin`, `InputPlugin`,
|
||||||
//! `AnimationPlugin`, etc.) land in later sub-phases of Phase 3.
|
//! `AnimationPlugin`, etc.) land in later sub-phases of Phase 3.
|
||||||
|
|
||||||
|
pub mod card_plugin;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod game_plugin;
|
pub mod game_plugin;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod resources;
|
pub mod resources;
|
||||||
pub mod table_plugin;
|
pub mod table_plugin;
|
||||||
|
|
||||||
|
pub use card_plugin::{CardEntity, CardLabel, CardPlugin};
|
||||||
pub use events::{
|
pub use events::{
|
||||||
CardFlippedEvent, DrawRequestEvent, GameWonEvent, MoveRequestEvent, NewGameRequestEvent,
|
CardFlippedEvent, DrawRequestEvent, GameWonEvent, MoveRequestEvent, NewGameRequestEvent,
|
||||||
StateChangedEvent, UndoRequestEvent,
|
StateChangedEvent, UndoRequestEvent,
|
||||||
};
|
};
|
||||||
pub use game_plugin::GamePlugin;
|
pub use game_plugin::{GameMutation, GamePlugin};
|
||||||
pub use layout::{compute_layout, Layout, LayoutResource};
|
pub use layout::{compute_layout, Layout, LayoutResource};
|
||||||
pub use resources::{DragState, GameStateResource, SyncStatus, SyncStatusResource};
|
pub use resources::{DragState, GameStateResource, SyncStatus, SyncStatusResource};
|
||||||
pub use table_plugin::{PileMarker, TableBackground, TablePlugin};
|
pub use table_plugin::{PileMarker, TableBackground, TablePlugin};
|
||||||
|
|||||||
Reference in New Issue
Block a user