@jgengine/core 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/dist/audio/uiCues.d.ts +26 -0
- package/dist/audio/uiCues.js +118 -0
- package/dist/commands/commandRegistry.d.ts +4 -0
- package/dist/commands/commandRegistry.js +5 -2
- package/dist/game/events.d.ts +12 -0
- package/dist/game/notices.d.ts +85 -0
- package/dist/game/notices.js +105 -0
- package/dist/game/playableGame.d.ts +37 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +92 -1
- package/dist/render/lightingBalance.d.ts +13 -0
- package/dist/render/lightingBalance.js +32 -0
- package/dist/render/lookPreset.d.ts +10 -1
- package/dist/render/lookPreset.js +28 -3
- package/dist/render/postProcessing.d.ts +6 -1
- package/dist/render/postProcessing.js +2 -0
- package/dist/runtime/commandDispatch.js +10 -3
- package/dist/runtime/commandRunner.d.ts +3 -0
- package/dist/runtime/commandRunner.js +3 -1
- package/dist/runtime/context/scene.d.ts +3 -0
- package/dist/runtime/context/scene.js +6 -0
- package/dist/runtime/gameContext.js +13 -0
- package/dist/runtime/gameContextTypes.d.ts +19 -0
- package/dist/runtime/transport.d.ts +4 -1
- package/dist/settings/graphicsProfile.d.ts +7 -0
- package/dist/settings/graphicsProfile.js +4 -0
- package/dist/settings/graphicsSettings.d.ts +9 -0
- package/dist/settings/graphicsSettings.js +29 -0
- package/dist/world/buildings.d.ts +7 -0
- package/dist/world/buildings.js +20 -0
- package/dist/world/environmentSummary.js +7 -3
- package/dist/world/features.d.ts +24 -5
- package/dist/world/features.js +2 -0
- package/dist/world/terraform.d.ts +2 -0
- package/dist/world/terraform.js +1 -0
- package/dist/world/terrain.d.ts +3 -2
- package/dist/world/terrain.js +16 -3
- package/package.json +1 -1
- package/skills/jgengine/api.md +18 -18
- package/skills/jgengine-gameplay/api.md +18 -4
- package/skills/jgengine-gameplay/capabilities.md +5 -0
- package/skills/jgengine-world/api.md +27 -14
- package/skills/jgengine-world/capabilities.md +4 -0
- package/skills/jgengine-world/reference.md +23 -8
package/dist/meta/changelog.js
CHANGED
|
@@ -1,7 +1,98 @@
|
|
|
1
1
|
/** Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating. */
|
|
2
|
-
export const VERSION = "0.
|
|
2
|
+
export const VERSION = "0.23.0";
|
|
3
3
|
/** Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`). */
|
|
4
4
|
export const CHANGELOG = {
|
|
5
|
+
"0.23.0": {
|
|
6
|
+
migrate: [
|
|
7
|
+
"Bump lockstep SDK packages to `^0.23.0`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.20.0`; `@jgengine/github` is `0.10.0`.",
|
|
8
|
+
],
|
|
9
|
+
added: [
|
|
10
|
+
"`player_pose` agent-bridge verb (`@jgengine/shell`, `window.__jgengineAgent`, `drive --rpc '{\"method\":\"player_pose\"}'`) returns the local player's position and heading, and `debug_snapshot` carries the same `player` field, so a drive can prove movement from position deltas instead of screenshots.",
|
|
11
|
+
"`sim_advance` agent-bridge verb (`@jgengine/shell`, `drive --rpc '{\"method\":\"sim_advance\",\"seconds\":30}'`) steps the mounted sim driver — input, movement, `onTick`, behaviors, bound actions — in 0.05 s frames without rendering, so a headless drive can reach waves, shifts and timers that never arrive at software-rendered frame rates. Capped at 120 s per call.",
|
|
12
|
+
"`time_scale` agent-bridge verb sets the game clock's timescale and reports `{ timescale, now }`.",
|
|
13
|
+
"`GameContextObjectEntry.scenery` (`@jgengine/core`): an object kind placed once and left alone. Its runtime placements now render through the shared instanced static-object batches that editor-authored props already used.",
|
|
14
|
+
"`BUILDING_STYLE_PROBABILITIES` and `resolveBuildingProbabilities` (`@jgengine/core/world/buildings`): each `BuildingStyle` now carries facade-part probabilities, so an industrial yard shows few windows and roof plant, a ruin loses awnings and clotheslines, coastal fronts gain awnings. `building({ probabilities })` overrides any part.",
|
|
15
|
+
"`debug_summary` agent-bridge verb and `debug_snapshot` `detail: \"summary\"`: fps, frame/sim ms, draw calls, triangles, entities, warnings and top culprit in about 1 KB. `drive --rpc '{\"method\":\"debug_snapshot\"}'` prints the summary; `\"detail\":\"full\"` keeps the full snapshot.",
|
|
16
|
+
"`modelLoadStarts()` (`@jgengine/shell/render/modelLoad`) counts model streaming bursts for capture quiescence checks.",
|
|
17
|
+
"`jgengine` warns when it is not the CLI the caller meant: a global install run inside a jgengine checkout, a project-local `jgengine` shadowed, or a CLI built for an older SDK than the project installs. Harness runs print the CLI version and harness revision.",
|
|
18
|
+
"`withGraphicsQualityOverride` and `parseGraphicsQuality` (`@jgengine/core/settings/graphicsSettings`) pin a graphics tier for one session without persisting it.",
|
|
19
|
+
"The player shell reads `?quality=<tier>` and pins that tier for the page. `bun run shoot`/`drive --quality draft` use it (low tier, half size) for fast iteration shots; `--quality full` stays the default for PR evidence.",
|
|
20
|
+
"`jgengine doctor` flags a silent game: one that neither declares `defineGame({ audio })` cues nor calls `ctx.game.audio.play`/`loop`. Four reference games shipped without a single sound; the fix text points at procedural `SoundDef` synth patches, which need no asset files.",
|
|
21
|
+
"`drive --key mouse0:<ms>` (also `mouse1`, `mouse2`) holds that mouse button over the play canvas, matching the shell's pointer binding codes, so hold-to-fire weapons can be driven headlessly. Keyboard codes are unchanged.",
|
|
22
|
+
"`drive --right-click-at x,y` sends a native right click in CSS viewport pixels, so RTS move/attack/gather orders and context actions can be driven headlessly; `--click-at` stays a left click.",
|
|
23
|
+
"`ctx.scene.entity.selection` (`@jgengine/core`): the player's selected entities as a reactive `SelectionSet`. The shell pointer (`pointer.select` click / marquee) now writes it instead of private component state, so game HUDs can show the selected unit's card or a \"3 units selected\" summary and can set the selection themselves (select-all, control groups); the selection ring follows either way. Despawned ids drop out.",
|
|
24
|
+
"`LightingConfig.iblAmbientScale` (default 0.5) and `LightingConfig.fallbackKey` (default true); `balanceLightingForIbl` in `@jgengine/core/render/lightingBalance` applies them.",
|
|
25
|
+
"`DirectionalLightingConfig.shadowRadius` (PCF penumbra in texels, default 1.25).",
|
|
26
|
+
"`DEFAULT_AO` in `@jgengine/core/render/postProcessing` and `mergePostProcessing` in `@jgengine/core/render/lookPreset`.",
|
|
27
|
+
"Loaded GLB models get a default painted finish: flat-colour, vertex-colour and palette-atlas materials have their exported roughness/metalness/`envMapIntensity` normalized and compile with one shared object-space detail shader (mottling, wear, roughness break-up, micro-normal, cavity). Normal-mapped, cutout and transparent materials are untouched.",
|
|
28
|
+
"`GraphicsProfile.surfaceDetail` (`\"off\" | \"lite\" | \"full\"`, `@jgengine/core/settings/graphicsProfile`): `\"lite\"` on low, `\"full\"` on medium and up.",
|
|
29
|
+
"`ModelConfig.surfaceDetail: false` opts one model out; `defineGame({ surfaceDetail: false })` opts a game out.",
|
|
30
|
+
"`createSurfaceRibbonMaterial`, `ribbonSurfaceFrame`, `resolvePathSurface` and `useSurfaceRibbonMaterial` (`@jgengine/shell/terrain`) shade ground ribbons with the terrain's procedural surface functions, plus wear and a noise-warped soft edge.",
|
|
31
|
+
"`road({ surface })` (`@jgengine/core/world/features`) picks the carriageway surface; `false` keeps flat colour.",
|
|
32
|
+
"`AuthoredScene`/`AuthoredPaths` `pathSurfaceDetail`/`surfaceDetail` props and path `meta.surface`, `meta.edgeSoftness`, `meta.wear`, `meta.surfaceDetail`.",
|
|
33
|
+
"`EditableTerrain.gridBounds` (`@jgengine/core/world/terraform`) exposes the world extent of the paint/height cell grid.",
|
|
34
|
+
"Painted terrain renders each layer's `surface` as its own procedural ground (grass, dirt, gravel, rock, sand, mud, snow, road) with albedo variation, roughness and micro-normals, blended along noise-warped, height-aware edges, in play and in the editor viewport. No game config or assets needed.",
|
|
35
|
+
"`TerrainDetailConfig.surfaceStrength` (0..1, default 1) scales the per-surface response; `detail: false` opts a terrain out to the flat vertex-colour ground.",
|
|
36
|
+
"`@jgengine/shell/terrain` exports `packTerrainPaintWeights`, `createTerrainPaintTextures`, `terrainSurfaceClass`, `createTerrainDetailMaterial` and `TerrainQualityContext`/`useTerrainQuality`.",
|
|
37
|
+
"`ctx.game.notices` (`@jgengine/core/game/notices`, `createNoticeFeed`): a player-facing notice feed with info/success/warn/deny tones, stacking dedupe (repeats show ×N and refresh their lifetime), a cap, TTL, `onPush`, `retune` and `snapshot`/`restore`. Pure state on the context clock, so headless and hosted runs are unaffected.",
|
|
38
|
+
"The shell mounts `NoticeStack` (`@jgengine/react/notices`) for every game: animated toned cards below top HUD bars, 14px text on `--jg-*` vars with fallbacks, no pointer capture. Configure with `notices: { placement, hidden, refusals, cues, cap, ttlMs }` or remove with `notices: false`.",
|
|
39
|
+
"Refusal feedback: `validate` may return `{ reason, message }`. `reason` stays the machine code; a `message` is shown automatically as a deduped deny notice with the deny cue when the local player's command is refused, locally or from a host. `ctx.game.commands` emits a `command.refused` event for every refusal; `attachRefusalNotices` is the headless hookup.",
|
|
40
|
+
"UI sound cues: `useUiCue()` / `playUiCue` (`@jgengine/react/uiCues`) play `click`, `hover`, `open`, `close`, `confirm`, `deny`, `notify`. Soft procedural defaults (`@jgengine/core/audio/uiCues`) mix on a new \"ui\" bus with a \"UI volume\" setting under master volume. A game sound named `ui.<cue>` replaces one; `audio.uiCues` overrides or silences cues (`false` for all).",
|
|
41
|
+
"`notice_push` and `run_command` agent-bridge verbs for `drive --rpc`.",
|
|
42
|
+
"`ui-feedback` dev probe (apps/dev) exercising refusals, the feed and cues.",
|
|
43
|
+
"`HudTheme` gains a type scale with a 12px floor, spacing, motion (durations and easings), elevation (layered shadows and a lit top rim), and hover/press/focus/disabled state tokens; `hudThemeVars` emits them as `--jg-type-*`, `--jg-space-*`, `--jg-motion-*`, `--jg-ease*`, `--jg-elevation-*`, `--jg-highlight`, `--jg-hover-*`, `--jg-press-*`, `--jg-focus-ring`, and `--jg-disabled-opacity`. `@jgengine/react/hudTokens` reads them in inline styles (`hudType`, `hudSpace`, `hudTransition`, `hudElevation`, `hudFont`, `HUD_FOCUS_RING`).",
|
|
44
|
+
"Engine stylesheet `@jgengine/react/styles.css` (also `ENGINE_STYLESHEET` / `useEngineStyles` in `@jgengine/react/engineStyles`): every `jg-*` keyframe the shared blocks and registry use, `[data-jg-*]` interaction states, quiet hover/press/focus defaults for plain HUD buttons, and reduced-motion handling for `prefers-reduced-motion` and `[data-reduced-motion=\"true\"]`. The `jgengine create` template imports it.",
|
|
45
|
+
"`HudThemeRoot`: the shell wraps every `GameUI` in it, applying `defaultHudTheme` and the stylesheet. `defineGame({ hudTheme })` swaps the root theme; `hudTheme: false` opts out.",
|
|
46
|
+
"Primitives `HudButton`, `HudPanel` (open/close transition via `usePresence`), and `TweenNumber` / `useTweenedValue` with an up/down delta flash; `useReducedMotion` in `@jgengine/react/accessibility`.",
|
|
47
|
+
"`HudInteractProvider` / `useHudInteract`, plus `cue` and `onInteract` on the primitives, windows, modals, and `ActionButton`: one seam for a UI sound system.",
|
|
48
|
+
],
|
|
49
|
+
changed: [
|
|
50
|
+
"Static-object batches group same-model props per 64 m cell from 3 placements (was 24 m cells and more than 8), so sparse city dressing such as palms, lamps and benches shares draws. Harbor Heat's opening drops from 5,310 to 4,650 draw calls.",
|
|
51
|
+
"`building()` districts with a non-generic `style` generate a different facade mix than before (previously a style changed colours only). `generic` is unchanged.",
|
|
52
|
+
"`bun run shoot/drive` and the scaffolded/bundled `shoot.mjs`/`drive.mjs` parse one shared flag table: `--click-at`, `--right-click-at`, `--width`, `--height`, `--port`, `--size`, `--settle`, `--param`, `--probe`, `--reload`, key chords and mouse holds work in both; unknown flags fail instead of being ignored or taken as a game id; dev-runner-only flags are refused by name in the portable harness.",
|
|
53
|
+
"Play-mode capture readiness waits until staged `--state`/`--run` commands have ticked, their entities mounted and model streaming stopped, and never declares ready over \"Loading game view…\". A rejected staged command fails the capture.",
|
|
54
|
+
"Play-mode captures of a game with a start menu and no `capture.play` click its start action and report what they clicked.",
|
|
55
|
+
"Monorepo tests, type-checks and `bun run gen` resolve `@jgengine/*` to package source through the `paths` in `tsconfig.base.json` (`bun run gen:source-paths` keeps every workspace tsconfig in sync), so a fresh checkout or worktree is testable with no build. Published packages are unchanged: `exports` still point at `dist`, and the built output is byte-identical.",
|
|
56
|
+
"The export manifest is computed from the files each package's build emits, not from `dist` on disk, so it can no longer drift with a stale or partial build. `bun run ship:preflight` now fails with the fix (`bun run gen`) when any generated artifact is stale.",
|
|
57
|
+
"`bun run agent:worktree -- <name>` produces a clean, ready worktree locally and in cloud sessions: it links dependencies, `Games` and the dev runner's ignored assets from the main checkout instead of installing and building. `agent:bootstrap` only installs; pass `--build` to also build dist.",
|
|
58
|
+
"The procedural IBL probe has a zenith/horizon/ground gradient and an HDR sun disc plus a broad lobe, with the horizon leaning toward the ambient colour, at intensity 0.45 (was a 2-colour gradient at 0.35), so forms get a lit and a shaded side from IBL alone.",
|
|
59
|
+
"Under IBL (every look but `\"flat\"`), authored ambient is halved, and lighting with no directional light gets a soft shadowless key light (intensity 0.6, tinted by the ambient colour).",
|
|
60
|
+
"A post chain that omits `ao` now runs `DEFAULT_AO` on medium tier and above; `ao: false` still opts out. `NEUTRAL_POST_PROCESSING` no longer sets `ao: false`.",
|
|
61
|
+
"Styled look presets deep-merge their post stages under a partial game `postProcessing` instead of being dropped whole.",
|
|
62
|
+
"Shadows use `PCFShadowMap` instead of `PCFSoftShadowMap`, which three r182 compiles as unfiltered basic shadows (stair-stepped edges), with a default radius of 1.25 texels on configured and cascaded lights.",
|
|
63
|
+
"GTAO samples are tiered: 8 on medium, 12 on high (was 16), 16 on ultra.",
|
|
64
|
+
"A model `material.rim` override now chains the material's live program cache key instead of a snapshot.",
|
|
65
|
+
"Authored paths now render as dirt (or their `meta.surface`/kind) with wheel ruts and edges that fade into the terrain, instead of flat-colour ribbons with hard cut edges.",
|
|
66
|
+
"`road()` carriageways, curbs, sidewalks and welded junctions, and city studio ground (streets, block land, sidewalks, drives, parks), shade with matching procedural asphalt, gravel, concrete and dirt. City volumes opt out with `meta.surfaceDetail: false`.",
|
|
67
|
+
"Terrain noise and micro-normal GLSL moved to shared exports in `terrainSurfaceShader.ts` so terrain and ribbons compile the same code.",
|
|
68
|
+
"A terrain without `detail` now renders with default procedural detail instead of flat vertex colour.",
|
|
69
|
+
"Slope rock no longer fades with `detail.strength`; it follows `surfaceStrength`.",
|
|
70
|
+
"The textured terrain path's normal map is applied in view space (it was added to the view-space normal as a world-space offset).",
|
|
71
|
+
"The `low` graphics tier takes a cheap terrain surface path without micro-normals.",
|
|
72
|
+
"The shell plays UI cues for every engine HUD primitive (`HudButton`, `HudPanel`, `Window`, `ModalHost`, `ActionButton`): activate → `click` (or the primitive's `cue` when it names a cue, e.g. `cue=\"confirm\"`), button/action hover → `hover`, panel open/close → `open`/`close`. Off with `notices: false` or `notices.cues: false`; `audio.uiCues` silences single cues. `uiCueForInteraction` (`@jgengine/react/uiCues`) exposes the mapping.",
|
|
73
|
+
"`CommandRejection`, `CommandValidationError`, `CommandResult`, `RunCommandResult` and `TransportRunCommandResult` carry an optional `message`; the ws hosts forward it. Results without one are unchanged.",
|
|
74
|
+
"`defaultHudTheme` is the engine's default look: a system UI font stack led by Inter, layered translucent panels with a lit rim and hairline borders, and gradient slots.",
|
|
75
|
+
"`Window` / `PanelHost` render on `HudPanel` with enter and exit transitions and a `HudButton` close; `ModalHost` fades its backdrop and lifts its dialog, with an exit; `ConfirmDialog` / `PauseMenu` use `HudPanel` and `HudButton`; `HudFrame` and `ActionButton` read the font, elevation, focus, and motion tokens.",
|
|
76
|
+
"Text under 12px in `@jgengine/react` and `registry/jgengine` now uses the type scale's `xs` step (12px floor).",
|
|
77
|
+
"Games on the `rts` and `topDown` camera rigs hear positional sound again. The spatial-audio listener sat at the camera, 35–50 m above the ground, so the default 24 m falloff silenced every ground event even at screen centre; for these rigs it now sits where the camera looks at the ground. Chase, first-person and other rigs keep the camera-anchored listener.",
|
|
78
|
+
"The bundled `jgengine drive` and `jgengine shoot` harnesses let Chrome pick a free debug port instead of guessing one from the clock, so they no longer attach to another Chrome that holds the same port. An interrupted drive or shoot (SIGINT/SIGTERM) now closes its Chrome instead of leaving it running.",
|
|
79
|
+
"`jgengine doctor` flags component folders reached through a `tsconfig` path alias (such as a shared `@/components/ui/*` registry) that game code imports but `src/index.css` never scans with `@source`. Their Tailwind classes were silently not generated, so those HUD blocks rendered tiny and unstyled; the fix text names the exact `@source` line to add.",
|
|
80
|
+
"`jgengine doctor` now flags a 3D game that sets neither a `look` preset nor `postProcessing`; such a game renders the unstyled `\"neutral\"` look with no tone-map grade, AO or bloom. Its fix text no longer claims cinematic is the default. Write `look: \"neutral\"` when unstyled output is deliberate.",
|
|
81
|
+
"`jgengine create` scaffolds name `look: \"cinematic\"` explicitly, so a new 3D game starts graded instead of on the unstyled default.",
|
|
82
|
+
"Generated city districts (`@jgengine/shell` `CityRenderer`) fade their developed ground to transparent over its outer 9 m, so it melts into the terrain or authored paint beyond it instead of ending in a hard, stair-stepped green rim.",
|
|
83
|
+
"City districts (`@jgengine/shell` `CityRenderer`) draw a crossing of gravel lanes as gravel instead of a dark asphalt slab; a crossing that touches any paved street stays asphalt.",
|
|
84
|
+
"The published CLI and scaffold browser driver restore game keyboard focus before key holds and wait for a rendered frame under the held key. Menu clicks and slow headless rendering no longer silently swallow movement inputs; failed frame sampling releases held keys.",
|
|
85
|
+
"After a click on a control that then unmounts (a start screen's \"Play\", a menu's last button), `@jgengine/shell` returns focus to the play surface instead of leaving it on `<body>`, so gameplay keys work without an extra click on the canvas.",
|
|
86
|
+
"`sim_advance` (`@jgengine/shell` agent bridge) yields to the browser every ten simulated seconds, so models for entities spawned mid-advance mount as in play; a 30 s advance through Beacon Bastion's first wave previously left every raider with health bars but no model.",
|
|
87
|
+
"Generated city districts no longer lay a checkerboard over their ground. The developed-ground fabric drew one flat quad per 2.5 m cell with a per-cell random roll that flipped neighbours between zone bands; it is now one shared-vertex sheet coloured from smooth noise, so yards and lots blend.",
|
|
88
|
+
"Authored terrain paint (paths, clearings, painted layers) now blends smoothly between neighbouring cells in the runtime ground and the editor sculpt view, instead of snapping each vertex to its 2 m cell and drawing grid-aligned stair-step edges. Unpainted cells blend toward the caller's base ground color, so painted edges fade into the terrain palette.",
|
|
89
|
+
"A terrain detail `material` no longer overrides the ground's authored roughness with its roughness map. The map now varies roughness around the authored value, so scanned ground maps (roughness near 0.45) stop giving dry terrain a wet specular sheen; games no longer need to lower `material.strength` to stay matte.",
|
|
90
|
+
"A terrain without a `waterLevel` no longer paints the detail shader's beach sand band. Previously the band defaulted to water at height 0, so any flat terrain near 0 (a park, a meadow) rendered as beach; six reference games set `detail.waterLevel` far below the world only to avoid it. Set `waterLevel` on the terrain or `detail.waterLevel` to keep a shoreline.",
|
|
91
|
+
"Atomic vitals bars (`HealthBar`, `ManaBar`, … in `@jgengine/react`) cap their frame at a quarter of the bar height, so thin bars (e.g. `height: \"4px\"`) show their fill instead of only the two 2px frame lines.",
|
|
92
|
+
"Built-in world health bars (`worldHealthBars`, `@jgengine/shell`) span about 1.4 m at the entity's on-screen scale (28–112 px) instead of a fixed 112 px, so top-down and RTS cameras no longer paint bars ten times wider than the units under them.",
|
|
93
|
+
],
|
|
94
|
+
removed: [],
|
|
95
|
+
},
|
|
5
96
|
"0.22.0": {
|
|
6
97
|
migrate: [
|
|
7
98
|
"Bump lockstep SDK packages to `^0.22.0`: `@jgengine/{core,rapier,react,ws,node,sql,convex,shell,editor,assets,navbake}`. CLI `jgengine` is `0.19.0`; `@jgengine/github` is `0.9.0`.",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DirectionalLightingConfig, LightingConfig } from "../game/playableGame.js";
|
|
2
|
+
/** Ambient multiplier applied under image-based light when `iblAmbientScale` is unset. */
|
|
3
|
+
export declare const DEFAULT_IBL_AMBIENT_SCALE = 0.5;
|
|
4
|
+
/** Soft key added when authored lighting has no directional light: high, off to one side, shadowless. */
|
|
5
|
+
export declare const FALLBACK_KEY_LIGHT: Readonly<DirectionalLightingConfig>;
|
|
6
|
+
/**
|
|
7
|
+
* Rebalance authored lighting for a scene that also has image-based light: trim the flat ambient term
|
|
8
|
+
* by `iblAmbientScale` (default 0.5) and, when no directional light is authored, add
|
|
9
|
+
* {@link FALLBACK_KEY_LIGHT} tinted by the ambient colour so forms keep a lit and a shaded side.
|
|
10
|
+
* Returns the input unchanged when there is nothing to adjust.
|
|
11
|
+
* @capability balance-ibl-lighting trim flat ambient under IBL and add a key light to directionless lighting
|
|
12
|
+
*/
|
|
13
|
+
export declare function balanceLightingForIbl(lighting: LightingConfig | undefined): LightingConfig | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Ambient multiplier applied under image-based light when `iblAmbientScale` is unset. */
|
|
2
|
+
export const DEFAULT_IBL_AMBIENT_SCALE = 0.5;
|
|
3
|
+
/** Soft key added when authored lighting has no directional light: high, off to one side, shadowless. */
|
|
4
|
+
export const FALLBACK_KEY_LIGHT = {
|
|
5
|
+
position: [-22, 40, 16],
|
|
6
|
+
intensity: 0.6,
|
|
7
|
+
castShadow: false,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Rebalance authored lighting for a scene that also has image-based light: trim the flat ambient term
|
|
11
|
+
* by `iblAmbientScale` (default 0.5) and, when no directional light is authored, add
|
|
12
|
+
* {@link FALLBACK_KEY_LIGHT} tinted by the ambient colour so forms keep a lit and a shaded side.
|
|
13
|
+
* Returns the input unchanged when there is nothing to adjust.
|
|
14
|
+
* @capability balance-ibl-lighting trim flat ambient under IBL and add a key light to directionless lighting
|
|
15
|
+
*/
|
|
16
|
+
export function balanceLightingForIbl(lighting) {
|
|
17
|
+
if (lighting === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
const scale = Math.max(0, lighting.iblAmbientScale ?? DEFAULT_IBL_AMBIENT_SCALE);
|
|
20
|
+
const needsKey = (lighting.directional?.length ?? 0) === 0 && lighting.fallbackKey !== false;
|
|
21
|
+
const trims = lighting.ambient !== undefined && scale !== 1;
|
|
22
|
+
if (!needsKey && !trims)
|
|
23
|
+
return lighting;
|
|
24
|
+
const next = { ...lighting };
|
|
25
|
+
if (trims)
|
|
26
|
+
next.ambient = { ...lighting.ambient, intensity: (lighting.ambient.intensity ?? 0.55) * scale };
|
|
27
|
+
if (needsKey) {
|
|
28
|
+
const color = lighting.ambient?.color ?? lighting.hemisphere?.skyColor;
|
|
29
|
+
next.directional = [color === undefined ? FALLBACK_KEY_LIGHT : { ...FALLBACK_KEY_LIGHT, color }];
|
|
30
|
+
}
|
|
31
|
+
return next;
|
|
32
|
+
}
|
|
@@ -34,7 +34,7 @@ export interface ResolvedGameLook {
|
|
|
34
34
|
export declare const CINEMATIC_SKY: SkyEnvironmentConfig;
|
|
35
35
|
/** Tuned tone-map + bloom + gentle SSAO + vignette/grade stack — the shipped-game post look. */
|
|
36
36
|
export declare const CINEMATIC_POST_PROCESSING: PostProcessingConfig;
|
|
37
|
-
/** Unstyled baseline:
|
|
37
|
+
/** Unstyled baseline: no bloom or grade; contact AO stays at the graphics-tier default. */
|
|
38
38
|
export declare const NEUTRAL_POST_PROCESSING: PostProcessingConfig;
|
|
39
39
|
/** High-contrast physically based presentation for authored HDR environments. */
|
|
40
40
|
export declare const PHOTOREAL_POST_PROCESSING: PostProcessingConfig;
|
|
@@ -44,11 +44,20 @@ export declare const TOON_POST_PROCESSING: PostProcessingConfig;
|
|
|
44
44
|
export declare const COMIC_POST_PROCESSING: PostProcessingConfig;
|
|
45
45
|
/** Chunky pixels and posterized colour with no smoothing — a low-resolution retro look. */
|
|
46
46
|
export declare const RETRO_POST_PROCESSING: PostProcessingConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Deep-merge a game's partial post chain over a preset's. Every field the game set wins, including
|
|
49
|
+
* `false` to drop a stage; fields it left unset come from the preset, and stage objects merge field by
|
|
50
|
+
* field (tuples such as `grade.lift` replace whole).
|
|
51
|
+
* @capability merge-post-processing layer a partial post chain over a look preset without losing its stages
|
|
52
|
+
*/
|
|
53
|
+
export declare function mergePostProcessing(preset: PostProcessingConfig, authored: PostProcessingConfig | undefined): PostProcessingConfig;
|
|
47
54
|
/**
|
|
48
55
|
* Expand a game's `look` into concrete lighting/backdrop/post. The default is `"neutral"`;
|
|
49
56
|
* `"flat"` passes the explicit knobs through untouched.
|
|
50
57
|
* Anything the game authored wins — the preset only fills unset knobs, and it never adds a sky when
|
|
51
58
|
* the world already owns one (so the sky's tuned sun/hemisphere serve as the lighting rig).
|
|
59
|
+
* A styled preset's post stages are deep-merged under a partial game `postProcessing`
|
|
60
|
+
* ({@link mergePostProcessing}); under `"neutral"` a game's own chain is used as authored.
|
|
52
61
|
* @capability resolve-game-look expand a look preset into concrete lighting/backdrop/post knobs
|
|
53
62
|
*/
|
|
54
63
|
export declare function resolveGameLook(input: GameLookInput): ResolvedGameLook;
|
|
@@ -9,11 +9,10 @@ export const CINEMATIC_POST_PROCESSING = {
|
|
|
9
9
|
ao: { intensity: 1.4 },
|
|
10
10
|
grade: {},
|
|
11
11
|
};
|
|
12
|
-
/** Unstyled baseline:
|
|
12
|
+
/** Unstyled baseline: no bloom or grade; contact AO stays at the graphics-tier default. */
|
|
13
13
|
export const NEUTRAL_POST_PROCESSING = {
|
|
14
14
|
toneMapping: "agx",
|
|
15
15
|
bloom: false,
|
|
16
|
-
ao: false,
|
|
17
16
|
grade: false,
|
|
18
17
|
};
|
|
19
18
|
/** High-contrast physically based presentation for authored HDR environments. */
|
|
@@ -56,11 +55,37 @@ const PRESET_POST = {
|
|
|
56
55
|
retro: RETRO_POST_PROCESSING,
|
|
57
56
|
cinematic: CINEMATIC_POST_PROCESSING,
|
|
58
57
|
};
|
|
58
|
+
function isPlainObject(value) {
|
|
59
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
function mergeStage(preset, authored) {
|
|
62
|
+
if (authored === undefined)
|
|
63
|
+
return preset;
|
|
64
|
+
if (!isPlainObject(authored) || !isPlainObject(preset))
|
|
65
|
+
return authored;
|
|
66
|
+
const merged = { ...preset };
|
|
67
|
+
for (const [key, value] of Object.entries(authored))
|
|
68
|
+
merged[key] = mergeStage(preset[key], value);
|
|
69
|
+
return merged;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Deep-merge a game's partial post chain over a preset's. Every field the game set wins, including
|
|
73
|
+
* `false` to drop a stage; fields it left unset come from the preset, and stage objects merge field by
|
|
74
|
+
* field (tuples such as `grade.lift` replace whole).
|
|
75
|
+
* @capability merge-post-processing layer a partial post chain over a look preset without losing its stages
|
|
76
|
+
*/
|
|
77
|
+
export function mergePostProcessing(preset, authored) {
|
|
78
|
+
if (authored === undefined)
|
|
79
|
+
return preset;
|
|
80
|
+
return mergeStage(preset, authored);
|
|
81
|
+
}
|
|
59
82
|
/**
|
|
60
83
|
* Expand a game's `look` into concrete lighting/backdrop/post. The default is `"neutral"`;
|
|
61
84
|
* `"flat"` passes the explicit knobs through untouched.
|
|
62
85
|
* Anything the game authored wins — the preset only fills unset knobs, and it never adds a sky when
|
|
63
86
|
* the world already owns one (so the sky's tuned sun/hemisphere serve as the lighting rig).
|
|
87
|
+
* A styled preset's post stages are deep-merged under a partial game `postProcessing`
|
|
88
|
+
* ({@link mergePostProcessing}); under `"neutral"` a game's own chain is used as authored.
|
|
64
89
|
* @capability resolve-game-look expand a look preset into concrete lighting/backdrop/post knobs
|
|
65
90
|
*/
|
|
66
91
|
export function resolveGameLook(input) {
|
|
@@ -76,6 +101,6 @@ export function resolveGameLook(input) {
|
|
|
76
101
|
return {
|
|
77
102
|
lighting: input.lighting,
|
|
78
103
|
backdrop,
|
|
79
|
-
postProcessing:
|
|
104
|
+
postProcessing: mergePostProcessing(PRESET_POST[look], input.postProcessing),
|
|
80
105
|
};
|
|
81
106
|
}
|
|
@@ -109,7 +109,10 @@ export interface PostProcessingConfig {
|
|
|
109
109
|
*/
|
|
110
110
|
aa?: PostAaMode;
|
|
111
111
|
bloom?: BloomConfig | false;
|
|
112
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Ambient occlusion (contact shading). Omitted runs {@link DEFAULT_AO} on graphics tiers whose
|
|
114
|
+
* `postStages.ao` is on (medium and up); `false` turns it off everywhere.
|
|
115
|
+
*/
|
|
113
116
|
ao?: AoConfig | false;
|
|
114
117
|
/** Depth-of-field / bokeh. Heavier stage — omit or `false` to skip. Default off (omitted). */
|
|
115
118
|
dof?: DofConfig | false;
|
|
@@ -117,6 +120,8 @@ export interface PostProcessingConfig {
|
|
|
117
120
|
/** Art-style stage: outlines, cel bands, pixelation. Default off (omitted). */
|
|
118
121
|
stylize?: StylizeConfig | false;
|
|
119
122
|
}
|
|
123
|
+
/** Contact shading used when a post chain leaves `ao` unset: soft creases, no dark halos. */
|
|
124
|
+
export declare const DEFAULT_AO: Readonly<AoConfig>;
|
|
120
125
|
/**
|
|
121
126
|
* A cinematic "product shot" post preset — the full chain on (contact-AO, soft bloom, a warm film
|
|
122
127
|
* grade with vignette + a touch of grain + chromatic aberration). Meant for a `StudioStage` where a
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Contact shading used when a post chain leaves `ao` unset: soft creases, no dark halos. */
|
|
2
|
+
export const DEFAULT_AO = { radius: 1.3, intensity: 1.3, distanceFalloff: 2.8, blend: 1 };
|
|
1
3
|
/**
|
|
2
4
|
* A cinematic "product shot" post preset — the full chain on (contact-AO, soft bloom, a warm film
|
|
3
5
|
* grade with vignette + a touch of grain + chromatic aberration). Meant for a `StudioStage` where a
|
|
@@ -13,7 +13,14 @@ export function dispatchCommand(ctx, name, input) {
|
|
|
13
13
|
const serverId = route.serverId();
|
|
14
14
|
if (serverId === null)
|
|
15
15
|
return { status: "rejected", reason: "not-joined" };
|
|
16
|
-
return route.transport.runCommand({ serverId, command: name, input }).then((result) =>
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
return route.transport.runCommand({ serverId, command: name, input }).then((result) => {
|
|
17
|
+
if (result.ok)
|
|
18
|
+
return { status: "applied", state: ctx };
|
|
19
|
+
ctx.game.events.emit("command.refused", result.message === undefined
|
|
20
|
+
? { command: name, reason: result.reason, actor: null }
|
|
21
|
+
: { command: name, reason: result.reason, message: result.message, actor: null });
|
|
22
|
+
return result.message === undefined
|
|
23
|
+
? { status: "rejected", reason: result.reason }
|
|
24
|
+
: { status: "rejected", reason: result.reason, message: result.message };
|
|
25
|
+
}, (error) => ({ status: "rejected", reason: error instanceof Error ? error.message : "Command transport failed" }));
|
|
19
26
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
/** `reason` is a stable machine code; `message`, when set, is player-facing text the client shows on refusal. */
|
|
1
2
|
export type CommandValidationError = {
|
|
2
3
|
reason: string;
|
|
4
|
+
message?: string;
|
|
3
5
|
};
|
|
4
6
|
/**
|
|
5
7
|
* Which slice of a server has to be hydrated before some work runs against it. Both fields default to
|
|
@@ -71,6 +73,7 @@ export type RunCommandResult = {
|
|
|
71
73
|
} | {
|
|
72
74
|
ok: false;
|
|
73
75
|
reason: string;
|
|
76
|
+
message?: string;
|
|
74
77
|
};
|
|
75
78
|
/** @internal */
|
|
76
79
|
export declare function runCommand<TInput>(snapshot: import("./snapshot.js").GameRuntimeSnapshot, commands: Record<string, CommandDef<TInput>>, commandName: string, input: TInput, actorUserId: string, nowMs?: number): RunCommandResult;
|
|
@@ -64,7 +64,9 @@ export function runCommand(snapshot, commands, commandName, input, actorUserId,
|
|
|
64
64
|
}
|
|
65
65
|
const validationError = command.validate(snapshot, parsed, actorUserId, nowMs);
|
|
66
66
|
if (validationError) {
|
|
67
|
-
return
|
|
67
|
+
return validationError.message === undefined
|
|
68
|
+
? { ok: false, reason: validationError.reason }
|
|
69
|
+
: { ok: false, reason: validationError.reason, message: validationError.message };
|
|
68
70
|
}
|
|
69
71
|
const next = command.apply(snapshot, parsed, actorUserId, nowMs);
|
|
70
72
|
return {
|
|
@@ -9,6 +9,7 @@ import { type CollisionMeshSource } from "../../scene/collisionMesh.js";
|
|
|
9
9
|
import { type ObjectStore } from "../../scene/objectStore.js";
|
|
10
10
|
import type { InventorySet, ItemTraits } from "../../inventory/inventoryModel.js";
|
|
11
11
|
import { type PaintLayer } from "../../scene/paintLayer.js";
|
|
12
|
+
import { type SelectionSet } from "../../scene/selection.js";
|
|
12
13
|
import { type SceneRaycastApi } from "../../scene/sceneRaycast.js";
|
|
13
14
|
import { type SpatialApi } from "../../scene/spatial.js";
|
|
14
15
|
import { type Targeting } from "../../scene/targeting.js";
|
|
@@ -31,6 +32,8 @@ export interface SceneSubsystemDeps {
|
|
|
31
32
|
/** @internal Always-on scene surface: entities, objects, colliders, spatial, raycast, targeting. */
|
|
32
33
|
export interface SceneSubsystem {
|
|
33
34
|
entities: EntityStore;
|
|
35
|
+
/** The player's selected entities, exposed as `ctx.scene.entity.selection`. */
|
|
36
|
+
entitySelection: SelectionSet;
|
|
34
37
|
objects: ObjectStore;
|
|
35
38
|
statsByInstance: Map<string, StatValueMap>;
|
|
36
39
|
entityStats: EntityStatsApi;
|
|
@@ -28,6 +28,9 @@ export function createSceneSubsystem(d) {
|
|
|
28
28
|
});
|
|
29
29
|
const objects = createObjectStore();
|
|
30
30
|
entities.subscribe(signalNotify);
|
|
31
|
+
// The player's selected entities (RTS units, picked actors): written by the shell pointer, readable
|
|
32
|
+
// by game HUDs; mutations notify like every ctx surface, and a despawned id leaves the selection.
|
|
33
|
+
const entitySelection = notifyAfter(createSelectionSet(), ["add", "remove", "toggle", "replace", "clear"], signalNotify);
|
|
31
34
|
objects.subscribe(signalNotify);
|
|
32
35
|
const statsByInstance = new Map();
|
|
33
36
|
const entityStats = notifyAfter(createEntityStatsApi((instanceId) => statsByInstance.get(instanceId)), ["set", "delta"], signalNotify);
|
|
@@ -271,6 +274,8 @@ export function createSceneSubsystem(d) {
|
|
|
271
274
|
}
|
|
272
275
|
function despawnEntity(instanceId) {
|
|
273
276
|
const existed = entities.despawn(instanceId);
|
|
277
|
+
if (entitySelection.has(instanceId))
|
|
278
|
+
entitySelection.remove(instanceId);
|
|
274
279
|
statsByInstance.delete(instanceId);
|
|
275
280
|
targeting.clearAll(instanceId);
|
|
276
281
|
entityColliders.delete(instanceId);
|
|
@@ -355,6 +360,7 @@ export function createSceneSubsystem(d) {
|
|
|
355
360
|
};
|
|
356
361
|
return {
|
|
357
362
|
entities,
|
|
363
|
+
entitySelection,
|
|
358
364
|
objects,
|
|
359
365
|
statsByInstance,
|
|
360
366
|
entityStats,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAuthoredSimulation } from "../world/authoredSimulation.js";
|
|
2
2
|
import { createCommandRegistry } from "../commands/commandRegistry.js";
|
|
3
|
+
import { createNoticeFeed } from "../game/notices.js";
|
|
3
4
|
import {} from "../game/cosmetics.js";
|
|
4
5
|
import { groundFieldFor } from "../world/terrain.js";
|
|
5
6
|
import { createWorldSolids, structureSolids } from "../world/worldSolids.js";
|
|
@@ -99,6 +100,14 @@ export function createGameContext(options) {
|
|
|
99
100
|
return rawSocial;
|
|
100
101
|
};
|
|
101
102
|
const commandRegistry = createCommandRegistry();
|
|
103
|
+
const announceRefusal = (command, result, actor) => {
|
|
104
|
+
if (result.status !== "rejected")
|
|
105
|
+
return;
|
|
106
|
+
events.emit("command.refused", result.message === undefined
|
|
107
|
+
? { command, reason: result.reason, actor }
|
|
108
|
+
: { command, reason: result.reason, message: result.message, actor });
|
|
109
|
+
};
|
|
110
|
+
const notices = createNoticeFeed({ now });
|
|
102
111
|
if (definition.lifecycle !== undefined) {
|
|
103
112
|
const lifecycle = definition.lifecycle;
|
|
104
113
|
commandRegistry.define(lifecycle.commands?.start ?? "start", {
|
|
@@ -395,6 +404,7 @@ export function createGameContext(options) {
|
|
|
395
404
|
list: entities.list,
|
|
396
405
|
ids: entities.ids,
|
|
397
406
|
subscribeMembership: entities.subscribeMembership,
|
|
407
|
+
selection: scene.entitySelection,
|
|
398
408
|
subscribeBehaviors: entities.subscribeBehaviors,
|
|
399
409
|
spawnPoseOf: entities.spawnPoseOf,
|
|
400
410
|
resetToSpawn: entities.resetToSpawn,
|
|
@@ -466,6 +476,7 @@ export function createGameContext(options) {
|
|
|
466
476
|
run(name, input) {
|
|
467
477
|
const result = commandRegistry.run(ctx, name, input);
|
|
468
478
|
signal.notify();
|
|
479
|
+
announceRefusal(name, result, null);
|
|
469
480
|
return result;
|
|
470
481
|
},
|
|
471
482
|
runAs(actorUserId, name, input) {
|
|
@@ -474,6 +485,7 @@ export function createGameContext(options) {
|
|
|
474
485
|
try {
|
|
475
486
|
const result = commandRegistry.run(ctx, name, input);
|
|
476
487
|
signal.notify();
|
|
488
|
+
announceRefusal(name, result, actorUserId);
|
|
477
489
|
return result;
|
|
478
490
|
}
|
|
479
491
|
finally {
|
|
@@ -483,6 +495,7 @@ export function createGameContext(options) {
|
|
|
483
495
|
actor: () => actingUserId,
|
|
484
496
|
},
|
|
485
497
|
events,
|
|
498
|
+
notices,
|
|
486
499
|
playEntityAnimation: (instanceId, event) => events.emit("entity.animation", { instanceId, event }),
|
|
487
500
|
audio: {
|
|
488
501
|
play: (sound, at) => events.emit("audio.play", at === undefined ? { sound } : { sound, at }),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AuthoredSimulationRuntime } from "../world/authoredSimulation.js";
|
|
2
2
|
import type { Territory } from "../world/territory.js";
|
|
3
|
+
import type { NoticeFeed } from "../game/notices.js";
|
|
3
4
|
import type { CurrencyDefinition } from "../economy/currency.js";
|
|
4
5
|
import type { CardPile, CardPileConfig } from "../cards/cardPile.js";
|
|
5
6
|
import type { EffectInput, EffectResult, EffectVia, ReceiveMap, SingleTargetEffectInput } from "../combat/effects.js";
|
|
@@ -104,6 +105,12 @@ export interface GameContextObjectEntry {
|
|
|
104
105
|
verbs?: readonly ContextVerb[];
|
|
105
106
|
colliders?: EntityColliderSet;
|
|
106
107
|
halfExtents?: readonly [number, number, number];
|
|
108
|
+
/**
|
|
109
|
+
* Placed once and left alone (trees, lamps, benches, buildings): runtime placements of this kind
|
|
110
|
+
* render through shared instanced draws like editor-authored props. Leave unset for objects that
|
|
111
|
+
* spawn and despawn often, where regrouping would rebuild instance buffers.
|
|
112
|
+
*/
|
|
113
|
+
scenery?: boolean;
|
|
107
114
|
}
|
|
108
115
|
export interface GameContextContent {
|
|
109
116
|
itemById?(itemId: string): GameContextItemEntry | null | undefined;
|
|
@@ -282,6 +289,13 @@ export interface SceneEntityContext {
|
|
|
282
289
|
vfxInstance: VfxInstanceStore;
|
|
283
290
|
telegraph(input: TelegraphInput): () => void;
|
|
284
291
|
hitReaction(input: HitReactionInput): HitReaction | null;
|
|
292
|
+
/**
|
|
293
|
+
* The player's selected entities — the RTS selection the shell pointer writes (`pointer.select`
|
|
294
|
+
* click / marquee) and the order command receives. Game HUDs read it to show the selected unit's
|
|
295
|
+
* card or a "3 units selected" summary, and may write it (select-all buttons, control groups).
|
|
296
|
+
* Mutations bump `ctx.version()`/notify `ctx.subscribe`; despawned ids drop out.
|
|
297
|
+
*/
|
|
298
|
+
selection: SelectionSet;
|
|
285
299
|
setTarget(fromId: string, toId: string | null): void;
|
|
286
300
|
getTarget(fromId: string): string | null;
|
|
287
301
|
cycleTarget(fromId: string, options?: CycleTargetOptions): string | null;
|
|
@@ -480,6 +494,11 @@ export interface GameContext {
|
|
|
480
494
|
territory?: Territory;
|
|
481
495
|
commands: GameContextCommands;
|
|
482
496
|
events: GameEvents;
|
|
497
|
+
/**
|
|
498
|
+
* Player-facing notice feed (info/success/warn/deny) with stacking dedupe and TTL. The shell renders it
|
|
499
|
+
* and plays a UI cue per push; refused commands that return `{ reason, message }` land here automatically.
|
|
500
|
+
*/
|
|
501
|
+
notices: NoticeFeed;
|
|
483
502
|
audio: GameAudio;
|
|
484
503
|
/** Play a model's one-shot animation clip bound to `event` in its `animation.oneShots` (e.g. an attack swing) — emits `entity.animation` for the shell to pick up. */
|
|
485
504
|
playEntityAnimation(instanceId: string, event: string): void;
|
|
@@ -40,9 +40,12 @@ export type RunCommandArgs = {
|
|
|
40
40
|
};
|
|
41
41
|
export type TransportRunCommandResult = {
|
|
42
42
|
ok: true;
|
|
43
|
-
}
|
|
43
|
+
}
|
|
44
|
+
/** `message` is the command's player-facing refusal text, when its `validate` gave one. */
|
|
45
|
+
| {
|
|
44
46
|
ok: false;
|
|
45
47
|
reason: string;
|
|
48
|
+
message?: string;
|
|
46
49
|
};
|
|
47
50
|
export type GameRuntimeServerView = {
|
|
48
51
|
serverId: string;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { GraphicsQuality } from "./settingsModel.js";
|
|
2
|
+
/**
|
|
3
|
+
* Procedural surface detail on loaded models: `"full"` adds mottling, wear, roughness break-up, a
|
|
4
|
+
* micro-normal and cavity shading; `"lite"` drops the micro-normal; `"off"` compiles the plain material.
|
|
5
|
+
*/
|
|
6
|
+
export type SurfaceDetailLevel = "off" | "lite" | "full";
|
|
2
7
|
/** Serializable renderer budget selected by the player's graphics tier. */
|
|
3
8
|
export interface GraphicsProfile {
|
|
4
9
|
renderScale: number;
|
|
@@ -14,6 +19,8 @@ export interface GraphicsProfile {
|
|
|
14
19
|
dof: boolean;
|
|
15
20
|
smaa: boolean;
|
|
16
21
|
};
|
|
22
|
+
/** Default painted-surface shader on loaded models. */
|
|
23
|
+
surfaceDetail: SurfaceDetailLevel;
|
|
17
24
|
}
|
|
18
25
|
/** Conservative defaults that preserve the existing high-quality shell behavior. */
|
|
19
26
|
export declare const DEFAULT_GRAPHICS_PROFILES: Record<GraphicsQuality, GraphicsProfile>;
|
|
@@ -8,6 +8,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
8
8
|
particleCap: 128,
|
|
9
9
|
anisotropy: 2,
|
|
10
10
|
postStages: { ao: false, bloom: true, dof: false, smaa: true },
|
|
11
|
+
surfaceDetail: "lite",
|
|
11
12
|
},
|
|
12
13
|
medium: {
|
|
13
14
|
renderScale: 1.5,
|
|
@@ -17,6 +18,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
17
18
|
particleCap: 256,
|
|
18
19
|
anisotropy: 4,
|
|
19
20
|
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
21
|
+
surfaceDetail: "full",
|
|
20
22
|
},
|
|
21
23
|
high: {
|
|
22
24
|
renderScale: 2,
|
|
@@ -26,6 +28,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
26
28
|
particleCap: 512,
|
|
27
29
|
anisotropy: 8,
|
|
28
30
|
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
31
|
+
surfaceDetail: "full",
|
|
29
32
|
},
|
|
30
33
|
ultra: {
|
|
31
34
|
renderScale: 2,
|
|
@@ -35,6 +38,7 @@ export const DEFAULT_GRAPHICS_PROFILES = {
|
|
|
35
38
|
particleCap: 1024,
|
|
36
39
|
anisotropy: 16,
|
|
37
40
|
postStages: { ao: true, bloom: true, dof: true, smaa: true },
|
|
41
|
+
surfaceDetail: "full",
|
|
38
42
|
},
|
|
39
43
|
};
|
|
40
44
|
/** Resolve one tier and apply game-authored field and post-stage overrides. */
|
|
@@ -38,3 +38,12 @@ export declare function readGraphicsSettings(store: Pick<SettingsStore, "get">,
|
|
|
38
38
|
* resets the individual overrides instead of leaving stale toggles behind.
|
|
39
39
|
*/
|
|
40
40
|
export declare function applyGraphicsQuality(store: Pick<SettingsStore, "set">, quality: GraphicsQuality, overrides?: GraphicsProfileOverrides): GraphicsProfile;
|
|
41
|
+
/**
|
|
42
|
+
* Wrap a settings store so the graphics tier reads as `quality` for this session only: every
|
|
43
|
+
* graphics field resolves from that tier until the player changes it, and nothing is persisted.
|
|
44
|
+
* Capture hosts use it (`?quality=low`) to render cheap iteration shots without leaving the
|
|
45
|
+
* draft tier in the browser profile for the next full-quality shot.
|
|
46
|
+
*/
|
|
47
|
+
export declare function withGraphicsQualityOverride(store: SettingsStore, quality: GraphicsQuality, overrides?: GraphicsProfileOverrides): SettingsStore;
|
|
48
|
+
/** Parse a `?quality=` value into a graphics tier, or null when absent or unknown. */
|
|
49
|
+
export declare function parseGraphicsQuality(raw: string | null | undefined): GraphicsQuality | null;
|
|
@@ -79,3 +79,32 @@ export function applyGraphicsQuality(store, quality, overrides) {
|
|
|
79
79
|
}
|
|
80
80
|
return tier;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Wrap a settings store so the graphics tier reads as `quality` for this session only: every
|
|
84
|
+
* graphics field resolves from that tier until the player changes it, and nothing is persisted.
|
|
85
|
+
* Capture hosts use it (`?quality=low`) to render cheap iteration shots without leaving the
|
|
86
|
+
* draft tier in the browser profile for the next full-quality shot.
|
|
87
|
+
*/
|
|
88
|
+
export function withGraphicsQualityOverride(store, quality, overrides) {
|
|
89
|
+
const tier = resolveGraphicsProfile(quality, overrides?.[quality]);
|
|
90
|
+
const pinned = new Map([
|
|
91
|
+
[SETTING_IDS.graphicsQuality, quality],
|
|
92
|
+
[SETTING_IDS.graphicsRenderScale, tier.renderScale],
|
|
93
|
+
[SETTING_IDS.graphicsAnisotropy, String(tier.anisotropy)],
|
|
94
|
+
...GRAPHICS_POST_STAGES.map((stage) => [graphicsPostStageSettingId(stage), tier.postStages[stage]]),
|
|
95
|
+
]);
|
|
96
|
+
return {
|
|
97
|
+
get(id, fallback) {
|
|
98
|
+
return pinned.has(id) ? pinned.get(id) : store.get(id, fallback);
|
|
99
|
+
},
|
|
100
|
+
set(id, value) {
|
|
101
|
+
pinned.delete(id);
|
|
102
|
+
store.set(id, value);
|
|
103
|
+
},
|
|
104
|
+
subscribe: store.subscribe,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Parse a `?quality=` value into a graphics tier, or null when absent or unknown. */
|
|
108
|
+
export function parseGraphicsQuality(raw) {
|
|
109
|
+
return isGraphicsQuality(raw) ? raw : null;
|
|
110
|
+
}
|
|
@@ -140,6 +140,13 @@ export declare function resolveBuildingSurface(surface: BuildingSurface): Buildi
|
|
|
140
140
|
export type BuildingStyle = "generic" | "capital" | "village" | "desert" | "industrial" | "coastal" | "neon" | "ruin" | "frontier" | "aerial";
|
|
141
141
|
export declare const DEFAULT_BUILDING_STYLE: BuildingStyle;
|
|
142
142
|
export declare const BUILDING_STYLE_PALETTES: Record<BuildingStyle, Record<BuildingPartKind, string>>;
|
|
143
|
+
/**
|
|
144
|
+
* Facade part probabilities per style, so an industrial yard, a ruined block and a coastal strip
|
|
145
|
+
* differ in shape as well as colour. Unset parts keep the generic probabilities.
|
|
146
|
+
*/
|
|
147
|
+
export declare const BUILDING_STYLE_PROBABILITIES: Record<BuildingStyle, Partial<BuildingProbabilities>>;
|
|
148
|
+
/** Style facade probabilities with explicit overrides applied on top. */
|
|
149
|
+
export declare function resolveBuildingProbabilities(style?: BuildingStyle, overrides?: Partial<BuildingProbabilities>): Partial<BuildingProbabilities>;
|
|
143
150
|
/**
|
|
144
151
|
* Per-style facade-tone family: the wall colours a district spreads across its buildings so
|
|
145
152
|
* neighbours differ in hue and value while the block stays one palette. Index 0 is the palette's
|