140 lines
25 KiB
Markdown
140 lines
25 KiB
Markdown
# Decisions
|
||
|
||
One line per decision, newest first, with a pointer to where it lives in full. A decision belongs here the moment
|
||
it is made, even if the doc it affects has not caught up. Never delete an entry; supersede it with a newer one and
|
||
say so.
|
||
|
||
Status: `Decided` · `Supersedes` (contradicts an earlier decision, names it) · `Deferred` (deliberately not now,
|
||
with a trigger)
|
||
|
||
## 2026-09-17 — Off the ladder, the terrain generator leaves numpy and gains a drainage network
|
||
|
||
| # | Decision | Where |
|
||
| --- | --- | --- |
|
||
| D-52 | **The continent mask is thresholded, not multiplied, into the uplift field: where the land ends does not decide how fast it is rising.** The rate was `r * l` with `l` the smoothstep continent mask, so uplift tapered to zero across the shore and every coastline on the map was the lowest-uplift ground on the map by construction. Steady state is `S = U/(K*A^m)`, so ground with no uplift grades to no slope: the surf had a flattened strip to work in wherever it looked. The mask now answers only "is this cell sea", which is the yes-or-no the solve needs for its base level, and a range that runs out to the water rises at range rates right up to the waterline. Measured, seed 7 at 1400 with everything else held: surf cut 23.32 → 38.87 Mm³, planed 3.0 → 3.9 km², new beach 3.41 → 5.59 km²; seed 9342 the same in direction, 22.42 → 30.35 Mm³. `Corrects a measurement, not only the code`: the "mean cliff 2 m" that motivated this was a broken metric — it measured the drop from a cell to its seaward neighbour, which is a gradient, and at the angle of repose one cell of a 10 m grid is 7 m, so it could never have exceeded 7 whatever the coast did. Replaced by backshore height, the land's elevation one to two surf reaches inland, median and P90; on that metric the *old* build already read 88 m at P90, so the taper was never the difference between cliffs and no cliffs — it flattened the ~100 m strip the surf works in, so the cliff began a hundred metres inland instead of at the water. The slope–area fit moved −0.480 → −0.312 on seed 7 and −0.698 → −0.720 on seed 9342, in opposite directions and both inside a seed-to-seed spread that is several times larger, so it is not a regression; it is a warning that the fit at five or six bins cannot judge a single change on a single seed. `Decided`. | [Terrain.md](Terrain.md) |
|
||
| D-51 | **The coastline becomes a pass of its own, run after the fluvial solve, and the continent outline gains the octaves that make it a coast rather than a blob.** The coast was a line in a mask: the sea floor dropped to a flat plane at −180 m in one step, the land met the water at whatever angle the last erosion step left, and no process knew the shoreline was there. `internal/coast` adds three, each derived rather than drawn. A shelf, from an exact signed distance transform: gentle to a break, then a continental slope to the abyss, its width read off the relief standing behind each stretch of shore, so a plain gets a wide shelf and a range that reaches the water gets a narrow one. A surf, which planes the land to a shore platform within a reach set by how open the water is — the cliff is the step where the reach ends, so its height is whatever the land behind it stands at. And a sediment budget: what the surf cuts is counted, carried a drift length along the shore, and laid in sheltered shallow water, with river mouths delivering their own load in proportion to what they drain, which is what makes a delta. It runs after the solve because two of the three need the finished terrain, and it owns the sea floor outright — `uplift.Result.Bathymetry` is gone, and ocean cells stay at sea level for the whole solve, which is the invariant that stopped rivers cutting to −180 m. Shelter is measured as effective fetch over the *seaward* half-space against fixed absolute anchors, not as a percentile of the map: casting in every direction counts the land behind the shore as shelter, and a percentile is a global statistic that two tiles would disagree about. Measured on seed 7: the outline's octave gain, swept with everything else held, gave 64 km of shoreline at 0.50, 81 at 0.58, 96 at 0.62 and 114 at 0.66, while the fetch's view of the median stretch of coast went 1.00, 0.98, 0.84, 0.51 — five octaves put the finest coastal feature at 450 m, which is a smooth blob with nothing to shelter anything from anything. 8 octaves at gain 0.62 is the setting that has islands, inlets and headlands without the outline breaking into speckle. The whole pass costs 83 ms at the manifest's geology grid. `Decided`. | [Terrain.md](Terrain.md) |
|
||
| D-49 | **Plains are made by lowering the intraplate uplift rate an order of magnitude, not by tuning erosion; and the hillslope law becomes Roering nonlinear diffusion with the repose clamp demoted to a constraint the diffusion smooths over.** Steady state is `S = U/(K*A^m)`, and with `critical_area_m2` at 0 that holds down to a single cell, so at K 5e-5, m 0.5 and a ~10 m geology cell an intraplate rate of 0.25-0.9 mm/yr puts *every divide on the map* at 32 degrees or past the 35 degree repose angle. Measured on the old numbers: 81 % of the land in the >0.5 mm/yr class, the plain class 1 % of it and all sea cliff. The plains were never over-dissected; they were being uplifted at mountain rates, and for n = 1 the uplift rate alone fixes the hillslope angle. `intraplate_mm_yr` 0.25 -> 0.03 and `intraplate_swell_mm_yr` 0.9 -> 0.08 keeps the ~2.5-fold swell that puts divides on a plain while making the mountain-to-plain ratio 30-fold, which is what real ones are; `relief.amplitude_m` lo 50 -> 15 so the plains do not start as hills the run must erode away. Measured after: plains 42 % of land at 0.8 degrees median and 9 m of relief over 500 m. The repose clamp stays in the step loop but nonlinear diffusion runs *after* it every step, because the clamp cuts along the eight D8 directions and leaves grid-aligned pyramid faces; a symmetric five-point stencil rounds them off before the next step. Clamping once at the end instead was measured and is worse. `Supersedes` the three-fold mountain-to-plain ratio D-47's uplift notes settled on. `Decided`. | [Terrain.md](Terrain.md) |
|
||
| D-50 | **The D8 router's flat-ground tie-breaks are jittered by a hash of the cell index, and `critical_area_m2` stays at 0.** Across a filled flat the only gradient is the priority-flood's millimetre of epsilon, applied in the order the flood happened to reach the cells, so the router draws the flood's own traversal geometry as ruler-straight diagonal rivers — the polygonal network that killed the first attempt at flat plains, and the dominant artefact once D-49 made the plains genuinely flat. The epsilon is scattered per cell by plus or minus half and the choice between two equally steep neighbours is broken by a hash of the cell and direction rather than by the fixed `dx8` order, both deterministic from the seed (cross-cutting rule 12). Measured: the slope-area fit went from R² 0.055 to 0.459 normalised, 0.039 to 0.664 raw, at no cost. A channelization threshold was re-measured after D-49 and still fails — at 1e4 the plains go back to 7.0 degrees and half the rolling class pins against the clamp — because the hillslope it creates must shed its uplift by diffusion and at D 0.02 it cannot. It stays at 0 until there is a transport law to pair it with. `Decided`. | [Terrain.md](Terrain.md) |
|
||
| D-48 | **The world's canvas becomes 7141 vertices at 200 cm, elevation −512…1536 m, and it keeps its coastline.** 7140 = 255 × 28, so the engine's importer gives 28×28 components of 255 quads by its own rule (D-45) and the side stays 14 280 m, identical to today's, so nothing `create_world.py` places moves; cells go from 3.5 m to 2.0 m. A 2048 m elevation span *is* Z scale 400, so the incoming spec's canvas is expressed in the manifest's metres contract with no loss, at 3.125 cm precision. The ceiling drops from 2800 m to 1536 m deliberately: 2800 m over 14 km is a Himalayan gradient, and 800–1500 m of relief is what a fluvial landscape this size looks like. `Supersedes` the 2600 m crests the noise was tuned for. The coast stays and sea level is the fixed base level on every ocean cell, which is a better-posed boundary for the stream-power solve than one outlet edge and closes the spec's own open question. A full run holds today's five-minute bar, which sets `fill_every` at 50. `Decided`. | [Terrain.md](Terrain.md) |
|
||
| D-47 | **The heightmap generator becomes a standalone Go CLI in `Tools/Terrain/`, and stream-power fluvial erosion replaces particle erosion as the thing that shapes the land.** Droplets carve the path a droplet takes; they do not make a drainage network, which is why the world reads as weathered noise rather than as terrain with a history. A Braun & Willett implicit solve over an uplift field on a coarse geology grid does, and plates, faults and lithology exist to feed it; particle erosion, thermal weathering and strata survive as detail passes at full resolution. The core is engine-free and deterministic from a seed (cross-cutting rule 12), and `World.json` stays the single contract, with its `erosion` block replaced by `pipeline`. Retires `heightmap_noise.py`, `heightmap_erosion.py`, `heightmap_io.py` and `generate_heightmap.py`, about 780 lines of tuned numpy whose lessons are carried into the port by name; keeps `create_world.py`, `dump_level.py`, `world_manifest.py` and `ULandscapeAuthoringLibrary`. `Supersedes` D-46 on the language and on which pass shapes the land. | [Terrain.md](Terrain.md) |
|
||
|
||
## 2026-09-17 — Off the ladder, the world's terrain is simulated, not only sampled
|
||
|
||
| # | Decision | Where |
|
||
| --- | --- | --- |
|
||
| D-46 | **The heightmap goes through geological passes after its source, in numpy, at the world's resolution.** Domain-warped ridged noise and cellular crest lines give the uplift; particle hydraulic erosion (a coarse pass for valleys, a fine pass for gullies), mass-conserving thermal weathering at an angle of repose and a strata hardness map shape it; flow, wear, deposition and curvature maps come out beside the weightmaps and drive the layer painting. All of it is engine-free so the source stays swappable, and `"enabled": false` in the manifest skips it for a real DEM. `Decided`. | [RawContent/World/README.md](../RawContent/World/README.md) |
|
||
|
||
## 2026-09-16 — Off the ladder, the world's terrain
|
||
|
||
| # | Decision | Where |
|
||
| --- | --- | --- |
|
||
| D-45 | **The world heightmap is 4081 vertices a side, not Epic's 4033.** The engine's importer picks the largest section size that divides the quad count, preferring one section per component; 4080 divides by 255 and gives 256 components, 4032 divides only by 63 and gives 4096. A resolution change stays on `255 x N + 1` or `127 x N + 1`. | [RawContent/World/README.md](../RawContent/World/README.md) |
|
||
| D-44 | **`L_World` is a product of a manifest, generated PNGs and one script, never hand-edited.** `RawContent/World/World.json` fixes the size and what a 16-bit value means in metres (world Z 0 is sea level); the height comes from a named source, seeded noise now, a real heightmap by editing the source block; the paint layers are always derived from the height. The level is dressed as Elite_RockyMeadows dresses its own maps, with the numbers read from those maps rather than guessed. `Decided`. | [RawContent/World/README.md](../RawContent/World/README.md), [Worklog](Worklog.md) |
|
||
|
||
## 2026-09-16 — Step 2, the telemetry seam
|
||
|
||
| # | Decision | Where |
|
||
| --- | --- | --- |
|
||
| D-43 | **The editor serves the engine's MCP plugin on `127.0.0.1:8000/mcp` whenever it is open.** `bAutoStartServer` in `Config/DefaultEditorPerProjectUserSettings.ini`, the port `.mcp.json` points Claude Code at, plus the `EditorToolset`, `AutomationTestToolset`, `GameplayTagsToolset`, `ConfigSettingsToolset` and `LiveCodingToolset` plugins. Loopback only; nothing listens when the editor is closed. Everything the plugin sends to an LLM is Licensed Technology under the UE EULA §6(e). | [CLAUDE.md](../CLAUDE.md) |
|
||
| D-42 | **The build string is `FApp::GetBuildVersion()` plus the git short hash read by `Salty.Build.cs` at build time.** A commit changes the definition and rebuilds the gameplay module; that cost is accepted so a telemetry file never lies about which build wrote it. `nogit` when git is unavailable. | [Telemetry](Spec/Telemetry.md) |
|
||
|
||
## 2026-09-16 — Step 1 closes the two open decisions
|
||
|
||
Taken when the project was created from the engine's Third Person template.
|
||
|
||
| # | Decision | Where |
|
||
| --- | --- | --- |
|
||
| D-41 | **The template's three gameplay variants stay in-tree as reference only.** `Source/Salty/Variant_*` and `Content/Variant_*` (Combat, Platforming, SideScrolling) compile and load but nothing new is built on them; they break the conventions (rules in Blueprint, casts, no server path) and are deleted when step 7 closes or the moment they block a build. The plain template character, game mode and controller are renamed `ATemplate*` and are the placeholder body until step 3. | [Architecture](Spec/Architecture.md) |
|
||
| D-40 | **The project is `Salty`.** `Salty.uproject` at the repository root, modules `Salty` and `SaltyCore`, test filter root `Salty.`. Closes OD-02. | [CLAUDE.md](../CLAUDE.md) |
|
||
| D-39 | **Unreal Engine 5.8, the launcher build.** Pinned in `Salty.uproject`; upgraded deliberately, never mid-step. Closes OD-01. Four Fab packs sit in `Content/` unused: `Medieval_Weapons` is expected at step 7, `RPGEnvironmentVFX` at step 14, `HouseForge_01` as dressing after 7, `Elite_RockyMeadows` serves no step on the ladder. A pack enters a step only when that step's spec names the asset. | [Steps](Steps.md) |
|
||
|
||
## 2026-09-15 — The stat block
|
||
|
||
Taken on the same day, after the first read of the doc set.
|
||
|
||
| # | Decision | Where |
|
||
| --- | --- | --- |
|
||
| D-38 | **Counters live on the receiver as tags and attributes**: immunity blocks by tag, resistance scales by attribute, cleanse removes by tag, and every status and every block is visible. The same status from several sources: strongest wins, longest remaining duration; different statuses multiply. | [Stats](Spec/Stats.md) |
|
||
| D-37 | **One stat block on everything with a body, and every outside influence is a gameplay effect on it.** Base values, buffs, debuffs, ground, carried weight, gear and being downed all go through the same effects; no system keeps its own multiplier. `Supersedes` the tagged speed-multiplier map the first draft of Movement gave the movement component. | [Stats](Spec/Stats.md) |
|
||
|
||
## 2026-09-15 — The move to Unreal and the reframing
|
||
|
||
Taken while rewriting the two earlier projects' documentation into this one. Everything below is `Decided`
|
||
unless marked.
|
||
|
||
| # | Decision | Where |
|
||
| --- | --- | --- |
|
||
| D-01 | **Unreal Engine 5, C++ first.** Blueprints compose, tune and decorate; they never hold a rule, a replicated property or a server RPC. | [Architecture](Spec/Architecture.md) |
|
||
| D-02 | **Two runtime modules**, `<Project>Core` (rules, data, tags, telemetry contract; no actors) and `<Project>` (gameplay). Dependency one way. A third arrives when a feature is stable, never before. | [Architecture](Spec/Architecture.md) |
|
||
| D-03 | **Server-authoritative from the first line; the dedicated server is the real target.** No listen-server design, no "are we multiplayer" branch. Tested with the editor's dedicated server option from step 1. | [Networking](Spec/Networking.md) |
|
||
| D-04 | **The client predicts only its own movement and its own animation.** Three responsiveness tiers assigned by one test: does a rollback have a visible victim? No rollback of world state a player can already see. | [Networking](Spec/Networking.md) |
|
||
| D-05 | **Ask, then show.** Commit-shaped moments wait for the server inside their own feedback beat. Never show a number as final before it is committed. | [Networking](Spec/Networking.md) |
|
||
| D-06 | **A player is an identity, not a body.** Class, attributes, cooldowns, discoveries and identity live on the player state; the character actor is lent. | [Networking](Spec/Networking.md), [Architecture](Spec/Architecture.md) |
|
||
| D-07 | **Persistence behind one provider interface with a local-file implementation.** Nothing is persisted yet; the seam exists first. | [Networking](Spec/Networking.md) |
|
||
| D-08 | **Gameplay Tags are the vocabulary.** `Config/Tags/<Feature>.ini` is the source; code-referenced tags are declared natively. No string ids anywhere. | [Architecture](Spec/Architecture.md) |
|
||
| D-09 | **Content is `UPrimaryDataAsset`s** addressed by primary asset id or tag, with soft references for art. | [Architecture](Spec/Architecture.md) |
|
||
| D-10 | **Rules are pure functions in the core module**, tested without a world, called by both the client preview and the server verdict. | [Architecture](Spec/Architecture.md) |
|
||
| D-11 | **Telemetry from step 2**, one subsystem, one sink interface, JSON Lines to a local file, no personal data, events not aggregates. | [Telemetry](Spec/Telemetry.md) |
|
||
| D-12 | **`UCharacterMovementComponent`, extended**, not a custom controller and not the Mover plugin. Sprint is a saved-move flag; dodge and blink are root-motion abilities. | [Movement](Spec/Movement.md) |
|
||
| D-13 | **One rig, two camera modes.** First and third person on the same mannequin; the camera never enters the authority path; both modes exist from step 3; the default is decided by playing (OD-03). | [Movement](Spec/Movement.md) |
|
||
| D-14 | **The deadzone look model**, salvaged: the head turns freely inside a yaw deadzone, then the body follows; the deadzone widens while carrying. | [Movement](Spec/Movement.md) |
|
||
| D-15 | **Sprint takes Shift.** The earlier four-slot layout (Q, E, Shift, R) becomes Q, E, R, C. Interact F, drop G tap, throw G hold. `Supersedes` the earlier project's Controls decision. | [Movement](Spec/Movement.md) |
|
||
| D-16 | **The controller is built first and proved against a written checklist in a gym level** that never leaves the project. | [Movement](Spec/Movement.md), [Steps](Steps.md) |
|
||
| D-17 | **One interaction system.** An interface, a world subsystem as the funnel, the player's component as the only RPC path; reach re-checked server-side with tolerance; prompt and permission from one function. | [Interaction](Spec/Interaction.md) |
|
||
| D-18 | **Interaction is a discrete act on a fixed prop; carrying is continuous possession.** Pick-up is routed by the interaction key but is a carry verb. | [Interaction](Spec/Interaction.md) |
|
||
| D-19 | **Two hands; two-handed objects block interaction and, in first person, the view.** Held objects attach and stop simulating; dropped and thrown ones are server physics. Throwing is in and scrappy; nothing is destroyed by it. | [Interaction](Spec/Interaction.md) |
|
||
| D-20 | **The Gameplay Ability System** for attributes, abilities, effects, cooldowns and cues. The component is on the player state for players and on the pawn for enemies. | [Combat](Spec/Combat.md) |
|
||
| D-21 | **One damage funnel**: one execution calculation every damaging effect uses. **No friendly fire**: the funnel discards player-on-player damage and keeps the impulse. | [Combat](Spec/Combat.md) |
|
||
| D-22 | **Down before death.** Bleed-out, revive by anyone, a solo down is an instant wipe. A downed player is a prop. | [Combat](Spec/Combat.md) |
|
||
| D-23 | **Kits define ability access; gear defines stats and crosses classes; mods never grant a signature.** The `bSignature` flag exists from the first ability; gear and mods do not. | [Combat](Spec/Combat.md) |
|
||
| D-24 | **The recoverability contract** applies to any verb that inconveniences a teammate: recovery takes less time than the verb, and none may down, kill or deprive. All emit `grief_action`. | [Combat](Spec/Combat.md) |
|
||
| D-25 | **Sublinear party scaling and role relaxation** are reserved rules applied to a count. | [Combat](Spec/Combat.md) |
|
||
| D-26 | **Part, piece, trait.** A family declares parts; a piece fills one and carries three trait layers. No recipes anywhere. | [Crafting](Spec/Crafting.md) |
|
||
| D-27 | **"Material" is "substance"** in code and docs, because `UMaterial` is the engine's. | [Crafting](Spec/Crafting.md) |
|
||
| D-28 | **Substances are objects, not stacks**, with identity, quality and provenance. Affordable only because storage is physical; an abstract bank is refused on that ground. | [Crafting](Spec/Crafting.md) |
|
||
| D-29 | **Discovery unlocks, choice applies.** Characteristics are discovered through play and chosen at the station. Known set is per player. | [Crafting](Spec/Crafting.md) |
|
||
| D-30 | **One activity runtime, three guarantees**: completion never gated on skill; skill modulates a quality band only; a second operator is additive, never required. The anvil is the growing zone; the forge is a 5×5 bed with two levels of detail bound by an equivalence rule. | [Crafting](Spec/Crafting.md) |
|
||
| D-31 | **The assembly bench is domain-neutral; domains gate the work, never the worker.** Class changes speed and quality only. | [Crafting](Spec/Crafting.md) |
|
||
| D-32 | **Durability is per part, summed; the item is lost at zero; nothing wears from crafting.** | [Crafting](Spec/Crafting.md) |
|
||
| D-33 | **A crafted weapon-family item is a weapon**: both authored and crafted weapons produce one `FWeaponProfile` that the swing, the funnel and the bar read. `[PROPOSED]` until step 13 closes. | [Crafting](Spec/Crafting.md), [Combat](Spec/Combat.md) |
|
||
| D-34 | **UMG with CommonUI; one theme asset by role; prop text is world-space with no canvas; every readable string is `FText` from a string table.** The menu never pauses the world. | [UI](Spec/UI.md) |
|
||
| D-35 | **Steps, not a roadmap.** A ladder closed by proofs; the next few steps concrete, the rest sketched and detailed only when reached. | [Steps](Steps.md), [Design](Design.md) |
|
||
| D-36 | **The two earlier loops are not carried.** Their ideas are catalogued as features with needs and constraints, none scheduled. | [Ideas](Ideas.md) |
|
||
|
||
## Open decisions
|
||
|
||
Questions that block steps. Each names what it blocks and the recommendation on record, so closing one is a
|
||
confirmation, not a fresh discussion. When one is taken it moves up as a D-line and the row is marked closed.
|
||
|
||
| id | Question | Blocks | Recommendation | Status |
|
||
| --- | --- | --- | --- | --- |
|
||
| OD-01 | **Engine version.** | 1 | The newest release with a hotfix out (5.6.x at the time of writing). Pin it in `CLAUDE.md` and the `.uproject` in step 1; upgrade deliberately, never mid-step. | Closed, D-39 (5.8) |
|
||
| OD-02 | **Project and module name.** | 1 | Short, one word, no spaces. It becomes the `.uproject`, the module prefix and the test filter root. The docs write `<Project>` until it exists. | Closed, D-40 (Salty) |
|
||
| OD-03 | **Default camera mode.** | none until 8 | Build both in step 3, play the gym and the first fight in each, let `camera_mode_changed` and `movement_sample` settle it by step 8. | Recommended |
|
||
| OD-04 | **Engine from source.** | the first packaged server | The launcher build until then; the editor's dedicated-server option covers every step in the ladder. | Recommended |
|
||
| OD-05 | **Placeholder character and animations.** | 3 | The engine's Third Person template mannequin and its animation blueprint; a retargeted pack only when the swing needs a clip the template lacks. | Recommended |
|
||
| OD-06 | **Physics replication mode for thrown objects.** | 6 | The engine's predictive interpolation mode; resimulation is not needed for objects nobody predicts. | Recommended |
|
||
| OD-07 | **Which two kits ship first.** | 9 | Warrior and Cleric: a taunt and a heal are what make the goblin's threat table and the downed state legible. | Recommended |
|
||
| OD-08 | **Crafting professions against combat kits.** | after 15 | A design session, not a step. See Ideas. | Open |
|
||
|
||
## Deferred, with triggers
|
||
|
||
Not gates. Listed so nobody reopens them as if they were undecided.
|
||
|
||
| Deferred | Until |
|
||
| --- | --- |
|
||
| The Mover plugin | It leaves experimental and a movement feature needs what it has (Movement Q2) |
|
||
| Stamina as a cost | A kit wants one (Combat Q2); the attribute exists so it is a cost effect, not a refactor |
|
||
| A blade sweep instead of the box hit check | Real animation makes the box read wrong (Combat Q4) |
|
||
| Storage beyond hands and station buffers | A step needs more than hands can hold (Crafting Q4, Ideas) |
|
||
| Alloys and the kiln | Post-fifteen; meanwhile nobody hard-codes one-to-one processing (Crafting Q5) |
|
||
| Generated glyph discovery | Its own session; the composition path never changes, any seed is global |
|
||
| Reconnect into a running session | Written down before the first playtest with strangers (Networking Q1) |
|
||
| Seamless travel, a second map | The second map |
|
||
| Replication Graph or Iris | A world with many actors and many players; dormancy and relevancy keep it cheap |
|
||
| Steam, lobbies, invitations, voice | A session layer in front of the server; never inside gameplay |
|
||
| Continuous integration | A reason for it; tests run by hand until then |
|
||
| Resistances per damage type beyond physical and magic | A second elemental damage type (Stats Q2) |
|
||
| Predicting surface effects on the owning client | The correction on entering mud is felt (Stats Q1) |
|
||
| A stat block on props that are not bodies | The first prop that should burn or freeze (Stats Q3) |
|