2Brute2Best
πŸ”₯0
C7

XP Calculation, JSON Export & Damage Reports Over Entities

Pro OnlyVisitor std::variant + std::visit
⚑ 60-Second Architecture SummaryC7 · Visitor Pattern
Naive Bottleneck→Visitor Pattern Cure
The Naive Bottleneck: Entity classes suffer from Single Responsibility Principle (SRP) violations

Adding a new virtual method (`toJson()`, `combatScore()`, `minimapIcon()`) to `WorldEntity` on every tool request bloats entity classes with unrelated responsibilities and triggers full-engine rebuilds. Conversely, writing external `dynamic_cast` cascades is brittle, slow, and prone to silent failures when new types appear.

HeroEntity is bloated with combat math, JSON serialization formatting, and minimap UI rendering code that have nothing to do with core hero gameplay logic.

The Pattern Cure: Visitor Pattern

Represent an operation to be performed on the elements of an object structure, letting you define a new operation without changing the classes of the elements on which it operates.

We use Double Dispatch. WorldEntity declares one pure virtual method: virtual void accept(EntityVisitor& visitor) const = 0;. Each concrete entity implements accept in one line: visitor.visit(*this);. Because the type of this is statically known at compile time, the compiler dispatches to the correct overloaded visit(const HeroEntity&) method. Now, each new operation (CombatScoreVisitor, JsonExportVisitor, MinimapRenderVisitor) is encapsulated in its own separate class. Entities are never touched again when adding new operations.

Sprint 7 of 8 Β· Adding Combat Analytics, JSON Export & Minimap Passes Over Entities
Story C β€” 2D RPG Game Engine β€” Progressive System Build
1. Where our codebase left off
← C6

In Sprint 6 (C6 Β· Bridge), we decoupled high-level scene entities from rendering implementations, enabling clean cross-platform visuals across Vulkan, OpenGL, DX12, and Headless CI servers. With rendering modularized, separate teams now need to run multiple diverse passes across world entities.

2. What we are building now

We need to run distinct analytical and tooling operations across our closed set of world entities (HeroEntity, MonsterEntity, TrapEntity, LootChestEntity): combat balance scoring, structured JSON savegame export, and minimap UI icon generation.

3. The pain point we hit

Adding a new virtual method (toJson(), combatScore(), minimapIcon()) to WorldEntity on every tool request bloats entity classes with unrelated responsibilities and triggers full-engine rebuilds. Conversely, writing external dynamic_cast cascades is brittle, slow, and prone to silent failures when new types appear.

4. What this unlocks next
Next: C8 β†’

The Visitor Pattern (and modern std::variant + std::visit) adds operations non-invasively via double dispatch (accept(EntityVisitor&)), keeping entities pristine and closed for modification. Next in Sprint 8 (C8 Β· Interpreter), where quest designers want to write data-driven trigger strings ("level >= 10 AND HAS_ITEM DragonKey") and dynamic formulas in config files without C++ code changes.

01

The product ticket

Ticket Β· C7RPG-107 Β· Combat Analytics, JSON Export & Minimap Rendering Over Entities

Our RPG world contains a fixed, closed set of core world entities: HeroEntity, MonsterEntity, TrapEntity, and LootChestEntity. Multiple development teams need to execute new operations across these entities: balance designers need a Combat Threat Score Calculator, tools programmers need a JSON Save Exporter, and UI developers need a Minimap Icon Renderer.

Requirements
  • β€’

    Execute diverse operations across heterogeneous collections of world entities (std::vector<std::unique_ptr<WorldEntity>>).

  • β€’

    Calculate aggregate combat threat scores: Heroes sum level + gear stats, Monsters compute HP + rage attack power, Traps contribute trigger damage, Chests contribute zero (unless mimics).

  • β€’

    Export structured JSON representations of all entities for savegame files.

  • β€’

    Support adding new operations (e.g. XP reward calculator or telemetry logging) without modifying any existing entity classes.

The next requirement that breaks naive code

Next sprint, the telemetry team wants a Heatmap Death Analytics Exporter. In the naive design, adding an operation requires adding a new virtual method to WorldEntity and touching all four entity classes, creating frequent merge conflicts and triggering engine-wide recompiles.

02

Step 1 β€” The naive code & why it breaks

The naive approach adds a new virtual method to WorldEntity for every new operation (calculateCombatScore(), toJson(), minimapIcon()), forcing every entity subclass to implement every operation.

Naive design (what not to ship) Β· naive.cpp
#include <format>
#include <memory>
#include <print>
#include <string>
#include <vector>

// Naive design: Every new operation (combat score, JSON export, minimap rendering)
// forces us to add a new virtual method to the base class and touch EVERY entity!

class NaiveWorldEntity {
public:
    virtual ~NaiveWorldEntity() = default;
    virtual int calculateCombatScore() const = 0;
    virtual std::string toJson() const = 0;
    virtual std::string minimapIcon() const = 0;
    // Next sprint: virtual int calculateXpReward() = 0; // Forces editing all 4 subclasses!
};

