Events and Integration
A combat system isn't useful in isolation. In this chapter, we'll add event emission for the killboard, integrate with other on-chain systems, and discuss testing strategies.
Combat Events
MUD auto-generates events when tables change, but we want custom semantic events for combat:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface ICombatEvents {
event AttackResolved(
uint256 indexed attackerShipId,
uint256 indexed targetShipId,
uint8 weaponSlot,
uint32 damageDealt,
uint8 damageType
);
event ShipDestroyed(
uint256 indexed shipId,
uint256 indexed destroyerShipId,
uint256 timestamp
);
event ShieldsDepleted(
uint256 indexed shipId,
uint256 timestamp
);
}Emitting Events
Add event emission to the damage resolution:
contract CombatSystem is System, ICombatEvents {
function resolveAttack(
uint256 attackerShipId,
uint8 weaponSlot,
uint256 targetShipId,
uint32 distance
) public returns (DamageResult memory) {
// ... damage calculation from previous chapter ...
emit AttackResolved(
attackerShipId,
targetShipId,
weaponSlot,
finalDamage,
weapon.damageType
);
if (result.shieldDamage >= defenses.shieldHp) {
emit ShieldsDepleted(targetShipId, block.timestamp);
}
if (result.destroyed) {
emit ShipDestroyed(
targetShipId,
attackerShipId,
block.timestamp
);
}
return result;
}
}Killboard Integration
The killboard can listen for ShipDestroyed events to record kills:
import { createPublicClient, parseAbiItem } from "viem";
const client = createPublicClient({ / config / });
// Watch for ship destructions
client.watchEvent({
event: parseAbiItem(
"event ShipDestroyed(uint256 indexed shipId, uint256 indexed destroyerShipId, uint256 timestamp)"
),
onLogs: (logs) => {
for (const log of logs) {
recordKill({
victimShipId: log.args.shipId,
killerShipId: log.args.destroyerShipId,
timestamp: log.args.timestamp,
blockNumber: log.blockNumber,
});
}
},
});Testing Complex Systems
For combat systems, comprehensive testing is critical:
contract CombatTest is Test {
function test_fullCombatScenario() public {
// Setup: two ships with known stats
setupShip(ATTACKER, / weapon config /);
setupShip(DEFENDER, / defense config /);
// Round 1: Attack at optimal range
DamageResult memory r1 = combat.resolveAttack(
ATTACKER, 0, DEFENDER, 100 // within optimal
);
assertGt(r1.shieldDamage, 0);
assertEq(r1.armorDamage, 0); // shields absorb all
// Round 2: Attack at falloff range
DamageResult memory r2 = combat.resolveAttack(
ATTACKER, 0, DEFENDER, 500 // in falloff
);
assertLt(r2.shieldDamage, r1.shieldDamage); // less damage
// Round 3: Weapon on cooldown
vm.expectRevert("Weapon on cooldown");
combat.resolveAttack(ATTACKER, 0, DEFENDER, 100);
// Round 4: Wait for cooldown, finish the fight
vm.warp(block.timestamp + 30); // skip cooldown
DamageResult memory r4 = combat.resolveAttack(
ATTACKER, 0, DEFENDER, 100
);
// Assert expected layer damage
}
}Design Patterns Recap
This combat system used several important patterns:
| Pattern | Where Used |
| Strategy | Swappable damage calculators per damage type |
| Pipeline | Sequential damage resolution stages |
| Observer | Event emission for external systems |
| Template Method | Base damage calculation with extension points |
Congratulations!
You've completed the Advanced Combat Systems tutorial. You now know how to:
- Design complex on-chain game systems
- Implement multi-stage damage resolution
- Use events to integrate with external systems
- Write comprehensive tests for combat scenarios
These patterns apply beyond combat — any complex Smart Assembly with multiple interacting systems can use this architecture.
Sign in to track your progress.