feat(editor): judge terrain with the game's own rule, and propose before committing #285

Merged
panda merged 39 commits from feat/map-editor-redesign into feat/terrain-cutaway 2026-08-08 15:41:58 +02:00
Collaborator

Closes #282.

Summary

The height tools were producing terrain the game could not use, and the editor could not show it. This
rebuilds them around one idea: nothing reaches the map until it has been judged, and it is judged with
the game's own rule
(StepRule, the same gate TileMapRules.CanStep applies) rather than with
arithmetic of the editor's own — an editor that scores terrain its own way ends up promising ground the
server refuses.

Preview. Every height tool now proposes. The proposal is drawn over the canvas tinted by the
validator's verdicts, with magenta for a seam, and the readout carries the numbers the decision needs
before it is made: cells changed, max step, walls, seams, undo cost. Enter commits it as one undoable
step; Escape and the map is byte-for-byte as it was. The 3D view carries the proposal too — a road is
a shape, and the top-down canvas cannot show a shape.

Overlays score the terrain in the mover's terms: slope (sustainable route / single ledge / wall),
walkability, and reachability by flood. Two rules deliberately never blurred into one "walkable" number:
a route must hold SustainableRisePerStep, a single ledge may clear MaxWalkStepZ once. A mountain built
of legal single steps is a wall you can stroll up, which is why they are counted apart.

Ridges and calderas are rebuilt to blend into the ground they land on, so a shape has no seam by
construction; the caldera is one continuous radial section with an explicit CliffWall, because a smooth
cone turned out to be walkable — spiral up it and you are on the rim. Measured, not deduced.

Roads are the largest piece, and the owner drove it to three revisions:

  • One centreline, one baker. Freehand and Route both hand a line to RoadBaker; the tools these replace
    stamped different geometry from the same path.
  • "la strada dovrebbe seguire naturalmente il terreno con meno pendenza, anche a costo di essere più
    lunga… come una vera strada di montagna"
    — so routing prices each step's rise by its square (pricing
    it linearly is a constant between fixed endpoints and changes nothing) and prices earthwork at 25 a Z
    instead of 2. On the authored massif, 116 Z over 260 cells: 272 cells instead of 262, steepest steps 6
    instead of 55, and no earthwork at all
    .
  • "è da renderla meno a scale… più naturale" — a grid path travelling at an angle is a staircase:
    measured, 107 direction changes in 272 cells, 75 of its 108 straight runs one single cell long. The line
    is simplified to the corners that carry information and those corners rounded off, held inside a corridor
    as wide as the simplification tolerance so it can never wander onto ground the router never looked at.
    How much to smooth is the ground's call, not a constant: RoadPlanner bakes each rung and keeps the
    smoothest line that costs the road nothing. Turning 369° → 206°, sharpest bend 5.7° → 2.9°, and the
    shoulder falls from 140 cells to 26.

The baker also stopped blaming a road for terrain it was laid across. Its postcondition measured the
absolute step at the edge of its earthwork, so a flat road on flat ground was refused because of a 131 Z
escarpment the author had drawn seventy cells away. It now measures the delta: a cut face is reported
(that is what a bench cut into a hillside is), a carriageway left standing on nothing refuses the bake.

content/worlds/caldera-fixture is a 512×512 crop of the authored world frozen as a regression fixture —
the terrain that showed what the tools were doing wrong, kept so a redrawn landform is compared against
something real instead of against a memory of it. Its numbers are pinned by tests, so editing it fails the
build.

Screenshots / recording

Captured by driving the editor through its own automation channel (docs/map-editor.md — a DEBUG-only
loopback on 127.0.0.1:5200 that drives the REAL editing paths), on a copy of the authored world so
nothing of the owner's was touched:

ui off · view 2d · goto 7449 4230 · zoom 2.6 · overlay slope
route road_dirt 3 96 7449 4356 7449 4096
screenshot <path>

The routed road before and after the rounding, side by side at the same framing and camera, and a
close-up at 7 px/cell of the road threading the saddle between two impassable masses.
Instradata contro linea dritta — stesso massiccio, stessi due estremi. Verde = pendenza sostenibile,
giallo = gradino, rosso = muro.

routed vs straight

Prima e dopo l'arrotondamento, stesso tratto ingrandito 3x:

before and after

A 7 px/cella, mentre infila la sella fra due masse invalicabili:

close up

Verified numerically on the same road: worst step 2 Z, 984/984 paved cells reachable from the foot,
summit reached, 0 seams.