class HeroEntity final : public NaiveWorldEntity {
    std::string name_;
    int level_;
    int attack_;
    int defense_;

public:
    HeroEntity(std::string name, int level, int attack, int defense)
        : name_(std::move(name)), level_(level), attack_(attack), defense_(defense) {}

    int calculateCombatScore() const override {
        return (level_ * 50) + (attack_ * 2) + defense_;
    }

    std::string toJson() const override {
        return std::format(R"({{"type":"Hero","name":"{}","level":{},"score":{}}})",
                           name_, level_, calculateCombatScore());
    }

    std::string minimapIcon() const override { return "[H]"; }
};

class MonsterEntity final : public NaiveWorldEntity {
    std::string name_;
    int hp_;
    int attack_;
    bool isBoss_;

public:
    MonsterEntity(std::string name, int hp, int attack, bool isBoss)
        : name_(std::move(name)), hp_(hp), attack_(attack), isBoss_(isBoss) {}

    int calculateCombatScore() const override {
        return (hp_ / 2) + (attack_ * (isBoss_ ? 4 : 2));
    }

    std::string toJson() const override {
        return std::format(R"({{"type":"Monster","name":"{}","hp":{},"boss":{}}})",
                           name_, hp_, isBoss_);
    }

    std::string minimapIcon() const override { return isBoss_ ? "[B]" : "[M]"; }
};

class TrapEntity final : public NaiveWorldEntity {
    std::string type_;
    int damage_;
    bool disarmed_;

public:
    TrapEntity(std::string type, int damage, bool disarmed)
        : type_(std::move(type)), damage_(damage), disarmed_(disarmed) {}

    int calculateCombatScore() const override {
        return disarmed_ ? 0 : damage_;
    }

    std::string toJson() const override {
        return std::format(R"({{"type":"Trap","hazard":"{}","dmg":{},"disarmed":{}}})",
                           type_, damage_, disarmed_);
    }

    std::string minimapIcon() const override { return disarmed_ ? "[.]" : "[X]"; }
};

int main() {
    std::vector<std::unique_ptr<NaiveWorldEntity>> world;
    world.push_back(std::make_unique<HeroEntity>("Valeria", 10, 30, 20));
    world.push_back(std::make_unique<MonsterEntity>("Fire Drake", 400, 45, true));
    world.push_back(std::make_unique<TrapEntity>("Poison Spikes", 80, false));

    int totalScore = 0;
    std::println("--- Naive Entity Methods ---");
    for (const auto& entity : world) {
        totalScore += entity->calculateCombatScore();
        std::println("Icon: {} | JSON: {}", entity->minimapIcon(), entity->toJson());
    }
    std::println("Total Combat Threat Score: {}", totalScore);

    return 0;
}
Entity classes suffer from Single Responsibility Principle (SRP) violations

HeroEntity is bloated with combat math, JSON serialization formatting, and minimap UI rendering code that have nothing to do with core hero gameplay logic.

Violates Open-Closed Principle (OCP) on every new feature

Adding a single new operation (like calculateXpReward()) forces you to modify and recompile WorldEntity, HeroEntity, MonsterEntity, TrapEntity, and LootChestEntity.

Fragile dynamic_cast workarounds

Teams avoiding entity modification write external loops with if (auto* h = dynamic_cast<HeroEntity*>(e)) chains. Missing a type fails silently at runtime with zero compiler warnings.

03

Step 2 β€” The fix: Visitor Pattern

Core idea in one sentence

Represent an operation to be performed on the elements of an object structure, letting you define a new operation without changing the classes of the elements on which it operates.

We use Double Dispatch. WorldEntity declares one pure virtual method: virtual void accept(EntityVisitor& visitor) const = 0;. Each concrete entity implements accept in one line: visitor.visit(*this);. Because the type of this is statically known at compile time, the compiler dispatches to the correct overloaded visit(const HeroEntity&) method. Now, each new operation (CombatScoreVisitor, JsonExportVisitor, MinimapRenderVisitor) is encapsulated in its own separate class. Entities are never touched again when adding new operations.

Pattern roles in our story
Element (Interface)

WorldEntity β€” declares virtual void accept(EntityVisitor& visitor) const = 0;.

Concrete Elements

HeroEntity, MonsterEntity, TrapEntity, LootChestEntity β€” implement accept(visitor) by executing visitor.visit(*this).

Visitor (Interface)

EntityVisitor β€” declares overloaded visit() methods for each concrete element type (visit(const HeroEntity&), visit(const MonsterEntity&), etc.).

Concrete Visitors

CombatScoreVisitor, JsonExportVisitor, MinimapRenderVisitor β€” encapsulate specific operations and accumulate results as they traverse entities.

Brute2Best Pro Content

Unlock Full UML Diagram & C++23 Solution

The interactive UML architecture diagrams, refactored C++23 code, real-world extensions, and interview Q&A are part of Brute2Best Pro. All 7 foundation chapters are 100% free.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Systems Integration

Related C++ Systems & Algorithmic Foundations

Deep dive into underlying layers