diff --git a/client/project.godot b/client/project.godot index 92d5d4da6..3383337a4 100644 --- a/client/project.godot +++ b/client/project.godot @@ -17,7 +17,7 @@ config/name="The Settled Reach" ; in the editor and in a shipped build, where res://../project.yaml does not ; exist at all (T-1241). Kept honest by `make check-client-version`, which the ; pre-push hook runs — do not edit this by hand without moving project.yaml too. -config/version="0.4.10" +config/version="0.4.11" run/main_scene="res://scenes/main_menu.tscn" config/features=PackedStringArray("4.6", "GL Compatibility") config/icon="res://icon.svg" diff --git a/client/ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd b/client/ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd index 5a5bf24ba..a1106324e 100644 --- a/client/ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd +++ b/client/ui/implant/apps/atlas/step_canvas/step_canvas_colorize.gd @@ -57,12 +57,47 @@ const MORPHOLOGY_OPEN_OCEAN: int = 0 ## and Barren(1) get none, which is the point: bare ground should read bare. const VEGETATION_SCRUB: int = 2 +## The rest of VegetationClass (district_profile.rs): Barren is bare rock above +## treeline or hyper-arid, Forest is closed canopy, and the two Riparian classes +## are the 1-3 tile band along perennial water that takes precedence over the +## base class. Each earns its own mark — see [method _cover_mark]. +const VEGETATION_BARREN: int = 1 +const VEGETATION_FOREST: int = 3 +const VEGETATION_RIPARIAN_SCRUB: int = 4 +const VEGETATION_RIPARIAN_THICKET: int = 5 + +## Per-class cover marks (T-1213). Densities are the share of a class's ground +## that carries a mark; strengths are how far it moves the base colour. +## +## Tuned so the classes separate at a glance without any of them becoming a +## second colour layer: rock is the sparsest and strongest (a few bright chips), +## forest the densest and coarsest (merging blobs), scrub the quietest (grain). +## Riparian sits above its dry counterpart on both axes, because a waterside +## thicket genuinely is the thickest cover on the map. +const ROCK_OUTCROP_DENSITY: float = 0.18 +const ROCK_OUTCROP_STRENGTH: float = 0.20 +const SCRUB_PATCH_DENSITY: float = 0.26 +const SCRUB_PATCH_STRENGTH: float = 0.08 +## Forest is the BACKGROUND on a wooded body, not a feature — measured on +## Ferrath's District patch it is 98.9% of the ground. A first pass marked 52% of +## it at a 4x4 lattice, which produced visibly axis-aligned dark SQUARES (a 4-cell +## block is 8 screen px at this rung) and read as a rendering artefact laid over +## the hillshade rather than as woodland. Pulled back to grain at a 2x2 lattice: +## the forest should texture the ground it covers, and the OCCASIONAL thing — +## the outcrop, the clearing — should be what catches the eye. +const FOREST_PATCH_DENSITY: float = 0.30 +const FOREST_PATCH_STRENGTH: float = 0.12 +const RIPARIAN_SCRUB_DENSITY: float = 0.38 +const RIPARIAN_THICKET_DENSITY: float = 0.62 +const RIPARIAN_THICKET_STRENGTH: float = 0.20 + ## T-1194 texture tuning. Deliberately gentle — the reference's texture reads ## as grain over a still-legible hue, never as a second colour layer competing ## with it. Raising RELIEF_STIPPLE_STRENGTH past ~0.35 starts reading as noise. const RELIEF_STIPPLE_STRENGTH: float = 0.22 -const VEGETATION_PATCH_STRENGTH: float = 0.10 -const VEGETATION_PATCH_DENSITY: float = 0.34 +# The single VEGETATION_PATCH_* pair that used to live here is gone: one density +# and one strength for every vegetated class is what made a wood look like scrub. +# Replaced by the per-class marks below (T-1213). ## The elev_q delta at which relief is "fully rugged" and every cell earns a ## mark. Measured, not guessed: on Ferrath at Region the mean |delta| over a @@ -266,16 +301,59 @@ static func _texture(planes: CellPlanes, col: int, row: int, base: Color) -> Col if rug > 0.0 and ScatterField.chance(_relief_salt, col, row, rug): out = out.darkened(RELIEF_STIPPLE_STRENGTH * rug) - # Half-frequency lattice: one mark per 2x2 cells, so the vegetation grain is - # visibly coarser than the relief grain rather than a second speckle at the - # same pitch. - var veg: int = _l8_value(planes.vegetation, col, row) - if ( - veg >= VEGETATION_SCRUB - and ScatterField.chance(_veg_salt, col >> 1, row >> 1, VEGETATION_PATCH_DENSITY) - ): - out = out.darkened(VEGETATION_PATCH_STRENGTH) - return out + return _cover_mark(out, _l8_value(planes.vegetation, col, row), col, row) + + +## Ground cover, marked PER CLASS — the copse, the thicket, the rocky outcrop +## (T-1213, D-258 invariant 2). +## +## This used to be one test, `veg >= Scrub`, at one density and one strength. So +## Forest, Scrub and both Riparian classes drew the identical mark and Barren +## drew NONE — meaning a wood looked like scrub, and exposed rock was invisible +## by construction, on a map whose whole purpose below Global is to show the +## minority the orbital summary suppressed. The composition tier goes to real +## trouble to put clearings and outcrops inside a forest cell (composition.rs's +## rare inclusions; the conservation harness measures ~1.07% minority over a +## District patch) and the renderer was averaging them back out. +## +## Each class now gets its own grammar, differing on all three axes a mark has: +## DENSITY how much of the class's ground carries it +## STRENGTH how far it moves the base colour +## LATTICE the block size marks are decided on — a bigger block reads as a +## CLUMP (a copse), a smaller one as grain (scrub) +## +## Rock LIGHTENS where everything else darkens, and that is the point rather than +## a flourish: bare stone catching the light is the one cover type that is +## brighter than the ground around it, so it separates from vegetation by sign +## alone and never reads as "denser plants". +## +## Water carries no mark at all — the caller returns before this on the two open +## water zones. +static func _cover_mark(base: Color, veg: int, col: int, row: int) -> Color: + match veg: + VEGETATION_BARREN: + # Outcrops: sparse, high-contrast, fine — a scatter of stone, not a + # field of it. Own salt so rock does not shadow the copse lattice. + if ScatterField.chance(_rock_salt, col >> 1, row >> 1, ROCK_OUTCROP_DENSITY): + return base.lightened(ROCK_OUTCROP_STRENGTH) + VEGETATION_SCRUB: + if ScatterField.chance(_veg_salt, col >> 1, row >> 1, SCRUB_PATCH_DENSITY): + return base.darkened(SCRUB_PATCH_STRENGTH) + VEGETATION_FOREST: + # 2x2, not 4x4: a coarser lattice quantises into axis-aligned squares + # at these rungs, which reads as a bug rather than as canopy. + if ScatterField.chance(_veg_salt, col >> 1, row >> 1, FOREST_PATCH_DENSITY): + return base.darkened(FOREST_PATCH_STRENGTH) + VEGETATION_RIPARIAN_SCRUB: + if ScatterField.chance(_veg_salt, col >> 1, row >> 1, RIPARIAN_SCRUB_DENSITY): + return base.darkened(SCRUB_PATCH_STRENGTH) + VEGETATION_RIPARIAN_THICKET: + # The densest cover on the map: a thicket along water is the one + # place vegetation genuinely closes over. Same 2x2 lattice as forest + # — it stays distinct on density and strength, not on block size. + if ScatterField.chance(_veg_salt, col >> 1, row >> 1, RIPARIAN_THICKET_DENSITY): + return base.darkened(RIPARIAN_THICKET_STRENGTH) + return base ## Directional HILLSHADE from relief_q as a -1..1 fraction: +1 fully lit, -1 @@ -397,6 +475,10 @@ static func _ruggedness(planes: CellPlanes, col: int, row: int) -> float: ## double contrast. static var _relief_salt: int = ScatterField.domain(&"atlas/relief_stipple") static var _veg_salt: int = ScatterField.domain(&"atlas/vegetation_patch") +## Own stream, so an outcrop does not preferentially land where a copse already +## did — the same reason composition.rs salts its inclusion test separately from +## its smooth field. +static var _rock_salt: int = ScatterField.domain(&"atlas/rock_outcrop") static func _base_color(planes: CellPlanes, col: int, row: int) -> Color: diff --git a/project.yaml b/project.yaml index 7f9ef250b..e2dd05799 100644 --- a/project.yaml +++ b/project.yaml @@ -58,7 +58,10 @@ name: The Settled Reach # entries are still valid content — but the client step_canvas cluster is in the # T-1242 registry and the rule is bump-when-touched, so it moves. Cheap: one # round of misses against a map that now reads as terrain. -version: 0.4.10 +# 0.4.11 — per-class cover marks (copse, thicket, rocky outcrop). Paint again, +# not derivation; bumped because the client step_canvas cluster is in the T-1242 +# registry. +version: 0.4.11 repository: settled-reach