Event-Driven Combat: Barbarian's Rage Attack

Alex Johnson
-
Event-Driven Combat: Barbarian's Rage Attack

Welcome, adventurers and developers, to a deep dive into the Combat Vertical Slice, a crucial step in building our event-driven architecture. Our primary goal here is to prove end-to-end functionality by implementing a complete attack flow. This isn't just about making a character hit a monster; it's about demonstrating how our modular system, with its distinct toolkit, API, and UI layers, can seamlessly handle complex interactions like combat modifiers. We’ll be focusing on a classic scenario: a Barbarian activating their Rage, attacking a Goblin, and seeing that Rage feature contribute +2 damage through our robust event system. This process will validate key architectural pillars, including the event bus's ability to manage combat modifiers, how features like Rage can influence attacks, the effectiveness of our LoadFromContext pattern for combat scenarios, and most importantly, the clean separation of concerns: the toolkit handling the rules, the API orchestrating the flow, and the UI presenting the information.

Architecture Validation: Building Trust in Our System

The Combat Vertical Slice serves as a rigorous testbed for our architectural choices. By successfully executing this attack flow, we are validating several critical components. Firstly, we confirm that our event bus effectively handles combat modifiers. This means that when an event is triggered, such as an attack roll or a damage calculation, the system can correctly capture and apply various modifiers from different sources. This is the backbone of our flexible combat system. Secondly, we are proving that features can indeed modify attacks via events. In our case, the Barbarian's Rage isn't hardcoded into the attack logic; instead, it listens for attack events and emits its own modifier. This proves the power of an event-driven approach, allowing new features to be added or existing ones modified without altering the core attack resolution logic. Thirdly, the LoadFromContext pattern is validated for combat. This pattern ensures that all necessary data, like character abilities, active features, and target information, is efficiently loaded and available when needed for combat calculations, preventing performance bottlenecks and simplifying data management. Finally, and perhaps most importantly, this slice confirms the clean separation of code across our layers: the rpg-toolkit is solely responsible for defining the rules and logic of combat, the API acts as the orchestrator, managing the flow between different services and data sources, and the UI provides a clear and informative presentation layer for the player. This separation is key to maintainability, scalability, and testability, ensuring that we can update or expand any part of the system without breaking the others. It's about building a solid foundation that we can confidently iterate upon.

Phase 1: Toolkit Combat Foundation (rpg-toolkit)

This initial phase lays the groundwork within our rpg-toolkit, focusing on the core rules and entities that govern combat. It’s here that the fundamental mechanics of our game world reside, ensuring that logic is centralized and reusable. We begin by defining the basic building blocks, starting with the Monster Entity. This isn't just a placeholder; it's a concrete representation of an enemy within our game. We'll create a simple Monster struct, ensuring it implements the core.Entity interface. This means it will have essential properties like an ID, name, current and maximum HP, Armor Class (AC), and ability scores. To make testing straightforward, we'll include a hardcoded factory function, NewGoblin, which will instantiate our basic Goblin enemy with its starting stats. This goblin will serve as our target for the attack, providing a clear objective for our combat slice. The key takeaway here is that these entities are designed to be purely data structures and logic containers, devoid of any presentation or orchestration concerns.

Following the establishment of our entities, we move to the heart of the combat mechanics: Implementing Attack Resolution. This is where the magic of our event-driven system truly begins to shine. We'll define AttackInput and AttackResult structs. The AttackInput will contain all the necessary information for an attack to occur: the attacker, the defender, the weapon being used, and crucially, a reference to the EventBus. The AttackResult will meticulously record every aspect of the attack's outcome, from the raw rolls to the final damage, and importantly, a list of ModifierInfo that details every bonus or penalty applied. The ResolveAttack function will orchestrate this process. It initiates by firing an AttackChain event, allowing various systems to hook in and provide modifiers for the attack roll itself. Once the hit is determined against the defender's AC, a DamageChain event is fired. This event allows other systems to contribute damage modifiers. The core principle here is the use of ChainedTopic from our events package. This mechanism ensures that events are processed sequentially, allowing each modifier to be collected and applied before the next step in the resolution process. This event-driven design is what allows features like Rage to seamlessly integrate without modifying the core attack logic.

