# The RTS (StarCraft & AoE-Style) Cell

This is V4's strategy game. The RTS cell is the top-down, click-to-command
surface where the loop is **gather, build, tech, expand, and macro an army into
a fight** — the genre where the camera floats above the battlefield, two
resources drive every decision, and a match is won by an economy three minutes
before it is won by a battle. It cares enormously whether two machines
re-running the same commands arrive at the _same_ world, because RTS networking
ships only inputs and trusts every client to simulate the rest. V4 builds two
macro games on that single foundation: a StarCraft-style contest of three
asymmetric factions on a competitive ladder, and an Age-of-Empires-style
historical game of twelve civilizations climbing five ages toward a Wonder. Both
feed one deterministic C++ simulation (`V4RTS`) and one lockstep transport
(`V4Netcode`). For the full mode taxonomy, the roster, and the scope this cell
slots into, start at the hub: [../V4_features.md](../V4_features.md).

## What ships, honestly

**The RTS simulation core is real, compiled, and pinned by domain-correct
tests.** The cell module `V4/ue/Source/V4RTS` carries 11 `.cpp` / 11 `.h` of
genre logic — a 32.32 fixed-point number type, a seeded A\* pathfinder that
self-audits its own determinism, an Ore/Crystal economy, a grid-footprint
builder, a timed production queue, a prerequisite-gated tech tree, an age-up
ladder, a three-state fog grid, control groups, and a Wonder-victory timer —
wired into GAS through four RTS-gated abilities in `V4RTSAbilities`. The numbers
are domain-specific, not placeholders: an `8`-per-second gather rate, a `10`-s
build and `5`-s train, a population-scaled age-up cost, a `420`-second
(seven-minute) Wonder defense. Three RTS specs under
`V4/ue/Source/V4Tests/Private/V4RTSTests` (`EconomySpec`, `PathSpec`, `FogSpec`)
plus the per-plugin `AsymmetricRTSSpec` and `HistoricalRTSSpec` assert
_computed_ values — a gather of exactly `48`, a Manhattan heuristic of exactly
`7`, a radius-one reveal of exactly `5` cells — the kind of check that fails
instantly against a hardcoded return.

Three honest qualifications, in the spirit of the architecture companion.
**First**, V4 has **zero cooked `.uasset` content**: factions, units, civs, tech
tiers, build orders, and maps are _data definitions_ and JSON-described content,
not authored levels or meshes. `UV4AsymmetricRTSCatalog::BuildMaps()` returns
twenty `FV4AsymRTSMapDefinition` rows with player slots and team sizes — a real,
validated map _manifest_, not twenty playable terrains. **Second**, the cell is
a library of individually-tested components and subsystems, not yet a single
running match. Each spec exercises a piece in isolation (`NewObject<>` then
drive it); there is no in-tree "RTS GameMode actor" that composes resource →
build → train → fog → pathfinding → lockstep into one ticking loop, and **unit
combat resolution is the thinnest part** — `UGA_AttackMove` assigns a control
group, trained units arrive as gameplay tags, and per-unit HP/targeting leans on
the shared GAS attributes rather than a bespoke RTS battle simulator. **Third**,
the lockstep layer is a _deterministic library, not a wired net driver_ (no
`FSocket`, no replicated `UPROPERTY`), and it hashes the input history, config,
and seed — **folding the RTS sim's own unit positions into that digest is
`V4RTS`'s job and is not yet wired or tested**. The honest netcode accounting
lives in
[../architecture/networking-determinism.md](../architecture/networking-determinism.md);
what follows is the strategy game on its own terms.

## The RTS experience — two macro games on one sim

Strip the genre to its verbs and the shared loop is small: send workers to a
resource, spend it to raise buildings, queue units out of those buildings,
research the tech that unlocks better units, push the fog back with scouts, and
trade armies until one economy collapses. V4 ships that loop twice, as two
`GameFeatures` plugins on top of the same `V4RTS` core.

### StarCraft-style — three asymmetric factions and a ladder

