Reconnection

Empire, Resistance, and Identity on the Continent of Duns: A Turn-Based RPG of Colonial Conquest

Project Overview

Reconnection is a D&D-inspired turn-based RPG built in Unreal Engine 5, set on the continent of Duns. The eastern empire of Serontil is pushing westward, encroaching on the ancestral lands of two proud indigenous nations: Gelantis, a northwestern civilization with echoes of the Aztec and Incan peoples, and Ysgilati, a southwestern nation with roots reminiscent of the Iroquois and Cherokee. Players navigate this morally complex world of conquest, identity, survival, and resistance through tactical combat and narrative choice.

As Lead Engineer, I architected the original combat system in C++, implementing dice-roll mechanics, stat-based calculations, and AI decision-making. That first production ran from August 2025 to February 2026, followed by a development hiatus.

Production resumed on May 27, 2026 under Mystic Die LLC with a significantly expanded team, rebuilt from a clean repository. My focus in this phase has shifted from gameplay systems alone to the engine-level foundations and the tooling the whole team commits through: a nullable vector type that removed a recurring class of sentinel-value bugs, the module architecture and written C++ standard the team codes against, a Blueprint diffing pipeline that made binary assets reviewable in pull requests, and an in-house version control application.

The expansion also changed the scope of my responsibilities. I serve as Producer alongside co-producer Jadyn Englett, coordinating a team of more than 30 people across engineering, art, design, narrative, and audio, and as Technical Director, reviewing every change that enters the codebase. I additionally hold the narrative lead position on an interim basis while the role is filled permanently, and I lead the backend of the studio's web presence.

Role on the Project

Four responsibilities on this production are mine. The engineering and tooling work documented below follows directly from the other three. I carry all four alongside a full-time engineering role at Starchild PBC.

Producer

Co-producing more than 30 people across eight departments on a six-sprint schedule, with a board of over 200 tracked tasks measured against dated milestones

Technical Director

Every change entering the project is reviewed by me. I authored the C++ standard, restructured the module layout, and built the pipeline that renders binary Blueprint changes readable in a diff

Interim Narrative Lead

Directing campaign beat sheets, character profiles, and the branching dialogue pipeline until the position is filled permanently, under the cultural responsibility this setting carries

Backend Web Lead

Responsible for the studio's web backend: the public site, an employee dashboard built on Django and React, and the internal applications the production depends on

Technical Highlights

Nullable Vector Type

FPRVector mirrors the full FVector API while letting a vector be meaningfully absent, with a complete Blueprint surface

Reviewable Blueprint Diffs

A pre-commit pipeline that mirrors every binary .uasset into committed Markdown so pull requests show what actually changed

In-House Version Control

A desktop application wrapping build, editor launch, time tracking, and an admin dashboard over Git for a non-programmer team

Engine Systems & Team Tooling (2026)

When production resumed under Mystic Die LLC, the bottleneck was no longer combat logic; it was everything around it. A nine-person engineering group inside a team of thirty was committing binary Unreal assets that nobody could review, writing C++ against no shared standard, and losing work to a version control workflow most of them were not comfortable with. As Technical Director I am the final reviewer before any change lands, which makes an unreviewable diff a failure of process rather than an inconvenience. These four systems were built to address that.

FPRVector: A Nullable Vector Type

Unreal has no way to say "this vector is absent." Code either overloads FVector::ZeroVector to mean "none" (which breaks the moment the origin is a valid answer) or carries a parallel bool that drifts out of step with the value it guards. FPRVector makes absence part of the value. The API mirrors FVector in full, governed by three rules that let null propagate predictably instead of requiring a check at every call site.

Null Propagation Rules & Core Interface

//A nullable FVector, so a vector can be meaningfully absent instead of overloading
//FVector::ZeroVector or a sentinel like (MAX_flt, MAX_flt, MAX_flt) to mean "none".
//Null is the default state.
//
//The API mirrors FVector. Three rules cover the whole surface:
//  - anything FVector returns as a vector comes back as an FPRVector that is null
//    if any operand was null
//  - anything FVector returns as a scalar comes back as a TOptional<double> that is
//    unset if any operand was null
//  - anything FVector answers with a bool answers false when an operand is null,
//    except Equals and operator==, which treat two nulls as equal and a null as
//    never equal to a set vector
//
//Mutators do nothing while null, so a null vector cannot be half-written into a set
//one. Construction from FVector is implicit, so every function also accepts a plain
//FVector.
USTRUCT( BlueprintType, meta = ( HasNativeMake = "...MakePRVector", HasNativeBreak = "...BreakPRVector" ) )
struct PROJECTRECONNECTION_API FPRVector
{
    GENERATED_BODY()
public:
    //The one constant FVector has no counterpart for.
    static FPRVector Null();

    bool IsSet() const;
    bool IsNull() const;

    //Asserts when null. Prefer Get( Default ) or TryGet() unless the value is
    //already known to be set.
    const FVector& GetValue() const;
    const FVector& Get( const FVector& DefaultValue ) const;

    //Leaves OutValue untouched when null.
    bool TryGet( FVector& OutValue ) const;

    //Component access returns TOptional rather than a reference. FVector's
    //reference-returning overloads have no counterpart here, because handing out a
    //reference into a null vector would let a caller write one component and leave
    //the other two undefined.
    TOptional<double> GetX() const;
    TOptional<double> Component( int32 Index ) const;

    //Vector in, vector out: null if either operand is null.
    FPRVector operator+( const FPRVector& V ) const;
    FPRVector Cross( const FPRVector& V ) const;

