# The 2D Run-and-Gun (Arcade) Cell

This is V4's arcade machine. The 2D Run-and-Gun cell is the Contra-shaped
surface where the moment-to-moment loop is **run, jump, drop through a platform,
grab a power-up, and never lose the second player off the edge of the screen** —
the genre that lives or dies on snappy controls, a readable side-scroll camera,
and the merciless one-hit-kill economy that makes three lives and two continues
feel like a real stake. It is the **smallest** of V4's six genre modules and the
catalogue says so plainly — "the smallest cell, but mandatory at launch" — but
it is also one of the most _complete_: the camera math, the S/L/M/F/C/H/B weapon
roster, the lives-and-continues ledger, and an eight-stage campaign are real,
compiled C++ pinned by automation, and the cell's 60-fps two-player budget is
verified by an honest-to-goodness timed simulation rather than a stat sheet.

V4 ships this cell as two hot-swappable `GameFeatures` plugins on top of one
shared `V4Arcade` engine module: `V4Mode_Arcade_Contra` (the side-scrolling
campaign) and `V4Mode_Arcade_BossGauntlet` (the weekly boss-rush). The promise
is the same per-ruleset feel the rest of V4 chases — a frantic couch co-op run
must read as Contra, not as a 2D skin on the tactical shooter — yet it sits on
the same GAS spine and the same mode machinery as the breach-and-clear cell.
This page inventories what that arcade core actually does on the player's side
of the screen, and points at the exact Unreal C++ that backs each feature. For
the full mode taxonomy, roster, and the scope this slots into, start at the hub:
[../V4_features.md](../V4_features.md).

## What ships, honestly

**The run-and-gun logic is real, compiled, and tested.** The engine module
`V4/ue/Source/V4Arcade` carries nine `.cpp` / nine `.h` of domain logic — a
side-scroll camera component, a lives/continues ledger, a power-up multiplier, a
weapon-pickup swap, drop-through-platform and ladder-climb components, three
cell-gated GAS abilities, and a Battle-Hub mini-game catalog — and the
`V4Mode_Arcade_Contra` plugin layers an authoring-and-validation model on top
(movement config, 8-way aim quantizer, health rules, co-op rules, a seven-weapon
catalog, an eight-stage catalog, and a feel simulator). The signature numbers
are designer-editable, not placeholders: `StartingLives = 3` /
`StartingContinues = 2`, a `1.5×` powered-up fire-rate, a
`CoOpSeparationCap = 900` co-op tether, a `ClimbSpeed = 300`. Most are pinned by
dedicated specs under `V4/ue/Source/V4Tests` — `ContraSpec`, `LivesSpec`,
`WeaponSpec`, `MiniGameSuiteSpec`, `BossGauntletSpec`, plus the GAS integration
in `AbilityClassSpec`.

Three honest qualifications, in the spirit of the architecture companion.
**First**, this is the smallest cell by code volume, and the design catalogue
labels several systems (branching routes, four-player online co-op) as authored
rules and metadata rather than fully simulated runtimes. **Second**, the _logic_
is in-tree C++ but the _content_ is not: V4 ships JSON-described content and
soft-object paths, not cooked `.uasset` binaries, so the Paper2D flipbook
sprites, parallax materials, tilemaps, and Niagara 2D projectiles the cell
references are described, not baked — `V4Arcade.Build.cs` honestly links
`Paper2D` so the 2D pipeline is wired at the dependency level. **Third**, the
Battle-Hub arcade _cabinets_ (pinball, pool, darts) are a validated catalog of
definitions whose physics — flippers, cue, puck — are named `RequiredSystemIds`,
not in-tree cabinet simulations. What follows is the run-and-gun engine on its
own terms.

## The 2D run-and-gun experience — what a run feels like

Strip the two plugins away and the shared verb set is small, fast, and tactile:
run a stage, jump variable-height, slide a wall, drop through a one-way
platform, climb a ladder, aim in eight directions, and swap whatever power-up
you just ran over — all while a co-op camera refuses to let you abandon player
two. Each verb is either a component on the arcade pawn or a function on the
Contra model, and each carries the specific tuning the genre argues about.

### The side-scroll camera and the co-op tether

