Lot of progress since my last post, although little of it shows up directly to the user. The following is now possible:
Yay! green and orange squares! What more can you want from a game?
Granted, it doesn’t look like three days worth of work, but here’s what that’s showing:
- The green cells are “poisonpit” cells, and the orange ones are ‘firepit’ cells. Both are defined completely in the defn xlsx, including new tiledefns (for visuals) that points into the tile spritesheets. It’s cool to be able to add new cells without touching code
- The poison pit cell is defined to have a CellEffect on it which triggers when an actor enters it. There are many other events that it could trigger off of – wielding a weapon, moving, taking damage, etc; but for CellEffects the HasBeenEntered event is the most common.
- When the CellEffect’s HasBeenEntered event has been triggered, it fires a TakeDamage action on the entering action and subtracts health. The cell effect also has a OnWorldTurn event which triggers, firing the same action as long as the actor is in the cell.
- The firepit cells are similar, except instead of firing a TakeDamage action on enter, they fire an ApplyBuff action to the action, which applies a “burning” Buff which lasts 5 turns, triggering TakeDamage actions on each OnWorldTurn Event.
I’ve also added AoE actions, modifier action, and a slew of other components to the system. The cool thing about this is that it’s all using the same event/action system, which means that I can now easily (e.g. without code) create:
- A sword that when equipped gives a +damage-dealt modifier
- A shield that, when its equipper takes damage, gives a Renew buff (+1 health per turn for 5 turns) to the equipper
- A “freeze” celleffect that when stepped in, disallows the enterer from moving for 5 turns, does 2 damage to them each turn, and negates any incoming damage from attacks.
- A mob that when hit has a chance to proc an ‘enrage’ buff which gives them 2x damage for 3 turns but also takes 25% of their health
- A skill that when used gives any of the above effects as buff to the caster, to an enemy, to a group, …
- etc
A varied and unique skill system is now going to be easy and fun to create… I can also easily add other new gameplay mechanisms using this; imagine adding “Traps” – place a trap in a cell, and if a mob steps in it it freezes them in place for a 5 turns. Sound familiar? it’s just adding a celleffect to the cell which the Trap was placed in.
All actions are (or will be) implemented as three phases:
- Phase 1: Send notification events, allowing cancellation (EventType.BeginMoving, BeginEnteringCell, BeginAttacking, BeginOpeningDoor, etc). Here’s phase 1 for the Action_Move action:
// ==== PHASE 1: Notify listeners of the intent to perform the Action, allowing them to modify or cancel the action ====
// Important: These listeners must not modify gamestate as subsequent listeners in the chain may cancel the action.
// If the Actor was previously in a Cell then trigger ExitingCell, Moving, and EnteringCell events; otherwise just trigger EnteringCell events
bool wasInCell = oldLocation != null;
if (wasInCell)
{
// Trigger any "BeingExited" events on the actor's previous location, and any "ExitingCell" events on the actor that is moving
if (EventMgr.FireTwoWayEvent(EventType.BeginBeingExited, EventType.BeginExitingCell, movingActor, oldLocation) == EventResult.Cancelled)
return;
// Trigger moving events on the moving Actor.
if (EventMgr.FireEvent(EventType.BeginMoving, movingActor, movingActor, movingActor) == EventResult.Cancelled)
return;
}
// Trigger any "BeingEntered" events on the destination cell and any "EnteringCell" events on the actor that is moving
if (EventMgr.FireTwoWayEvent(EventType.BeginBeingEntered, EventType.BeginEnteringCell, movingActor, newLocation) == EventResult.Cancelled)
return;
// If here, then the Action has not been cancelled and will be performed. At this point the Move cannot be cancelled or modified further.
- Phase 2: At this point, the event can no longer be cancelled. Modifications to the game state are performed during this phase (e.g. moving the actor to the destination cell), but other event/actions can modify some of the changes; e.g. the Action_MeleeAttack flow fires events that allow modifier actions (e.g. “+10% damage” buff) to modify the amount of damage dealt or received. Here’s phase 2 for Action_MeleeAttack:
// Calculate damage
float damageAmount = attacker.RawDamage;//.WieldedWeapon.BaseDamage;
// TODO: Go through flow - block, dodge, etc
// TODO: Give damage modifiers a chance. Armor, Buffs, etc
// Give the attacker's Damage modifiers a chance to modify the amount of damage dealt
if (attacker.Events.ContainsKey(EventType.CalculatingDamageDealt))
{
foreach (EventModel evt in attacker.Events[EventType.CalculatingDamageDealt])
{
// The CalculatingDamageDealt Modifier event will modify DamageAmount
evt.ActionInfo.DamageAmount = damageAmount;
EventMgr.FireEvent(EventType.CalculatingDamageDealt, attacker, attacker, target);
damageAmount = evt.ActionInfo.DamageAmount;
}
}
// Give the target's Damage modifiers a chance to modify the amount of damage taken
if (target.Events.ContainsKey(EventType.CalculatingDamageTaken))
{
foreach (EventModel evt in target.Events[EventType.CalculatingDamageTaken])
{
// The CalculatingDamageTaken Modifier event will modify DamageAmount
evt.ActionInfo.DamageAmount = damageAmount;
EventMgr.FireEvent(EventType.CalculatingDamageTaken, target, target, attacker);
damageAmount = evt.ActionInfo.DamageAmount;
}
}
// Do the deed
target.Health -= (int)damageAmount;
- Phase 3: Send notification events that the action occurred (EventType.Moved, EnteredCell, Attacked, OpenedDoor, …). This allows chaining of events; e.g. a buff on a player that gives them +1 HP every time they open a door. Here’s phase 3 for Action_Move:
// ==== PHASE 3: Notify listeners that the Action was performed, allowing chaining ====
// If user was in a cell then trigger ExitedCell and Moved events; otherwise, just trigger an EnteredCell event
if (wasInCell)
{
// Trigger any "HasBeenExited" events on the actor's previous location, and any "HasExited" events on the actor that moved
EventMgr.FireTwoWayEvent(EventType.HasExitedCell, EventType.HasBeenExited, movingActor, oldLocation);
// Trigger Moved event on the actor
EventMgr.FireEvent(EventType.Moved, movingActor, movingActor, movingActor);
}
// Trigger any "HasBeenEntered" events on the destination cell and any "HasEntered" events on the actor that is moving
EventMgr.FireTwoWayEvent(EventType.HasEnteredCell, EventType.HasBeenEntered, movingActor, newLocation);
There are a few things that I had initially implemented but pulled out when the system got too complex for this game. A couple of those:
- EventFilters: only trigger the event when a particular object (by TypeId or InstanceId) is used. This would allow for data-driven generation of event-based puzzles as well as interesting weapons (a sword that gives a buff when used against an undead mob).
- While modifiers: events only trigger WHILE a condition is met; e.g. equipped or acquired. This allowed more complex event/action combos like a sword that gives one buff while in your inventory and another while equipped. This really complicated some of the lifetime code and enables some interesting but frankly unnecessary scenarios, so I axed it. ‘while equipped’ modifiers on items (very common) are just implemented as two events – HasBeenEquipped (typically adds a buff) and HasBeenUnEquipped (typically removes the same buff).
ProtoBuf made persistence of the events and actions surprisingly easy (given ample use of the AsReference modifier on ProtoMember). Persisting pointers to objects (e.g. the source of an event or target of an action) can be a royal pain, but ProtoBuf made quick work of it.
A large part of the last few days work was building up tests for this. Events and Actions are ripe for errors that are hard to track down or reproduce, and that gets even worse when persistence gets thrown into the mix. To help combat that, I wrote about 2500 lines of test code to test many aspects of the system. To test persistence, 500 lines of that is a fuzzing test which creates a 100×100 map and throws a bunch of mobs, celleffects, and weapons (most with onwielded events) into it. It then spins for 1000 turns, picking random actions (move mob, mob1 attacks mob2, wield weapon). Every 100 turns it persists the gamestate and then rehydrates it, and does a complete deep-comparison between the two, asserting that they match perfectly. The eventing system is definitely not totally bug-free, but my confidence in it is much higher given this test.
btw, here’s all the code that’s required in order to create the celleffects listed above:
</pre>
// Poison pit
CellEffectEventDefn eventDefn = new CellEffectEventDefn("poisonPit1", true);
ActionInfo effectAction = Action_TakeDamage.GenerateActionInfo(1);
cell.Effects.Add(new CellEffectDefn(eventDefn, effectAction));
// Firepit
CellEffectEventDefn eventDefn = new CellEffectEventDefn("firePit1", true);
BuffDefn buffDefn1 = new BuffDefn()
{
ActionInfo = Action_TakeDamage.GenerateActionInfo(2),
InitialDuration = 4,
HasLimitedLifetime = true,
Name = "Burning",
Id = "burning1",
BuffTileId = "burning1",
};
ActionInfo effectAction = Action_ApplyBuff.GenerateActionInfo(buffDefn1);
cell.Effects.Add(new CellEffectDefn(eventDefn, effectAction));
All of the above was first done in a standalone command-line event/action prototype before being merged back into the actual app. Here’s a link to the source code of the event/action prototype as a zip file for those masochistic enough to look: www.wanderlinggames.com/7yrl/eventandactionprototype.zip
Cheers,
Jeff