    //Vector in, scalar out: unset if either operand is null.
    TOptional<double> Dot( const FPRVector& V ) const;
    TOptional<double> Size() const;
    static TOptional<double> Dist( const FPRVector& V1, const FPRVector& V2 );

    //Two nulls are equal. A null never equals a set vector, whatever the set one holds.
    bool operator==( const FPRVector& V ) const;
    bool Equals( const FPRVector& V, double Tolerance = UE_KINDA_SMALL_NUMBER ) const;

    //ResultIfZero defaults to null, so an unnormalizable vector reports itself as one
    //rather than quietly reporting itself as zero.
    FPRVector GetSafeNormal( double Tolerance = UE_SMALL_NUMBER,
                             const FPRVector& ResultIfZero = FPRVector() ) const;

private:
    //Only meaningful when bIsSet, and kept zeroed while null so serialized data
    //stays stable.
    UPROPERTY( EditAnywhere, BlueprintReadOnly, meta = ( EditCondition = "bIsSet" ) )
    FVector Value = FVector::ZeroVector;

    UPROPERTY( EditAnywhere, BlueprintReadOnly, meta = ( DisplayName = "Has Value" ) )
    bool bIsSet = false;
};
                        
Blueprint Surface & Branching On Null

//Blueprints have no unset float and no TOptional, so the struct's C++ surface cannot
//map one to one. Two shapes cover the gap: scalar results come back as a bool plus an
//out parameter, and functions that mutate the vector take it as a ref parameter.
//
//Only the members Blueprints genuinely cannot represent are missing: GetValue (it
//asserts on null, so Get Or Default and Try Get stand in), ToOptional and FromOptional,
//the compound assignment operators (a node output feeding a Set does the same job),
//and NetSerialize and GetTypeHash, which the engine calls directly.
UCLASS()
class PROJECTRECONNECTION_API UPRVectorLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()
public:
    //Registered as the struct's NativeMakeFunc and NativeBreakFunc, so the standard
    //Make and Break nodes work on FPRVector exactly as they do on FVector.
    static FPRVector MakePRVector( FVector Value );
    static void BreakPRVector( const FPRVector& PRVector, FVector& Value, bool& bHasValue );

    //BlueprintAutocast, so a plain FVector pin connects straight into an FPRVector pin.
    static FPRVector Conv_VectorToPRVector( FVector Value );
};

//Paired with the library so a graph can branch on null as execution flow rather than
//threading a bool through every downstream node.
UENUM( BlueprintType )
enum class EPRVectorBranch : uint8
{
    HasValue,
    Null
};
                        
Adoption: Click-To-Move Destination Caching

//The first real adoption site. Click to move needs to answer "has the player chosen a
//destination yet?" on every tick of a held input. The previous shape was an FVector
//plus a separate bHasDestination flag, which is two things to keep in agreement.
class PROJECTRECONNECTION_API AExplorationPlayerController : public APlayerController
{
protected:
    void HandleSetDestinationStarted();
    void HandleSetDestinationTriggered();
    void HandleSetDestinationReleased();

    //Runs a cursor trace and writes the hit location, leaving the destination null
    //when the trace lands on nothing.
    void UpdateDestinationFromCursor();

    //Null until a cursor trace lands somewhere, so "no destination yet" is part of the
    //value rather than a separate flag that can drift out of step with it.
    FPRVector CachedDestination;

    float FollowTime = 0.0f;
};
                        

Module Architecture & A Written C++ Standard

With several engineers landing C++ on this project for the first time, "follow the surrounding file" was not a usable rule, because the surrounding files disagreed. I restructured the module and wrote STYLE.md at the repository root as the authority the team codes against. The layout change is the part with real teeth: dropping Unreal's conventional Public/ and Private/ split depends on a single build setting, so the document records why, and nobody removes the line and breaks every include in the module. That document is what makes review tractable at this team size: 121 lines covering file layout, naming, comment style, and formatting, so that a review comment cites a rule the team has already agreed to rather than a reviewer's personal preference.

Feature-Folder Layout & The Build Setting It Rests On

# One type per file. Every enum, struct, class, and function library gets its own
# header, including a helper enum that exists only for the type beside it.
#
# No Public/ and Private/ split. A header and its .cpp sit next to each other in a
# folder named for the feature. The module root is the include root.

Source/ProjectReconnection/
    Math/
        PRVector.h            # the struct
        PRVector.cpp
        PRVectorBranch.h      # the enum it is used with
        PRVectorLibrary.h     # the Blueprint function library for it
        PRVectorLibrary.cpp
    Player/
        ExplorationPlayerController.h
        ExplorationPlayerController.cpp
    UI/
        Subsystem/
            UIMenuManager.h
            UIMenuManager.cpp

# This layout depends on PublicIncludePaths.Add( ModuleDirectory ) in
# ProjectReconnection.Build.cs. Unreal Build Tool only adds the module root
# automatically through its legacy include paths, which modern build settings turn
# off, so removing that line breaks every include in the module.
#
# Include your own headers by their path from the module root:
#   #include "Math/PRVector.h"        yes
#   #include "../Math/PRVector.h"     no
                        
Type Prefix Convention

//Keep Unreal's type prefixes (F, U, A, E, I). On top of that:
//
//Types that do not extend an Unreal class are prefixed PR. Plain structs, plain
//classes, and enums we author are project types and carry the project prefix.
struct FPRVector { ... };               //ours, no Unreal base
enum class EPRVectorBranch : uint8 {};  //ours

