Events and systems should be fairly easy to implement, and to extend when adding future effects. An event class can contain anything, as long as it extends the general Event class/interface.
Systems only have a single receiveEvent(event, objectTargetedByEvent) signature that is called for every event, every time an event is sent to an object. This can be optimized in the future by having systems subscribe to certain events sent to certain objects.
Example: Damage. When dealing damage to an object A, a damage event E is sent to the object by having every system call receiveEvent(E, A). Some of these systems will do nothing since they don't handle damage events, but the DealDamage system will confirm that A has health, and if so reduce the health by the damage amount in E.
Example: Armor. A damage event E is sent to A again, but this time A has armor. E is again sent to DealDamage, but before that it is sent to the ArmorSystem using its receiveEvent(E, A). ArmorSystem checks if A has armor, and if so reduces the damage amount in E by the armor amount to max(damage - armor, 0).
Note that this only works if ArmorSystem gets to handle the event before DealDamage, or else you get a bug.
Example: Element immunity (if a target has immunity to fire, any fire damage is null). An ElementImmunitySystem handles damage events, and checks if the damage dealt is of an element, and if the target has element immunity. If so, it cancels the event (every event needs an isCanceled value that prevents them from being sent to any further systems). Lowering the damage to 0 isn't enough, since future systems could increase it again.
Example: Armor parasite, a bug that spawns whenever damage is reduced by armor. There are two ways to implement this. The easiest is to simply have a SpawnArmorBug system that handles damage events and checks if the target has armor > 0. If so, the damage will obviously be reduced later on by the ArmorSystem. The SpawnArmorBug now creates a bug unit on whatever location it should spawn on.
The downside to this method is that it links the logic in SpawnArmorBug to the logic in ArmorSystem, making it cumbersome to maintain them both. If for some reason you add additional rules to the ArmorSystem (such as, armor now only kicks in if the owner isn't affected by Confusion), you now have to add this rule to every system that checks for armor reduction since they can't just rely on the presence of armor any longer.
A better way would be to have the ArmorSystem spawn an event of its own after reducing a damage amount, an ArmorReducedEvent, and send it to A using receiveEvent (by now you should realize that you need some globally available mail system that takes and event E and an object A and calls receiveEvent(E, A) on every system it stores). SpawnArmorBug no longer listens for damage events, only ArmorReducedEvents. This way it knows that a damage amount was reduced whenever such an event is received without having to know why or how it happened, or by which system. It also means that if you decide to remove the armor system, but forget to remove SpawnArmorBug, SpawnArmorBug will simply do nothing since no ArmorReducedEvents will be sent. If you add additional systems that reduce armor, SpawnArmorBug will automatically trigger off of all of them as long as they send ArmorReducedEvents.
If you want other systems to react to the spawning of the bug, rather than spawning it by itself, the SpawnArmorBug could send a SpawnUnit(bug) event to the object T representing the tile. Another system (SpawnUnitSystem) checks if T can have units on it (is it occupied by another unit? Is it within the map?) and creates it there.
Note that some systems still have to listen to events that "belong" to other systems. ArmorSystem for example, can't listen to ObjectHasRecievedDamage events, since by that time it's already too late to change anything.
Example: Vampirism. VampireSystem listens for DamageEvents and checks how much damage was dealt. It then increases the health of the unit responsible for the damage. Note that VampireSystem must receive events after ArmorSystem, otherwise it won't take armor effects into consideration since the event will not yet have its damage amount adjusted. Also note that VampireSystem requires a new piece of data, namely the source of the damage. This source is not present in receiveEvent(E, A), so the easiest solution is to include it in the damage event. This could cause problems later on however, since other systems may need other sources as well. If something takes damage from a fireball, one system may be interested in the fireball being the source, while others may care about the caster.
Last example: Alien Parasite: damage, activateOverTime, onTargetDeath (would create the Summon Parasite effect)
3 systems:
AlienParasiteSystem: Delas damage over time (i.e whenever a NewTurn event is recieved by an object hositing an alien parasite).
ParasiteOnDeathSystem: Handles ObjectDeathEvents, and checks if the object hosts a parasite. If so, sends a SpawnUnit(new parasite) event to the location where it died)
SpawnUnitSystem: Handles unit spawning, same as before.
If at some point you wish to add more functionality to the parasite (parasites now randomly applies Panic conditions every turn), add a ParasitePanicSystem that handles NewTurn events, checks if the receiver has a parasite and randomly applies Panic.