`UV4SideScrollComponent`
(`V4/ue/Source/V4Arcade/Private/V4SideScrollComponent.cpp`) is the camera brain.
`CalculateCameraLocation` follows a single player directly, or — for couch co-op
— frames the **midpoint** of two: it takes the min and max player X, caps the
span at `CoOpSeparationCap = 900` so the frame can't stretch unboundedly,
centres on that span, averages the players' Z, and then clamps the focus inside
the authored stage so `Focus.X` stays within
`[StageMinX + HalfScreenWidth, StageMaxX - HalfScreenWidth]` with
`HalfScreenWidth = 650`. The matching `ClampPlayerToScreen` keeps each player
inside the live frame — it derives the camera's focus X back out of the
`CameraOffset = (-900, 0, 250)`, clamps the player to `±650` of it, then to the
stage bounds. That pair is the exact "you can't run off and leave player two
behind" constraint the genre is built on. The plugin's
`UV4ContraSideScrollModel` mirrors the same math for authoring
(`CameraOffset = (-850, 0, 260)`, `HalfScreenWidth = 640`) and adds
`QuantizeAim8Way`, which `Atan2`s a raw stick vector, rounds to the nearest 45°
sector, wraps it into `0…7`, and returns a re-normalized direction —
`ContraSpec` checks a `(0.7, 0.7)` diagonal snaps to direction index `1` and
stays unit-length.

### Lives, continues, and the one-hit-kill economy

`UV4LivesComponent` (`V4/ue/Source/V4Arcade/Private/V4LivesComponent.cpp`) is
the arcade ledger. `ApplyHit` refuses to act once the game is over, otherwise
decrements `Lives` toward a floor of zero, broadcasts `OnLivesChanged`, and
returns whether the player is still alive; `UseContinue` only fires when lives
have hit zero _and_ continues remain, spends one continue, and restores `Lives`
to `StartingLives`; `IsGameOver` is true only when both lives and continues are
exhausted. `LivesSpec` walks the whole ladder — three lives, three hits to zero,
"not game over while continues remain," then a continue that restores three
lives and decrements the continue count to one. The Contra plugin's
`UV4ContraHealthModel` defines the rule the lives sit under: `BuildHealthRules`
returns `bOneHitKillDefault = true` with a `HitCapacity` of `1`, and the
accessibility branch (`bOneExtraHitEnabled`) bumps capacity to `2` so a new
player survives the first contact per life — `IsDeathOnHit(HitsRemaining)`
resolves death the instant `HitsRemaining <= 1`. One-hit-kill by default, one
forgiving extra hit when toggled.

### Weapons and the powered-up state

Power-ups are the genre's whole progression curve, and the roster is
unmistakably Contra. `EV4ArcadeWeaponType`
(`V4/ue/Source/V4Arcade/Public/V4ArcadeTypes.h`) is the literal S/L/M/F/C/H/B
set — `Spread, Laser, MachineGun, Fire, Crush, Homing, Barrier`. The plugin's
`UV4ContraWeaponCatalog::BuildWeapons` gives each one real combat data: Spread
is a five-projectile fan (`FireRate 4.5`, `ProjectileCount 5`), the Laser is a
single high-damage shot (`3.0`, `1`, `Damage 4.0`), the Machine Gun is the
narrow rapid default (`9.0`), Crush is the slow heavy hitter (`Damage 6.0`), and
Barrier carries no fire data at all because it is the temporary shield, not a
gun. `UV4WeaponPickupComponent` enforces "one weapon at a time": `PickupWeapon`
hands back the previously held spec as the dropped weapon, installs the new one,
and broadcasts the swap — `WeaponSpec` proves picking up a Laser drops the
default Machine Gun and a later Spread keeps its five-projectile fan data. The
**powered-up** state is its own component: `UV4PowerUpComponent::ApplyFireRate`
returns `BaseFireRate × 1.5` while powered and the base rate otherwise, and
`ClearOnDeath` drops the powered state on a death — the exact "one death and
your yellow gun reverts" rule, verified in both `ContraSpec` and `WeaponSpec`.

### Traversal — jump, drop-through, and ladders

Platforming is three small, honest components.
`UV4DropPlatformComponent::BeginDropThrough` arms a `DropThroughSeconds = 0.25`
window during which the pawn ignores one-way collision, ticked down by
`TickDropThrough`; `UV4LadderClimbComponent::ApplyClimbInput` only moves while
`bOnLadder`, then clamps `CurrentZ + AxisValue × ClimbSpeed × DeltaSeconds`
between the ladder's authored `MinZ` and `MaxZ` (`ClimbSpeed = 300`); and the
plugin's `BuildMovementConfig` publishes the run-and-jump tuning the catalogue
promises — `RunSpeed = 760`, `JumpVelocity = 980`, `WallSlideSpeed = 180`, with
run, jump, wall-slide, and drop-through all enabled. `ContraSpec` asserts all
four verbs are on and that run and jump carry non-zero authored values.

## The systems behind the two faces

