Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/plans/2026-07-21-lush-lowlands-unification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Lush Lowlands palette unification

## Scope

This draft PR is presentation-only and is based on the reviewed PR5 head
`fc95959d3c2128c267e535a6e6cffec5aaa692ff` (`agent/luminous-broad-grass-v3`).
It does not change terrain membership, passability, structures, resources,
castle state, routes, economy, SpacetimeDB, Pages, releases, or production.

## Diagnosis and policy

- Heath cells previously tinted broad ground purple and generated one purple
`heath-bloom` dodecahedron on High/Balanced quality. The historical feature
type remains accepted for compatibility, but ordinary generation and the
live layer now draw zero blooms.
- Heath now shares the Lowlands moss/olive family with a bounded semantic tint;
Ancient Stone is neutral slate/moss. Ridge and monolith readability comes
from neutral geometry, height, and deliberate clearances rather than violet
contrast.
- Meadow, Lowland, Forest, and Heath candidate/retention policies are denser
while remaining deterministic, slope-aware, structure-safe, and within the
existing quality budgets. Ridge and Ancient Stone receive only modest grass.

## Measurable contracts

The scene telemetry now exposes feature counts by kind (including an explicit
zero for `heath-bloom`), candidate and active grass cells by terrain, retained
instances by terrain, average vegetation density by terrain, bare-cell and
rejection counts, grass luminance/green ranges, draw calls, triangle totals,
cache state, and overview-hidden state. These remain bounded diagnostics; they
do not become authority or gameplay inputs.

The active-window cache, shared ambient scheduler, shader coverage path,
selection flattening, no-op grass raycast, lifecycle disposal, and quality
budgets are unchanged.

## Verification

