Skip to content
Merged
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
8 changes: 7 additions & 1 deletion .claude/skills/jgengine-ui/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2458,7 +2458,7 @@
- `BuildingBlockProps` (interface): interface BuildingBlockProps — ⚠ undocumented
- `BuildingFacade` (type): type BuildingFacade = "front" | "back" | "left" | "right" | "roof" — ⚠ undocumented
- `BuildingKitRenderer` (interface): interface BuildingKitRenderer — ⚠ undocumented
- `BuildingMaterialPalette` (interface): interface BuildingMaterialPalette — ⚠ undocumented
- `BuildingMaterialPalette` (type): type BuildingMaterialPalette = Partial<Record<BuildingPartKind, BuildingSurface>> — Per part kind: a hex colour, or a colour plus tiled PBR maps (`BuildingSurfaceMaterial`).
- `BuildingPartKind` (type): type BuildingPartKind = | "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner" — ⚠ undocumented
- `BuildingPartPlacement` (interface): interface BuildingPartPlacement — ⚠ undocumented
- `GeneratedBuilding` (function): function GeneratedBuilding({ building, palette, partRenderer, kit, resolveModelUrl, visibleKinds, }: GeneratedBuildingProps): React.JSX.Element — ⚠ undocumented
Expand Down Expand Up @@ -2490,6 +2490,12 @@
- `listBuildingKits` (function): function listBuildingKits(): string[] — Every registered kit id — the editor's kit picker and diagnostics read this.
- `registerBuildingKit` (function): function registerBuildingKit(kit: BuildingKit): void — Registers a kit under its own `id`, replacing any kit already registered under that name.

## @jgengine/shell/structures/buildingSurface

- `TexturedBuildingSurface` (type): type TexturedBuildingSurface = BuildingSurfaceMaterial & { maps: BuildingSurfaceMaps } — A surface that names at least one map URL.
- `surfaceHasMaps` (function): function surfaceHasMaps(surface: BuildingSurfaceMaterial | undefined): surface is TexturedBuildingSurface — True when the surface names at least one map URL, so the batch needs the textured path.
- `surfaceKey` (function): function surfaceKey(surface: BuildingSurfaceMaterial | undefined): string — A stable key for bucketing instances that share one surface (same colour, maps, tiling, and response).

## @jgengine/shell/terrain

- `CarvedTerrain` (function): function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }: CarvedTerrainProps): React.JSX.Element — Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain. Because the geometry samples `field.sampleHeight`, a `CarvableField.carve(...)` shows as a real bowl once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
Expand Down
9 changes: 7 additions & 2 deletions .claude/skills/jgengine-world/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2772,7 +2772,7 @@

## @jgengine/core/world/buildings