The arcade cell wears two faces from one module. The first is the **Contra**
side-scrolling campaign; the second is the **Battle-Hub arcade cabinets**. Both
are real C++; they differ in how much of the runtime is simulated versus
authored.

```mermaid
flowchart LR
    Input["V4Input · Enhanced Input"] --> Abilities
    subgraph Abilities["V4ArcadeAbilities · GAS (cell-gated: Arcade)"]
        Shoot[UGA_Shoot]
        Jump[UGA_Jump]
        Drop[UGA_DropPlatform]
    end
    subgraph Arc["V4Arcade — engine components"]
        Scroll["UV4SideScrollComponent<br/><sub>midpoint cam · 650 frame · 900 tether</sub>"]
        Lives["UV4LivesComponent<br/><sub>3 lives · 2 continues</sub>"]
        Power["UV4PowerUpComponent<br/><sub>×1.5 · drop on death</sub>"]
        Wpn["UV4WeaponPickupComponent<br/><sub>one weapon · S/L/M/F/C/H/B</sub>"]
        Ladder[UV4LadderClimbComponent]
        DropC[UV4DropPlatformComponent]
        Mini["UV4ArcadeMiniGameCatalog<br/><sub>7 Battle-Hub cabinets</sub>"]
    end
    subgraph Plug["V4Mode_Arcade_Contra / _BossGauntlet — plugins"]
        Contra["UV4Contra* models<br/><sub>stages · weapons · feel sim</sub>"]
        Boss["UV4BossGauntlet*<br/><sub>12 bosses · weekly chain · board</sub>"]
    end
    Shoot --> Wpn
    Shoot --> Power
    Jump --> Scroll
    Drop --> DropC
    Wpn --> Attr["UV4Attr_Arcade<br/><sub>Health · Lives · ScoreMultiplier</sub>"]
    Plug --> Arc
    Paper["Paper2D"] --> Arc
    Arc --> Modes["V4Modes<br/><sub>mode registry · GameFeature URLs</sub>"]
```

### Contra — the eight-stage campaign and the feel gate

`UV4ContraStageCatalog::BuildStages`
(`V4/ue/Plugins/V4Mode_Arcade_Contra/Source/V4Mode_Arcade_Contra/Private/V4ContraSystems.cpp`)
authors the full launch campaign: eight named stages — Jungle Approach, Base
Interior, Waterfall Climb, Snowfield Relay, Fortress Runway, Alien Lair, City
Siege, Final Core — each with **two or three branching routes** that meet at a
boss arena, and a `ValidateLaunchContent` linter that rejects any stage with the
wrong route count, a mismatched route-id list, or a missing boss arena.
`UV4ContraCoopModel` encodes the co-op contract: 2P local couch is
`bLaunchMandatory` with `RequiredLaunchLocalPlayers = 2`, 4P online co-op is
supported (a fifth online player is rejected), and `ShouldApplyScreenEdgeTether`
fires only when separation exceeds the `MaxTetherDistance = 900`.

The standout is the **feel gate**.
`UV4ContraFeelModel::CaptureTwoPlayer60FPSProfile` is not a hardcoded "60 fps:
pass" — it runs a real two-player co-op frame loop and measures it. Inside the
loop it advances both players at the real `RunSpeed`, drives the actual
`UV4ContraSideScrollModel` camera and screen-clamp, applies the co-op tether,
spawns enemy waves from a **deterministic LCG** so every capture replays the
identical battle, fires both players' real weapon-catalog weapons at their real
fire rates (through `QuantizeAim8Way`), integrates and collides every
projectile, and resolves one-hit deaths that drop the powered state — all while
timing each frame with `FPlatformTime::Seconds()`. It then reports the measured
average, p95, and max frame times and sets `bPasses` only when
`P95FrameMs <= 16.67`. `ContraSpec` asserts the profile is `bMeasured`, that it
captured every one of 600 frames, that p95 never exceeds max, and that the pass
verdict _equals_ the measured budget comparison — a test that would fail
instantly against a fabricated metric. The result is then fed into the shared
`UV4FeelTestSuite` gate as the `TwoPlayerP95FrameMs` metric, the same cross-cell
feel-test harness every other cell registers against.

### Boss Gauntlet — the weekly rotation and the no-death board