- Focused visual/scene contracts: 54 tests passing.
- TypeScript `tsc --noEmit`: passing.
- Remaining build and full-suite results are recorded in the draft PR body.
- Render QA uses the existing fixed-camera harness when available; no runtime
texture or new binary asset is introduced by this PR.
20 changes: 20 additions & 0 deletions src/components/realm/RealmMapScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,7 @@ function CanonicalRealmMapScreen({
root.dataset.semanticTerrainKindCount = String(telemetry.semanticKindCount);
root.dataset.semanticTerrainFeatureCount = String(telemetry.semanticFeatureCount);
root.dataset.semanticTerrainFeatureDrawCalls = String(telemetry.semanticFeatureDrawCalls);
root.dataset.semanticTerrainFeatureCounts = JSON.stringify(telemetry.semanticFeatureCounts);
root.dataset.totalTerrainDetailInstanceCount = String(telemetry.totalDetailInstanceCount);
root.dataset.totalTerrainDetailDrawCalls = String(telemetry.totalDetailDrawCalls);
root.dataset.forestPlacementSource = telemetry.forestPlacementSource;
Expand All @@ -817,7 +818,20 @@ function CanonicalRealmMapScreen({
root.dataset.grassCacheEntries = String(telemetry.grassCacheEntries);
root.dataset.grassAnimated = String(telemetry.grassAnimated);
root.dataset.grassTargetAnimationCadence = String(telemetry.grassTargetAnimationCadence);
root.dataset.grassCandidateCellsByTerrain = JSON.stringify(telemetry.grassCandidateCellsByTerrain);
root.dataset.grassActiveCellsByTerrain = JSON.stringify(telemetry.grassActiveCellsByTerrain);
root.dataset.grassCountsByTerrain = JSON.stringify(telemetry.grassCountsByTerrain);
root.dataset.grassAverageVegetationDensityByTerrain = JSON.stringify(
telemetry.grassAverageVegetationDensityByTerrain
);
root.dataset.grassPaletteLuminance = JSON.stringify({
min: telemetry.grassPaletteLuminanceMin,
max: telemetry.grassPaletteLuminanceMax
});
root.dataset.grassPaletteGreen = JSON.stringify({
min: telemetry.grassPaletteGreenMin,
max: telemetry.grassPaletteGreenMax
});
root.dataset.grassCompletelyBareActiveCells = String(telemetry.grassCompletelyBareActiveCells);
root.dataset.grassRejectedByStructureClearance = String(
telemetry.grassRejectedByStructureClearance
Expand Down Expand Up @@ -936,6 +950,7 @@ function CanonicalRealmMapScreen({
rootRef.current.dataset.semanticTerrainKindCount = '0';
rootRef.current.dataset.semanticTerrainFeatureCount = '0';
rootRef.current.dataset.semanticTerrainFeatureDrawCalls = '0';
rootRef.current.dataset.semanticTerrainFeatureCounts = '{}';
rootRef.current.dataset.totalTerrainDetailInstanceCount = '0';
rootRef.current.dataset.totalTerrainDetailDrawCalls = '0';
rootRef.current.dataset.forestPlacementSource = 'blocked';
Expand All @@ -948,7 +963,12 @@ function CanonicalRealmMapScreen({
rootRef.current.dataset.grassCacheEntries = '0';
rootRef.current.dataset.grassAnimated = 'false';
rootRef.current.dataset.grassTargetAnimationCadence = '0';
rootRef.current.dataset.grassCandidateCellsByTerrain = '{}';
rootRef.current.dataset.grassActiveCellsByTerrain = '{}';
rootRef.current.dataset.grassCountsByTerrain = '{}';
rootRef.current.dataset.grassAverageVegetationDensityByTerrain = '{}';
rootRef.current.dataset.grassPaletteLuminance = '{}';
rootRef.current.dataset.grassPaletteGreen = '{}';
rootRef.current.dataset.grassCompletelyBareActiveCells = '0';
rootRef.current.dataset.grassRejectedByStructureClearance = '0';
rootRef.current.dataset.grassRejectedBySlope = '0';
Expand Down
30 changes: 29 additions & 1 deletion src/components/realm/createRealmGrassLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,16 @@ export type RealmGrassTelemetry = Readonly<{
averageBladeHeight: number;
paletteLuminanceMin: number;
paletteLuminanceMax: number;
paletteGreenMin: number;
paletteGreenMax: number;
alphaHashActive: boolean;
alphaToCoverageActive: boolean;
shaderFallbackActive: boolean;
edgeFadeCount: number;
candidateCellsByTerrain: Readonly<Record<RealmGrassTerrainKind, number>>;
activeCellsByTerrain: Readonly<Record<RealmGrassTerrainKind, number>>;
countsByTerrain: Readonly<Record<RealmGrassTerrainKind, number>>;
averageVegetationDensityByTerrain: Readonly<Record<RealmGrassTerrainKind, number>>;
completelyBareActiveCells: number;
rejectedByStructureClearance: number;
rejectedBySlope: number;
Expand Down Expand Up @@ -117,11 +122,16 @@ function emptyTelemetry(plan: RealmGrassRenderPlan, alphaToCoverage = false): Re
averageBladeHeight: 0,
paletteLuminanceMin: 0,
paletteLuminanceMax: 0,
paletteGreenMin: 0,
paletteGreenMax: 0,
alphaHashActive: true,
alphaToCoverageActive: alphaToCoverage,
shaderFallbackActive: false,
edgeFadeCount: 0,
candidateCellsByTerrain: Object.freeze(emptyCounts()),
activeCellsByTerrain: Object.freeze(emptyCounts()),
countsByTerrain: Object.freeze(emptyCounts()),
averageVegetationDensityByTerrain: Object.freeze(emptyCounts()),
completelyBareActiveCells: 0,
rejectedByStructureClearance: 0,
rejectedBySlope: 0,
Expand Down Expand Up @@ -253,11 +263,15 @@ export function createRealmGrassLayer(options: CreateRealmGrassLayerOptions): Re
let completelyBareActiveCells = 0;
let rejectedByStructureClearance = 0;
let rejectedBySlope = 0;
const candidateCellsByTerrain = emptyCounts();
const activeCellsByTerrain = emptyCounts();
window.cells.forEach((activeCell) => {
const data = cellDataFor(activeCell.cell);
candidateCellsByTerrain[data.terrainKind] += 1;
// At zero fade the alpha-hashed geometry is fully discarded. Do not
// spend cache/instance capacity on that invisible boundary ring.
if (activeCell.edgeFade <= 0) return;
const data = cellDataFor(activeCell.cell);
activeCellsByTerrain[data.terrainKind] += 1;
if (data.completelyBare) completelyBareActiveCells += 1;
rejectedByStructureClearance += data.rejectedByStructure + data.rejectedByExclusion;
rejectedBySlope += data.rejectedBySlope;
Expand All @@ -282,6 +296,8 @@ export function createRealmGrassLayer(options: CreateRealmGrassLayerOptions): Re
let heightTotal = 0;
let luminanceMin = Number.POSITIVE_INFINITY;
let luminanceMax = 0;
let greenMin = Number.POSITIVE_INFINITY;
let greenMax = 0;
let edgeFadeCount = 0;
packedByVariant.forEach((variantPoints, variant) => {
const currentMesh = meshes[variant]!;
Expand All @@ -304,6 +320,8 @@ export function createRealmGrassLayer(options: CreateRealmGrassLayerOptions): Re
const luminance = 0.2126 * point.tint.r + 0.7152 * point.tint.g + 0.0722 * point.tint.b;
luminanceMin = Math.min(luminanceMin, luminance);
luminanceMax = Math.max(luminanceMax, luminance);
greenMin = Math.min(greenMin, point.tint.g);
greenMax = Math.max(greenMax, point.tint.g);
if (edgeFade < 0.999) edgeFadeCount += 1;
});
currentMesh.count = variantPoints.length;
Expand Down Expand Up @@ -347,11 +365,21 @@ export function createRealmGrassLayer(options: CreateRealmGrassLayerOptions): Re
averageBladeHeight: packed.length > 0 ? heightTotal / packed.length : 0,
paletteLuminanceMin: Number.isFinite(luminanceMin) ? luminanceMin : 0,
paletteLuminanceMax: luminanceMax,
paletteGreenMin: Number.isFinite(greenMin) ? greenMin : 0,
paletteGreenMax: greenMax,
alphaHashActive: alphaHash,
alphaToCoverageActive: alphaCoverage,
shaderFallbackActive: false,
edgeFadeCount,
candidateCellsByTerrain: Object.freeze(candidateCellsByTerrain),
activeCellsByTerrain: Object.freeze(activeCellsByTerrain),
countsByTerrain: Object.freeze(counts),
averageVegetationDensityByTerrain: Object.freeze(Object.fromEntries(
(Object.keys(candidateCellsByTerrain) as RealmGrassTerrainKind[]).map((kind) => [
kind,
counts[kind] / Math.max(1, activeCellsByTerrain[kind])
])
) as Record<RealmGrassTerrainKind, number>),
completelyBareActiveCells,
rejectedByStructureClearance,
rejectedBySlope,
Expand Down
43 changes: 42 additions & 1 deletion src/components/realm/createRealmScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import { resolveRealmSharedForestLayout } from '../../game/map/realmSharedForest
import { generateTerrainDecorations } from '../../game/map/terrainDecorations';
import type { RealmGrassExclusion, RealmGrassTerrainKind } from '../../game/map/realmGrass';
import { generateRealmTerrainFeatures } from '../../game/map/realmTerrainFeatures';
import type { RealmTerrainFeaturePoint } from '../../game/map/realmTerrainFeatures';
import type {
RealmTerrainFeatureKind,
RealmTerrainFeaturePoint
} from '../../game/map/realmTerrainFeatures';
import {
indexRealmTerrainSemantics,
type RealmTerrainKind,
Expand Down Expand Up @@ -326,6 +329,7 @@ export type RealmTerrainPresentationTelemetry = Readonly<{
semanticKindCount: number;
semanticFeatureCount: number;
semanticFeatureDrawCalls: number;
semanticFeatureCounts: Readonly<Record<RealmTerrainFeatureKind, number>>;
totalDetailInstanceCount: number;
totalDetailDrawCalls: number;
/** Canonical shared rows render only when their full layout validates. */
Expand All @@ -339,7 +343,14 @@ export type RealmTerrainPresentationTelemetry = Readonly<{
grassCacheEntries: number;
grassAnimated: boolean;
grassTargetAnimationCadence: number;
grassCandidateCellsByTerrain: Readonly<Record<RealmGrassTerrainKind, number>>;
grassActiveCellsByTerrain: Readonly<Record<RealmGrassTerrainKind, number>>;
grassCountsByTerrain: Readonly<Record<RealmGrassTerrainKind, number>>;
grassAverageVegetationDensityByTerrain: Readonly<Record<RealmGrassTerrainKind, number>>;
grassPaletteLuminanceMin: number;
grassPaletteLuminanceMax: number;
grassPaletteGreenMin: number;
grassPaletteGreenMax: number;
grassCompletelyBareActiveCells: number;
grassRejectedByStructureClearance: number;
grassRejectedBySlope: number;
Expand Down Expand Up @@ -1143,10 +1154,24 @@ function initializeRealmScene(
alphaToCoverageActive: grassAlphaToCoverage,
shaderFallbackActive: false,
edgeFadeCount: 0,
paletteGreenMin: 0,
paletteGreenMax: 0,
candidateCellsByTerrain: Object.freeze({
meadow: 0, lowland: 0, forest: 0, heath: 0, ridge: 0, lake: 0,
'ancient-stone': 0, apron: 0
}),
activeCellsByTerrain: Object.freeze({
meadow: 0, lowland: 0, forest: 0, heath: 0, ridge: 0, lake: 0,
'ancient-stone': 0, apron: 0
}),
countsByTerrain: Object.freeze({
meadow: 0, lowland: 0, forest: 0, heath: 0, ridge: 0, lake: 0,
'ancient-stone': 0, apron: 0
}),
averageVegetationDensityByTerrain: Object.freeze({
meadow: 0, lowland: 0, forest: 0, heath: 0, ridge: 0, lake: 0,
'ancient-stone': 0, apron: 0
}),
completelyBareActiveCells: 0,
rejectedByStructureClearance: 0,
rejectedBySlope: 0,
Expand All @@ -1169,6 +1194,7 @@ function initializeRealmScene(
+ (currentForestTelemetry?.instanceCount ?? 0),
semanticFeatureDrawCalls: semanticFeatures.drawCalls
+ (currentForestTelemetry?.drawCalls ?? 0),
semanticFeatureCounts: semanticFeatures.counts,
totalDetailInstanceCount: decorationData.points.length
+ semanticFeatures.instanceCount
+ (currentForestTelemetry?.instanceCount ?? 0),
Expand All @@ -1187,7 +1213,14 @@ function initializeRealmScene(
grassCacheEntries: grass.cacheEntries,
grassAnimated: grass.animated,
grassTargetAnimationCadence: grass.targetAnimationCadence,
grassCandidateCellsByTerrain: grass.candidateCellsByTerrain,
grassActiveCellsByTerrain: grass.activeCellsByTerrain,
grassCountsByTerrain: grass.countsByTerrain,
grassAverageVegetationDensityByTerrain: grass.averageVegetationDensityByTerrain,
grassPaletteLuminanceMin: grass.paletteLuminanceMin,
grassPaletteLuminanceMax: grass.paletteLuminanceMax,
grassPaletteGreenMin: grass.paletteGreenMin,
grassPaletteGreenMax: grass.paletteGreenMax,
grassCompletelyBareActiveCells: grass.completelyBareActiveCells,
grassRejectedByStructureClearance: grass.rejectedByStructureClearance,
grassRejectedBySlope: grass.rejectedBySlope,
Expand All @@ -1200,6 +1233,7 @@ function initializeRealmScene(
const signature = [
telemetry.semanticFeatureCount,
telemetry.semanticFeatureDrawCalls,
Object.values(telemetry.semanticFeatureCounts).join(','),
telemetry.totalDetailInstanceCount,
telemetry.totalDetailDrawCalls,
telemetry.forestPlacementSource,
Expand All @@ -1209,7 +1243,14 @@ function initializeRealmScene(
telemetry.grassTriangleCount,
telemetry.grassCacheEntries,
telemetry.grassAnimated,
Object.values(telemetry.grassCandidateCellsByTerrain).join(','),
Object.values(telemetry.grassActiveCellsByTerrain).join(','),
Object.values(telemetry.grassCountsByTerrain).join(','),
Object.values(telemetry.grassAverageVegetationDensityByTerrain).join(','),
telemetry.grassPaletteLuminanceMin,
telemetry.grassPaletteLuminanceMax,
telemetry.grassPaletteGreenMin,
telemetry.grassPaletteGreenMax,
telemetry.grassCompletelyBareActiveCells,
telemetry.grassRejectedByStructureClearance,
telemetry.grassRejectedBySlope,
Expand Down
37 changes: 13 additions & 24 deletions src/components/realm/createRealmTerrainFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,6 @@ function geometryForKind(kind: RealmTerrainFeatureKind) {
geometry.translate(0, 0.17, 0);
return geometry;
}
if (kind === 'heath-bloom') {
const geometry = new THREE.DodecahedronGeometry(0.052, 0);
geometry.scale(1.45, 0.78, 1.45);
geometry.translate(0, 0.043, 0);
return geometry;
}
if (kind === 'ridge-outcrop') {
const geometry = new THREE.DodecahedronGeometry(0.14, 0);
geometry.scale(1.28, 0.7, 0.92);
Expand All @@ -56,15 +50,6 @@ function materialForKind(kind: RealmTerrainFeatureKind): THREE.Material {
metalness: 0
});
}
if (kind === 'heath-bloom') {
return new THREE.MeshStandardMaterial({
color: '#72527d',
emissive: '#2b1538',
emissiveIntensity: 0.12,
roughness: 0.88,
metalness: 0
});
}
if (kind === 'ridge-outcrop') {
return new THREE.MeshStandardMaterial({
color: '#625f5b',
Expand All @@ -83,11 +68,11 @@ function materialForKind(kind: RealmTerrainFeatureKind): THREE.Material {
});
}
return new THREE.MeshStandardMaterial({
color: '#5a5364',
emissive: '#291634',
emissiveIntensity: 0.1,
roughness: 0.78,
metalness: 0.08
color: '#667263',
emissive: '#1d2a20',
emissiveIntensity: 0.035,
roughness: 0.86,
metalness: 0.02
});
}

Expand All @@ -106,11 +91,15 @@ export function createRealmTerrainFeatureLayers(
const scale = new THREE.Vector3();
const axis = new THREE.Vector3(0, 1, 0);
const meshes: THREE.InstancedMesh[] = [];
// Heath bloom remains in the historical type union for replay/fixture
// compatibility, but is no longer a live presentation primitive.
const renderablePoints = data.points.filter((point) => point.kind !== 'heath-bloom');
const counts = Object.freeze({ ...data.counts, 'heath-bloom': 0 });
let disposed = false;

try {
(Object.keys(data.counts) as RealmTerrainFeatureKind[]).forEach((kind) => {
const points = data.points.filter((point) => point.kind === kind);
(Object.keys(counts) as RealmTerrainFeatureKind[]).forEach((kind) => {
const points = renderablePoints.filter((point) => point.kind === kind);
if (points.length === 0) return;
const geometry = geometryForKind(kind);
const material = materialForKind(kind);
Expand Down Expand Up @@ -173,9 +162,9 @@ export function createRealmTerrainFeatureLayers(

return Object.freeze({
group,
counts: data.counts,
counts,
drawCalls: meshes.length,
instanceCount: data.points.length,
instanceCount: renderablePoints.length,
dispose: () => {
if (disposed) return;
disposed = true;
Expand Down
Loading
Loading