// stack data structure #[derive(Component, Clone, Debug)] pub struct TriggerStack { /// LIFO queue of pending triggers pub stack: Vec, /// Current player with priority pub priority_player: PlayerId, /// Players who have passed priority pub passed_priority: HashSet, /// State snapshot for rollback (AI simulation, undo) pub snapshot_history: Vec, } #[derive(Clone, Debug)] pub struct TriggerObject { pub id: TriggerId, pub source_entity: Entity, pub trigger_type: TriggerType, pub controller: PlayerId, pub priority: TriggerPriority, pub parameters: TriggerParams, pub state: TriggerState, // Pending, Resolving, Resolved, Counteracted } #[derive(Clone, Debug, PartialEq)] pub enum TriggerType { /// "When you play a card" - Slay the Spire OnCardPlayed(CardId), /// "When a creature enters battlefield" - MTG OnEntersBattlefield(EntityType), /// "When damage is dealt" - Hearthstone OnDamageDealt { source: Entity, amount: u32 }, /// "At start of turn" - All games OnTurnStart(PlayerId), /// "When health drops below X" - Conditional OnHealthThreshold { entity: Entity, threshold: u32 }, /// Custom game-specific triggers Custom(String), } #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub enum TriggerPriority { /// State-based actions (checked before priority) StateBased = 0, /// Replacement effects (modify what happens) Replacement = 1, /// Triggered abilities (go on stack) Triggered = 2, /// Activated abilities (player chooses) Activated = 3, /// Spells (highest level) Spell = 4, } // Stack Resolution System, APNAP Order pub fn resolve_trigger_stack( stack: &mut TriggerStack, game_state: &mut GameState, ) { loop { // Check state-based actions first (don't use stack) check_state_based_actions(game_state); // If all players passed priority and stack is empty → next phase if stack.stack.is_empty() && stack.passed_priority.len() == game_state.player_count { advance_phase(game_state); return; } // If all players passed priority, resolve top of stack if stack.passed_priority.len() == game_state.player_count { if let Some(trigger) = stack.stack.pop() { // Clear passed priority for next resolution stack.passed_priority.clear(); stack.priority_player = trigger.controller; // Execute trigger, may add new triggers to stack execute_trigger(trigger, stack, game_state); } continue; } // Current player with priority can: // 1. Add spell/ability to stack // 2. Pass priority // For AI/simulation, we auto-resolve if game_state.is_ai_game() { auto_resolve_priority(stack, game_state); } else { // Wait for player input (network message in multiplayer) return; } } } // APNAP Order for simultaneous triggers pub fn add_simultaneous_triggers( stack: &mut TriggerStack, triggers: Vec, active_player: PlayerId, player_order: &[PlayerId], ) { // Group by controller let mut by_controller: HashMap> = HashMap::new(); for trigger in triggers { by_controller.entry(trigger.controller).or_default().push(trigger); } // APNAP: Active Player first (lowest on stack), then Non-Active in turn order // Last added resolves first (LIFO) let mut ordered = Vec::new(); // Active player's triggers go on stack first (bottom) if let Some(ap_triggers) = by_controller.remove(&active_player) { ordered.extend(ap_triggers); } // Non-active players in turn order for player in player_order { if *player != active_player { if let Some(nap_triggers) = by_controller.remove(player) { ordered.extend(nap_triggers); } } } // Add to stack (last in = first to resolve) stack.stack.extend(ordered); } // mbf #[derive(Component, Clone, Debug)] pub struct ModifierStack { /// All active modifiers on this entity pub modifiers: Vec, /// Cached computed values (for performance) pub cached_values: HashMap, /// Dirty flag for recalculation pub dirty: bool, } #[derive(Clone, Debug)] pub struct Modifier { pub id: ModifierId, pub source_entity: Entity, pub modifier_type: ModifierType, pub value: i32, pub priority: u32, // Higher priority applies first pub layer: ModifierLayer, // For same-type modifiers pub timestamp: u64, // For timestamp order within layer pub duration: Option, // Turns/rounds remaining pub is_cumulative: bool, // Stack with same modifiers? } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ModifierType { Attack, Health, MaxHealth, Cost, Damage, Healing, DrawCount, CardTargetCount, // Game-specific SpellPower, // Hearthstone Power, // MTG Strength, // Yu-Gi-Oh EnergyCost, // Slay the Spire } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ModifierLayer { /// Base value (card's printed stats) Base = 0, /// Characteristic-defining abilities (MTG) CharacteristicDefining = 1, /// Control-changing effects Control = 2, /// All other effects (timestamp order) General = 3, /// Setting to specific value (overwrites all) Set = 4, /// Multiplying/dividing Multiply = 5, /// Additive (most common) Additive = 6, } // mbf resolution system pub fn compute_modified_value( entity: Entity, modifier_type: ModifierType, base_value: i32, modifiers: &ModifierStack, game_state: &GameState, ) -> i32 { if !modifiers.dirty { return *modifiers.cached_values.get(&modifier_type).unwrap_or(&base_value); } let mut value = base_value; // Get all modifiers of this type, sorted by priority → layer → timestamp let mut applicable: Vec<&Modifier> = modifiers .modifiers .iter() .filter(|m| m.modifier_type == modifier_type) .collect(); applicable.sort_by(|a, b| { a.priority.cmp(&b.priority) .then(a.layer.cmp(&b.layer)) .then(a.timestamp.cmp(&b.timestamp)) }); // Apply in order for modifier in applicable { value = match modifier.layer { ModifierLayer::Set => modifier.value, ModifierLayer::Multiply => value * modifier.value, ModifierLayer::Additive => value + modifier.value, _ => value + modifier.value, // Default to additive }; } // Cache result modifiers.cached_values.insert(modifier_type, value); modifiers.dirty = false; value } // Example: "Give all minions +2/+2" (Hearthstone) pub fn apply_buff( commands: &mut Commands, target_query: &Query>, source: Entity, attack_buff: i32, health_buff: i32, duration: Option, ) { let timestamp = get_game_timestamp(); for entity in target_query.iter() { commands.entity(entity).with_modifiers(|mods: &mut ModifierStack| { mods.modifiers.push(Modifier { id: generate_modifier_id(), source_entity: source, modifier_type: ModifierType::Attack, value: attack_buff, priority: 100, layer: ModifierLayer::Additive, timestamp, duration, is_cumulative: true, }); mods.modifiers.push(Modifier { id: generate_modifier_id(), source_entity: source, modifier_type: ModifierType::Health, value: health_buff, priority: 100, layer: ModifierLayer::Additive, timestamp, duration, is_cumulative: true, }); mods.dirty = true; }); } } // ecs core components #[derive(Component, Clone, Debug)] pub struct CardComponent { pub card_id: CardId, pub card_type: CardType, pub cost: u32, pub owner: PlayerId, pub controller: PlayerId, pub zone: CardZone, // Hand, Deck, Battlefield, Graveyard, Exile } #[derive(Component, Clone, Debug)] pub struct StatsComponent { pub base_attack: i32, pub base_health: i32, pub current_attack: i32, // Computed from MBF pub current_health: i32, // Computed from MBF pub damage_taken: i32, } #[derive(Component, Clone, Debug)] pub struct TriggerComponent { pub triggers: Vec, } #[derive(Clone, Debug)] pub struct TriggerDefinition { pub event_type: TriggerType, pub condition: Option, pub effects: Vec, pub is_mandatory: bool, pub once_per_turn: bool, } // ============ PLAYER COMPONENTS ============ #[derive(Component, Clone, Debug)] pub struct PlayerComponent { pub player_id: PlayerId, pub hand: Vec, pub deck: Vec, pub graveyard: Vec, pub exile: Vec, pub health: i32, pub max_health: i32, pub resource: u32, // Mana/Energy pub max_resource: u32, } // ============ GAME STATE ============ #[derive(Resource, Clone, Debug)] pub struct GameState { pub current_player: PlayerId, pub phase: GamePhase, pub turn_number: u32, pub player_order: Vec, pub stack: TriggerStack, pub is_multiplayer: bool, pub authority: AuthorityType, // Server/Client } // dsl atomic commands #[derive(Clone, Debug, Serialize, Deserialize)] pub enum EffectCommand { // Damage/Healing DealDamage { amount: u32, target: TargetSelector }, Heal { amount: u32, target: TargetSelector }, // Card Operations DrawCards { count: u32, player: PlayerId }, DiscardCards { count: u32, player: PlayerId }, AddCardToHand { card_id: CardId, player: PlayerId }, // Resource GainResource { amount: u32, player: PlayerId }, SpendResource { amount: u32, player: PlayerId }, // Modifiers ApplyModifier { modifier: Modifier, target: TargetSelector }, RemoveModifier { modifier_id: ModifierId, target: TargetSelector }, // Movement MoveCard { entity: Entity, from: CardZone, to: CardZone }, SummonMinion { card_id: CardId, player: PlayerId, position: BoardPosition }, // Trigger Management AddTrigger { trigger: TriggerDefinition, target: Entity }, RemoveTrigger { trigger_id: TriggerId, target: Entity }, // Game State SetPhase { phase: GamePhase }, EndTurn, WinGame { player: PlayerId }, LoseGame { player: PlayerId }, // Conditional IfCondition { condition: EffectCondition, then_effects: Vec, else_effects: Option>, }, // Repeat ForEach { targets: TargetSelector, effects: Vec, }, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum TargetSelector { Self, RandomEnemy, AllEnemies, AllAllies, SpecificEntity(Entity), LowestHealthEnemy, HighestAttackEnemy, // MTG-style TargetCreature, TargetPlayer, TargetArtifact, // Yu-Gi-Oh-style TargetMonster, TargetSpellTrap, } /// Examples /// Slay the Spire // "When you play a card, draw 1 card" TriggerDefinition { event_type: TriggerType::OnCardPlayed(CardId::Any), condition: Some(TriggerCondition::PlayerControlled), effects: vec![EffectCommand::DrawCards { count: 1, player: TriggerSource }], is_mandatory: true, once_per_turn: false, } // "When you take damage, gain 1 block" TriggerDefinition { event_type: TriggerType::OnDamageTaken { min_amount: 1 }, condition: None, effects: vec![EffectCommand::ApplyModifier { modifier: Modifier::new_block(1), target: TargetSelector::Self, }], is_mandatory: true, once_per_turn: false, } /// Magic: The Gathering // "Whenever a creature enters the battlefield, draw a card" TriggerDefinition { event_type: TriggerType::OnEntersBattlefield(EntityType::Creature), condition: Some(TriggerCondition::ControllerIsSelf), effects: vec![EffectCommand::DrawCards { count: 1, player: TriggerSource }], is_mandatory: true, once_per_turn: false, } // "At the beginning of your upkeep, sacrifice a creature" TriggerDefinition { event_type: TriggerType::OnTurnStart(TriggerSource), condition: Some(TriggerCondition::Phase(Phase::Upkeep)), effects: vec![EffectCommand::SelectAndSacrifice { selector: TargetSelector::TargetCreature, count: 1, }], is_mandatory: true, once_per_turn: true, } /// Hearthstone // "Whenever you summon a minion, deal 1 damage to all enemies" TriggerDefinition { event_type: TriggerType::OnSummon(EntityType::Minion), condition: Some(TriggerCondition::ControllerIsSelf), effects: vec![EffectCommand::DealDamage { amount: 1, target: TargetSelector::AllEnemies, }], is_mandatory: true, once_per_turn: false, } // "Deathrattle: Draw a card" TriggerDefinition { event_type: TriggerType::OnDeath, condition: None, effects: vec![EffectCommand::DrawCards { count: 1, player: Owner }], is_mandatory: true, once_per_turn: false, } /// Yu-Gi-Oh! // "When this card is Normal Summoned: You can add 1 Spell from deck to hand" TriggerDefinition { event_type: TriggerType::OnNormalSummon, condition: Some(TriggerCondition::ThisCard), effects: vec![EffectCommand::SearchDeck { card_type: CardType::Spell, count: 1, to: Zone::Hand, }], is_mandatory: false, // Optional trigger once_per_turn: true, } // "When a monster declares an attack: Negate the attack" TriggerDefinition { event_type: TriggerType::OnAttackDeclared, condition: Some(TriggerCondition::FacingThisCard), effects: vec![EffectCommand::NegateAttack], is_mandatory: true, once_per_turn: false, }