//Types that extend an Unreal class do not take the PR prefix. They are named for
//what they are.
class UUIMenuManager : public UEnhancedInputLocalPlayerSubsystem { ... };
class AExplorationPlayerController : public APlayerController { ... };

//The one place PR appears on an Unreal-derived class is when it is part of the name
//of the project type the class serves, not a prefix on the class itself:
//UPRVectorLibrary is the function library for FPRVector.

//Comments are written //words here, with no space after the slashes. This applies to
//documentation comments too; a multi-line comment is several // lines, never a block
//comment.
                        

Blueprint Digests: Making Binary Assets Reviewable

A .uasset is binary, so every pull request touching a Blueprint showed Binary file differs and went through unreviewed. Git can be taught to diff binary files with a textconv driver, but that only works on a machine that can run the converter, and GitHub renders pull request diffs on its own servers with no Unreal Engine available. Converting ahead of time and committing the result is what makes the diff visible where review actually happens. There are now 78 digests committed beside their assets, regenerated automatically so a digest can never disagree with the Blueprint it describes.

Digest Output: What A Reviewer Actually Reads

# WBP_Objectives

- Path: `/Game/UI/Widgets/WBP_Objectives.WBP_Objectives`
- Parent: `UserWidget`

## Variables

- **Objective**: `text` = `"Get to the Chopper!"` - editable

## Functions

### UpdateObjectiveText()

    Set Text  (Text <- Get Objective, self <- Get ObjectiveText)

## Graphs

### EventGraph (Ubergraph)

    Event Construct
    Event Pre Construct
    UpdateObjectiveText
    Event Tick

## Asset dependencies

- `/Game/UI/Widgets/WBP_Objectives.WBP_Objectives_C`
                        
Pre-Commit Generation & Cost Control

# For every Content/**/BP_Thing.uasset there is a Content/**/BP_Thing.bp.md committed
# beside it. Git diffs the Markdown; GitHub renders it in the PR.
#
#   Content/TurnManagerAssets/
#     BP_CombatManager.uasset      binary, unreviewable
#     BP_CombatManager.bp.md       what a reviewer actually reads

git add Content/TurnManagerAssets/BP_CombatManager.uasset
git commit -m "combat: end turn when enemies are cleared"
  [bp-digest] serializing 1 Blueprint(s) via Unreal (this takes a minute)
  [bp-digest] updated 1 digest(s)
  [bp-digest] staged them

# Staging a Blueprint regenerates and stages its digest in the same commit, so the two
# can never disagree. Commits that touch no Blueprints skip Unreal entirely and cost
# about 0.3s: the generator reads .uasset headers directly to decide whether starting
# the editor is worth it.
#
# The hook also declines to run mid-merge, mid-rebase, mid-cherry-pick, and mid-revert.
# Those commits are resolving history that already exists, and rewriting a tracked file
# underneath them would fight the operation.

python Tools/BlueprintDigest/generate.py --staged --stage   # what the hook runs
python Tools/BlueprintDigest/generate.py --all              # rebuild everything
BP_DIGEST_SKIP=1 git commit ...                             # skip for one commit
                        

Unreal Version Control: A Git Front End For The Whole Team

Most of the team are artists, designers, and narrative writers for whom a Git command line is a daily obstacle rather than a tool. UVC is a Python and CustomTkinter desktop application that puts the operations they actually need behind buttons: compile the project's C++ and open it in Unreal, clock in and out with a background activity monitor, and, for admins, review users, roles, and time data on a dashboard. Two files, .uvc_config.json and .uvc_timeclock.json, are committed so the whole team shares them, which creates a merge problem that the pre-commit hook solves.

Shared Timeclock Reconciliation

# Two jobs before a commit is created:
#   1. Reconcile .uvc_timeclock.json with origin/main so parallel sessions from
#      different teammates merge instead of clobbering each other.
#   2. Regenerate the readable .bp.md digest beside any staged Blueprint, so
#      pull requests show what changed instead of "Binary file differs".
#
# Escape hatches:
#   UVC_TIMECLOCK_SKIP=1 git commit ...      skip the timeclock merge
#   UVC_TIMECLOCK_NO_FETCH=1 git commit ...  merge against the cached ref, no network
#   BP_DIGEST_SKIP=1 git commit ...          skip Blueprint digests
#   git commit --no-verify ...               skip all hooks

# Git for Windows ships its own sh but no python, so the hook probes the usual names
# rather than assuming an interpreter is on PATH. If none is found it warns and exits
# clean, because a missing interpreter should not block a teammate's commit.
for candidate in python3 python py; do
    if command -v "$candidate" >/dev/null 2>&1; then
        PYTHON="$candidate"
        break
    fi
done
                        

Production & Studio Tooling

Producing a team of this size and leading the studio's web backend impose the same requirement: coordination holds only when the tools the team uses daily are dependable. The following applications are the ones I built and maintain outside the engine, each deployed to a studio subdomain.

Issue Kanban

A PHP front end over the studio's GitHub Projects board, providing drag-and-drop columns, a sprint selector, per-person completion and story-point statistics, and full issue editing. GitHub remains the source of truth, so the board cannot drift from it, and the OAuth token is held server-side rather than exposed to the browser

Employee Dashboard

A Django REST and React dashboard for the studio. I built the founding version on both sides, covering GitHub OAuth, a bridge into the Kanban board, and the contribution tracking API, and I lead it as another engineer continues its development

Mood Board