Finally, in this foundational phase, we focus on Wiring Rage to Attack Events. The Rage feature, likely defined in rulebooks/dnd5e/features/rage.go, needs to be integrated into our event-driven combat flow. We'll verify and potentially update the Rage implementation to subscribe to the relevant attack events. When Rage is active, it will listen for attack events and, based on the Barbarian's level, apply a damage modifier (e.g., +2, +3, or +4). Crucially, this modifier will only be applied to specific attack types, such as melee attacks using Strength, adhering to D&D 5e rules. The ultimate goal here is to ensure that when an attack is resolved, the AttackResult explicitly lists the Rage modifier, confirming that our event system successfully delivered the bonus. This step demonstrates that game features can dynamically influence combat outcomes through the event bus, validating a core tenet of our architecture. The output of Phase 1 is a robust, event-aware combat engine within the toolkit, ready to be orchestrated.

Phase 2: API Orchestration (rpg-api)

Moving from the core rules in the toolkit, Phase 2 focuses on the API Orchestration layer, housed within rpg-api. This layer acts as the central nervous system, coordinating actions and managing the flow of data between the game's logic and its persistent state. Our primary task here is to implement the Attack Endpoint. This involves creating a new RPC handler, likely within internal/handlers/dnd5e/v1alpha1/encounter_handler.go, that will receive attack requests. When an attack request comes in, the API needs to perform several key steps. First, it will load the character initiating the attack from the database. This character data will include their active features, such as the Barbarian's Rage. Concurrently, it will load the target monster from the current encounter state. With both participants loaded, the API then calls the combat.ResolveAttack() function from our rpg-toolkit. This is a critical delegation step, where the API orchestrates the action but relies entirely on the toolkit for the actual combat rules resolution. After the attack result is returned – complete with all modifiers, including the Rage bonus – the API will update the monster's HP in the encounter state and save this updated state back to the database. Finally, it will return the detailed attack result to the caller, typically the UI. This ensures that the client has all the information needed to display the outcome accurately.

Integral to this process is the Monster Storage component. Within the internal/repositories/encounters/ directory, we need to enhance our encounter repository to reliably track monster status. This involves storing each monster's state within the overall encounter data. Key to this is tracking not just their current HP but also their maximum HP, which is essential for many game mechanics and UI displays. Furthermore, the system should be intelligent enough to remove a monster from the encounter entirely once its HP drops to zero. This means the repository needs logic to check for defeated monsters after an attack and update the encounter data accordingly. This might involve marking them as defeated or removing them from an active combat list. The output of this task is an API that can effectively orchestrate a combat turn, accurately persisting the results and delegating the complex rule calculations to the rpg-toolkit, demonstrating a clear separation of responsibilities and a robust data management strategy for ongoing encounters.

Phase 3: UI Integration (rpg-dnd5e-web)

Finally, we arrive at Phase 3, where we focus on the UI Integration within the rpg-dnd5e-web project. This is where all the hard work from the toolkit and API layers culminates into a tangible experience for the player. Our main task is to update the Attack Result Display. This involves modifying the existing combat user interface to reflect the outcome of the attack initiated through our new API endpoint. When a player clicks to perform an attack, the UI will now call the Attack RPC endpoint we implemented in Phase 2. Upon receiving the response, the UI must be capable of displaying the full combat breakdown. This means not only showing the final damage dealt but also clearly indicating how that damage was calculated. Critically, it needs to display the damage breakdown, explicitly showing the bonus damage contributed by the Barbarian's Rage feature separately from the base damage. Following this, the UI will update the visual representation of the monster's HP, reflecting the damage taken. If the monster was defeated, this should be clearly communicated, perhaps by removing it from the combat view. Additionally, the UI should display the details of the attack roll itself, including any modifiers that influenced its success or failure. The ultimate goal is to provide a clear, informative, and interactive combat experience where the player can see exactly what happened during the attack, including the impact of their character's abilities like Rage. The successful completion of this phase means that a player can now, for instance, click an attack button, see the Rage +2 damage clearly itemized in the results, and observe the Goblin's HP decrease accordingly. This completes the vertical slice, proving that our event-driven architecture, from core rules to user interface, functions as a cohesive whole.