- `BUILDING_STYLE_PALETTES` (const): const BUILDING_STYLE_PALETTES: Record<BuildingStyle, BuildingPalette> — ⚠ undocumented
- `BUILDING_STYLE_PALETTES` (const): const BUILDING_STYLE_PALETTES: Record<BuildingStyle, Record<BuildingPartKind, string>> — ⚠ undocumented
- `BUILDING_STYLE_WALL_TONES` (const): const BUILDING_STYLE_WALL_TONES: Record<BuildingStyle, readonly string[]> — Per-style facade-tone family: the wall colours a district spreads across its buildings so neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's own `wall`; the rest fan warmer/cooler and lighter/darker around it.
- `BuildingCellRef` (interface): interface BuildingCellRef — ⚠ undocumented
- `BuildingConfig` (interface): interface BuildingConfig — ⚠ undocumented
Expand All @@ -2781,22 +2781,27 @@
- `BuildingGridConfig` (interface): interface BuildingGridConfig — ⚠ undocumented
- `BuildingKitSlot` (interface): interface BuildingKitSlot — ⚠ undocumented
- `BuildingLot` (interface): interface BuildingLot — ⚠ undocumented
- `BuildingPalette` (type): type BuildingPalette = Record<BuildingPartKind, string> — ⚠ undocumented
- `BuildingPalette` (type): type BuildingPalette = Record<BuildingPartKind, BuildingSurface> — ⚠ undocumented
- `BuildingPaletteOverrides` (type): type BuildingPaletteOverrides = Partial<BuildingPalette> — ⚠ undocumented
- `BuildingPartKind` (type): type BuildingPartKind = | "wall" | "window" | "awning" | "airConditioner" | "clothesline" | "storefront" | "shutter" | "storeSign" | "roof" | "roofProp" | "guardrail" | "corner" — ⚠ undocumented
- `BuildingPartPlacement` (interface): interface BuildingPartPlacement — ⚠ undocumented
- `BuildingPartTag` (type): type BuildingPartTag = "ground" | "upper" | "open" | "closed" | "decor" | "structure" — ⚠ undocumented
- `BuildingProbabilities` (interface): interface BuildingProbabilities — ⚠ undocumented
- `BuildingSeed` (type): type BuildingSeed = number | string — ⚠ undocumented
- `BuildingStyle` (type): type BuildingStyle = | "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial" — ⚠ undocumented
- `BuildingSurface` (type): type BuildingSurface = string | BuildingSurfaceMaterial — What a building part kind is painted with: a hex colour, or a colour plus tiled PBR maps.
- `BuildingSurfaceMaps` (interface): interface BuildingSurfaceMaps — URLs of the PBR maps a building surface tiles; `buildMaterialCatalog(...).resolve(id)!.maps` fits directly.
- `BuildingSurfaceMaterial` (interface): interface BuildingSurfaceMaterial — A textured building surface: a base colour plus optional tiled PBR maps. With `maps`, `color` multiplies the albedo (leave it light to show the map as authored); without, it is the flat colour the part has always had.
- `BuildingVariantCounts` (interface): interface BuildingVariantCounts — ⚠ undocumented
- `DEFAULT_BUILDING_CONFIG` (const): const DEFAULT_BUILDING_CONFIG: BuildingConfig — ⚠ undocumented
- `DEFAULT_BUILDING_STYLE` (const): const DEFAULT_BUILDING_STYLE: BuildingStyle — ⚠ undocumented
- `GeneratedBuilding` (interface): interface GeneratedBuilding — ⚠ undocumented
- `Vec3` (type): type Vec3 = readonly [number, number, number] — ⚠ undocumented
- `buildingSurfaceColor` (function): function buildingSurfaceColor(surface: BuildingSurface | undefined, fallback = "#808080"): string — The flat colour of a surface: the hex itself, or a material's `color`, else `fallback`.
- `createBuildingConfig` (function): function createBuildingConfig(input: BuildingConfigInput = {}): BuildingConfig — Fill a partial building description out to a complete {@link BuildingConfig}, clamping counts and probabilities. Use it to inspect or adjust the resolved shape before generating.
- `generateBuilding` (function): function generateBuilding(input: BuildingConfigInput = {}): GeneratedBuilding — Generate one building as a flat list of placed facade parts: bays × floors per facade, a ground row, corners, and a roof, with every decorative part rolled from `probabilities` and every part tagged with a `kit` slot the renderer binds to a model (see `world/buildingKit`). Deterministic from `seed`.
- `generateBuildingDistrict` (function): function generateBuildingDistrict(config: BuildingGridConfig): GeneratedBuilding[] — Generate a grid of buildings on lots sized by `lotSize` and separated by `streetWidth`, each with its floor count rolled from `floorRange`.
- `resolveBuildingSurface` (function): function resolveBuildingSurface(surface: BuildingSurface): BuildingSurfaceMaterial — Normalises a surface to its material form so a renderer reads one shape.

## @jgengine/core/world/carve

Expand Down
5 changes: 1 addition & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,10 @@ between (`--json` for structured output).

### Added

- `sky({ sun: { azimuth, elevation } })` places the dome sun and the sky-owned sun light by compass bearing and height; under `timeOfDay` the day arc swings around that noon position. Unset, the shell derives it from the first authored `lighting.directional` light so the dome glow agrees with the shadows. Editor lighting panel gains bearing/height sliders (`sunAzimuth`/`sunElevation` on the scene document).
- `warnOnce` in `@jgengine/core/devtools/warnOnce`: dev-only, once-per-site console warnings. Used for a `sky.radius` at or past the camera far plane (the dome depth-clips to black) and for `zenithColor`/`horizonColor` under `timeOfDay` (they only set the noon keyframe).
- Building palettes take textured surfaces: a `BuildingPalette` part (and `BuildingKitPart.material`) may be `{ color?, maps?, repeat?, roughness?, metalness? }` with `maps` straight from `buildMaterialCatalog(...).resolve(id)!.maps`, not only a hex colour. Generated facade boxes and kit models tile the PBR maps per slot; unbound kinds keep their flat colour. `buildingSurfaceColor` / `resolveBuildingSurface` read either form.