A shared drawing surface for the concept art department, supporting image upload and resizing, shapes, text, and freehand sketching, stored server-side so boards can be reopened and extended. Built on PHP and Fabric.js with flat JSON storage and no database

Roll for Inspiration

A Slack-native gacha in which the collectibles are the studio's actual reference material, each card annotated with where it applies to the project. Built in Python and SQLAlchemy, synchronized from a producer-authored sheet, with a two-currency economy and pity rules across every rarity tier

Each of these follows the same constraint: no database where flat files are sufficient, and no build step. A deployment consists of pulling the repository onto a subdomain, which keeps every one of them transferable to another maintainer without a toolchain to install first.

Original Production (2025 to 2026)

The first build of Reconnection ran from August 2025 to February 2026. I was Lead Engineer and wrote the combat system in C++ from scratch: a d20 resolution layer, an initiative-driven turn manager, and a utility-scored enemy AI. That codebase was retired when production restarted from a clean repository in May 2026, and the current game is being built on a different combat architecture. The work below is preserved as a record of that first production rather than a description of the game as it stands today.

D&D Combat System

Automated dice rolls, attack/defense calculations, and turn management

Turn-Based Mechanics

UTurnManager auto-discovers fighters, sorts by initiative, and drives the full round/turn loop via multicast delegates

Enemy AI System

Utility-scored AI weighs attack, heal, block, and buff actions each turn using health ratios, line-of-sight, and weapon type affinity

C++ Architecture

Clean, extensible parent classes with virtual functions

Buff System

Dynamic stat modifications for strategic depth

Dialogue Choices

Player-driven narrative with branching conversations

Blueprint Integration & Designer Tools

The C++ AFighter and UEnemy classes are designed for Blueprint extensibility, allowing designers to create unique enemy behaviors, player abilities, and combat encounters without touching code. Key Blueprint-implementable functions include:

🤖 Enemy AI Behaviors

  • ChooseAction() - Implement custom AI decision trees
  • Conditional logic - Enemy evaluates player health, buffs, and position
  • Action selection - Choose between Attack, Heal, Block, or special abilities
  • Difficulty scaling - Adjust AI aggression and strategy per encounter

⚔️ Combat Actions

  • Attack() - Custom attack animations and VFX triggers
  • Heal() - Healing effects with particle systems
  • Block() - Defensive stance animations
  • Die() - Death sequences, loot drops, victory conditions

🎯 Event Delegates

  • OnStartTurn - Trigger UI updates, camera effects
  • OnEndTurn - Queue next fighter in initiative order
  • OnHitAttack - Play hit reactions, damage numbers
  • OnHitMiss - Miss animations, combat feedback

💬 Dialogue Integration

  • Dynamic choices - Player dialogue affects combat stats
  • Narrative branching - Choices influence enemy behavior
  • Combat triggers - Dialogue can start/end encounters
  • Character relationships - Track player decisions for story outcomes

AFighter Class - Combat System Foundation

The AFighter parent class implements all core combat mechanics including turn management, dice-roll calculations (d20 system), damage/healing, and buff systems. Designed with Blueprint integration for designer flexibility while maintaining C++ performance.

Fighter Base Class: Properties & Public Interface

// Base class for every combatant. Both player characters and enemies
// inherit from AFighter so UTurnManager can drive them identically.
UCLASS()
class RECONNECTION_API AFighter : public AActor
{
    GENERATED_BODY()
public:
    AFighter();

    // Whether it is currently this fighter's turn.
    // Set true by UTurnManager before calling StartTurn().
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stored Variables")
    bool bIsTurn;

    // Rolled once at combat start; UTurnManager sorts descending
    // so the highest score acts first (classic D&D initiative order).
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stored Variables")
    int32 InitiativeScore;

    // Sets bIsTurn = true and broadcasts the OnStartTurn delegate
    // so Blueprint can respond with UI highlights, camera moves, etc.
    UFUNCTION(BlueprintCallable, Category="Stored Functions")
    void StartTurn();

    // Sets bIsTurn = false and broadcasts OnEndTurn,
    // which UTurnManager listens to in order to advance the queue.
    UFUNCTION(BlueprintCallable, Category="Stored Functions")
    void EndTurn();

    // Routes a typed damage value to the target's ReceiveDamage().
    // Type string ("Physical", "Magic", etc.) allows resistances later.
    UFUNCTION(BlueprintCallable, Category="Stored Functions")
    void SendDamage(float Damage, const FString& Type, AFighter* Target);

    // Applies incoming damage. Virtual so UEnemy can override
    // to cache LastDamageReceived for block utility scoring.
    UFUNCTION(BlueprintCallable, Category="Stored Functions")
    virtual void ReceiveDamage(float Damage, const FString& Type);

protected:
    virtual void BeginPlay() override;
};
                        

Turn Management & Combat Flow

Implements initiative-based turn order system with clear turn start/end demarcation. The turn management system allows for complex action queuing and supports both player and AI-controlled fighters.

Turn State Flow

// Constructor: initializes bIsTurn = false and InitiativeScore = 0.
// PrimaryActorTick is disabled — all event flow is delegate-driven.
AFighter::AFighter();

// Sets bIsTurn = true and logs entry.
// UTurnManager calls this after advancing CurrentTurnIndex.
void AFighter::StartTurn();

// Sets bIsTurn = false and logs exit.
// Broadcasts OnEndTurn so UTurnManager knows to call NextTurn().
void AFighter::EndTurn();