Success Criteria: Mission Accomplished

Our Combat Vertical Slice is considered a resounding success if all the following conditions are met, demonstrating the robustness and efficacy of our event-driven architecture in a real-world combat scenario. Firstly, we must be able to create a Barbarian character who possesses the Rage feature. This confirms our character creation and feature management systems are functioning correctly. Secondly, the system must allow for the activation of Rage by the Barbarian, ensuring that character states and abilities can be dynamically changed. Thirdly, the Barbarian must be able to initiate an attack against a Goblin. This validates the basic attack flow and target selection. The crucial fourth criterion is that Rage must successfully add +2 damage to the attack, and this bonus must be clearly visible in the combat results presented to the player. This is the core validation of our event-driven modifier system. Fifth, the calculated damage, including the Rage bonus, must be correctly applied to the Goblin's HP, visually reflected in the UI and persisted in the game state. Sixth, and a testament to our architectural design, all the underlying combat logic must reside solely within the toolkit, with the API purely acting as an orchestrator and the UI focused on presentation. No core combat rules should be embedded within the API or UI layers. Lastly, the UI must display the full combat breakdown, allowing players to understand the attack's success, the damage dealt, and the specific modifiers that contributed, including our Rage bonus. Meeting these criteria signifies that our event-driven combat system is not only functional but also well-architected for future expansion and maintainability.

Non-Goals (For Now): Keeping Our Focus Sharp

To ensure the successful and timely completion of this Combat Vertical Slice, we are deliberately defining certain aspects as Non-Goals for this iteration. It’s crucial to maintain focus on proving the core event-driven attack mechanism. Therefore, we will not be implementing support for multiple monster types beyond the single Goblin; our focus is on the interaction, not the variety of targets. Similarly, spell attacks are out of scope for this slice; we are concentrating on basic melee attacks to streamline the process. Complex actions such as Reactions or Opportunity Attacks will also not be included, as these introduce additional timing and trigger complexities that are best addressed in separate, dedicated vertical slices. We are also simplifying the weapon system by hardcoding a single weapon, such as a longsword, for the Barbarian, thus avoiding the need for a complex weapon data structure and selection logic at this stage. Lastly, monster abilities and actions – beyond simply being a target with HP and AC – are not part of this slice. These elements will be integrated in future iterations once the foundational combat event system is proven. By setting these clear boundaries, we can concentrate our efforts on validating the core event-driven attack flow and modifier system, ensuring a high-quality outcome for this specific vertical slice. These non-goals are not omissions but strategic decisions to maintain project velocity and focus.

Architecture Benefits: The Power of Event-Driven Design

The approach taken in this Combat Vertical Slice yields significant architectural benefits, reinforcing our commitment to an event-driven paradigm. Firstly, by implementing functionality directly within the intended layers – rpg-toolkit for rules, rpg-api for orchestration, and rpg-dnd5e-web for presentation – we put code where it belongs. This inherently avoids the costly and complex process of migrating prototype code later, ensuring a clean and maintainable codebase from the outset. Secondly, this slice serves as a practical proof that our event system works with real features. By successfully integrating the Barbarian's Rage, we demonstrate that features can dynamically influence combat through events, validating a core design principle. This leads to a reusable combat foundation within the rpg-toolkit that can be expanded with numerous other features and mechanics without requiring extensive refactoring. Furthermore, the modular nature of this approach allows us to test each component locally without cross-repo complexity. The toolkit can be tested independently, the API can mock the toolkit and UI, and the UI can mock the API, significantly speeding up development and debugging cycles. Finally, this methodology enables incremental development. Each phase of the vertical slice delivers working, testable code, allowing for continuous integration and early feedback. This iterative process ensures that we are always building upon a stable and functional base, making the overall development process more agile and less prone to catastrophic integration failures. The long-term advantages of this clean, event-driven architecture include enhanced scalability, improved maintainability, and greater flexibility in adapting to new game mechanics or design changes.

For further reading on robust architectural patterns and event-driven systems, consider exploring resources from established software engineering principles. A great starting point for understanding decoupled systems and microservices is the documentation and articles available on microservices.io. Their insights into domain-driven design and event sourcing can provide valuable context for the architectural choices made here.

You may also like