### Fixed

- Time-of-day and biome-driven sky domes now draw the sun disc and glow, tracking the sun through the day; previously only the fixed-preset dome had one.

- Separate SDK publication validation from external Games quality checks while retaining the full local and main-branch gates.

- Correct foot-IK endpoint recomputation after hip rotation; verify reachable targets and clamping without changing bone lengths.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions packages/core/src/world/buildingGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/
import { registerAssetGenerator, partsBounds, type GeneratedAsset, type GeneratedPart } from "../scene/assetGenerator";
import type { ParamSchema, ParsedParams } from "../scene/sceneKinds";
import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_CONFIG, generateBuilding, resolveBuildingPalette, type BuildingStyle } from "./buildings";
import { BUILDING_STYLE_PALETTES, DEFAULT_BUILDING_CONFIG, buildingSurfaceColor, generateBuilding, resolveBuildingPalette, type BuildingStyle } from "./buildings";

/** The generator id a catalog entry / placed marker references. */
export const BUILDING_GENERATOR_ID = "building";
Expand Down Expand Up @@ -51,7 +51,7 @@ export function generateBuildingAsset(params: ParsedParams, seed: string): Gener
position: part.position,
size: part.scale,
rotationY: part.rotationY,
color: palette[part.kind],
color: buildingSurfaceColor(palette[part.kind]),
}));
return { parts, bounds: partsBounds(parts) };
}
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/world/buildingKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* as an instanced asset kit instead of untextured blocks. Pure serializable data — core resolves
* bindings, the renderer loads them.
*/
import type { BuildingKitSlot, BuildingPartKind, BuildingVariantCounts, Vec3 } from "./buildings";
import type { BuildingKitSlot, BuildingPartKind, BuildingSurfaceMaterial, BuildingVariantCounts, Vec3 } from "./buildings";

/**
* How a kit model's native bounds are mapped onto the slot box `BuildingPartPlacement.scale`
Expand Down Expand Up @@ -34,6 +34,8 @@ export interface BuildingKitPart {
/** Multiplier applied after `fit`. */
scale?: Vec3;
tint?: string;
/** Tiled PBR maps applied over the model's own materials; `color` tints, `repeat` sets the tiling. */
material?: BuildingSurfaceMaterial;
/**
* Quarter-turn the model when its long horizontal axis disagrees with the slot's, so a panel
* authored running along Z still tiles a bay that runs along X. Default true — modular kits do not
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/world/buildings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { describe, expect, test } from "bun:test";

import {
BUILDING_STYLE_PALETTES,
buildingSurfaceColor,
createBuildingConfig,
createBuildingGrid,
generateBuilding,
generateBuildingDistrict,
resolveBuildingPalette,
resolveBuildingSurface,
} from "./buildings";

describe("building palettes", () => {
Expand All @@ -24,6 +26,17 @@ describe("building palettes", () => {
expect(() => resolveBuildingPalette("aerodrome" as never)).toThrow(/Valid styles:/);
});

test("a palette part may be a textured surface, and colour readers still see a hex", () => {
const wall = { color: "#d9cbb0", maps: { color: "/materials/plaster/color.jpg" }, repeat: [2, 1.5] as const };
const palette = resolveBuildingPalette("coastal", { wall });
expect(palette.wall).toBe(wall);
expect(buildingSurfaceColor(palette.wall)).toBe("#d9cbb0");
expect(buildingSurfaceColor({ maps: {} }, "#123456")).toBe("#123456");
expect(buildingSurfaceColor(undefined, "#abcdef")).toBe("#abcdef");
expect(resolveBuildingSurface("#ffffff")).toEqual({ color: "#ffffff" });
expect(resolveBuildingSurface(wall)).toBe(wall);
});

test("every building style palette has a distinct wall color", () => {
const walls = Object.values(BUILDING_STYLE_PALETTES).map((palette) => palette.wall);
expect(new Set(walls).size).toBe(walls.length);
Expand Down
48 changes: 45 additions & 3 deletions packages/core/src/world/buildings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,51 @@ export const DEFAULT_BUILDING_CONFIG: BuildingConfig = {
variants: DEFAULT_VARIANTS,
};

export type BuildingPalette = Record<BuildingPartKind, string>;
/** URLs of the PBR maps a building surface tiles; `buildMaterialCatalog(...).resolve(id)!.maps` fits directly. */
export interface BuildingSurfaceMaps {
color?: string;
normal?: string;
roughness?: string;
ao?: string;
metalness?: string;
}