// Logs the outgoing damage type and amount,
// then calls Target->ReceiveDamage() to apply the value.
void AFighter::SendDamage(float Damage, const FString& Type, AFighter* Target);

// Base implementation logs received damage.
// UEnemy overrides this to also cache LastDamageReceived
// so GetBlockUtility() can score based on recent incoming hits.
void AFighter::ReceiveDamage(float Damage, const FString& Type);
                        

D&D-Style Dice Roll System

From the original UFighter implementation: Authentic d20 attack rolls with modifiers, defense calculations, and random damage ranges. This creates unpredictable, strategic combat that feels like tabletop D&D.

Attack Resolution Logic

// Generates a hit value by rolling 1d20 and adding BaseAttack + AttackBuff.
// Higher result = better chance to overcome the target's defense.
int UFighter::RollToHit();

// Returns BaseDefense + DefenseBuff.
// The target's armor class equivalent — what an attacker must beat to land a hit.
float UFighter::GetDefense();

// Full attack action sequence:
//   1. Calls RollToHit() and compares against Target->GetDefense().
//   2. On a hit: calls RollDamage() for a randomized value (MinDamage..MaxDamage + DamageBuff),
//      passes it to SendDamage(), then broadcasts OnHitAttack for Blueprint VFX/SFX hooks.
//   3. On a miss: broadcasts OnHitMiss for miss animations and feedback.
//   4. Always calls EndTurn() to return control to UTurnManager.
void UFighter::Attack(UFighter* Target);

// Returns a random float in [MinDamage, MaxDamage] plus DamageBuff.
// Called inside Attack() after a successful roll-to-hit check.
float UFighter::RollDamage();

// Blueprint-native event: applies incoming damage with reduction.
//   1. Subtracts DamageReduction from Damage for an effective hit value.
//   2. Clamps CurrentHealth to [0, MaxHealth].
//   3. If CurrentHealth reaches 0, calls Die() and broadcasts OnDeath.
void UFighter::ReceiveDamage_Implementation(float Damage);
                        

Strategic Action System

Multiple combat actions beyond basic attacks: healing, blocking, and buff management. Each action has strategic tradeoffs, encouraging thoughtful decision-making during combat.

Heal, Block, and Buff Application

// Blueprint-native event: restores health by BaseHeal, clamped to MaxHealth.
// Immediately calls EndTurn() — healing costs the fighter's full action.
void UFighter::Heal_Implementation();

// Blueprint-native event: sets DamageReduction to BaseBlock + BlockBuff.
// This value is subtracted in ReceiveDamage_Implementation for the next hit received.
// Calls EndTurn() after committing the defensive stance.
void UFighter::Block_Implementation();

// Adds BuffAmount to the named stat's buff field.
// Stat keys: "Attack", "Damage", "Defense", "Block", "Heal".
// Buff values stack additively and are included in all relevant roll calculations.
// Complements RemoveBuff(), which decrements by the same amount when a buff expires.
void UFighter::AddBuff(float BuffAmount, const FString& stat);
void UFighter::RemoveBuff(float BuffAmount, const FString& stat);
                        

UEnemy - Fighter Enemy Architecture

UEnemy inherits from UFighter and adds a full utility-based AI decision system. Enemies track allies and opponents separately, evaluate weighted utility scores for every possible action, and automatically choose the highest-value action each turn. Weapon type awareness (Melee, Ranged, Magic) further biases the AI's attack preference, and all utility weights are EditAnywhere so designers can tune behavior per enemy type without touching code.

Enemy Class: Architecture & Designer-Facing Properties

// Weapon type determines which attack utility scorer is preferred.
// Designers set this per enemy in the Details panel; no code change required.
UENUM(BlueprintType)
enum class EWeaponType : uint8 { Melee, Ranged, Magic };

UCLASS(ClassGroup=(Fighters), meta=(BlueprintSpawnableComponent))
class RECONNECTION_API UEnemy : public UFighter
{
    GENERATED_BODY()
public:
    // Overrides StartTurn() to immediately call ChooseAction() after setup.
    virtual void StartTurn() override;

    // Blueprint-implementable AI entry point. The C++ implementation
    // evaluates each utility scorer and calls the highest-scoring action.
    // Designers can override this in Blueprint to create fully custom behaviors.
    UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category="Enemy|Combat")
    void ChooseAction();
    virtual void ChooseAction_Implementation();

    // Builds the Allies/Enemies rosters from the full fighter list.
    // Called once during UTurnManager::InitializeCombat().
    UFUNCTION(BlueprintCallable, Category="Enemy|Setup")
    void InitializeEnemy(const TArray<UFighter*>& AllFighters);

    // Rebuilds rosters when a fighter joins or dies mid-combat.
    UFUNCTION(BlueprintCallable, Category="Enemy|Setup")
    void UpdateAlliesAndEnemies(const TArray<UFighter*>& AllFighters);

    // Delegate receiver bound to UTurnManager's OnFighterJoined and OnFighterDeath.
    // Looks up the active UTurnManager and triggers UpdateAlliesAndEnemies.
    UFUNCTION(BlueprintCallable, Category="Enemy|Setup")
    void OnFighterListChanged(UFighter* ChangedFighter);

    // Overrides ReceiveDamage to cache LastDamageReceived before calling Super,
    // making recent incoming damage available to GetBlockUtility().
    virtual void ReceiveDamage(float Damage) override;

    // --- Utility scorers (one per possible action) ---
    // Each returns a float score; ChooseAction_Implementation picks the highest.
    UFUNCTION(BlueprintCallable, Category="Enemy|Utility")
    float GetAttackUtility();   // Aggression vs closest visible enemy
    float GetMeleeUtility();    // Preferred weapon type affinity
    float GetRangedUtility();
    float GetMagicUtility();
    float GetSelfHealUtility(); // Urgency based on own health ratio
    float GetAllyHealUtility(); // Need based on most wounded visible ally
    float GetBlockUtility();    // Reactivity based on last hit received
    float GetBuffUtility();     // Reward for buffing when no active buffs exist

    // All weights are EditAnywhere so designers tune behavior per-enemy
    // in the Details panel without touching any C++.
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    float AttackUtilityWeight;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    bool bHasMelee;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    bool bHasRanged;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    bool bHasMagic;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    float HealUtilityWeight;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    bool bHasSelfHeal;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    bool bHasAllyHeal;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    float BuffUtilityWeight;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    bool bHasBuff;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    float BlockUtilityWeight;
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="Enemy|Utility|Weight")
    bool bHasBlock;

    UFUNCTION(BlueprintCallable, Category="Enemy|StorageAccess")
    UFighter* GetClosestEnemy();
    UFUNCTION(BlueprintCallable, Category="Enemy|StorageAccess")
    UFighter* GetLowestAlly();

