diff --git a/README.md b/README.md index baae4de..5fadbf1 100644 --- a/README.md +++ b/README.md @@ -6,36 +6,44 @@ AER World is a prototype game state container designed to model games like [Hear AER (Action Event Reaction) is a design pattern used in AER World that describes a way to clearly track and manage nested state changes caused by actions. The best way to demonstrate this is with an example. -The following is an example of AER World simulating the game state as `Golem 1` attempts to move away from `Player 0`. +The following is an example of AER World simulating the game state as `Golem 2` attempts to move away from `Player 1`. ``` ----- Player 0 ---- + +---- Player 1 ---- life: 10/10 + 10 position: (0, 0) reactions: [OpportunityAttack { damage_amount: 3 }] ----- Golem 1 ---- +---- Golem 2 ---- life: 2/3 + 2 position: (0, 0) reactions: [Reinforce { armor_amount: 3 }] -[Action] 1 -> 1 Move { to_position: (0, 1) } -[Event] 1 -> 1 Moved { from_position: (0, 0) } -[Reaction] 0 OpportunityAttack { damage_amount: 3 } - [Action] 0 -> 1 DealDamage { amount: 3 } - [Event] 0 -> 1 Damaged - [Reaction] 1 Reinforce { armor_amount: 3 } - [Action] 1 -> 1 GainArmor { amount: 3 } +[Action] 2 -> 2 Move { to_position: (0, 1) } +[Event] 2 -> 2 AfterMove { from_position: (0, 0) } +[Reaction] 1 OpportunityAttack { damage_amount: 3 } + [Action] 1 -> 2 Damage { amount: 3 } + [Event] 1 -> 2 AfterDamage + [Reaction] 2 Reinforce { armor_amount: 3 } + [Action] 2 -> 2 GainArmor { amount: 3 } ----- Golem 1 ---- +---- Player 1 ---- +life: 10/10 + 10 +position: (0, 0) +reactions: [OpportunityAttack { damage_amount: 3 }] + + +---- Golem 2 ---- life: 1/3 + 3 position: (0, 1) reactions: [Reinforce { armor_amount: 3 }] + ``` -1. When `Golem 1` moves away from `Player 0` as its enemy, `Player 0`'s "Opportunity Attack" reaction triggers. This interrupts the move action, and spawns a new action to deal 3 damage to the Golem. -2. When `Golem 1` takes 3 damage, its own "Reinforce" reaction triggers. This interrupts the damage action, and spawns a new action to gain 3 armor for the Golem. +1. When `Golem 2` moves away from `Player 1` as its enemy, `Player 1`'s "Opportunity Attack" reaction triggers. This interrupts the move action, and spawns a new action to deal 3 damage to the Golem. +2. When `Golem 2` takes 3 damage, its own "Reinforce" reaction triggers. This interrupts the damage action, and spawns a new action to gain 3 armor for the Golem. 3. Since there are no more reactions to any of the actions on the stack, the actions are popped. The golem is left with 1 HP and 3 Armor as a result of moving away from the player. The AER pattern does not permit direct mutation of systems. Instead, every possible state change must come from an **action**. An action must be performed by a source entity onto a target entity (the target can also be the source). @@ -46,7 +54,7 @@ An action may mutate state multiple times, across multiple systems. For instance After each system state mutation, an action may choose to emit an event based on the result having met some criteria. -**Events** should only be created and used if there is a reaction that depends on it. For example, it does not make sense to create a `Death` event if there are no reactions in the world that care about death. However, it does make sense to add an event for `Move` because Opportunity Attacks may occur if an entity moves away from an enemy. +**Events** should only be created and used if there is a reaction that depends on it. For example, it does not make sense to create an `AfterDestroy` event if there are no reactions in the world that trigger after an entity is destroyed. However, it does make sense to add an event for `AfterMove` because Opportunity Attacks may occur if an entity moves away from an enemy. **Reactions** are pre-defined event handlers, which conditionally perform more actions. They happen after an event is fired, which can happen at any point while an existing action is occuring. When a reaction occurs, it pauses the existing action and executes immediately. For example, the `Reinforce` reaction gains the reactor 3 armor whenever they are damaged. diff --git a/crates/playground/src/main.rs b/crates/playground/src/main.rs index 427d095..feb0844 100644 --- a/crates/playground/src/main.rs +++ b/crates/playground/src/main.rs @@ -19,20 +19,23 @@ fn main() { Some(Armor { current: 0 }), Some(Health { current: 3, max: 3 }), Some(Position { x: 0, y: 5 }), - vec![Reaction::Reinforce { armor_amount: 3 }], + vec![ + Reaction::Reinforce { armor_amount: 3 }, + Reaction::Spite { damage_amount: 50 }, + ], ); world.describe(&player); world.describe(&golem); - world.perform(Action::DealDamage { amount: 1 }, player, golem, 0); + world.perform(Action::Damage { amount: 1 }, player, golem, 0); let query = EntityQuery { allegiance_filter: ComponentFilter::Include(&[Allegiance::Golem]), position_filter: ComponentFilter::Include(&[Position { x: 0, y: 5 }]), }; - world.perform_with_query(Action::DealDamage { amount: 1 }, player, query, 0); + world.perform_with_query(Action::Damage { amount: 1 }, player, query, 0); world.perform(Action::GainArmor { amount: 5 }, player, player, 0); world.perform( diff --git a/crates/world/src/action.rs b/crates/world/src/action.rs index 1aa18fb..b13ef8e 100644 --- a/crates/world/src/action.rs +++ b/crates/world/src/action.rs @@ -14,7 +14,7 @@ pub enum Action { Move { to_position: Position, }, - DealDamage { + Damage { amount: i64, }, GainArmor { @@ -62,6 +62,8 @@ impl World { self.reaction_system.insert(entity, reactions); } Action::Destroy => { + self.emit(&Event::BeforeDestroy, source, target, stack_depth); + self.allegiance_system.remove(&target); self.armor_system.remove(&target); self.health_system.remove(&target); @@ -74,9 +76,15 @@ impl World { }; self.position_system.move_to(target, to_position); - self.emit(&Event::Moved { from_position }, source, target, stack_depth) + + self.emit( + &Event::AfterMove { from_position }, + source, + target, + stack_depth, + ) } - Action::DealDamage { amount } => { + Action::Damage { amount } => { let overflow_damage = self.armor_system.lose(&target, amount).unwrap_or(amount); let Some(is_alive) = self.health_system.lose(&target, overflow_damage) else { @@ -84,11 +92,11 @@ impl World { }; if overflow_damage > 0 { - self.emit(&Event::Damaged, source, target, stack_depth) + self.emit(&Event::AfterDamage, source, target, stack_depth) } if !is_alive { - self.perform(Action::Destroy, source, target, stack_depth + 1) + self.perform(Action::Destroy, source, target, stack_depth) } } Action::GainArmor { amount } => self.armor_system.gain(&target, amount), diff --git a/crates/world/src/event.rs b/crates/world/src/event.rs index 17ebb08..5aebe0d 100644 --- a/crates/world/src/event.rs +++ b/crates/world/src/event.rs @@ -2,22 +2,23 @@ use crate::{log_with_indentation, systems::components::*, Action, EntityId, Worl #[cfg_attr(debug_assertions, derive(Debug))] pub enum Event { - Moved { from_position: Position }, - Damaged, + AfterMove { from_position: Position }, + AfterDamage, + BeforeDestroy, } impl World { fn handle_event( &mut self, event: &Event, - _source: EntityId, + source: EntityId, target: EntityId, reactor: EntityId, reaction: &Reaction, stack_depth: u64, ) { match (event, reaction) { - (Event::Moved { from_position }, Reaction::OpportunityAttack { damage_amount }) => { + (Event::AfterMove { from_position }, Reaction::OpportunityAttack { damage_amount }) => { let Some(reactor_allegiance) = self.allegiance_system.allegiance(&reactor) else { return; }; @@ -41,7 +42,7 @@ impl World { log_with_indentation!(stack_depth, "[Reaction] {reactor:?} {reaction:?}"); self.perform( - Action::DealDamage { + Action::Damage { amount: *damage_amount, }, reactor, @@ -49,7 +50,7 @@ impl World { stack_depth + 1, ) } - (Event::Damaged, Reaction::Reinforce { armor_amount }) => { + (Event::AfterDamage, Reaction::Reinforce { armor_amount }) => { if !(target == reactor) { return; } @@ -65,6 +66,22 @@ impl World { stack_depth + 1, ) } + (Event::BeforeDestroy, Reaction::Spite { damage_amount }) => { + if !(target == reactor) { + return; + } + + log_with_indentation!(stack_depth, "[Reaction] {reactor:?} {reaction:?}"); + + self.perform( + Action::Damage { + amount: *damage_amount, + }, + reactor, + source, + stack_depth + 1, + ) + } _ => (), } } diff --git a/crates/world/src/lib.rs b/crates/world/src/lib.rs index eff2631..f4c3854 100644 --- a/crates/world/src/lib.rs +++ b/crates/world/src/lib.rs @@ -29,7 +29,6 @@ impl World { Default::default() } - // TODO: Remove this pub fn spawn( &mut self, allegiance: Option, diff --git a/crates/world/src/systems/reaction.rs b/crates/world/src/systems/reaction.rs index 0b25de9..a9477c5 100644 --- a/crates/world/src/systems/reaction.rs +++ b/crates/world/src/systems/reaction.rs @@ -5,6 +5,7 @@ use crate::{EntityId, EntityMap}; pub enum Reaction { OpportunityAttack { damage_amount: i64 }, Reinforce { armor_amount: i64 }, + Spite { damage_amount: i64 }, } #[derive(Default)]