`V4Mode_RTS_Asymmetric` is the micro-heavy, build-order-driven competitive game.
`UV4AsymmetricRTSCatalog::BuildFactions()`
(`V4/ue/Plugins/V4Mode_RTS_Asymmetric/.../V4AsymmetricRTSSystems.cpp:108`)
authors three deliberately asymmetric factions, original IP standing in for the
Terran/Zerg/Protoss design space: **Vanguard** (an industrial mech faction with
`MobileBases`, `RepairableUnits`, `ScanAndCover`, hero
`Vanguard.SovereignBattlecruiser`), **Swarm** (a biomass faction with
`CreepSpread`, `LarvaMorph`, `FastEconomy`, hero `Swarm.BroodMother`), and
**Choir** (a psionic faction with `WarpInProduction`, `ShieldedUnits`,
`ArchonMerge`, hero `Choir.ArchTemplar`). `BuildUnits()` defines **24 launch
units, exactly eight per faction**, each carrying flags that drive real
mechanics — `bWorker`, `bCloaked`, `bDetector`, `bShielded`, `bRepairable` — and
an Ore/Crystal cost (a Choir `ArchTemplar` costs `500/500`, a Swarm `Zergling`
just `25/0`). On top sit **nine tech tiers** (three per faction) and **three
build-order wiki docs** with timed steps — the Vanguard one-base-pressure opener
hits `TrainWorker` at 12 s, `BuildBarracks` at 42 s, a marine pair at 78 s, all
at the genre's worker counts.

The competitive backbone is `UV4AsymmetricRTSLadder`. `BuildRanks()` returns the
**seven-rank ladder** (Bronze → Silver → Gold → Platinum → Diamond → Master →
Grandmaster), and the matchmaking math is a _real_ Glicko-2 implementation, not
a stub: `ApplyGlicko2Result` (`:350`) computes the `q = ln(10)/400` constant,
the `g(RD)` deviation-scaling factor, the expected score `E`, the estimated
variance `d²`, and the updated rating and deviation — then `ApplySeasonalReset`
pulls a player's rating halfway back to the `1500` baseline each season.
`AsymmetricRTSSpec` proves a win raises the rating, a placement match decrements
the five-match counter, and a reset restores it. Replays are input-only:
`UV4AsymmetricRTSReplayLibrary::BuildInputStreamReplay` sorts a command list by
frame and mints a public `https://replays.v4.game/rts/asymmetric/…` URL — the
"download any ladder match" promise as concrete code.

### Age-of-Empires-style — twelve civilizations, five ages, Wonders

`V4Mode_RTS_Historical` is the slower, population-heavy historical game.
`UV4HistoricalRTSCatalog::BuildCivilizations()` authors **twelve launch
civilizations**, each with unique units and a Wonder: Britons (Longbowmen,
Stonehenge), Mongols (Mangudai), Aztecs (Jaguar Warrior), Japanese (Samurai),
Vikings (Berserker), Mali (Camel Archer + Gbeto), Khmer (Ballista Elephant),
Ethiopians (Shotel Warrior), Yorùbá (Ile-Ife Cavalry + Ogboni), Bantu/Kongo
(Nkisi Mage + Kanga Spearman), Persians (War Elephant), and Aboriginal
Australians (Boomerang Skirmisher, Uluru). Crucially, every civ row carries
**sensitivity-review metadata** — `bSensitivityReviewed`, a `CulturalReviewId`,
and a non-empty `RepresentationNote` — and `HistoricalRTSSpec` _fails the build_
if any civ is missing it, encoding the §61 cultural-review gate as a test rather
than a promise.