How it was tested

  • 192 tests in IsoMmo.MapEditor.Core.Tests, all pure: the validator against StepRule, the edit
    window/plan/history (including the undo budget refusing rather than committing without undo), the crop
    and survey, ridge and caldera geometry, and the road baker and router.
  • Road regressions pinned from real failures the owner hit: a wide road climbing a hill is not a conflict
    with itself; a road beside a natural cliff is not blamed for the cliff; an earthwork that cannot reach
    the ground leaves a reported cut face rather than a refusal; a carriageway that cannot be supported is
    refused; a gentle detour beats a steep short line; a road over walkable ground moves no earth at all.
  • Measured end-to-end on the owner's own massif in asterra (read-only): the baked road is walkable by
    StepRule from foot to summit with zero seams.
  • Gates: CSharpier clean (608 files), dotnet test green — 1,200 tests across 10 projects — whole
    solution builds with zero warnings.

Checklist

  • just lint passes (CSharpier + analyzers, zero warnings)
  • just test is green
  • The whole solution builds (client and tools included)
  • Multi-platform preserved (server on Win/macOS/Linux, client on Win/macOS)
  • Tests added/updated for this change
  • Linked the related issue (#282) and its Definition of Done is met

Stack: 3 of 3, based on feat/terrain-cutaway (#284), itself based on feat/volume-world-f1 (#283).
Merge in order.

Closes #282. ## Summary The height tools were producing terrain the game could not use, and the editor could not show it. This rebuilds them around one idea: **nothing reaches the map until it has been judged, and it is judged with the game's own rule** (`StepRule`, the same gate `TileMapRules.CanStep` applies) rather than with arithmetic of the editor's own — an editor that scores terrain its own way ends up promising ground the server refuses. **Preview.** Every height tool now *proposes*. The proposal is drawn over the canvas tinted by the validator's verdicts, with magenta for a seam, and the readout carries the numbers the decision needs before it is made: cells changed, max step, walls, seams, undo cost. **Enter** commits it as one undoable step; **Escape** and the map is byte-for-byte as it was. The 3D view carries the proposal too — a road is a shape, and the top-down canvas cannot show a shape. **Overlays** score the terrain in the mover's terms: slope (sustainable route / single ledge / wall), walkability, and reachability by flood. Two rules deliberately never blurred into one "walkable" number: a route must hold `SustainableRisePerStep`, a single ledge may clear `MaxWalkStepZ` once. A mountain built of legal single steps is a wall you can stroll up, which is why they are counted apart. **Ridges and calderas** are rebuilt to blend into the ground they land on, so a shape has no seam by construction; the caldera is one continuous radial section with an explicit `CliffWall`, because a smooth cone turned out to be *walkable* — spiral up it and you are on the rim. Measured, not deduced. **Roads** are the largest piece, and the owner drove it to three revisions: - One centreline, one baker. Freehand and Route both hand a line to `RoadBaker`; the tools these replace stamped different geometry from the same path. - *"la strada dovrebbe seguire naturalmente il terreno con meno pendenza, anche a costo di essere più lunga… come una vera strada di montagna"* — so routing prices each step's rise by its **square** (pricing it linearly is a constant between fixed endpoints and changes nothing) and prices earthwork at 25 a Z instead of 2. On the authored massif, 116 Z over 260 cells: **272 cells instead of 262, steepest steps 6 instead of 55, and no earthwork at all**. - *"è da renderla meno a scale… più naturale"* — a grid path travelling at an angle **is** a staircase: measured, 107 direction changes in 272 cells, 75 of its 108 straight runs one single cell long. The line is simplified to the corners that carry information and those corners rounded off, held inside a corridor as wide as the simplification tolerance so it can never wander onto ground the router never looked at. How much to smooth is the **ground's** call, not a constant: `RoadPlanner` bakes each rung and keeps the smoothest line that costs the road nothing. Turning 369° → 206°, sharpest bend 5.7° → 2.9°, and the shoulder falls from 140 cells to 26. The baker also stopped blaming a road for terrain it was laid across. Its postcondition measured the *absolute* step at the edge of its earthwork, so a flat road on flat ground was refused because of a 131 Z escarpment the author had drawn seventy cells away. It now measures the delta: a **cut face** is reported (that is what a bench cut into a hillside *is*), a carriageway left **standing on nothing** refuses the bake. `content/worlds/caldera-fixture` is a 512×512 crop of the authored world frozen as a regression fixture — the terrain that showed what the tools were doing wrong, kept so a redrawn landform is compared against something real instead of against a memory of it. Its numbers are pinned by tests, so editing it fails the build. ## Screenshots / recording Captured by driving the editor through its own automation channel (`docs/map-editor.md` — a DEBUG-only loopback on 127.0.0.1:5200 that drives the REAL editing paths), on a **copy** of the authored world so nothing of the owner's was touched: ``` ui off · view 2d · goto 7449 4230 · zoom 2.6 · overlay slope route road_dirt 3 96 7449 4356 7449 4096 screenshot <path> ``` The routed road before and after the rounding, side by side at the same framing and camera, and a close-up at 7 px/cell of the road threading the saddle between two impassable masses. **Instradata contro linea dritta** — stesso massiccio, stessi due estremi. Verde = pendenza sostenibile, giallo = gradino, rosso = muro. ![routed vs straight](https://git.homelab.devncode.it/attachments/fe08b153-8e52-4d7b-b4ea-aff391450981) **Prima e dopo l'arrotondamento**, stesso tratto ingrandito 3x: ![before and after](https://git.homelab.devncode.it/attachments/00fd61d4-bd8d-49a3-b5d7-5c6b35a6b7b6) **A 7 px/cella**, mentre infila la sella fra due masse invalicabili: ![close up](https://git.homelab.devncode.it/attachments/995ca474-8c33-46c3-a131-d6c64cd55cb5) Verified numerically on the same road: worst step 2 Z, **984/984 paved cells reachable from the foot**, summit reached, 0 seams. ## How it was tested - 192 tests in `IsoMmo.MapEditor.Core.Tests`, all pure: the validator against `StepRule`, the edit window/plan/history (including the undo budget refusing rather than committing without undo), the crop and survey, ridge and caldera geometry, and the road baker and router. - Road regressions pinned from real failures the owner hit: a wide road climbing a hill is not a conflict with itself; a road beside a natural cliff is not blamed for the cliff; an earthwork that cannot reach the ground leaves a reported cut face rather than a refusal; a carriageway that cannot be supported is refused; a gentle detour beats a steep short line; a road over walkable ground moves no earth at all. - Measured end-to-end on the owner's own massif in `asterra` (read-only): the baked road is walkable by `StepRule` from foot to summit with zero seams. - Gates: CSharpier clean (608 files), `dotnet test` green — 1,200 tests across 10 projects — whole solution builds with zero warnings. ## Checklist - [x] `just lint` passes (CSharpier + analyzers, zero warnings) - [x] `just test` is green - [x] The whole solution builds (client and tools included) - [x] Multi-platform preserved (server on Win/macOS/Linux, client on Win/macOS) - [x] Tests added/updated for this change - [x] Linked the related issue (#282) and its Definition of Done is met --- **Stack:** 3 of 3, based on `feat/terrain-cutaway` (#284), itself based on `feat/volume-world-f1` (#283). Merge in order.
A road is now routed like a mountain road and judged on what it changes.

Routing prices every step's rise by its SQUARE, so the search lengthens
rather than steepens (pricing rise linearly is a constant between fixed
endpoints and changes nothing), and prices earthwork at 25 a Z instead of
2, so a detour of several cells beats moving the ground at all. On the
authored massif, 116 Z over 260 cells: 272 cells instead of 262, steepest
steps 6 instead of 55, and no earthwork at all.

The baker's postcondition measured the ABSOLUTE step at the edge of its
earthwork, so a flat road on flat ground was refused because of a 131 Z
escarpment drawn seventy cells away - a wall the road neither made nor
worsened. It now measures the delta: a cut face is reported, a carriageway
left standing on nothing refuses the bake. The shoulder stops at the first
cell where the ground already lies inside its limit rather than crossing
it, and fills only under carriageway the road raised, so a road along the
lip of a cliff no longer proposes an embankment down the side of it.

The 3D view carries the pending proposal, and the harness gains `route`.
feat(editor): round off a routed road instead of leaving the grid's staircase
All checks were successful
ci / Lua content lint (pull_request) Successful in 1m13s
ci / Lint & Test (pull_request) Successful in 10m32s
8f98fc93a7
A search returns a grid path, and a grid path travelling at an angle is a
staircase: on the authored massif, 107 direction changes in 272 cells,
75 of its 108 straight runs one single cell long, and 369 degrees of
turning on a road that goes essentially one way. None of that is a
decision about the ground - it is the lattice, and it is what makes a
routed road read as angular.

The line is now simplified to the corners that carry information and
those corners rounded off. The smoothed curve is held inside a corridor
as wide as the simplification tolerance, so it cannot wander onto ground
the router never looked at, and a hairpin whose legs are further apart
than that survives with its apex a cell or two shallower.

How much to smooth is the ground's call, not a constant: RoadPlanner
bakes each rung of the ladder and keeps the smoothest line that costs the
road nothing against the routed one - no more shoulder, no taller cut
face, no new wall on the carriageway. On the massif it takes the
strongest rung: turning 369 to 206 degrees, sharpest bend 5.7 to 2.9, and
the shoulder falls from 140 cells to 26, because a wobbling centreline
drags a three-cell carriageway across the contours and every wobble has
to be graded out.

A hand-drawn stroke is never eased. That one is the author's own line.
panda merged commit 90772b9c5c into feat/terrain-cutaway 2026-08-08 15:41:58 +02:00
panda deleted branch feat/map-editor-redesign 2026-08-08 15:41:58 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo!285
No description provided.