private:
    TArray<UFighter*> Allies;
    TArray<UFighter*> Enemies;
    float LastDamageReceived;   // Cached for block utility
    int ClosestEnemyIndex;
    int LowestAllyIndex;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Enemy|Weapon",
              meta=(AllowPrivateAccess="true"))
    EWeaponType CurrentWeaponType = EWeaponType::Melee;
};
                        
Enemy Roster Initialization and Faction Tracking

// Iterates every fighter in AllFighters (provided by UTurnManager).
// Checks whether each fighter's owner also carries a UEnemy component:
//   - Same faction (UEnemy present): added to the Allies array.
//   - Opposing faction (no UEnemy): added to the Enemies array.
// Skips self to avoid self-targeting.
void UEnemy::InitializeEnemy(const TArray<UFighter*>& AllFighters);

// Same logic as InitializeEnemy but clears both arrays first.
// Called mid-combat whenever a fighter joins or dies,
// ensuring utility scorers always work against a fresh roster.
void UEnemy::UpdateAlliesAndEnemies(const TArray<UFighter*>& AllFighters);

// Receives the UFighter* that changed (join or death) from UTurnManager delegates.
// Iterates world objects to find the active UTurnManager,
// then calls UpdateAlliesAndEnemies with its current Fighters array.
void UEnemy::OnFighterListChanged(UFighter* ChangedFighter);

// Calls Super::ReceiveDamage() for standard health reduction,
// then stores the Damage value in LastDamageReceived
// so GetBlockUtility() can score the block action based on recent pressure.
void UEnemy::ReceiveDamage(float Damage);
                        
Utility AI Scoring Functions

// Attack utility — drives aggression toward the nearest visible enemy.
//   1. Finds the closest enemy by horizontal distance and records its index.
//   2. Returns 0 if no enemies are in range (for melee) or if no line-of-sight exists.
//   3. Scores via -ln(healthRatio) so near-dead targets produce exponentially
//      higher values, making finishing moves feel natural.
//   4. Final score is multiplied by AttackUtilityWeight (designer-tunable).
float UEnemy::GetAttackUtility();

// Weapon affinity scalars — preferred weapon type scores 1.0;
// off-type variants score lower to bias the AI toward its equipped weapon.
// Returns 0 if the enemy doesn't possess that weapon type.
float UEnemy::GetMeleeUtility();
float UEnemy::GetRangedUtility();
float UEnemy::GetMagicUtility();

// Self-heal utility — exponential urgency scaled by own health ratio.
//   Returns FLT_MAX (force-heal) when health drops to or below 10%,
//   guaranteeing survival over any other action at critical thresholds.
float UEnemy::GetSelfHealUtility();

// Ally-heal utility — finds the most wounded visible ally and scores
// by their need using the same exponential curve as self-heal.
// LowestAllyIndex is stored so ChooseAction knows which ally to target.
float UEnemy::GetAllyHealUtility();

// Block utility — reactive defense scored on recent incoming damage.
//   Uses exp(damageRatio * weight) - 1 so enemies who just took a heavy hit
//   will prioritize blocking next turn proportionally to how hard they were hit.
float UEnemy::GetBlockUtility();

// Buff utility — rewards the first buff action highly (score = 1.0),
// then diminishes per already-active buff using 0.5 / BuffTracker.Num().
// Prevents enemies from wasting turns stacking redundant buffs.
float UEnemy::GetBuffUtility();
                        

Comprehensive Stat System

Tracks 15+ combat statistics including initiative, health, damage ranges, attack bonuses, defense values, and buff modifiers. Provides a complete stat query system for UI and game logic.

Complete Stat Roster

// Returns all 15 combat stats in a flat array in this order:
//   InitiativeScore, MaxHealth, CurrentHealth,
//   MinDamage, MaxDamage, DamageBuff,
//   BaseAttack, AttackBuff,
//   BaseDefense, DefenseBuff,
//   BaseBlock, BlockBuff,
//   BaseHeal, HealBuff,
//   DamageReduction
// Used by the UI to populate the stat panel without needing
// direct references to individual properties.
TArray<float> UFighter::GetAllStats();

// Blueprint-native event: routes outgoing damage to the target.
// Base implementation calls Target->ReceiveDamage(Damage) directly.
// Blueprint can override to insert hit effects, sounds, or damage-type logic.
void UFighter::SendDamage_Implementation(float Damage, UFighter* Target);

