- C# 96.4%
- Python 1.8%
- HLSL 0.9%
- Lua 0.4%
- Shell 0.2%
- Other 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
## Summary The client removes the layers above you **only when the world's own structure says you are inside it** — never because something happens to be drawn in front of you. That distinction is the whole PR, and it was arrived at by getting it wrong first, in the open. Cutting on *occlusion* — "something is between the camera and the body" — is true on any slope with a hill in front of it, and it blacked out half the screen in an open forest. Cutting on "there is higher ground nearby" is the same mistake spelled differently; the forest has that too. What separates a crater from a forest is **measured**: of the eight compass directions, how many still offer a way out (0-1 against 4-5, at every radius tried). So there are exactly two ways in: - **UNDER** — your column has another interval above the one you stand on: a cave, a tunnel, an overhang; - **SHUT IN** — at most one of the eight directions offers a way out within `EscapeRadiusCells`. A canyon has two ways out along its length and is deliberately left alone; so is a plain with a hill on it. **Nothing in it is absolute**, so a canyon at z −80 behaves exactly like the same canyon at z +80 and terrain authored below zero needs no special case — the owner's requirement, in his words: *"van scritte regole strutturali precise, un domani creerò canyon e valli sotto z0, non voglio ripetere sta discussione"*. And nothing in it is a camera, so the answer is identical from every viewing angle. What is then removed is everything above a **structural height** — the bottom of the ceiling you are under, or head height over the floor you are shut in on — and never a shape derived from where the player stands. A boundary anchored to the walker slides across the landscape at every step (*"il problema non è la zona nera, è che si muove"*); one anchored to a height is a contour of the terrain and holds still. The full model lives in the header of `TerrainCutaway` (Client.Core) — it is the reference, and `CLAUDE.md` carries the invariant so it cannot quietly drift back to occlusion. ## Screenshots / recording **Not attached, and I am not going to pretend otherwise.** This behaviour was developed and judged interactively with the owner over several rounds on his own world, against his screenshots — that is how the occlusion version was caught and rejected. What I can attach now would be a re-staging after the fact, not the evidence that drove the decisions. What replaces them here, and is stronger for a reviewer: `TerrainCutaway` is **pure and unit-tested**, and the landform contract is pinned as a table — cave → cut, crater (0-1 ways out) → cut, canyon (2 ways out) → no cut, valley and plain → no cut. `CutawayState` also exposes `Under`, `ShutIn`, `CutMode` and a one-word `Shape` to the admin overlay, so the decision is legible in-game rather than being a black screen with no explanation. Three known gaps are filed rather than hidden: **#279** (a v3 that preserved a reachable region and cut by screen-space occlusion — built, judged worse by the owner, reverted; the revert pair is not in this branch because the two cancel exactly), **#280** (the character reads as detached from the terrain when the cut is active) and **#281** (silhouette in the open). ## How it was tested - Unit tests in `IsoMmo.Client.Core.Tests` over `TerrainCutaway`: the two entries (UNDER / SHUT IN), the eight-direction escape count, and the landform table above. - The Z fed to it is the **authoritative** surface, never the smoothed `DisplayZ` — asserted, because that is the one substitution that would make the answer depend on presentation. - Gates on this exact commit: CSharpier clean (580 files), `dotnet test` green (10 test 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 issues (#279, #280, #281 stay open as follow-ups) --- **Stack:** 2 of 3, based on `feat/volume-world-f1` (#283). Merge that one first; this PR's base can then be retargeted to `main`. --- # Added by the merge of #285: the map editor redesign (closes #282) While this PR was being rebased onto the squashed `main`, **#285 was merged into this branch**, so it now carries the editor work as well. Nothing was lost or changed by that: the tree here is byte-for-byte identical to the original 43-commit branch, verified by diff. The two halves stay independent — the cutaway touches the client renderer, the editor touches `tools/IsoMmo.MapEditor*` — so they can still be read separately. ## Summary of the editor half The height tools were producing terrain the game could not use, and the editor could not show it. They are rebuilt around one idea: **nothing reaches the map until it has been judged, and it is judged with the game's own rule** (`StepRule`) 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 proposes. 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 a top-down canvas cannot show a shape. - **Overlays** score slope / walkability / reachability, keeping a *route* (`SustainableRisePerStep`) and a single *ledge* (`MaxWalkStepZ`) apart: a mountain built of legal single steps is a wall you can stroll up. - **Ridges and calderas** blend into the ground they land on, so a shape has no seam by construction. The caldera gained an explicit `CliffWall` because a smooth cone turned out to be *walkable* — measured, not deduced. - **Roads**, driven to three revisions by the owner: one centreline and one baker for freehand and routed alike; routing that prices each step's rise by its **square** and earthwork at 25 a Z, so it takes the gentlest line and accepts being longer (*"come una vera strada di montagna"*); and the routed line rounded off, because a grid path at an angle **is** a staircase (107 direction changes in 272 cells). How much to round is the ground's call — `RoadPlanner` keeps the smoothest line that costs the road nothing. - The baker stopped blaming a road for terrain it was laid across: it measured the *absolute* step at the edge of its earthwork, so a flat road on flat ground was refused for a 131 Z escarpment seventy cells away. It now measures the delta. Measured on the authored massif, 116 Z over 260 cells: **272 cells instead of 262, steepest steps 6 instead of 55, no earthwork at all**; turning 369° → 206° after rounding, shoulder 140 cells → 26. The baked road is walkable by `StepRule` from foot to summit, **984/984 paved cells reachable**, 0 seams. ## Screenshots (editor half) Driven through the editor's own automation channel on a **copy** of the authored world: ``` ui off · view 2d · goto 7449 4230 · zoom 2.6 · overlay slope route road_dirt 3 96 7449 4356 7449 4096 screenshot <path> ``` Instradata contro linea dritta — verde = pendenza sostenibile, giallo = gradino, rosso = muro:  Prima e dopo l'arrotondamento, stesso tratto ingrandito 3x:  A 7 px/cella, mentre infila la sella fra due masse invalicabili:  ## Gates, on this exact head CSharpier clean (608 files), `dotnet test` green — **1,200 tests across 10 projects** — whole solution builds with zero warnings. 192 of those tests are the editor's, all pure, including the road regressions pinned from real failures: a wide road climbing a hill is not a conflict with itself; a road beside a natural cliff is not blamed for the cliff; a gentle detour beats a steep short line; a road over walkable ground moves no earth at all. Reviewed-on: #284 |
||
| .claude/skills | ||
| .forgejo | ||
| .vscode | ||
| art | ||
| client | ||
| content | ||
| docs | ||
| scripts | ||
| spikes | ||
| src | ||
| tests | ||
| tools | ||
| .csharpierignore | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| .luacheckrc | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| Directory.Build.props | ||
| dotnet-tools.json | ||
| IsoMmo.slnLaunch.user | ||
| IsoMmo.slnx | ||
| justfile | ||
| README.md | ||
| renovate.json | ||
IsoMmo
A small isometric MMO in the spirit of Ultima Online. .NET 10 on the server, MonoGame (DesktopGL) on the client — .NET all the way down, no game engine or editor to install.
Log in from the client (accounts are created silently on first login), pick a character name once (min 3 chars, unique in the world), then walk an endless green field in eight directions and see everyone else doing the same — rendered with the legacy UO human animations.
Layout
src/IsoMmo.Shared protocol contracts, grid math (shared by server and client)
src/IsoMmo.Auth accounts + characters, issues JWTs http://localhost:5080
src/IsoMmo.GameServer authoritative world, WebSocket /ws http://localhost:5100
client/IsoMmo.Client MonoGame (DesktopGL) C# client (.NET 10)
client/IsoMmo.Client.Core engine-independent client logic (unit-tested)
tools/IsoMmo.AssetExtractor exports UO human art from a legacy client
tests/ xUnit (unit + integration)
Getting started (development)
Prerequisites: the .NET 10 SDK. Optionally just as a task runner, and a legacy UO client install for the sprites (only anim.mul/anim.idx are read). There is no engine or editor: the client is a plain MonoGame app run with dotnet run, and its native dependencies (SDL2/OpenAL) come from NuGet.
With just:
just test # build + run all tests
just extract "D:\Games\UO" # extract human sprites once (output is git-ignored)
just dev # Auth + GameServer + client (Ctrl+C stops everything)
just client # just the client (servers already running)
Plain dotnet (no just):
dotnet test
dotnet run --project src/IsoMmo.Auth # :5080
dotnet run --project src/IsoMmo.GameServer # :5100
dotnet run --project client/IsoMmo.Client
# servers-only helper: ./scripts/dev.sh (add --game to also launch the client)
In the client: on the login screen enter any username + a password of at least 8 characters — the account is created on the fly. Pick a character name, then hold the right mouse button to walk toward the cursor in eight directions; the camera follows you. Press Enter to chat (Enter sends, Esc cancels) — messages appear above the speaker's head. Launch a second client with a different username to see other players move and talk in real time.
Without extracted art the client falls back to placeholder diamonds and logs a warning. A bundled font (Roboto, Apache-2.0) and the sprite PNGs are loaded at runtime — there is no MonoGame content-pipeline build step.
The auth database is a local SQLite file (isommo-auth.db), created and kept up to date by EF migrations at startup; delete it to start fresh. Schema changes need one migration — see CLAUDE.md (or just migrate-auth <Name>).
Multi-platform
The server (Auth + GameServer) runs on Windows, macOS, and Linux; the client runs on Windows and macOS (Linux is a bonus). It is pure .NET / MonoGame DesktopGL — a single build runs everywhere, launched with dotnet run (no code signing needed for development). Extract the UO art once on any machine and copy client/IsoMmo.Client/assets/ across by hand; it is deliberately not in git.
Development rules
- Definition of done — a feature is complete if and only if:
dotnet testis fully green, the whole solution builds (client and tools included), and there is not a single warning (warnings are errors viaDirectory.Build.props). - Authoritative server. Clients send intents, never state. Movement rules are enforced server-side; the client only renders.
- Protocol lives in
IsoMmo.Sharedonly. JSON with atypediscriminator, camelCase everywhere, enums as strings. - Tests are not optional: unit tests for game logic,
WebApplicationFactoryintegration tests for HTTP and WebSocket flows. Coverage >85% per project. - Conventional Commits, one commit per feature.
- Extracted UO art never enters git (
client/IsoMmo.Client/assets/is ignored). The repo must always work without it.
See CLAUDE.md for the full invariants list.