Action filtering

Complexities involved

A design that may work

We use an ActionFilter interface with these methods:

  public Action filterAction(Action action);
  public int getPriority();  //or something like that

The filter can do lots of things: modify the action, change the action to a completely new one, cancel the action, or even queue new actions (kind of like "reactions"...).

ActionFilters can be "combined".  In practice this will probably mean they are connected in priority order.  To simplify this I'll use an ActionFilterCollection (implements ActionFilter) with these methods:

  public void addFilter(ActionFilter filter);
  public void removeFilter(ActionFilter filter);
  public Action filterAction(Action action);
  public int getPriority();

So if you stick lots of ActionFilters into an ActionFilterCollection, you can treat the ActionFilterCollection as a single ActionFilter (while it internally mainains a sorted list of ActionFilters).  When filterAction is called, the ActionFilterCollection will allow all it's ActionFilters to filter the action in priority order.  This algorithm can be improved, it doesn't really matter how it works.  As long as it in one way or another executes multiple filtering.

Each item has an ActionFilterCollection object which any other item can add or remove filters from.

Note: the difference between an ActionFilter and an EventFilter is that an ActionFilter may modify what actually happens - EventFilter modifies only the information about what happened.  Let's say Joe is talking to Fred.  If Joe is drunk, an ActionFilter would modify his TalkActions to make him less understandable.  If instead Fred has bad hearing, an EventFilter would modify what Fred hears.

But this document is only about ActionFilters.

A concrete example