The historical game runs a four-resource economy (Food/Wood/Gold/Stone, distinct
from the StarCraft cell's two-resource Ore/Crystal) and a **five-age ladder**
(Dark → Feudal → Castle → Imperial → **Industrial**). `UV4HistoricalAgeModel`
scales age-up cost with the number of bases, and
`UV4HistoricalWonderVictoryComponent` models the Wonder win exactly: you must
finish construction _before_ the defense timer can start, then survive the
seven-minute (`419 + 1` s in the spec) defense to win. The headline scale claim
is **measured, not asserted**:
`UV4HistoricalPopulationModel::CaptureFourPlayerFFAProfile` simulates a
four-player free-for-all at a 200-population cap — `4 × 200 = 800` units — over
250 frames and records real `Average/P95/Max` frame times, and the feel gate
passes only if `P95 ≤ 16.7 ms` and `Max ≤ 22 ms`. That is the "200 villagers
without stutter" requirement turned into a timed automation profile.

## Base, unit, and economy systems

The pieces below live in the shared `V4RTS` core, so both macro games inherit
the same simulation behavior. Each is a `UActorComponent` with
`bCanEverTick = false` — the sim is _advanced explicitly_ by the match loop,
never by the engine tick, which is what keeps it lockstep-safe.

### The Ore/Crystal economy

`UV4ResourceComponent` (`V4/ue/Source/V4RTS/Private/V4ResourceComponent.cpp`)
holds integer `Ore` and `Crystal` and exposes `AddResources`, `SpendResources`,
and `CanAfford` over an `FV4RTSResourceCost`. Gathering is the load-bearing
formula: `GatherFromPatch` returns
`min(Patch.Remaining, GatherPerSecond · DeltaSeconds · WorkerCount)`, floors the
result to whole resources, banks them into the right resource (Ore or Crystal by
the patch's `bCrystal` flag), and depletes the patch. With the default
`GatherPerSecond = 8`, `EconomySpec` pins the arithmetic: three workers on a
100-unit patch for two seconds gather exactly `48` (`8 × 2 × 3`), bank `48` Ore,
leave the patch at `52`, and a `30`-Ore spend leaves `18`. That is a genuine
economic rate model — the thing a build order is timed against — not a counter
that ticks up by a constant.

### Base building and the production queue

`UV4BuildComponent` makes placement spatial. `CanPlaceStructure` walks every
cell of an `FV4RTSBuildOrder`'s footprint against the `FV4RTSGrid`, rejecting
any cell that is out of bounds or blocked, and `QueueStructure` _atomically_
validates placement **and** spends the cost before enqueuing — if the resource
spend fails, nothing is placed. Units come from `UV4TrainComponent`: `QueueUnit`
charges the cost up front, and `TickProduction(DeltaSeconds)` drains real time
against the head order's `TrainSeconds` (default `5`), completing it and
broadcasting `OnUnitTrained` with the finished unit's gameplay tag, then rolling
to the next queued order with any leftover delta. Both are FIFO queues with
honest partial progress — the macro player's "queue five workers and tab away"
loop.

### Tech trees and age-up

`UV4TechTreeComponent` is a real dependency graph, not a flat unlock list.
`CanResearchTech` checks four conditions in order: the tech exists, it is not
already unlocked, the requesting **faction tag matches** (so a Vanguard player
cannot research a Choir tech), **every prerequisite tag is already unlocked**,
and the cost is affordable; only then does `ResearchTech` spend and unlock. Age
progression is `UV4AgeUpComponent` over
`EV4RTSAge { Dark, Feudal, Castle, Imperial }`: `TryAgeUp` starts from a base
cost (`500` Ore, `200` Crystal), **scales it by population pressure** (`+4` Ore
and `+2` Crystal for every point of population above 50), refuses to advance
past Imperial, and broadcasts `OnAgeChanged`. The cost-scaling is the design
rule — "age-up cost scales with population pressure" — implemented as arithmetic
on the live population count.

### Fog of war and unit control

`UV4FogOfWarComponent` implements the canonical three-state fog over a flat cell
array: `EV4RTSFogState { Unexplored, Explored, Visible }`. `RevealCircle` uses a
true squared-distance radius test (`Δx² + Δy² ≤ r²`), and `BeginVisibilityFrame`
demotes every `Visible` cell back to `Explored` at the top of each frame so
vision is recomputed from current unit positions — the standard "grayed memory
vs. live sight" model. `FogSpec` checks a radius-one reveal lights exactly the
five-cell plus-shape, the diagonal stays dark, and a frame reset returns every
cell to `Explored`. Selection is `UV4UnitGroupComponent`:
`AssignGroup(Hotkey, Units)` stores a control group keyed by number, filtering
out invalid actors on both write and read, with `GetGroup` and `ClearGroup` —
the `Ctrl+1` muscle memory of the genre. The asymmetric plugin extends fog with
cloak/detector logic: `CanSeeCloakedUnit` returns true only when a cell is
`Visible` **and** a detector is in range.

### Wonders and the co-op tower-defense variant

`UV4WonderComponent` is the win-condition timer: `StartWonderTimer` arms it,
`TickWonder` accumulates defended time against `RequiredDefenseSeconds = 420.0f`
(seven minutes), and crossing it latches `bVictoryReady` and fires
`OnWonderVictoryReady`. A third plugin, `V4Mode_RTS_TowerDefense`, reuses the
same map and Wonder substrate for a co-op PvE variant: `V4TowerDefenseSystems`
authors `FV4TDPlayerSlot`s where **one Defender controls the Wonder and up to
three Attackers control the incoming waves**, plus `FV4TDWaveDefinition` rows
carrying enemy budget, spawn-lane count, target pace, and boss-wave flags across
lane-typed maps — the "one player defends, one-to-three attack" asymmetric
tower-defense the catalogue describes.

## The deterministic sim

The RTS cell is the one place in V4 where the simulation **must** be
bit-identical across machines, because lockstep replicates only inputs. `V4RTS`
takes that seriously at the type level.

```mermaid
flowchart TB
    Input["player commands<br/><sub>build · train · research · attack-move</sub>"] --> Abil
    subgraph Abil["V4RTSAbilities · GAS (cell-gated to RTS)"]
        Build[UGA_BuildStructure]
        Train[UGA_TrainUnit]
        Tech[UGA_ResearchTech]
        Move[UGA_AttackMove]
    end
    subgraph Sim["V4RTS — deterministic simulation"]
        Fixed["FV4RTSFixed32<br/><sub>32.32 fixed-point</sub>"]
        Path["UV4PathfindingSubsystem<br/><sub>seeded A* · self-audited</sub>"]
        Econ["UV4ResourceComponent<br/><sub>Ore/Crystal · 8/s gather</sub>"]
        BuildC["UV4BuildComponent / UV4TrainComponent<br/><sub>10s build · 5s train · FIFO</sub>"]
        TechC["UV4TechTreeComponent / UV4AgeUpComponent<br/><sub>prereqs · Dark→Imperial</sub>"]
        Fog["UV4FogOfWarComponent<br/><sub>Unexplored/Explored/Visible</sub>"]
    end
    Build --> BuildC
    Train --> BuildC
    Tech --> TechC
    Move --> Path
    Econ --> BuildC
    Fixed --> Path
    Sim --> Lock["V4Netcode · UV4RTSLockstepSubsystem<br/><sub>25 Hz · chained hash + SplitMix64 · desync recovery</sub>"]
```

### Fixed-point and seeded pathfinding

`FV4RTSFixed32` (`V4/ue/Source/V4RTS/Public/V4RTSTypes.h:24`) is a 32.32
fixed-point number — `FractionalBits = 32`, `OneRaw = 1ll << 32` — with integer
add, subtract, and ordering operators and a `ToIntegerFloor`. It exists so the
simulation never touches a `float` where two machines might round differently.
`UV4PathfindingSubsystem::FindPath` is a real A\* over an `FV4RTSGrid` that uses
**fixed-point G/F scores** and a Manhattan-distance `Heuristic`, and its
tiebreak is the determinism-critical detail: when two open-set nodes have equal
`F`, it orders them by `V4CellSortKey` = `HashCombine(cellHash, Seed)`, so the
expansion order is reproducible from the match seed rather than pointer- or
insertion-dependent. The neighbor list is seed-sorted for the same reason.
`AuditDeterministicPathfinding` is a built-in self-test that runs the same query
twice under seed 314, asserts the paths match step-for-step, and checks that the
`(0,0) → (3,4)` heuristic equals exactly `7` cells — determinism verified in
code, not asserted in prose. `BuildFlowFieldNextSteps` derives a per-cell flow
field toward a goal for cheap group movement.

### Lockstep, hashing, and desync recovery

`V4RTS.Build.cs` depends on `V4Netcode`, where the lockstep _transport_ lives.
`UV4RTSLockstepSubsystem` (a `UGameInstanceSubsystem`) advances on a fixed
integer step — **25 Hz, 40 ms** — so the same input stream produces the same
frame count regardless of render rate. Each frame it gathers pending inputs,
runs them through a single canonical sort (by player id, then command, then
values, then target cell) so two clients that received inputs in different
network order hash them identically, then chains a determinism digest via
`ComputeDeterministicHash`. The load-bearing subtlety is `FName` hashing: a raw
`GetTypeHash(FName)` hashes the process-local name-table index (different per
machine), so the subsystem uses `StableNameHash` = `FCrc::StrCrc32` over the
name _string_, making the digest content-based and machine-stable. Match RNG is
a named `V4SplitMix64` seeded per match, and every draw is logged and
order-audited so non-deterministic iteration cannot silently consume the stream
differently on two peers. When a peer hash disagrees, `RecoverFromDesync` rolls
back `Frame − RecoveryRollbackFrames` (default `4`) and applies the
authoritative snapshot; the whole input stream serializes to the input-only
replay (~1 MB per 30-minute match). The full, honestly-qualified treatment —
including why this is a deterministic library rather than a live net driver — is
in
[../architecture/networking-determinism.md](../architecture/networking-determinism.md).

### What's real, and what's thin

Read adversarially, the maturity story is uneven but honest. **Real and
tested:** the fixed-point type, the self-auditing seeded A\*, the gather/spend
economy, grid-footprint building, the timed production queue, the
prerequisite-and-faction-gated tech tree, population-scaled age-up, three-state
fog with cloak/detector, control groups, the Wonder timer, the Glicko-2 ladder,
the input-only replay, and the measured 800-unit FFA frame profile — each pinned
by a spec that checks a computed value. **Thin or not-yet-wired:** there is no
single in-tree match actor that ties these components into one running loop (the
specs drive them in isolation); **unit combat resolution is the least-developed
system** — `UGA_AttackMove` only assigns a control group and trained units are
gameplay tags, so there is no RTS-specific damage/targeting/death sim yet; the
lockstep digest hashes inputs and seed but **not** the sim's own unit state; and
every map, faction, and civ is a validated _data manifest_, not authored, cooked
content. The bones of the genre are real and correct; the flesh of a shippable
match — fought battles, a composed match loop, baked terrain — remains.

## How the RTS cell ties to the shared engine

Like every V4 cell, the RTS surface routes player intent through the **Gameplay
Ability System**, and its abilities are **cell-gated** so an RTS ability cannot
fire on a non-RTS pawn. The four abilities in `V4RTSAbilities.cpp` —
`UGA_BuildStructure` (a default `16×16` grid, `2×2` footprint), `UGA_TrainUnit`,
`UGA_ResearchTech`, and `UGA_AttackMove` — each set
`RequiredCell = EV4RulesetCell::RTS`, **delegate to the components** rather than
reimplementing them, and **fail loud** with a precise reason string
(`"build-unavailable"`, `"train-unavailable"`, `"research-unavailable"`,
`"missing-tech-tree"`) instead of faking success. That is the same GAS spine
that carries an operator's attributes from a Wukong boss duel to an RTS hero
unit, and the same fail-loud seam the whole project favors.

The shared roster is the connective tissue: a named operator who is a hero in
the Action-RPG cell appears here as an RTS hero unit, earning the same
cross-cell Operator XP. For the souls-like sibling on the same GAS backbone, see
[./cell-action-rpg-wukong.md](./cell-action-rpg-wukong.md); for the GAS layout,
the perception model, the netcode authority, and the mode machinery every cell
sits on, see [./shared-cross-cell-engine.md](./shared-cross-cell-engine.md). For
the engine-side, genre-by-genre view of this module's determinism and maturity,
read the architecture companion,
[../architecture/per-cell-deep-dives.md](../architecture/per-cell-deep-dives.md).

## Related

- The feature hub: [../V4_features.md](../V4_features.md)
- [Action RPG (Wukong) cell](./cell-action-rpg-wukong.md) — the souls-like
  sibling on the same GAS spine and shared hero roster
- [Shared cross-cell engine](./shared-cross-cell-engine.md) — the GAS spine,
  perception model, netcode authority, and mode machinery every cell sits on
- Architecture companions:
  [../architecture/per-cell-deep-dives.md](../architecture/per-cell-deep-dives.md)
  (the `V4RTS` engine view) and
  [../architecture/networking-determinism.md](../architecture/networking-determinism.md)
  (the lockstep transport, honestly qualified) </content> </invoke>
