ITelemetrySink with the null, log and JSON Lines sinks, FTelemetryEvent and the envelope, TelemetryEvents, UTelemetrySubsystem on the game instance, ASaltyGameMode minting the session id into the replicated ASaltyGameState, bs.TelemetryTest, the git hash in the build string (D-42) and four Salty.Core.Telemetry tests replacing the placeholder. Proved standalone and with a headless server plus client sharing one session id. Also enables the engine's Editor, AutomationTest, GameplayTags, ConfigSettings and LiveCoding MCP toolsets (D-43) so the editor's MCP server exposes more than the skills toolset. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
20 KiB
Steps
The order of work. Not a roadmap: there are no phases, no dates and no milestones, only a ladder of small proofs where each rung stands on the one below. The next few steps are written in full. The rest are sketches, and a sketch is detailed only when the step before it closes; anything written further ahead than that would be a plan pretending not to be one.
The Spec/ documents specify; this document schedules. A step links the sections it builds and
never restates them. If a step and its spec disagree, the spec wins and this file is corrected.
The rules of the ladder
- A step is closed by a proof. A test passes, a checklist is ticked, a person performs the sentence. "Implemented" closes nothing.
- A step lands with its docs. The spec's "what was built, and where it differs" section, a decision-log line if a decision was taken, a worklog line, and the status here, all in the same change.
- A step that cannot be closed by one proof is two steps. A step under half a day belongs inside a neighbour.
- Ids are stable. A dropped step is struck through with a reason, never renumbered.
- Every replicated step is proved with a dedicated server in the editor and network emulation at 100 ms and 5 % loss. See Networking.md.
Status: ☐ not started · ◐ in progress · ☑ done · ⊘ dropped · ⏸ blocked (names the blocker)
The ladder
| # | Step | Proves | Status |
|---|---|---|---|
| 1 | The project, two modules, tests, a dedicated server in the editor | It builds, tests run headless, and the server posture is real from the first commit | ◐ |
| 2 | The telemetry seam | Every later step can emit into something | ☑ |
| 3 | A body, a camera, an input map and the gym | You can walk, in both camera modes, on a server | ☐ |
| 4 | The feel pass | The written checklist passes with a person at the keyboard | ☐ |
| 5 | The ability foundation and the stat block, proved on dodge, blink and mud | Abilities predict cleanly, and every body has the one set of numbers every effect changes | ☐ |
| 6 | Interaction and carrying | One system touches every prop; things you hold are real | ☐ |
| 7 | The swing | One damage funnel, one weapon, one dummy, and it feels good to hit | ☐ |
| 8 | The goblin | An enemy with a tell, going down, being revived, a wipe | ☐ |
| 9 | Kits and the action bar | Two classes with a signature each, and the bar tells the truth about keys | ☐ |
| 10 | The second pair of kits | Parity: four kits finish the same fight | ☐ |
| 11 | Projectiles | Aim matters at range | ☐ |
| 12 | Crafting data and rules | The pure rules pass their tests; the reference example composes | ☐ |
| 13 | The bench | You assemble a sword, equip it, and its quality changes the damage number | ☐ |
| 14 | Substances and the forge | Ore becomes ingot through a bed you tend, and walking away is not an exploit | ☐ |
| 15 | The anvil and enchanting | Ore to ingot to blade to a sword of embers, all physical, all server-validated | ☐ |
What comes after fifteen is decided when fifteen closes. The candidates are in Ideas.md.
1 · The project, two modules, tests, a dedicated server in the editor
Build: The .uproject at the repository root; runtime modules <Project>Core and <Project> with the folder
layout from Architecture; the Server build target; plugins enabled: Gameplay Abilities, Gameplay Tags, Enhanced
Input, CommonUI, StateTree; Config/Tags/ with the top-level namespaces; the asset manager configured for the
definition types; Scripts/run-tests.sh and Scripts/build.sh; .gitattributes with Git LFS for binary assets;
one placeholder automation test in Core; an empty L_Gym.
Spec: Architecture.md (modules, content, tags, testing, conventions).
Depends: the open decisions on engine version and project name (Decisions.md, OD-01, OD-02).
Done when: the editor opens the project with no warnings about the modules; Scripts/run-tests.sh runs the
placeholder test headless and reports green; Play In Editor with two clients and "Run Dedicated Server" puts two
default pawns in the empty gym; git lfs ls-files lists the first .uasset.
Notes: The launcher build of the engine cannot compile a packaged server; the editor's dedicated-server option
is the everyday test and is enough until a packaged build is wanted. Do not create a third module, a plugin or an
editor module here; each arrives when something needs it.
2 · The telemetry seam
Build: ITelemetrySink with the null, log and JSON Lines sinks; FTelemetryEvent and the envelope; the
event-name constants; UTelemetrySubsystem on the game instance; app_started, session_started,
session_ended, cheat_used; the session id minted by the game mode and adopted by clients through the game state.
Spec: Telemetry.md.
Depends: 1.
Done when: launching the gym with -telemetry produces Saved/Telemetry/session_*.jsonl with app_started
then session_started, every line parses, and a client's file carries the server's session id; the sink test
passes; bs.TelemetryTest emits an event and sets cheats_used.
Notes: Before the character, deliberately. Adding emit calls to a system that already exists means reading
it again and guessing what mattered; adding them as it is written costs a line.
3 · A body, a camera, an input map and the gym
Build: ABaseCharacter, APlayerCharacter, UExtendedCharacterMovement with the sprint flag in the saved
move, UMovementTuning and DA_Tuning_Player; IMC_Gameplay and every IA_* from the layout table, bound by
asset; FLookModel in Core with its tests; UCameraModeDefinition for both modes and UCameraModeComponent with
the stabilised head socket, the hidden local head, the spring arm; RemoteHeadYaw; the mannequin with the
template's animation blueprint; the gym greyboxed to the section table; the first movement_sample, jump,
land and camera_mode_changed events; the Settings widget's comfort sliders (FOV, stabilisation, bob, motion
scale) and the rebind rows through the engine's user settings.
Spec: Movement.md (input, the character, the look model, camera modes, the gym),
UI.md (the menu and settings).
Depends: 2.
Done when: the whole gym can be walked in both camera modes; the five look-model tests pass; two clients and a
dedicated server at 100 ms and 5 % show smooth remote bodies and no visible local correction with
p.NetShowCorrections 1; a rebind of Interact survives a restart; every setting change emits settings_changed.
Notes: Sprint is a compressed flag, not an ability. Coyote time and the jump buffer are in this step because
they are the movement component's business; the feel numbers are guesses until step 4. GetMaxSpeed already
multiplies by the MoveSpeed attribute, which reads as one until the stat block exists in step 5. The head is
hidden by bone on the local pawn only; this is the rigging rule that everything head-worn must parent to the head
bone.
4 · The feel pass
Build: Nothing new. The tuning asset's numbers, the gym's sections adjusted where they lied, the three gym
functional tests (FT_Gym_Stairs, FT_Gym_Slopes, FT_Gym_Gaps), and a worklog entry with the numbers that made
the checklist pass and the ones that did not.
Spec: Movement.md, The feel checklist.
Depends: 3.
Done when: every line of the checklist is ticked in both camera modes by a person at the keyboard, the three
functional tests pass, and DA_Tuning_Player is committed with the values that did it.
Notes: This is the step most likely to be called done early. It is not done while any line says "mostly".
Q1 in Movement (the default camera mode) is not decided here; both modes must pass.
5 · The ability foundation and the stat block, proved on dodge, blink and mud
Build: UExtendedAbilitySystemComponent on the player state with input-tag routing, ApplyDefaults and
ApplyStatus; InitAbilityActorInfo on both sides; UStatBlockAttributeSet with every attribute in the table
and its clamps, FStatBlockDefaults and GE_InitStats; the movement component reading MoveSpeed and
JumpPower; GE_Status_Slow, GE_Status_Haste, GE_Immune_Slow, GE_Cleanse with their cues and the
Status.*, Immune.* and Surface.* tags; AEffectVolume; UPhysicalMaterialWithTags, the ground trace and
the gym's mud and ice patches; UKitAbility; GA_Dodge (constant-force root motion, State.Invulnerable for its
duration, a cooldown effect) and GA_Blink (move-to-force along the aim, flattened); IA_Dodge and IA_Ability1
routed by tag; the HUD's status icon row; ability_used, status_applied, status_blocked, status_removed.
Spec: Stats.md, Combat.md (abilities and kits),
Movement.md, Hooks for other systems.
Depends: 4.
Done when: dodge and blink activate on their keys, predict locally and show no visible correction at 100 ms and
5 %; a dodge through a debug damage volume takes no damage during its i-frames and damage after; walking onto the
mud slows the body to the surface's magnitude and the icon appears, walking off restores it; a slow volume over
the mud does not stack with it but a haste multiplies against it; bs.ApplyStatus Slow 0.5 10 then
bs.GrantImmunity Slow shows the blocked cue on the next application; the resistance and carry-factor tests and
the attribute clamp test pass; bs.GrantAbility, bs.ApplyStatus, bs.GrantImmunity and bs.Cleanse exist.
Notes: GAS proves itself on movement before combat depends on it, because a movement ability that snaps is
the most visible thing prediction can get wrong. The stat block lands here rather than in combat so that the
first debuff, the first patch of ground and the carry penalty in step 6 all arrive on a system that exists,
instead of each growing a field of its own. UAbilitySystemGlobals::InitGlobalData is called from the asset
manager's startup or target data will not serialise; it is the classic first-day GAS bug.
6 · Interaction and carrying
Build: IInteractable, FInteractionPrompt, UInteractionSubsystem with the server funnel,
UInteractionComponent with the camera sweep, the highlight sweep and hold-to-confirm; the Interactable and
Carryable trace channels; UInteractionHighlightComponent and the outline post-process; WBP_InteractionPrompt
reading the live glyph; ICarryable, UCarryableComponent, UCarryComponent with hands, pick up, drop, throw
(G tap and hold) and hand over; State.Carrying tags blocking the right verbs; GE_Encumbered from
CarryMath::SpeedFactor against CarryCapacity; two test props in the gym (a lever that toggles a light, a
crate that is two-handed); all six interaction events.
Spec: Interaction.md.
Depends: 5.
Done when: the lever's prompt appears only in reach and greys with a reason from too far; a request forged
from beyond reach is refused and emits interaction_refused; the crate blocks the lever with "hands full"; a throw
lands within tolerance on two clients; the outline shows which of two crates the press will take; the interaction
functional test passes.
Notes: Before combat, because a downed teammate is a prop and reviving must not invent a mechanism. Pick-up is
routed by the interaction component but is not an interaction; the boundary is in the spec and it is load-bearing.
7 · The swing
Build: DamageMath and its tests; UDamageExecution, GE_Damage and the friendly-fire gate through
IGenericTeamAgentInterface; FWeaponProfile, UWeaponDefinition, AWeaponActor on the hand socket;
GA_MeleeAttack with the montage, ANS_HitWindow, the server box sweep and the line-of-sight check; the hit
cues: impact, client-side hit stop, camera kick, the procedural hit reaction; GE_FallDamage from Landed; a
training dummy in the arena with a health number over it; the health HUD; player_damaged, enemy_damaged.
Spec: Combat.md (the damage funnel, weapons and the melee swing).
Depends: 6.
Done when: the dummy takes exactly the profile's damage once per swing and none through a wall; a swing at
another player moves them and costs no health, and the functional test asserts both; a 500 cm drop damages; hit
stop never runs on the server; and a person says hitting the dummy feels good and writes the numbers down.
Notes: Combat feel is the honest unknown of the whole ladder. Budget iteration here and do not move on until
one dummy is satisfying. The box after the wind-up is the shipped shape; a blade sweep is the fallback in Q4.
8 · The goblin
Build: UEnemyDefinition with its BaseStats and DA_Enemy_Goblin; AEnemyCharacter with its own ability
system and the same stat block, so the mud from step 5 slows it;
AEnemyController with perception, the StateTree (idle, chase, attack with wind-up, flee, leash) and
FThreatTable; UCombatantComponent with the downed state, GA_Downed, GE_BleedOut, the revive prop,
GA_Revive, the solo-down-is-a-wipe rule; UEncounterSubsystem spawning from spawn points in the arena with the
sublinear table; respawn on wipe; every remaining combat event.
Spec: Combat.md (health, down, revive, wipe; enemies).
Depends: 7.
Done when: three goblins fight one player and the tells can be read and stepped out of; a goblin chasing
across the mud is slowed exactly as the player is; a downed player is revived through the interaction path by a
second client; alone, a down is an immediate wipe and the arena resets; the goblin flees for its flee time when an
ally dies; the threat table tests pass.
Notes: The enemy uses the same melee ability class as the player on its own component. Numbers are the earlier
project's guesses (30 health, 8 damage every 1.2 s with a 0.35 s wind-up) until they are not.
9 · Kits and the action bar
Build: UClassKitDefinition with BaseStats, GrantKit; the Warrior (Taunt as Status.Debuff.Taunted, Shoulder charge) and the
Cleric (Mass heal, Mend with the overheal launch, faster revive); bs.SetClass; cooldown effects per ability;
WBP_ActionBar with live glyphs and cooldowns; grief_action from the charge and the launch.
Spec: Combat.md, Abilities and kits, UI.md, The HUD.
Depends: 8.
Done when: a taunt pulls every goblin in range off a teammate; a charge damages goblins and launches a
teammate without damaging them; an overheal launches; the bar shows the rebound key after a rebind; the kit
validation test refuses a second signature.
Notes: Class lives on the player state and survives a body dying. The recoverability contract applies to the
charge and the launch from the first day they exist: the victim's recovery is standing up and walking back.
10 · The second pair of kits
Build: The Rogue (Backstab, Shadowstep) and the Mage's Blink promoted into a kit with a placeholder second
ability; the parity measurement: enemy_killed.killer_class against time_to_kill_s over a scripted arena wave.
Spec: Combat.md, Abilities and kits.
Depends: 9.
Done when: four kits each clear the same three-goblin wave solo within a spread the worklog records; no
ability writes health directly (grep the abilities for the attribute setter and find nothing).
Notes: Sketch. Detail when 9 closes.
11 · Projectiles
Build: AProjectileActor, the local cosmetic copy on activation, GA_Volley and the bow profile, GA_Fireball
for the Mage; the Ranger kit (Mark, Volley).
Spec: Combat.md, Projectiles.
Depends: 10.
Done when: an arrow leaves the hand without a visible round trip at 100 ms, drops over distance, and damages
through the funnel with Damage.Source.Projectile; a marked goblin takes more from a teammate's sword.
Notes: Sketch.
12 · Crafting data and rules
Build: Every definition class, FPieceInstance, FAssembledItem, FSubstanceInstance, UCraftingConfig;
the Item.*, Piece.*, Substance.*, Theme.*, Domain.* and Craft.Reason.* tags; every pure rule in
CraftingRules with its named tests; DA_Family_Sword with the four piece types, four characteristics, iron and
oak, two enchantments; bs.SpawnPiece.
Spec: Crafting.md (data, runtime state, the pure rules).
Depends: 6 (for the carryable piece actor). Can run in parallel with 7 to 11 in the core module.
Done when: the reference example composes to Blunt Mithril Sword of Embers; every rejection reason has a
passing named test; a guardless sword validates; the durability case computes; MakeWeaponProfile lands a
mid-quality crafted sword inside the authored sword's numbers.
Notes: Sketch. The rules exist before a bench does, on purpose: word salad in names is the risk that surfaces
earliest this way.
13 · The bench
Build: AStationActor, AAssemblyBench with staging, the live preview on a world-space panel laid out by the
family's grid, and the commit through ServerAssembleItem; AAssembledItemActor built from resolved attach
chains on named sockets, tinted by substance; the Equip verb and the crafted weapon in hand; item_assembled,
assembly_rejected, weapon_equipped.
Spec: Crafting.md, Stations, From item to weapon.
Depends: 12, 7.
Done when: three spawned pieces are inserted, the panel shows the name and the synergy segment, the commit
produces the sword on the bench, a second client sees the blade on the handle's socket, equipping it changes the
damage number on the dummy in proportion to coherence, and a piece the slot rejects shows the reason.
Notes: Sketch. This is the step where crafting and fighting meet; it is why both exist.
14 · Substances and the forge
Build: ASubstanceActor as a carryable; the raw-to-processed pipeline; UActivityRuntime,
UActivityDefinition, FActivityResult; AForgeStation with the 5×5 bed, the near simulation at 10 Hz, the far
lumped model, the collapse and expand, the equivalence harness; place, rake, fuel and bellows inputs; the
bellows as a second operator; the substance spawner cheat.
Spec: Crafting.md, Substances, Activities.
Depends: 13.
Done when: iron ore in the bed becomes an ingot with a quality that reflects time in the working band; a
second client on the bellows raises the ceiling; walking away and coming back yields the same ingot as staying,
within the harness's tolerance; the output buffer full stalls the forge and says so.
Notes: Sketch. The heat harness is written with the first heat code, not after.
15 · The anvil and enchanting
Build: AAnvilStation and the strike activity with the growing zone, the temperature gate, the timestamped
strike, two strikers alternating; characteristic discovery on the player state; an enchanting station and
ServerApplyEnchantment; the enchantment reaching the weapon profile's damage type and granted tags.
Spec: Crafting.md, Activities, Enchanting.
Depends: 14.
Done when: ore to ingot to blade to a sword of embers, all physical objects, all server-validated, in one
session with two clients; a strike at 150 ms of emulated latency scores the same as one at 0 ms pressed at the
same moment; with no input the anvil completes at the baseline score; the enchanted sword's hit carries
Item.Enchant.Fire.Penetration into the funnel.
Notes: Sketch. When this closes, the three feels exist, and what to build next is a decision taken then, from
Ideas.md, with the telemetry of fifteen steps to take it with.