/**
* A textured building surface: a base colour plus optional tiled PBR maps. With `maps`, `color`
* multiplies the albedo (leave it light to show the map as authored); without, it is the flat
* colour the part has always had.
*/
export interface BuildingSurfaceMaterial {
/** Base colour; the style's colour for that part kind when unset. */
color?: string;
maps?: BuildingSurfaceMaps;
/**
* Texture tiles per part slot along [x, y]; a number tiles both. Slots within one building share
* a size, so one repeat per kind is uniform there; it drifts across buildings with a different
* `bayWidth`. Default 1.
*/
repeat?: number | readonly [number, number];
roughness?: number;
metalness?: number;
}

/** What a building part kind is painted with: a hex colour, or a colour plus tiled PBR maps. */
export type BuildingSurface = string | BuildingSurfaceMaterial;

export type BuildingPalette = Record<BuildingPartKind, BuildingSurface>;
export type BuildingPaletteOverrides = Partial<BuildingPalette>;

/** The flat colour of a surface: the hex itself, or a material's `color`, else `fallback`. */
export function buildingSurfaceColor(surface: BuildingSurface | undefined, fallback = "#808080"): string {
if (surface === undefined) return fallback;
return typeof surface === "string" ? surface : (surface.color ?? fallback);
}

/** Normalises a surface to its material form so a renderer reads one shape. */
export function resolveBuildingSurface(surface: BuildingSurface): BuildingSurfaceMaterial {
return typeof surface === "string" ? { color: surface } : surface;
}

export type BuildingStyle =
| "generic"
| "capital"
Expand All @@ -182,7 +224,7 @@ export type BuildingStyle =

export const DEFAULT_BUILDING_STYLE: BuildingStyle = "generic";

export const BUILDING_STYLE_PALETTES: Record<BuildingStyle, BuildingPalette> = {
export const BUILDING_STYLE_PALETTES: Record<BuildingStyle, Record<BuildingPartKind, string>> = {
generic: {
wall: "#83766a",
window: "#8ecae6",
Expand Down Expand Up @@ -351,7 +393,7 @@ export const BUILDING_STYLE_WALL_TONES: Record<BuildingStyle, readonly string[]>
export function resolveBuildingWallTones(style: BuildingStyle = DEFAULT_BUILDING_STYLE): readonly string[] {
const tones = BUILDING_STYLE_WALL_TONES[style] as readonly string[] | undefined;
if (tones !== undefined && tones.length > 0) return tones;
return [resolveBuildingPalette(style).wall];
return [buildingSurfaceColor(resolveBuildingPalette(style).wall)];
}

/** @internal */
Expand Down
11 changes: 6 additions & 5 deletions packages/shell/src/scene/CityRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { zoneBand, zoneMetric, type CityTreeSpecies, type CityPieceRole, type CityZoneBand } from "@jgengine/core/world/cityContent";
import {
generateBuilding,
buildingSurfaceColor,
resolveBuildingPalette,
resolveBuildingWallTones,
type BuildingPalette,
Expand Down Expand Up @@ -390,10 +391,10 @@ function unitGeometries() {
const ROLE_JITTER: Record<CityPieceRole, number> = { wall: 1, roof: 0.5, trim: 0.35, accent: 0.6 };

function roleColor(palette: BuildingPalette, role: CityPieceRole): string {
if (role === "wall") return palette.wall;
if (role === "roof") return palette.roof;
if (role === "trim") return palette.corner;
return palette.awning;
if (role === "wall") return buildingSurfaceColor(palette.wall);
if (role === "roof") return buildingSurfaceColor(palette.roof);
if (role === "trim") return buildingSurfaceColor(palette.corner);
return buildingSurfaceColor(palette.awning);
}

interface DetailBuilding {
Expand Down Expand Up @@ -651,7 +652,7 @@ function OneCity({ object, context }: { object: SceneKindObject; context: SceneK
return mesh;
};
const plain = new THREE.MeshStandardMaterial({ roughness: 0.85, metalness: 0.04 });
const banded = createCityWindowMaterial({ floorHeight: resolved.rules.floorHeight, windowColor: palette.window });
const banded = createCityWindowMaterial({ floorHeight: resolved.rules.floorHeight, windowColor: buildingSurfaceColor(palette.window) });
const meshes = [
makeMesh("box", units.box, plain),
makeMesh("banded", units.box, banded),
Expand Down
Loading
Loading