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

Commit 1a8d13f

Browse files
committed
Rename some actions / events and add Spite reaction
1 parent 5060601 commit 1a8d13f

File tree

5 files changed

+65
-28
lines changed

5 files changed

+65
-28
lines changed

README.md

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,44 @@ AER World is a prototype game state container designed to model games like [Hear
66

77
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.
88

9-
The following is an example of AER World simulating the game state as `Golem 1` attempts to move away from `Player 0`.
9+
The following is an example of AER World simulating the game state as `Golem 2` attempts to move away from `Player 1`.
1010

1111
```
12-
---- Player 0 ----
12+
13+
---- Player 1 ----
1314
life: 10/10 + 10
1415
position: (0, 0)
1516
reactions: [OpportunityAttack { damage_amount: 3 }]
1617
1718
18-
---- Golem 1 ----
19+
---- Golem 2 ----
1920
life: 2/3 + 2
2021
position: (0, 0)
2122
reactions: [Reinforce { armor_amount: 3 }]
2223
23-
[Action] 1 -> 1 Move { to_position: (0, 1) }
24-
[Event] 1 -> 1 Moved { from_position: (0, 0) }
25-
[Reaction] 0 OpportunityAttack { damage_amount: 3 }
26-
[Action] 0 -> 1 DealDamage { amount: 3 }
27-
[Event] 0 -> 1 Damaged
28-
[Reaction] 1 Reinforce { armor_amount: 3 }
29-
[Action] 1 -> 1 GainArmor { amount: 3 }
24+
[Action] 2 -> 2 Move { to_position: (0, 1) }
25+
[Event] 2 -> 2 AfterMove { from_position: (0, 0) }
26+
[Reaction] 1 OpportunityAttack { damage_amount: 3 }
27+
[Action] 1 -> 2 Damage { amount: 3 }
28+
[Event] 1 -> 2 AfterDamage
29+
[Reaction] 2 Reinforce { armor_amount: 3 }
30+
[Action] 2 -> 2 GainArmor { amount: 3 }
3031
31-
---- Golem 1 ----
32+
---- Player 1 ----
33+
life: 10/10 + 10
34+
position: (0, 0)
35+
reactions: [OpportunityAttack { damage_amount: 3 }]
36+
37+
38+
---- Golem 2 ----
3239
life: 1/3 + 3
3340
position: (0, 1)
3441
reactions: [Reinforce { armor_amount: 3 }]
42+
3543
```
3644

37-
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.
38-
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.
45+
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.
46+
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.
3947
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.
4048

4149
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
4654

4755
After each system state mutation, an action may choose to emit an event based on the result having met some criteria.
4856

49-
**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.
57+
**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.
5058

5159
**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.
5260

crates/playground/src/main.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,23 @@ fn main() {
1919
Some(Armor { current: 0 }),
2020
Some(Health { current: 3, max: 3 }),
2121
Some(Position { x: 0, y: 5 }),
22-
vec![Reaction::Reinforce { armor_amount: 3 }],
22+
vec![
23+
Reaction::Reinforce { armor_amount: 3 },
24+
Reaction::Spite { damage_amount: 50 },
25+
],
2326
);
2427

2528
world.describe(&player);
2629
world.describe(&golem);
2730

28-
world.perform(Action::DealDamage { amount: 1 }, player, golem, 0);
31+
world.perform(Action::Damage { amount: 1 }, player, golem, 0);
2932

3033
let query = EntityQuery {
3134
allegiance_filter: ComponentFilter::Include(&[Allegiance::Golem]),
3235
position_filter: ComponentFilter::Include(&[Position { x: 0, y: 5 }]),
3336
};
3437

35-
world.perform_with_query(Action::DealDamage { amount: 1 }, player, query, 0);
38+
world.perform_with_query(Action::Damage { amount: 1 }, player, query, 0);
3639
world.perform(Action::GainArmor { amount: 5 }, player, player, 0);
3740

3841
world.perform(

crates/world/src/action.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub enum Action {
1414
Move {
1515
to_position: Position,
1616
},
17-
DealDamage {
17+
Damage {
1818
amount: i64,
1919
},
2020
GainArmor {
@@ -62,6 +62,8 @@ impl World {
6262
self.reaction_system.insert(entity, reactions);
6363
}
6464
Action::Destroy => {
65+
self.emit(&Event::BeforeDestroy, source, target, stack_depth);
66+
6567
self.allegiance_system.remove(&target);
6668
self.armor_system.remove(&target);
6769
self.health_system.remove(&target);
@@ -74,21 +76,27 @@ impl World {
7476
};
7577

7678
self.position_system.move_to(target, to_position);
77-
self.emit(&Event::Moved { from_position }, source, target, stack_depth)
79+
80+
self.emit(
81+
&Event::AfterMove { from_position },
82+
source,
83+
target,
84+
stack_depth,
85+
)
7886
}
79-
Action::DealDamage { amount } => {
87+
Action::Damage { amount } => {
8088
let overflow_damage = self.armor_system.lose(&target, amount).unwrap_or(amount);
8189

8290
let Some(is_alive) = self.health_system.lose(&target, overflow_damage) else {
8391
return;
8492
};
8593

8694
if overflow_damage > 0 {
87-
self.emit(&Event::Damaged, source, target, stack_depth)
95+
self.emit(&Event::AfterDamage, source, target, stack_depth)
8896
}
8997

9098
if !is_alive {
91-
self.perform(Action::Destroy, source, target, stack_depth + 1)
99+
self.perform(Action::Destroy, source, target, stack_depth)
92100
}
93101
}
94102
Action::GainArmor { amount } => self.armor_system.gain(&target, amount),

crates/world/src/event.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@ use crate::{log_with_indentation, systems::components::*, Action, EntityId, Worl
22

33
#[cfg_attr(debug_assertions, derive(Debug))]
44
pub enum Event {
5-
Moved { from_position: Position },
6-
Damaged,
5+
AfterMove { from_position: Position },
6+
AfterDamage,
7+
BeforeDestroy,
78
}
89

910
impl World {
1011
fn handle_event(
1112
&mut self,
1213
event: &Event,
13-
_source: EntityId,
14+
source: EntityId,
1415
target: EntityId,
1516
reactor: EntityId,
1617
reaction: &Reaction,
1718
stack_depth: u64,
1819
) {
1920
match (event, reaction) {
20-
(Event::Moved { from_position }, Reaction::OpportunityAttack { damage_amount }) => {
21+
(Event::AfterMove { from_position }, Reaction::OpportunityAttack { damage_amount }) => {
2122
let Some(reactor_allegiance) = self.allegiance_system.allegiance(&reactor) else {
2223
return;
2324
};
@@ -41,15 +42,15 @@ impl World {
4142
log_with_indentation!(stack_depth, "[Reaction] {reactor:?} {reaction:?}");
4243

4344
self.perform(
44-
Action::DealDamage {
45+
Action::Damage {
4546
amount: *damage_amount,
4647
},
4748
reactor,
4849
target,
4950
stack_depth + 1,
5051
)
5152
}
52-
(Event::Damaged, Reaction::Reinforce { armor_amount }) => {
53+
(Event::AfterDamage, Reaction::Reinforce { armor_amount }) => {
5354
if !(target == reactor) {
5455
return;
5556
}
@@ -65,6 +66,22 @@ impl World {
6566
stack_depth + 1,
6667
)
6768
}
69+
(Event::BeforeDestroy, Reaction::Spite { damage_amount }) => {
70+
if !(target == reactor) {
71+
return;
72+
}
73+
74+
log_with_indentation!(stack_depth, "[Reaction] {reactor:?} {reaction:?}");
75+
76+
self.perform(
77+
Action::Damage {
78+
amount: *damage_amount,
79+
},
80+
reactor,
81+
source,
82+
stack_depth + 1,
83+
)
84+
}
6885
_ => (),
6986
}
7087
}

crates/world/src/systems/reaction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::{EntityId, EntityMap};
55
pub enum Reaction {
66
OpportunityAttack { damage_amount: i64 },
77
Reinforce { armor_amount: i64 },
8+
Spite { damage_amount: i64 },
89
}
910

1011
#[derive(Default)]

0 commit comments

Comments
 (0)