Files
UnrealPrototyping/Docs/Steps.md
T
Moto 0e61a77346 Add comprehensive design and specification documentation (#1)
* Write the Unreal documentation set: design, steps, ideas, decisions and specs

Rewrites the design and engineering docs of the two earlier Unity projects
(Adventurer Guild, Project Malleable) for an Unreal Engine 5 prototype that
builds and proves three things in order: a movement controller, fighting and
crafting. Neither earlier core loop is carried; their ideas are catalogued as
features with what each needs.

- Docs/Design.md: the human summary (pillars, fixed decisions, glossary)
- Docs/Steps.md: a ladder of fifteen proofs, the first six in full
- Docs/Ideas.md: salvaged ideas from both projects, none scheduled
- Docs/Decisions.md: D-01..D-36, open decisions OD-01..OD-08, deferrals
- Docs/Spec/: notation, Architecture, Movement, Interaction, Combat,
  Crafting, Networking, Telemetry, UI, written as Unreal C++ skeletons
- CLAUDE.md and README.md for the repository


* Add the stat block: one attribute set on every body, every influence an effect

Every body (player, enemy, NPC) carries the same UStatBlockAttributeSet, and
every outside influence on it is a gameplay effect: base values, buffs,
debuffs, ground surfaces, carried weight, gear, being downed. Counters are
tags and attributes on the receiver: immunity blocks by tag, resistance
scales by attribute, cleanse removes by tag. Same status from several
sources: strongest wins; different statuses multiply.

- Docs/Spec/Stats.md: the block, effects and their five sources (abilities,
  areas, surfaces, items, the world), stacking, counters, readers, tests
- Movement: the movement component's tagged speed-multiplier map is removed;
  speed is tuning times the MoveSpeed attribute; the ground surface trace
  turns mud into an effect; the gym gets mud and ice patches
- Combat: the attribute set moves to Stats; enemies and kits carry
  FStatBlockDefaults; the funnel reads MagicResist and Immune.Damage tags;
  taunt and mark are statuses
- Interaction: carried weight is GE_Encumbered against CarryCapacity
- Crafting: class bonuses are the WorkSpeed and WorkQuality attributes;
  enchantments may grant a holder effect
- Steps: step 5 builds the block with mud, a volume and an immunity
- Decisions D-37 and D-38; design summary, CLAUDE.md, indexes, worklog
2026-09-15 17:54:00 +03:00

271 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/`](Spec/README.md) 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](Spec/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`](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](Spec/Architecture.md) (modules, content, tags, testing, conventions).
**Depends:** the open decisions on engine version and project name ([Decisions.md](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](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](Spec/Movement.md) (input, the character, the look model, camera modes, the gym),
[UI.md](Spec/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](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](Spec/Stats.md), [Combat.md](Spec/Combat.md) (abilities and kits),
[Movement.md, Hooks for other systems](Spec/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](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](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](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](Spec/Combat.md#abilities-and-kits), [UI.md, The HUD](Spec/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](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](Spec/Combat.md#projectiles-step-11).
**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](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](Spec/Crafting.md#stations), [From item to weapon](Spec/Crafting.md#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](Spec/Crafting.md#substances), [Activities](Spec/Crafting.md#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](Spec/Crafting.md#activities), [Enchanting](Spec/Crafting.md#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`](Ideas.md), with the telemetry of fifteen steps to take it with.