// Blueprint-native event: handles fighter death.
// C++ signals the event; Blueprint implements death animations,
// loot drops, VFX spawning, and any game-over state transitions.
void UFighter::Die_Implementation();
                        

UTurnManager - Combat Orchestration

UTurnManager is an ActorComponent that owns the entire combat loop. On BeginPlay it scans every actor in the world for UFighter components, sorts them by initiative, binds to their death and end-turn delegates, and waits for a StartCombat() call. Five Blueprint-assignable multicast delegates let UI, cameras, and audio respond to every state change without any hard coupling.

Turn Manager: Delegates & Public Interface

// Five multicast delegates give Blueprint full observability over combat state.
// UI, cameras, audio, and narrative systems bind to these without any C++ changes.
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTurnChanged,   UFighter*, CurrentFighter);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnRoundStarted);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCombatEnded);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFighterDeath,  UFighter*, DeadFighter);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFighterJoined, UFighter*, NewFighter);

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class RECONNECTION_API UTurnManager : public UActorComponent
{
    GENERATED_BODY()
public:
    UTurnManager();

    // The sorted fighter roster — publicly readable so UEnemy can query it.
    UPROPERTY(BlueprintReadOnly,  Category="Turn Manager") TArray<UFighter*> Fighters;
    UPROPERTY(BlueprintReadOnly,  Category="Turn Manager") int32 CurrentTurnIndex;
    UPROPERTY(BlueprintReadOnly,  Category="Turn Manager") int32 CurrentRound;
    UPROPERTY(BlueprintReadWrite, Category="Turn Manager") bool  bCombatActive;
    UPROPERTY(BlueprintReadOnly,  Category="Turn Manager") int32 EnemiesLeft = 0;
    // Set in Blueprint to control which level loads when all enemies die.
    UPROPERTY(BlueprintReadWrite, Category="Turn Manager") FName NextLevel;

    // Broadcast-assignable events — wire anything from Blueprint
    UPROPERTY(BlueprintAssignable, Category="Turn Manager|Events") FOnTurnChanged   OnTurnChanged;
    UPROPERTY(BlueprintAssignable, Category="Turn Manager|Events") FOnRoundStarted  OnRoundStarted;
    UPROPERTY(BlueprintAssignable, Category="Turn Manager|Events") FOnCombatEnded   OnCombatEnded;
    UPROPERTY(BlueprintAssignable, Category="Turn Manager|Events") FOnFighterDeath  OnFighterDeath;
    UPROPERTY(BlueprintAssignable, Category="Turn Manager|Events") FOnFighterJoined OnFighterJoined;

    // Scans the world for UFighter components, binds delegates, sorts by initiative.
    UFUNCTION(BlueprintCallable, Category="Turn Manager") void InitializeCombat();
    // Activates combat, resets state, and starts the first fighter's turn.
    UFUNCTION(BlueprintCallable, Category="Turn Manager") void StartCombat();
    // Advances CurrentTurnIndex, wraps at end of round, hands off to next fighter.
    UFUNCTION(BlueprintCallable, Category="Turn Manager") void NextTurn();
    UFUNCTION(BlueprintCallable, Category="Turn Manager") UFighter* GetCurrentFighter();
    // Descending sort by InitiativeScore — highest acts first.
    UFUNCTION(BlueprintCallable, Category="Turn Manager") void SortFightersByInitiative();
    // Removes fighter, adjusts index, calls EndCombat() if only one side remains.
    UFUNCTION(BlueprintCallable, Category="Turn Manager") void RemoveFighter(UFighter* Fighter);
    UFUNCTION(BlueprintCallable, Category="Turn Manager") void EndCombat();

private:
    // Bound to each fighter's OnDeath delegate during InitializeCombat().
    UFUNCTION() void HandleFighterDeath(UFighter* DeadFighter);
    // Bound to each fighter's OnEndTurn delegate — simply calls NextTurn().
    UFUNCTION() void HandleFighterEndTurn(UFighter* Fighter);
};
                        
Combat Initialization and Start Sequence

// BeginPlay calls InitializeCombat() automatically.
// StartCombat() is intentionally separate so Blueprint can delay the
// start with cutscenes, dialogue, or camera transitions.
void UTurnManager::BeginPlay();

// World scan phase:
//   1. Calls GetAllActorsOfClass to iterate every actor.
//   2. Checks each for a UFighter component; skips those without one.
//   3. Adds valid fighters to the Fighters array.
//   4. Binds each fighter's OnDeath and OnEndTurn delegates
//      to HandleFighterDeath and HandleFighterEndTurn respectively.
//   5. If the actor also has a UEnemy component:
//      - Calls InitializeEnemy() to build that enemy's ally/enemy rosters.
//      - Binds OnFighterJoined and OnFighterDeath to the enemy's
//        OnFighterListChanged so its rosters self-update mid-combat.
//      - Increments EnemiesLeft.
//   6. Broadcasts OnFighterJoined for each fighter (UI can react).
//   7. Calls SortFightersByInitiative() to finalize turn order.
void UTurnManager::InitializeCombat();

// Activation phase:
//   1. Guards against an empty Fighters array.
//   2. Sets bCombatActive = true, CurrentTurnIndex = 0, CurrentRound = 1.
//   3. Broadcasts OnRoundStarted.
//   4. Clears any stale bIsTurn flags from all fighters.
//   5. Calls StartTurn() on the first fighter in sorted order
//      and broadcasts OnTurnChanged.
void UTurnManager::StartCombat();
                        
