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
This commit is contained in:
+270
@@ -0,0 +1,270 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user