`V4Mode_Arcade_BossGauntlet` is the boss-rush face.
`UV4BossGauntletCatalog::BuildBossPool` authors **twelve** bosses (Iron Hydra
through the final-flagged Void Emperor at difficulty tier 7), and
`BuildWeeklyRotatingChain` deterministically picks a five-boss chain for a given
week by walking the pool from `WeekIndex % PoolSize` in stride-two steps — so
the chain is stable within a week but rotates between weeks, exactly as
`BossGauntletSpec` checks (week 0 equals itself, week 0's lead boss differs from
week 1's). The leaderboard is the genre's bragging-rights layer:
`UV4BossGauntletLeaderboard::SubmitClear` **rejects any run that is not a clean
no-death clear** (a death count above zero or a non-positive clear time is
refused), dedups by week-and-account, and keeps the board sorted by clear time;
`GetTopNoDeathClears` returns the fastest hundred no-death runs for a week. The
spec confirms a death-bearing run is rejected and the fastest clean clear ranks
first.

### The Battle-Hub arcade cabinets

The second face is social: `UV4ArcadeMiniGameCatalog`
(`V4/ue/Source/V4Arcade/Private/V4ArcadeMiniGameCatalog.cpp`) defines the seven
launch cabinets that live in the persistent lobby — two pinball machines (Astra
Circuit and Contra Multiball), plus air hockey, mini-golf, darts, eight-ball
pool, and a co-op cooking game — each with a score target, a player cap, a
Battle-Hub map and widget soft-path, and a list of `RequiredSystemIds`.
`ValidateMiniGameSuite` enforces the shape: exactly seven entries, at least two
pinball tables, exactly one of each other type, and every entry complete with at
least three system ids. This layer is honest about its maturity — it is a
_validated catalog_ of cabinet definitions, and the cabinet physics those system
ids name (`System.Pinball.Flippers`, `System.Pool.CuePhysics`) are referenced,
not implemented in-tree. `MiniGameSuiteSpec` resolves Contra Multiball by id and
the cooking game by cabinet id and confirms the suite validates.

## How the arcade cell ties to GAS and the shared engine

Every arcade action routes through the Gameplay Ability System, and the cell's
three abilities in `V4ArcadeAbilities.cpp` are **cell-gated** so they cannot
fire on a non-arcade pawn — each sets `RequiredCell = EV4RulesetCell::Arcade`
(`V4/ue/Source/V4Core/Public/V4CellState.h`) and funnels real work into the
shared `UV4GameplayAbilityBase::ExecuteV4Ability` seam
(`V4/ue/Source/V4Gameplay/Public/V4GameplayAbilities.h`). `UGA_Shoot` finds the
pawn's weapon-pickup and power-up components, computes the effective fire rate,
and **fails loud** with `"arcade-weapon-invalid"` when the held weapon has no
rate, projectiles, or damage (or `"missing-arcade-weapon"` when the component is
absent) — it honestly reports what it could not do instead of faking a shot.
`UGA_Jump` raises the pawn by `JumpHeight = 120` with a swept world offset, and
`UGA_DropPlatform` arms the drop-through window, each reporting a precise
rejection tag on failure. `AbilityClassSpec` does this for real: it spawns an
arcade pawn, attaches the components, grants and activates all three abilities,
and asserts the jump actually raised the pawn's Z and the drop actually started
— GAS integration, not stubs.

Underneath, the cell carries its own replicated attribute set. `UV4Attr_Arcade`
(`V4/ue/Source/V4Gameplay/Public/V4AttributeSets.h`) holds `Health`, `Lives`,
and a `ScoreMultiplier`, driven by real gameplay effects (`UGE_V4_ArcadeDamage`,
`UGE_V4_ArcadeExtraLife`, `UGE_V4_ArcadeScoreMultiplierBoost`) — so an
extra-life pickup or a score-multiplier boost is a GAS effect on the same spine
the other cells use. Both plugins register themselves through the mode
machinery: `UV4ContraModeLibrary::BuildModeDefinition` and the Boss-Gauntlet
equivalent publish `FV4ModeDefinition` rows (`Arcade.Contra`,
`Arcade.BossGauntlet`) with `GameFeaturePlugin` URLs and soft-pathed entry maps
and HUDs — described content, not baked binaries, but real registration into
`V4Modes`. That throughline is the architecture's unifying claim: six genres
stay one game because even the smallest cell sits on the same GAS spine, the
same shared mode registry, and the same feel-test harness. For the engine-side
treatment — the GAS layout, mode machinery, and the cross-cell seams named here
— 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 third-person
  souls-like surface that, like this cell, is animation- and feel-led but trades
  arcade pacing for stamina-economy combat
- [Shared cross-cell engine](./shared-cross-cell-engine.md) — the GAS spine,
  mode machinery, feel-test harness, and netcode authority every cell sits on
- Architecture companion:
  [../architecture/per-cell-deep-dives.md](../architecture/per-cell-deep-dives.md)
  — the genre-by-genre engine view, including `V4Arcade` maturity