Turn Advancement and Initiative Sorting

// Advances the combat queue:
//   1. Guards against inactive combat or an empty roster.
//   2. Increments CurrentTurnIndex.
//   3. If the index exceeds the last slot, wraps to 0,
//      increments CurrentRound, and broadcasts OnRoundStarted.
//   4. Clears lingering bIsTurn flags across all fighters.
//   5. Calls StartTurn() on the new CurrentFighter
//      and broadcasts OnTurnChanged.
void UTurnManager::NextTurn();

// Returns Fighters[CurrentTurnIndex] if the index is valid, else nullptr.
UFighter* UTurnManager::GetCurrentFighter();

// Sorts Fighters descending by InitiativeScore using a lambda comparator.
// Ties are resolved by the natural array order (earlier discovery = higher priority).
// Called once at the end of InitializeCombat().
void UTurnManager::SortFightersByInitiative();

// Delegate receiver bound during InitializeCombat.
// Calls NextTurn() as long as bCombatActive is true.
void UTurnManager::HandleFighterEndTurn(UFighter* Fighter);
                        
Fighter Removal, Victory, and Level Transition

// Safely removes a dead or retreating fighter from the sorted roster:
//   1. Finds the fighter's index; returns immediately if not found.
//   2. Removes the fighter from the array.
//   3. Adjusts CurrentTurnIndex to stay valid:
//      - If removed index is before current: decrement index.
//      - If removed index equals current and now out-of-bounds: wrap to 0.
//   4. If only one fighter (or zero) remain, calls EndCombat().
void UTurnManager::RemoveFighter(UFighter* Fighter);

// Sets bCombatActive = false and broadcasts OnCombatEnded.
// Blueprint listens to this event to show victory UI, trigger cutscenes, etc.
void UTurnManager::EndCombat();

// Bound to each fighter's OnDeath delegate during InitializeCombat().
//   1. Broadcasts OnFighterDeath (UEnemy.OnFighterListChanged listeners rebuild rosters).
//   2. Calls RemoveFighter() to clean up the roster and advance turn if needed.
//   3. If the dead fighter had a UEnemy component: decrements EnemiesLeft.
//      When EnemiesLeft reaches 0, calls UGameplayStatics::OpenLevel with NextLevel
//      to progress to the next combat encounter.
void UTurnManager::HandleFighterDeath(UFighter* DeadFighter);
                        

Production Resumed: A New Studio, A Bigger Team

Reconnection was on hiatus from February 2026 until May 27, 2026, when full production resumed under Mystic Die LLC from a clean repository. The scope of what we are building grew dramatically over the break, and the rebuild was a chance to put real engineering foundations underneath it before the team scaled up.

At GDC 2026, co-producer Jadyn Englett and I recruited nearly 30 people to join the project. We spoke with Josh Labelle, Creative Director of Disney Dreamlight Valley, about managing creative vision and maintaining design coherence across a team of this scale. Their insight on aligning a large, distributed team around a shared emotional core directly shaped how we structured the resumed production.

The growth from a small group to more than 30 contributors is what moved my role from engineering into production. Jadyn and I co-produce the project across eight departments: 2D art, 3D art, engineering, level design, UI, narrative, web, and leadership. Coordination runs through a board of over 200 tracked tasks organized into six sprints, from pre-production through a testable build, measured against dated milestones the studio commits to. My responsibility is determining what each department owns and resolving the dependencies between them before they block work.

I also serve as Technical Director. Every change entering the project is reviewed by me, and the standards those changes are measured against are ones I authored: a 121-line C++ style guide committed at the repository root, a module layout that replaces Unreal's conventional folder split, and a digest pipeline that makes the contents of a Blueprint legible in a pull request. Reviewing an entire team's Unreal work is only feasible when the diffs are readable, which is why the diff tooling came first.

Two further responsibilities accompanied the expansion. I hold the interim narrative lead position while we search for a permanent lead, directing campaign and region beat sheets, character profiles for both the party and the antagonist, and the Ink-authored dialogue that feeds the progression table tracking story state. The story of Duns must remain coherent across multiple writers and aligned with the cultural care the setting demands. I also lead the backend of the studio's web presence: the public-facing site, the employee dashboard, and a browser interface for the version control application, which together support recruitment, press, and eventually the Steam release.

That scale is also what drove the tooling work above. Onboarding a team this size onto a binary-asset Unreal project meant that code review, C++ conventions, and version control could not stay informal, so I spent the first months of the new production making those three things dependable before they became the constraint on everyone else's work.

We are actively planning a meeting with indigenous American cultural representatives to ensure that the stories of Gelantis and Ysgilati are handled with honesty, sensitivity, and genuine respect. Drawing on inspirations from Mesoamerican and North American indigenous cultures carries real responsibility, and we are committed to telling these stories in a way that honors those perspectives while crafting something fantastical, singular, and imaginative.

We will have a table at UCF's Digital Media Workshop Showcase 2026, where we plan to debut a playable demo and recruit additional talented people to the team. Our goal is to grow the project further and bring it to a shippable state on Steam.

Team Credits

Developer Reflection

Building Reconnection taught me the importance of designing C++ systems that empower designers. By creating a robust combat foundation with Blueprint extensibility, our team could iterate rapidly on enemy behaviors and game balance while maintaining performant, clean code. Translating the unpredictability and strategy of tabletop D&D into automated combat was an incredibly rewarding challenge!
← Back to All Projects