Combat is by definition complex since it involves more than one participant, but here's a reasonably simple scenario.  Joe strikes the Orc with his sword, and the Orc tries to dodge but fails.  The Orc's armour protectes him a bit, so he only gets 2 damage.  How is this done?
  1. Joe will queue an AttackEvent (implements Action, remember).  The AttackEvent includes all information necessary about the attack - which weapon is used, which technique is used, and all that.  When the attack comes out of the queue it is time for filtering.
  2. The AttackEvent will round up all the "participants" (see below) and ask for their ActionFilterCollection.  All these will be stuck together into a final ActionFilterCollection (the combination of all participants' filters).  In this example, this collection is empty since no participants want to filter the result.
  3. The AttackEvent will be filtered through this collection, in this case the original action will be unmodified.  So it is now verified that Joe is attacking the orc.  It is now time to figure out the result of the action.
  4. The AttackEvent wants to give the target a chance to defend itself.  So it retrieves the Orc's CombatStrategy object (or something like that) and asks it to produce a DefenseEvent (given the AttackEvent), which turns out to be a DodgeEvent int this case (extends DefenseEvent).  The DodgeEvent is asked to filter itself, which means that:
  5. The DefenseEvent will go through steps 2-3 above.   If the orc was tied down the DodgeEvent might have been cancelled or heavily modifed, but in this example it goes through unaffected.  So the orc is allowed to dodge unhindered.  Now what, will he succeed?
  6. The AttackEvent is still controlling the flow here.  It now has all the information necessary to figure out the preliminary outcome of the attack, or the AttackResultEvent.  It uses whatever formulas and algorithms to generate an AttackResultEvent, which will contain the AttackEvent, the DefenseEvent, and information about what happened.  In this case the attack will succeed, and 3hp damage will be inflicted (or whatever).  But wait, it's not done yet.  We have now figured out what should happen - but the AttackResultEvent needs to be (guess what) filtered!
  7. But haven't we already filtered enough?  Why filter again?  Well we've filtered the actions involved in the attack - not the actual result of the attack.  The Orc might be wearing armour.  Or if Joe has the OrcThrasherSword the damage might be increased, even though it didn't affect the chance of hitting.  In most cases the participants will be the same, so the AttackResultEvent will probably just ask the AttackEvent and the DefenseEvent for their filters (which they already gathered and combined), combine them, and use the resulting ActionFilterCollection to filter the ActionResultEvent.  Get it?  Joe's filters are used to filter the AttackEvent and the AttackResultEvent.  The Orc's filters are used to filter the DefenseEvent and the AttackResultEvent.
  8. The armour of the orc decreases the damage from 3hp to 2hp during this filtering process.  It may get slightly damaged itself in the process.
  9. Now we actually execute the ActionResultEvent.  It reduces the Orc's hp by 2.   The ActionResultEvent is propagated around for all observers to see.  A passive observer might see the message "Joe slashes at the the Orc who tries to dodge.  The dodge fails and the Orc shrieks in pain as Joe cuts deep into his armour.".  Or it might simply look like "Joe attacks Orc.  Orc tries to dodge.  Joe damages Orc".  The last way is simple - the ActionResultEvent simple describes the ActionEvent, the DefenseEvent, and the ActionResultEvent (in that order).

Who are the "participants"?

This is hard-coded, but will vary for different types of actions.  In an AttackEvent the participants might be the attacker, the weapon used, and the terrain he is standing in.  In a DefenseEvent it is the defender, the weapon or shield used (if any) and the terrain he is standing in.  In the AttackResultEvent it will be all of the above participants.  It is not really that important (as you will see below)

How do other objects ("non-participants") affect the actions?

Each Physical has an ActionFilterCollection instance variable.  Anyone can add and remove ActionFilters from this collection.  So let's take armour for example.  When the Orc wears his armour, the armour will add a "reduce damage" filter to the Orc's filter collection.  The filter will reduce damage from certain types of attacks, and possibly damage the armour in the process.  If the armour is taken off, it will automatically remove the filter from the Orc (see the diagram further down).

What's the difference between participants and non-participants?

A non-participant can be seen as an "indirect participant" in that it can affect the actions if it wants.  Well if both participants and non-participants can affect actions, what is the difference?

Not much, in fact.  That's why it's not such a big deal who is a participant and who isn't. The only difference is that non-participants have to add filters to participants, while participants only have to add filters to their own collection.  Let's say Joe has the OrcThrasherSword and attacks the Orc.

Here is a container-hierarchy style diagram.

The OrcTrasherSword is a participant, which means it will be asked for an ActionFilter directly - so it doesn't need to put any filters in Joe's collection.  The armour worn by the Orc, however, is not a direct participant.  So it needs to explicitely add a filter to the Orc, since the armour itself will never be asked.

The disadvantage of having to add a filter to someone else (like the armour does) is that the filter will be used for completely different, unrelated actions as well.  If the Orc goes fishing, the "decrease damage to myself" filter will be used (although it won't do anything).  So we might later decide that armour is almost always used in battle, and therefore include it as a direct participant to avoid using armour-related filters unnecessarily.

Does this design solve the complexities mentioned?

I hope so.

Q & A:

Isn't it ineffective to recursively ask all parents whenever asked for a filter? It can limited to whatever level we wish (depending on how advanced we want the game to be).  The Area-limit is probably a good compromise.  The good thing about it is that it is simple and not so error-prone.  Imagine the temple of internal peace.  It contains a bag.  If a pixie from the astral plane suddenly warps directly into the bag, the temple may not notice.  In my design, the pixie cannot sneak past the peace-keeping effect.
When will I receive actionCompleted?  For which actions? You receive actionCompleted only for eact actions that you have initiated.  In the example above this means only one actionCompleted will happen.  Joe will receive actionCompleted for his attackEvent.  The Orc's DefenseEvent and the final AttackResultEvent were automatic results of the intial AttackEvent, so noone receives actionCompleted. 

Put more simply, you only receive actionCompleted for actions that you have actually queued.  In the above example the actionCompleted will be called after the ActionResultEvent has been propagated around.

What is an actual item designer required to do when making a cool item Place ActionFilters in the right places, at the right times.
How do the ActionFilterCollection and the Effects inside a Physical relate to each other? ActionFilters respond to Actions and never find out about events that have already occurred.  ActionFilters are not in the container hierarchy.  Effects are in the container hierarchy and can receive events from their parent.  If an Effect wants to filter actions as well, it can implement ActionFilter and add itself to the ActionFilterCollection as well. 

Another possibility is to change so that Effect is not a member of the container hierarchy, and is added directly to the ActionFilterCollection (ála Julio?).  This may be simpler and more consistent.  An effect is an effect is an effect, regardless of whether it modifies the actions or listens to the events.

What about rules? Many ActionFilters will be Rules that implement the ActionFilter interface.  ActionFilter is what makes the rule system fit into the core. 
Whew, that was long...


Henrik Kniberg

Last updated: