Skip to content
This repository has been archived by the owner on Jun 9, 2024. It is now read-only.

Commit

Permalink
Rename some actions / events and add Spite reaction
Browse files Browse the repository at this point in the history
  • Loading branch information
george-lim committed Jan 29, 2024
1 parent 5060601 commit 1a8d13f
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 28 deletions.
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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.

Expand Down
9 changes: 6 additions & 3 deletions crates/playground/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
18 changes: 13 additions & 5 deletions crates/world/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub enum Action {
Move {
to_position: Position,
},
DealDamage {
Damage {
amount: i64,
},
GainArmor {
Expand Down Expand Up @@ -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);
Expand All @@ -74,21 +76,27 @@ 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 {
return;
};

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),
Expand Down
29 changes: 23 additions & 6 deletions crates/world/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -41,15 +42,15 @@ impl World {
log_with_indentation!(stack_depth, "[Reaction] {reactor:?} {reaction:?}");

self.perform(
Action::DealDamage {
Action::Damage {
amount: *damage_amount,
},
reactor,
target,
stack_depth + 1,
)
}
(Event::Damaged, Reaction::Reinforce { armor_amount }) => {
(Event::AfterDamage, Reaction::Reinforce { armor_amount }) => {
if !(target == reactor) {
return;
}
Expand All @@ -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,
)
}
_ => (),
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/world/src/systems/reaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down

0 comments on commit 1a8d13f

Please sign in to comment.