@jgengine/shell 0.12.0 → 0.14.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 +73 -1
- package/dist/CombatPresentation.js +2 -1
- package/dist/GameHost.d.ts +2 -0
- package/dist/GameHost.js +25 -2
- package/dist/GamePlayerShell.js +15 -1
- package/dist/Shell3dPresentation.js +2 -2
- package/dist/camera/GameOrbitCamera.js +7 -1
- package/dist/camera/cameraCollision.d.ts +28 -0
- package/dist/camera/cameraCollision.js +34 -0
- package/dist/camera/cameraRigs.js +8 -8
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +7 -0
- package/dist/camera/rigMath.js +16 -0
- package/dist/devtools/CollisionDebugWorld.js +15 -1
- package/dist/devtools/collisionDebugMath.d.ts +8 -0
- package/dist/devtools/collisionDebugMath.js +14 -5
- package/dist/environment/EnvironmentScene.js +6 -1
- package/dist/environment/RoadJunctions.d.ts +12 -0
- package/dist/environment/RoadJunctions.js +69 -0
- package/dist/environment/RoadRibbons.d.ts +8 -2
- package/dist/environment/RoadRibbons.js +26 -8
- package/dist/postfx/PostProcessing.js +27 -2
- package/dist/postfx/postfxOverlay.d.ts +24 -0
- package/dist/postfx/postfxOverlay.js +35 -0
- package/dist/presentationResolve.d.ts +5 -1
- package/dist/presentationResolve.js +3 -1
- package/dist/render/EntityPreview.d.ts +53 -0
- package/dist/render/EntityPreview.js +23 -0
- package/dist/render/PartMotion.d.ts +18 -0
- package/dist/render/PartMotion.js +91 -0
- package/dist/render/SceneModels.d.ts +9 -2
- package/dist/render/SceneModels.js +43 -179
- package/dist/render/measureBounds.d.ts +34 -0
- package/dist/render/measureBounds.js +131 -0
- package/dist/render/measureCollisionMesh.d.ts +26 -0
- package/dist/render/measureCollisionMesh.js +108 -0
- package/dist/render/resolveModel.js +16 -10
- package/dist/render/useModelAnimation.d.ts +10 -0
- package/dist/render/useModelAnimation.js +198 -0
- package/dist/structures/GeneratedBuilding.d.ts +4 -0
- package/dist/structures/GeneratedBuilding.js +17 -7
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +2 -2
- package/dist/world/WorldScene.js +3 -2
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +5 -2
- package/skills/jgengine/api.md +13 -5
- package/skills/jgengine-ui/SKILL.md +9 -4
- package/skills/jgengine-ui/api.md +138 -8
- package/skills/jgengine-ui/capabilities.md +72 -0
- package/skills/jgengine-ui/reference.md +40 -3
- package/skills/jgengine-world/api.md +82 -25
- package/skills/jgengine-world/capabilities.md +12 -4
- package/skills/jgengine-world/reference.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,7 +10,79 @@ let consumers pick up the better stuff, not just to list what moved.
|
|
|
10
10
|
Agents building on the published SDK can also read this programmatically:
|
|
11
11
|
`import { VERSION, CHANGELOG } from "@jgengine/core/meta/changelog"` gives the
|
|
12
12
|
same data as typed values, so an updater can diff its installed version against
|
|
13
|
-
the latest and surface the migration steps.
|
|
13
|
+
the latest and surface the migration steps. Easier still: `npx jgengine upgrade`
|
|
14
|
+
run inside a game project diffs the installed `@jgengine/*` versions against the
|
|
15
|
+
latest release and prints every Migrate step and Adopt-worthy addition in
|
|
16
|
+
between (`--json` for structured output).
|
|
17
|
+
|
|
18
|
+
## [Unreleased]
|
|
19
|
+
|
|
20
|
+
<!--
|
|
21
|
+
Every PR that changes `packages/*/src` records its consumer-facing change here, so
|
|
22
|
+
the next release's notes are complete by construction. Add a bullet under the right
|
|
23
|
+
subhead below. `bun run check-changelog` fails a source PR whose `[Unreleased]` block
|
|
24
|
+
is untouched; pure refactors/tests bypass with `[skip changelog]` in a commit message.
|
|
25
|
+
At publish, rename this heading to the new version and mirror the entries into
|
|
26
|
+
`packages/core/src/meta/changelog.ts` (the typed `CHANGELOG` export).
|
|
27
|
+
-->
|
|
28
|
+
|
|
29
|
+
### Migrate
|
|
30
|
+
|
|
31
|
+
- _Nothing yet._
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
## 0.14.0
|
|
40
|
+
|
|
41
|
+
### Migrate
|
|
42
|
+
|
|
43
|
+
- **Nothing required** — this release is additive drop-in primitives, editor authoring, CLI discovery, and fixes. Bump `@jgengine/*` pins to `^0.14.0` (CLI `jgengine` is 0.11.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
|
|
44
|
+
- One behavior fix to know about: in a `rig: "chase"` game, on-foot movement and aim are now camera-relative (the chase rig reports its yaw to the shell like every other rig). If a game somehow relied on world-yaw-0 movement under a chase camera, that was the bug this fixes.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- One authoring gesture for scatterable coverage: `@jgengine/core/world/scatterCoverage` owns shared density/budget semantics (per-kind unit, requested→count→capped, one clamp-and-warn phrasing) for `grass_field`/`scatter`/`city`; the editor inspector leads each kind with the same Area → Assets → Density coverage section, scatter truncates to the shared 250k instance budget, and city single-sources its 2,600-lot cap.
|
|
49
|
+
- **Created games ship a drive/playtest script** (#1248) — `npx jgengine create` scaffolds now include `scripts/drive.mjs` (`bun run drive`): dependency-free headless play/testing of the running game — ordered `--click`/`--key`/`--wait`/`--shot`/`--rpc` steps against the agent bridge, plus `--playtest --strict` progress/softlock verdicts off `capture.probe`. Shared Chrome/CDP machinery lives in `scripts/browser.mjs`; `scripts/shoot.mjs` is unchanged in behavior, now a thin CLI over it.
|
|
50
|
+
- Editor: viewport clip preview for rigged assets (Animation dock "Clips" mode — pick a rig or placed instance, play/scrub/loop/speed any catalog clip) and an Inspector "Animation" section that authors a placement's `ModelConfig.animation` (role→clip dropdowns, auto/none, walk/run/fade, one-shot event bindings) as undoable `marker.meta.animation` edits. New subpaths: `@jgengine/editor/shell/clipPreview`, `@jgengine/editor/modelAnimationAuthoring`, `@jgengine/editor/ClipPreviewLayer`.
|
|
51
|
+
- **`npx jgengine find <intent>`** — active capability discovery: searches every shipped domain's `capabilities.md` (staged inside the CLI tarball, so it works regardless of which `@jgengine/*` packages a project installed) and prints the drop-in primitive + its import for an intent like `"toggleable window"`, `inventory`, or `minimap`. Scaffolded games are now briefed (in `AGENTS.md` and the `jgengine` intake skill) to reach for it before hand-rolling a HUD/inventory/window/rig.
|
|
52
|
+
- **`EntityPreview`** (`@jgengine/shell/render/EntityPreview`) — drop-in live 3D entity portrait for character screens, unit inspectors, and loadout viewers. Owns the nested `<Canvas>`, the `GameContextBridge`, and a `StudioStage` lighting rig with optional turntable / face-camera; the game passes its own `renderEntity` as children (or a native `model` + `instanceId`) and, bound to a live entity, the portrait walks/flinches/topples in sync with the world.
|
|
53
|
+
- **`GameContextBridge`** (`@jgengine/react`) — re-provide the running `GameContext` across a nested React reconciler boundary (the R3F `<Canvas>`), so a game building its own preview canvas no longer re-derives the bridge by hand.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- **Chase rig reports its yaw to the shell** — `ChaseRig` now writes the camera yaw back to the shared yaw ref like every other player-facing rig, so on-foot movement and aim in a `rig: "chase"` game are camera-relative instead of frozen to world yaw 0 (WASD no longer fights the camera the moment it swings behind the player). Interior views (cockpit/hood/rear) report the vehicle heading.
|
|
58
|
+
- **Combat VFX no longer render as black squares under AO/DOF post-processing** (#1247) — GTAO/Bokeh scene prepasses skip overlay effects. Games with custom additive overlay effects opt out the same way: spread `POSTFX_OVERLAY_USERDATA` (`@jgengine/shell/postfx/postfxOverlay`) onto the overlay group's `userData`.
|
|
59
|
+
- **`PanelHost` windows stack above the HUD by default** (`@jgengine/react`) — the host establishes its own stacking context at `zIndexBase` (default 40), so open windows always paint over stat bars / nameplates / frames instead of bleeding through. Overridable per instance.
|
|
60
|
+
|
|
61
|
+
## 0.13.0
|
|
62
|
+
|
|
63
|
+
### Migrate
|
|
64
|
+
|
|
65
|
+
- **Nothing required** — this release is additive fixes, defaults, and new drop-in primitives. Bump `@jgengine/*` pins to `^0.13.0` (CLI `jgengine` is 0.10.0; `@jgengine/github` unchanged at 0.1.0) and rebuild.
|
|
66
|
+
- Two defaults changed in the player's favor, both overridable: **offline games now persist to localStorage automatically** (#1207) and **rigged catalog assets animate by default** via clip metadata + semantic clip roles (#1209). If a game relied on rigged models staying frozen or on saves being memory-only, opt out explicitly; otherwise you get better behavior for free.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- **Drop-in inventory grid, window manager, and character sheet** (#1220) — `@jgengine/react` ships `InventoryGrid` (real drag/stack/split), a toggleable-window system (`panels`: bag, character sheet, spellbook-style windows with `@jgengine/core/ui/panelModel` state), and a `CharacterSheet` paperdoll, all HudTheme-skinnable — wire in a line or two instead of re-deriving from raw divs.
|
|
71
|
+
- **Serializable runtime snapshots** (#1179) — capture/restore of live runtime state, including magazine state (`@jgengine/core/combat/magazine`) and stat modifiers, for saves and replication.
|
|
72
|
+
- **Portable weapon plumbing** (#1211) — `@jgengine/core/combat/weaponFire` composes trigger → magazine → spread → projectile/hitscan as data, no game-local glue.
|
|
73
|
+
- **Portable leveling** (#1206) and **portable damage-pool access** (#1208) — progression and damage pools exposed as genre-agnostic seams on the game context.
|
|
74
|
+
- **Named combat VFX presets** (#1204) — `@jgengine/core/combat/vfxPresets`: a visible attack is one line.
|
|
75
|
+
- **Runtime-measured render-bounds hitboxes** (#1227) — colliders wrap what the shell actually mounts, not the raw asset bounds.
|
|
76
|
+
- **Character motion feel** — procedural part motion for rig-less part-composed characters (#1222) plus squash & stretch and death splat (#1229).
|
|
77
|
+
- **City fabric** (#1191) — `streetGenerator` + `cityGenerator`, editor bake verb, street-aware lots, road junctions, grounding, and a web playground.
|
|
78
|
+
- **`jgengine recipe` CLI** (#1214, #1231) — vetted wired compositions, compile-pinned against the SDK, listed and copied from the CLI.
|
|
79
|
+
- **Editor/debug modes mirror to the URL** with a proper editor exit (#1203); game template ships a dependency-free WebGL screenshot script (#1202).
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- Offline games persist to localStorage by default (#1207); rigged assets animate by default (#1209).
|
|
84
|
+
- RTS camera pan inversion fixed (#1228); sprite raycast warning silenced and duplicate nameplate health bars removed (#1194); scaffolded F2+E editor Tailwind `@source` renders its theme (#1195).
|
|
85
|
+
- Outside-game DX: stat-pool no-op stability, camera-transparent decor, cleaner install/version output, headless UI-intent seam (#1223); new-game briefs direct engine bugs/gaps upstream (#1200).
|
|
14
86
|
|
|
15
87
|
## 0.12.0
|
|
16
88
|
|
|
@@ -2,7 +2,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { CombatCameraShake, ProjectileTracers, WorldFloatText, WorldTelegraphs, } from "./world/WorldHud.js";
|
|
3
3
|
import { WorldSpellVfx } from "./world/WorldVfx.js";
|
|
4
4
|
import { RetainedVfx } from "./world/RetainedVfx.js";
|
|
5
|
+
import { POSTFX_OVERLAY_USERDATA } from "./postfx/postfxOverlay.js";
|
|
5
6
|
/** Mounts the opt-in combat VFX stack inside the 3D canvas. @internal */
|
|
6
7
|
export function CombatPresentation({ effects }) {
|
|
7
|
-
return (_jsxs(
|
|
8
|
+
return (_jsxs("group", { userData: POSTFX_OVERLAY_USERDATA, children: [effects.telegraphs ? _jsx(WorldTelegraphs, {}) : null, effects.vfx ? (_jsxs(_Fragment, { children: [_jsx(WorldSpellVfx, {}), _jsx(RetainedVfx, {})] })) : null, effects.floatText ? _jsx(WorldFloatText, {}) : null, effects.tracers ? _jsx(ProjectileTracers, {}) : null, effects.shake ? _jsx(CombatCameraShake, {}) : null] }));
|
|
8
9
|
}
|
package/dist/GameHost.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface EditorSummonModule {
|
|
|
8
8
|
gameId: string;
|
|
9
9
|
playable: PlayableGame;
|
|
10
10
|
catalogs?: readonly EditorCatalogDefinition[];
|
|
11
|
+
/** Passed by the host when the editor was summoned over a game — powers the "Exit to game" control. */
|
|
12
|
+
onExitEditor?: () => void;
|
|
11
13
|
}>;
|
|
12
14
|
}
|
|
13
15
|
/** Props for {@link GameHost}: the playable, optional multiplayer overrides, and the optional editor loader that enables the engine-owned F2+E summon. */
|
package/dist/GameHost.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
|
3
3
|
import { installSaveEndpoint } from "@jgengine/core/devtools/saveEndpoint";
|
|
4
|
+
import { readUrlParam, subscribeUrlChange, writeUrlParam } from "@jgengine/core/devtools/urlFlags";
|
|
4
5
|
import { multiplayerAdapterKind } from "@jgengine/core/runtime/adapter";
|
|
5
6
|
import { GamePlayerShell } from "./GamePlayerShell.js";
|
|
6
7
|
import { resolveShellMultiplayer } from "./multiplayer.js";
|
|
@@ -12,10 +13,13 @@ function warnUndrivenAdapter(kind) {
|
|
|
12
13
|
console.warn(`GameHost: game declares multiplayer adapter "${kind}", which the built-in shell resolver cannot drive. ` +
|
|
13
14
|
"Running offline. Pass a prebuilt session via the multiplayer prop, or a resolveMultiplayer prop, to GameHost.");
|
|
14
15
|
}
|
|
16
|
+
/** The query param that mirrors editor mode into the URL — strip it (and reload) to drop back to the game. */
|
|
17
|
+
const EDITOR_MODE_PARAM = "mode";
|
|
18
|
+
const EDITOR_MODE_VALUE = "editor";
|
|
15
19
|
function initialEditorMode(enabled) {
|
|
16
20
|
if (!enabled || typeof window === "undefined")
|
|
17
21
|
return false;
|
|
18
|
-
return
|
|
22
|
+
return readUrlParam(EDITOR_MODE_PARAM) === EDITOR_MODE_VALUE;
|
|
19
23
|
}
|
|
20
24
|
/**
|
|
21
25
|
* The one documented mount: resolves multiplayer for the playable and renders the shell. With the
|
|
@@ -32,6 +36,25 @@ export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultipla
|
|
|
32
36
|
// Self-guards against production builds; the endpoint just marks where dev saves land.
|
|
33
37
|
return installSaveEndpoint("/__jgengine/save", resolvedGameId);
|
|
34
38
|
}, [editor, resolvedGameId]);
|
|
39
|
+
// Mirror editor mode into the URL so the address bar is the honest record of what's open: sharing
|
|
40
|
+
// the URL reopens the editor, and stripping `?mode=editor` (then reloading) drops back to the game.
|
|
41
|
+
// Only ever clear a `mode` we set to editor — never a stray value the app put there.
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (editor === undefined)
|
|
44
|
+
return;
|
|
45
|
+
if (editorOpen)
|
|
46
|
+
writeUrlParam(EDITOR_MODE_PARAM, EDITOR_MODE_VALUE);
|
|
47
|
+
else if (readUrlParam(EDITOR_MODE_PARAM) === EDITOR_MODE_VALUE)
|
|
48
|
+
writeUrlParam(EDITOR_MODE_PARAM, null);
|
|
49
|
+
}, [editor, editorOpen]);
|
|
50
|
+
// Keep in step with browser back/forward: navigating away from `?mode=editor` closes the editor.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (editor === undefined)
|
|
53
|
+
return;
|
|
54
|
+
return subscribeUrlChange(() => {
|
|
55
|
+
setEditorOpen(readUrlParam(EDITOR_MODE_PARAM) === EDITOR_MODE_VALUE);
|
|
56
|
+
});
|
|
57
|
+
}, [editor]);
|
|
35
58
|
useEffect(() => {
|
|
36
59
|
if (editor === undefined || editorOpen)
|
|
37
60
|
return;
|
|
@@ -83,7 +106,7 @@ export function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultipla
|
|
|
83
106
|
return session;
|
|
84
107
|
}, [playable, resolvedGameId, wsUrl, multiplayer, resolveMultiplayer]);
|
|
85
108
|
if (editorOpen && EditorLazy !== null) {
|
|
86
|
-
return (_jsx(Suspense, { fallback: null, children: _jsx(EditorLazy, { gameId: resolvedGameId, playable: playable, catalogs: playable.editorCatalogs }) }));
|
|
109
|
+
return (_jsx(Suspense, { fallback: null, children: _jsx(EditorLazy, { gameId: resolvedGameId, playable: playable, catalogs: playable.editorCatalogs, onExitEditor: () => setEditorOpen(false) }) }));
|
|
87
110
|
}
|
|
88
111
|
return _jsx(GamePlayerShell, { playable: playable, multiplayer: resolved });
|
|
89
112
|
}
|
package/dist/GamePlayerShell.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createSettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
|
11
11
|
import { applyBindingOverrides, clearBindingOverride, loadBindingOverrides, saveBindingOverride, } from "@jgengine/core/input/bindingOverrides";
|
|
12
12
|
import { orientationGateActive, orientationHintActive, resolveOrientationRequirement, } from "@jgengine/core/ui/orientation";
|
|
13
13
|
import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams";
|
|
14
|
+
import { readUrlFlag, subscribeUrlChange, writeUrlParam } from "@jgengine/core/devtools/urlFlags";
|
|
14
15
|
import { createAudioEngine } from "./audio/audioEngine.js";
|
|
15
16
|
import { attachAudioEventWire } from "./audio/audioWire.js";
|
|
16
17
|
import { installAgentBridge } from "./devtools/agentBridge.js";
|
|
@@ -25,6 +26,8 @@ import { useShellMultiplayerSync } from "./useShellMultiplayerSync.js";
|
|
|
25
26
|
import { ShellHudPresentation } from "./ShellHudPresentation.js";
|
|
26
27
|
import { Shell3dPresentation } from "./Shell3dPresentation.js";
|
|
27
28
|
const DEV_USER_ID = "dev-player";
|
|
29
|
+
/** The query param that mirrors the devtools overlay into the URL — present = open. */
|
|
30
|
+
const DEBUG_PARAM = "debug";
|
|
28
31
|
export { applyMotionImpulses } from "@jgengine/core/runtime/motionIntents";
|
|
29
32
|
export { nearbyObstacles } from "@jgengine/core/movement/movementModel";
|
|
30
33
|
export { resolvePhysicsTuning } from "@jgengine/core/movement/playerMovement";
|
|
@@ -38,9 +41,20 @@ export function GamePlayerShell({ playable, multiplayer: rawMultiplayer = null,
|
|
|
38
41
|
const multiplayer = useMemo(() => (rawMultiplayer === null ? null : withDevtoolsLatency(rawMultiplayer)), [rawMultiplayer]);
|
|
39
42
|
const devtoolsEnabled = playable.devtools !== false && !poster;
|
|
40
43
|
armFallbackSeams(devtoolsEnabled);
|
|
41
|
-
|
|
44
|
+
// `?debug` mirrors the devtools overlay into the URL: open with a link, strip the param to close.
|
|
45
|
+
const [devtoolsOpen, setDevtoolsOpen] = useState(() => devtoolsEnabled && readUrlFlag(DEBUG_PARAM));
|
|
42
46
|
const devtoolsOpenRef = useRef(false);
|
|
43
47
|
devtoolsOpenRef.current = devtoolsOpen;
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!devtoolsEnabled)
|
|
50
|
+
return;
|
|
51
|
+
writeUrlParam(DEBUG_PARAM, devtoolsOpen ? "1" : null);
|
|
52
|
+
}, [devtoolsEnabled, devtoolsOpen]);
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!devtoolsEnabled)
|
|
55
|
+
return;
|
|
56
|
+
return subscribeUrlChange(() => setDevtoolsOpen(readUrlFlag(DEBUG_PARAM)));
|
|
57
|
+
}, [devtoolsEnabled]);
|
|
44
58
|
useEffect(() => installAgentBridge({
|
|
45
59
|
playable,
|
|
46
60
|
devtoolsEnabled,
|
|
@@ -19,7 +19,7 @@ import { PostProcessing } from "./postfx/PostProcessing.js";
|
|
|
19
19
|
import { EnvironmentLighting } from "./render/EnvironmentLighting.js";
|
|
20
20
|
import { CollisionDebugWorld } from "./devtools/CollisionDebugWorld.js";
|
|
21
21
|
import { DevtoolsRendererProbe } from "./devtools/DevtoolsOverlay.js";
|
|
22
|
-
import { GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
|
|
22
|
+
import { CAMERA_TRANSPARENT_USERDATA, GameCameraRig, PlayerFovProvider, PlayerFovSlider, resolveRigKind, rtsPanKeysConflict, } from "./camera/index.js";
|
|
23
23
|
import { CullingProvider } from "./visibility/CullingProvider.js";
|
|
24
24
|
import { SkyDaylight, TimeOfDayDaylight } from "./environment.js";
|
|
25
25
|
import { resolveSkyLightOwnership, skyEmitsLights } from "./environment/skyLightingPolicy.js";
|
|
@@ -289,7 +289,7 @@ export function Shell3dPresentation({ playable, ctx, multiplayer, tracker, point
|
|
|
289
289
|
fov: playable.camera?.frustum?.fov ?? CAMERA_FRUSTUM_DEFAULTS.fov,
|
|
290
290
|
near: playable.camera?.frustum?.near ?? CAMERA_FRUSTUM_DEFAULTS.near,
|
|
291
291
|
far: playable.camera?.frustum?.far ?? CAMERA_FRUSTUM_DEFAULTS.far,
|
|
292
|
-
}, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? _jsx(EnvironmentLighting, {}) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? _jsx(WorldOverlay, { ctx: ctx }) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
|
|
292
|
+
}, shadows: graphics.shadows, dpr: [Math.min(1, graphics.dpr), graphics.dpr], gl: { preserveDrawingBuffer: true }, style: { touchAction: "none" }, children: [backgroundColor !== undefined ? _jsx("color", { attach: "background", args: [backgroundColor] }) : null, cinematicLook ? _jsx(EnvironmentLighting, {}) : null, lighting !== undefined ? (_jsx(ConfiguredLighting, { lighting: lighting })) : effectiveSky === undefined ? (_jsxs(_Fragment, { children: [_jsx("ambientLight", { intensity: 0.55 }), _jsx("directionalLight", { position: [10, 16, 6], intensity: 1.3 })] })) : null, effectiveSky !== undefined ? (effectiveSky.timeOfDay ? (_jsx(TimeOfDayDaylight, { sky: effectiveSky, clock: ctx.time, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) })) : (_jsx(SkyDaylight, { sky: effectiveSky, bands: effectiveSky === worldSky ? biomeBands : undefined, lights: skyEmitsLights(resolveSkyLightOwnership(lighting !== undefined)) }))) : null, _jsx(BackdropFog, { fog: backdrop?.fog }), _jsxs(GameProvider, { context: ctx, children: [_jsx(CullingProvider, { config: playable.visibility, children: _jsx(WorldView, { entitySprites: playable.entitySprites, entityModels: playable.entityModels, objectModels: playable.objectModels, objectStyles: playable.objectStyles, environment: AutoEnvironment, assets: playable.game.assets, renderEntity: playable.renderEntity, renderObject: playable.renderObject, selectedIds: selectedIds, hideLocalActor: firstPerson }) }), WorldOverlay !== undefined ? (_jsx("group", { userData: CAMERA_TRANSPARENT_USERDATA, children: _jsx(WorldOverlay, { ctx: ctx }) })) : null, bars !== null ? (_jsx(WorldEntityBars, { statId: bars.statId, roles: bars.roles, resolveRole: resolveEntityRole, ...(bars.maxDistance === undefined ? {} : { maxDistance: bars.maxDistance }) })) : null, nameplates !== null ? (_jsx(WorldNameplates, { statId: nameplates.statId, roles: nameplates.roles, resolveRole: resolveEntityRole, ...(nameplates.maxDistance === undefined ? {} : { maxDistance: nameplates.maxDistance }), ...(nameplates.showHealth === undefined ? {} : { showHealth: nameplates.showHealth }) })) : null, _jsx(WorldItems, { config: playable.worldItem }), _jsx(CombatPresentation, { effects: effects }), devtoolsEnabled ? _jsx(CollisionDebugWorld, {}) : null, _jsx(AudioListener, { engine: audioEngine }), _jsx(EntityAudioEmitters, { engine: audioEngine, entitySounds: playable.entitySounds }), _jsx(ObjectAudioEmitters, { engine: audioEngine, objectSounds: playable.objectSounds }), _jsx(GameCameraRig, { yawRef: yawRef, pitchRef: pitchRef, config: cameraConfig, viewmodel: playable.viewmodel, pointerControls: pointerUsesLeft, panKeysEnabled: rtsPanKeysEnabled, director: ctx.camera, onDragChange: (dragging) => {
|
|
293
293
|
cameraDraggingRef.current = dragging;
|
|
294
294
|
} }), _jsx(PointerProbe, { service: pointerService })] }), _jsx(RemotePlayers, { rows: remotePlayers }), _jsx(FrameDriver, { ctx: ctx, playable: playable, tracker: tracker, yawRef: yawRef, pitchRef: pitchRef, primaryClickRef: primaryClickRef, pointerAxisRef: pointerAxisRef, gateRef: gateRef, onRuntimeError: reportRuntimeError, multiplayer: multiplayer, serverIdRef: serverIdRef, pointerService: pointerService, pointerAim: pointer?.aim === true, pingCommand: pointer?.pingCommand, poster: poster, onPosterSettled: onPosterSettled }), _jsx(DevtoolsRendererProbe, {}), resolvedLook.postProcessing !== undefined && resolvedLook.postProcessing.enabled !== false ? (_jsx(PostProcessing, { config: resolvedLook.postProcessing, quality: graphics.quality })) : null] }), !poster &&
|
|
295
295
|
!orientationGate &&
|
|
@@ -3,6 +3,7 @@ import { OrbitControls } from "@react-three/drei";
|
|
|
3
3
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
4
4
|
import { useEffect, useRef } from "react";
|
|
5
5
|
import { MOUSE, PerspectiveCamera, Raycaster, Vector3 } from "three";
|
|
6
|
+
import { isCameraOccluderTransparent } from "./cameraCollision.js";
|
|
6
7
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
8
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
8
9
|
import { cameraLookPitch, compensatedFov, ORBIT_CAMERA_FRAME_PRIORITY, orbitFollowStep, orbitYawFromCamera, resolveFollowTargetFromPosition, resolveOrbitCameraConfig, seedOrbitFollowState, } from "./orbitCameraMath.js";
|
|
@@ -81,6 +82,10 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
81
82
|
ray.set(collisionOriginRef.current.set(t.x, t.y, t.z), dir);
|
|
82
83
|
ray.near = config.collision.minTargetDistance;
|
|
83
84
|
ray.far = dist;
|
|
85
|
+
// Sprites (VFX, billboards) live in the scene; three's Sprite.raycast
|
|
86
|
+
// errors every frame unless the raycaster has a camera, even though we
|
|
87
|
+
// discard sprite hits below. Setting it silences that per-frame log.
|
|
88
|
+
ray.camera = camera;
|
|
84
89
|
let blocked = 0;
|
|
85
90
|
for (const hit of ray.intersectObjects(scene.children, true)) {
|
|
86
91
|
const obj = hit.object;
|
|
@@ -88,7 +93,8 @@ export function GameOrbitCamera({ yawRef, pitchRef, config: configPatch, followE
|
|
|
88
93
|
continue;
|
|
89
94
|
if (obj.isSprite === true)
|
|
90
95
|
continue;
|
|
91
|
-
|
|
96
|
+
// Inheritable: decor flags a group jgCameraTransparent and all children pass through.
|
|
97
|
+
if (isCameraOccluderTransparent(obj))
|
|
92
98
|
continue;
|
|
93
99
|
blocked = hit.distance;
|
|
94
100
|
break;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera spring-arm occlusion filtering.
|
|
3
|
+
*
|
|
4
|
+
* The orbit rig raycasts target→camera each frame and pulls the boom in past
|
|
5
|
+
* occluders. Author decor mounted through the `WorldOverlay` presentation seam
|
|
6
|
+
* is dressing, not collision geometry, so it should never yank the camera. A
|
|
7
|
+
* decor group flags itself camera-transparent once (`jgCameraTransparent`) and
|
|
8
|
+
* every descendant inherits it — a child can opt back into blocking the camera
|
|
9
|
+
* with `jgCameraCollide`.
|
|
10
|
+
*/
|
|
11
|
+
/** Minimal structural view of a raycast hit object: its own tags plus a parent link to walk. Keeps this filter free of a three.js import so it stays unit-testable. */
|
|
12
|
+
export interface CameraOccluder {
|
|
13
|
+
readonly userData?: Record<string, unknown>;
|
|
14
|
+
readonly parent?: CameraOccluder | null;
|
|
15
|
+
}
|
|
16
|
+
/** userData applied to the group wrapping author `WorldOverlay` decor so the spring-arm ignores it by default. */
|
|
17
|
+
export declare const CAMERA_TRANSPARENT_USERDATA: {
|
|
18
|
+
readonly jgCameraTransparent: true;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Should the camera spring-arm ignore this raycast hit? Walks the object up its
|
|
22
|
+
* `.parent` chain and honors the nearest camera tag: `jgCameraCollide === true`
|
|
23
|
+
* blocks (opt back in), `jgCameraTransparent === true` passes through. Untagged
|
|
24
|
+
* geometry blocks as before, so engine-owned ground/entities are unaffected.
|
|
25
|
+
*
|
|
26
|
+
* @capability camera-transparent-decor let author decor pass through the orbit spring-arm; a child opts back in with jgCameraCollide
|
|
27
|
+
*/
|
|
28
|
+
export declare function isCameraOccluderTransparent(object: CameraOccluder | null | undefined): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Camera spring-arm occlusion filtering.
|
|
3
|
+
*
|
|
4
|
+
* The orbit rig raycasts target→camera each frame and pulls the boom in past
|
|
5
|
+
* occluders. Author decor mounted through the `WorldOverlay` presentation seam
|
|
6
|
+
* is dressing, not collision geometry, so it should never yank the camera. A
|
|
7
|
+
* decor group flags itself camera-transparent once (`jgCameraTransparent`) and
|
|
8
|
+
* every descendant inherits it — a child can opt back into blocking the camera
|
|
9
|
+
* with `jgCameraCollide`.
|
|
10
|
+
*/
|
|
11
|
+
/** userData applied to the group wrapping author `WorldOverlay` decor so the spring-arm ignores it by default. */
|
|
12
|
+
export const CAMERA_TRANSPARENT_USERDATA = {
|
|
13
|
+
jgCameraTransparent: true,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Should the camera spring-arm ignore this raycast hit? Walks the object up its
|
|
17
|
+
* `.parent` chain and honors the nearest camera tag: `jgCameraCollide === true`
|
|
18
|
+
* blocks (opt back in), `jgCameraTransparent === true` passes through. Untagged
|
|
19
|
+
* geometry blocks as before, so engine-owned ground/entities are unaffected.
|
|
20
|
+
*
|
|
21
|
+
* @capability camera-transparent-decor let author decor pass through the orbit spring-arm; a child opts back in with jgCameraCollide
|
|
22
|
+
*/
|
|
23
|
+
export function isCameraOccluderTransparent(object) {
|
|
24
|
+
for (let node = object ?? null; node != null; node = node.parent ?? null) {
|
|
25
|
+
const data = node.userData;
|
|
26
|
+
if (data === undefined)
|
|
27
|
+
continue;
|
|
28
|
+
if (data.jgCameraCollide === true)
|
|
29
|
+
return false;
|
|
30
|
+
if (data.jgCameraTransparent === true)
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
@@ -4,7 +4,7 @@ import { PerspectiveCamera, Quaternion, Vector3 } from "three";
|
|
|
4
4
|
import { usePlayer } from "@jgengine/react/hooks";
|
|
5
5
|
import { useGameContext } from "@jgengine/react/provider";
|
|
6
6
|
import { ORBIT_CAMERA_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
7
|
-
import { bankRollStep, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, leadFollowPoint, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, seatPose, shoulderPose, sideScrollFollowBlend, smoothYaw, speedToFov, springArmStep, topDownPose, velocityYawTarget, } from "./rigMath.js";
|
|
7
|
+
import { bankRollStep, chaseDesiredPosition, chaseLookAt, cinematicSample, clamp, leadFollowPoint, lockOnPose, observerPose, resolveChase, resolveObserver, resolveShoulder, resolveSideScroll, resolveSideScrollPose, resolveTopDown, rtsPanWorldDir, seatPose, shoulderPose, sideScrollFollowBlend, smoothYaw, speedToFov, springArmStep, topDownPose, velocityYawTarget, } from "./rigMath.js";
|
|
8
8
|
import { usePlayerFov } from "./PlayerFov.js";
|
|
9
9
|
import { useCameraShake } from "./shakeChannel.js";
|
|
10
10
|
import { applyCameraBlendStep, captureCameraBlendFrom, createCameraBlendScratch, } from "./cameraBlendMath.js";
|
|
@@ -262,13 +262,9 @@ export function RtsRig(props) {
|
|
|
262
262
|
}
|
|
263
263
|
const center = centerRef.current;
|
|
264
264
|
if (panX !== 0 || panZ !== 0) {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const worldZ = panZ * cos - panX * sin;
|
|
269
|
-
const len = Math.hypot(worldX, worldZ) || 1;
|
|
270
|
-
center.x += (worldX / len) * panSpeed * dt;
|
|
271
|
-
center.z += (worldZ / len) * panSpeed * dt;
|
|
265
|
+
const dir = rtsPanWorldDir(panX, panZ, yawRef.current);
|
|
266
|
+
center.x += dir.x * panSpeed * dt;
|
|
267
|
+
center.z += dir.z * panSpeed * dt;
|
|
272
268
|
}
|
|
273
269
|
else {
|
|
274
270
|
const follow = readFollow(ctx, followId);
|
|
@@ -452,6 +448,7 @@ export function ChaseRig(props) {
|
|
|
452
448
|
if (lookBack) {
|
|
453
449
|
pose.position = seatPose(follow, yaw, seat, fov).position;
|
|
454
450
|
}
|
|
451
|
+
props.yawRef.current = yaw;
|
|
455
452
|
commit(pose, dt);
|
|
456
453
|
return;
|
|
457
454
|
}
|
|
@@ -464,6 +461,9 @@ export function ChaseRig(props) {
|
|
|
464
461
|
anchorYaw = smoothYaw(anchorYawRef.current ?? targetYaw, targetYaw, resolved.velocityYawResponse, dt);
|
|
465
462
|
anchorYawRef.current = anchorYaw;
|
|
466
463
|
}
|
|
464
|
+
// Report camera yaw back to the shell like every other player-facing rig, so
|
|
465
|
+
// on-foot movement and aim stay camera-relative instead of frozen at yaw 0.
|
|
466
|
+
props.yawRef.current = anchorYaw;
|
|
467
467
|
const led = leadFollowPoint(follow, last, dt, resolved);
|
|
468
468
|
const desired = chaseDesiredPosition(led, anchorYaw, resolved);
|
|
469
469
|
const prev = posRef.current ?? desired;
|
package/dist/camera/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { PlayerFovProvider, PlayerFovSlider, usePlayerFov, type PlayerFovState,
|
|
|
7
7
|
export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, type CameraShakeChannel, } from "./shakeChannel.js";
|
|
8
8
|
export { rtsPanKeysConflict } from "./rigMath.js";
|
|
9
9
|
export { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
10
|
+
export { CAMERA_TRANSPARENT_USERDATA, isCameraOccluderTransparent, type CameraOccluder, } from "./cameraCollision.js";
|
package/dist/camera/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { PlayerFovProvider, PlayerFovSlider, usePlayerFov, } from "./PlayerFov.j
|
|
|
7
7
|
export { CameraShakeContext, cameraShake, createCameraShakeChannel, defaultCameraShakeChannel, useCameraShake, } from "./shakeChannel.js";
|
|
8
8
|
export { rtsPanKeysConflict } from "./rigMath.js";
|
|
9
9
|
export { GAME_SIM_FRAME_PRIORITY } from "./orbitCameraMath.js";
|
|
10
|
+
export { CAMERA_TRANSPARENT_USERDATA, isCameraOccluderTransparent, } from "./cameraCollision.js";
|
package/dist/camera/rigMath.d.ts
CHANGED
|
@@ -37,6 +37,13 @@ export declare function resolveTopDown(config: TopDownCameraConfig | undefined):
|
|
|
37
37
|
* @internal
|
|
38
38
|
*/
|
|
39
39
|
export declare function topDownPose(follow: Vec3, resolved: ResolvedTopDown, fov: number): CameraPose;
|
|
40
|
+
/**
|
|
41
|
+
* World-space direction for RTS pan input: `panX` slides along screen-right,
|
|
42
|
+
* `panZ` along the camera's ground-plane forward, for a camera posed by
|
|
43
|
+
* `topDownPose` at the same yaw. Normalized; zero input returns zero.
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
export declare function rtsPanWorldDir(panX: number, panZ: number, yaw: number): Vec3;
|
|
40
47
|
/** Exponential spring-arm approach toward a desired point (frame-rate independent).
|
|
41
48
|
* @internal
|
|
42
49
|
*/
|
package/dist/camera/rigMath.js
CHANGED
|
@@ -74,6 +74,22 @@ export function topDownPose(follow, resolved, fov) {
|
|
|
74
74
|
fov,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* World-space direction for RTS pan input: `panX` slides along screen-right,
|
|
79
|
+
* `panZ` along the camera's ground-plane forward, for a camera posed by
|
|
80
|
+
* `topDownPose` at the same yaw. Normalized; zero input returns zero.
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export function rtsPanWorldDir(panX, panZ, yaw) {
|
|
84
|
+
const forward = forwardVector(yaw);
|
|
85
|
+
const right = rightVector(yaw);
|
|
86
|
+
const x = panX * right.x + panZ * forward.x;
|
|
87
|
+
const z = panX * right.z + panZ * forward.z;
|
|
88
|
+
const len = Math.hypot(x, z);
|
|
89
|
+
if (len === 0)
|
|
90
|
+
return { x: 0, y: 0, z: 0 };
|
|
91
|
+
return { x: x / len, y: 0, z: z / len };
|
|
92
|
+
}
|
|
77
93
|
/** Exponential spring-arm approach toward a desired point (frame-rate independent).
|
|
78
94
|
* @internal
|
|
79
95
|
*/
|
|
@@ -4,6 +4,7 @@ import { useFrame } from "@react-three/fiber";
|
|
|
4
4
|
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import * as THREE from "three";
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
|
+
import { POSTFX_OVERLAY_USERDATA } from "../postfx/postfxOverlay.js";
|
|
7
8
|
import { aimProbeNeeded, anyCollisionLayerOn, colliderScanNeeded, collisionDebug, projectileListenNeeded, } from "./collisionDebug.js";
|
|
8
9
|
import { AIM_DAMAGE_COLOR, AIM_LASER_COLOR, AIM_MISS_COLOR, AIM_SOLID_COLOR, BODY_WIRE_COLOR, collectDebugShapes, computeAimLaser, HITBOX_WIRE_COLOR, muzzleMarkerFromOrigin, PROJECTILE_PATH_COLOR, } from "./collisionDebugMath.js";
|
|
9
10
|
function useCollisionDebugState() {
|
|
@@ -15,11 +16,24 @@ function WireSphere({ center, radius, color, label, }) {
|
|
|
15
16
|
function WireAabb({ center, halfExtents, rotationY, color, label, }) {
|
|
16
17
|
return (_jsxs("group", { position: [center[0], center[1], center[2]], rotation: [0, rotationY, 0], children: [_jsxs("mesh", { children: [_jsx("boxGeometry", { args: [halfExtents[0] * 2, halfExtents[1] * 2, halfExtents[2] * 2] }), _jsx("meshBasicMaterial", { color: color, wireframe: true, transparent: true, opacity: 0.85, depthTest: false })] }), _jsx(Html, { position: [0, halfExtents[1] + 0.15, 0], center: true, distanceFactor: 14, zIndexRange: [25, 0], style: { pointerEvents: "none" }, children: _jsx("span", { className: "rounded bg-black/70 px-1 text-[9px] font-medium text-white/90", children: label }) })] }));
|
|
17
18
|
}
|
|
19
|
+
function WireTriangleMesh({ entry, shape, color, }) {
|
|
20
|
+
const geometry = useMemo(() => {
|
|
21
|
+
const built = new THREE.BufferGeometry();
|
|
22
|
+
built.setAttribute("position", new THREE.BufferAttribute(shape.mesh.positions, 3));
|
|
23
|
+
built.setIndex(new THREE.BufferAttribute(shape.mesh.indices, 1));
|
|
24
|
+
return built;
|
|
25
|
+
}, [shape.mesh]);
|
|
26
|
+
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
27
|
+
return (_jsxs("group", { children: [_jsx("group", { position: [entry.position[0], entry.position[1], entry.position[2]], rotation: [0, entry.rotationY, 0], children: _jsx("group", { position: [shape.meshTranslate[0], shape.meshTranslate[1], shape.meshTranslate[2]], scale: shape.meshScale, children: _jsx("mesh", { geometry: geometry, children: _jsx("meshBasicMaterial", { color: color, wireframe: true, transparent: true, opacity: 0.85, depthTest: false }) }) }) }), _jsx(Html, { position: [shape.center[0], shape.center[1] + shape.halfExtents[1] + 0.15, shape.center[2]], center: true, distanceFactor: 14, zIndexRange: [25, 0], style: { pointerEvents: "none" }, children: _jsx("span", { className: "rounded bg-black/70 px-1 text-[9px] font-medium text-white/90", children: entry.label }) })] }));
|
|
28
|
+
}
|
|
18
29
|
function ColliderShapeMesh({ entry }) {
|
|
19
30
|
const color = entry.style === "hitbox" ? HITBOX_WIRE_COLOR : BODY_WIRE_COLOR;
|
|
20
31
|
if (entry.shape.kind === "sphere") {
|
|
21
32
|
return (_jsx(WireSphere, { center: entry.shape.center, radius: entry.shape.radius, color: color, label: entry.label }));
|
|
22
33
|
}
|
|
34
|
+
if (entry.shape.kind === "mesh") {
|
|
35
|
+
return _jsx(WireTriangleMesh, { entry: entry, shape: entry.shape, color: color });
|
|
36
|
+
}
|
|
23
37
|
return (_jsx(WireAabb, { center: entry.shape.center, halfExtents: entry.shape.halfExtents, rotationY: entry.rotationY, color: color, label: entry.label }));
|
|
24
38
|
}
|
|
25
39
|
function endpointColor(kind) {
|
|
@@ -174,7 +188,7 @@ export function CollisionDebugWorld() {
|
|
|
174
188
|
}, [state.layers.muzzles, state.layers.aimLaser, state.projectileTraces, laser]);
|
|
175
189
|
if (!anyCollisionLayerOn(state.layers))
|
|
176
190
|
return null;
|
|
177
|
-
return (_jsxs("group", { children: [shapes.map((entry) => (_jsx(ColliderShapeMesh, { entry: entry }, entry.key))), projectilePoints.map((path) => (_jsx(Line, { points: path.points, color: PROJECTILE_PATH_COLOR, lineWidth: 2, transparent: true, opacity: 0.9, depthTest: false }, path.id))), muzzleOrigins.map((entry) => (_jsx(MuzzleMark, { origin: entry.origin }, `muzzle-${entry.id}`))), laser !== null ? (_jsxs(_Fragment, { children: [_jsx(Line, { points: [
|
|
191
|
+
return (_jsxs("group", { userData: POSTFX_OVERLAY_USERDATA, children: [shapes.map((entry) => (_jsx(ColliderShapeMesh, { entry: entry }, entry.key))), projectilePoints.map((path) => (_jsx(Line, { points: path.points, color: PROJECTILE_PATH_COLOR, lineWidth: 2, transparent: true, opacity: 0.9, depthTest: false }, path.id))), muzzleOrigins.map((entry) => (_jsx(MuzzleMark, { origin: entry.origin }, `muzzle-${entry.id}`))), laser !== null ? (_jsxs(_Fragment, { children: [_jsx(Line, { points: [
|
|
178
192
|
new THREE.Vector3(laser.origin[0], laser.origin[1], laser.origin[2]),
|
|
179
193
|
new THREE.Vector3(laser.end[0], laser.end[1], laser.end[2]),
|
|
180
194
|
], color: AIM_LASER_COLOR, lineWidth: 1.5, transparent: true, opacity: 0.95, depthTest: false }), _jsx(AimEndpointMark, { point: laser.end, kind: laser.kind })] })) : null] }));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EntityPosition } from "@jgengine/core/scene/entityStore";
|
|
2
2
|
import { type ColliderPurpose, type EntityColliderSet, type ResolvedCollider } from "@jgengine/core/scene/colliders";
|
|
3
|
+
import type { PreparedCollisionMesh } from "@jgengine/core/scene/collisionMesh";
|
|
3
4
|
import { type SceneRaycastApi, type SceneRaycastHit } from "@jgengine/core/scene/sceneRaycast";
|
|
4
5
|
import { type ShotOriginPolicy } from "@jgengine/core/combat/shotOrigin";
|
|
5
6
|
import type { Aim } from "@jgengine/core/scene/spatial";
|
|
@@ -24,6 +25,13 @@ export interface DebugShapeEntry {
|
|
|
24
25
|
kind: "aabb";
|
|
25
26
|
halfExtents: EntityPosition;
|
|
26
27
|
center: EntityPosition;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "mesh";
|
|
30
|
+
mesh: PreparedCollisionMesh;
|
|
31
|
+
meshScale: number;
|
|
32
|
+
meshTranslate: EntityPosition;
|
|
33
|
+
halfExtents: EntityPosition;
|
|
34
|
+
center: EntityPosition;
|
|
27
35
|
};
|
|
28
36
|
label: string;
|
|
29
37
|
style: "hitbox" | "body";
|
|
@@ -36,11 +36,20 @@ function pushResolved(out, targetKind, instanceId, catalogId, position, rotation
|
|
|
36
36
|
const center = shapeWorldCenter(collider, position, rotationY);
|
|
37
37
|
const shape = collider.shape.kind === "sphere"
|
|
38
38
|
? { kind: "sphere", radius: collider.shape.radius, center }
|
|
39
|
-
:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
: collider.shape.kind === "mesh"
|
|
40
|
+
? {
|
|
41
|
+
kind: "mesh",
|
|
42
|
+
mesh: collider.shape.mesh,
|
|
43
|
+
meshScale: collider.shape.meshScale,
|
|
44
|
+
meshTranslate: collider.shape.meshTranslate,
|
|
45
|
+
halfExtents: collider.shape.halfExtents,
|
|
46
|
+
center,
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
kind: "aabb",
|
|
50
|
+
halfExtents: collider.shape.halfExtents,
|
|
51
|
+
center,
|
|
52
|
+
};
|
|
44
53
|
out.push({
|
|
45
54
|
key: `${targetKind}:${instanceId}:${collider.purpose}:${collider.name}`,
|
|
46
55
|
targetKind,
|
|
@@ -6,8 +6,10 @@ import * as THREE from "three";
|
|
|
6
6
|
import { useGameContext } from "@jgengine/react/provider";
|
|
7
7
|
import { resolveBuildingPalette } from "@jgengine/core/world/buildings";
|
|
8
8
|
import { resolveStructureBuildings } from "@jgengine/core/world/environmentSummary";
|
|
9
|
+
import { findRoadJunctions, junctionExclusions } from "@jgengine/core/world/streets";
|
|
9
10
|
import { createTerrainPaletteSampler, resolveEnvironmentField, resolveTerrainDetail, resolveTerrainPalette, } from "@jgengine/core/world/terrain";
|
|
10
11
|
import { GroundPad } from "./GroundPad.js";
|
|
12
|
+
import { RoadJunctions } from "./RoadJunctions.js";
|
|
11
13
|
import { RoadRibbons } from "./RoadRibbons.js";
|
|
12
14
|
import { terrainGroundColorSampler } from "./terrainGroundColor.js";
|
|
13
15
|
import { InstancedBuildings } from "../structures/GeneratedBuilding.js";
|
|
@@ -142,6 +144,7 @@ function Structures({ structures, field }) {
|
|
|
142
144
|
const placements = useMemo(() => resolveStructureBuildings(structures).map((building) => ({
|
|
143
145
|
building,
|
|
144
146
|
position: [0, field.sampleHeight(building.center[0], building.center[1]), 0],
|
|
147
|
+
...(building.rotationY === undefined ? {} : { rotationY: building.rotationY, pivot: building.center }),
|
|
145
148
|
})), [structures, field]);
|
|
146
149
|
const palette = useMemo(() => resolveBuildingPalette(structures.style, structures.palette), [structures.style, structures.palette]);
|
|
147
150
|
return _jsx(InstancedBuildings, { buildings: placements, palette: palette });
|
|
@@ -154,5 +157,7 @@ export function EnvironmentScene({ feature }) {
|
|
|
154
157
|
const pads = feature.pads ?? [];
|
|
155
158
|
const islands = feature.islands ?? [];
|
|
156
159
|
const roads = feature.roads ?? [];
|
|
157
|
-
|
|
160
|
+
const junctions = useMemo(() => findRoadJunctions(roads), [roads]);
|
|
161
|
+
const exclusions = useMemo(() => junctionExclusions(junctions), [junctions]);
|
|
162
|
+
return (_jsxs(_Fragment, { children: [feature.terrain !== undefined ? _jsx(TerrainGround, { terrain: feature.terrain, field: field }) : null, islands.map((entry, index) => (_jsx(TerrainGround, { terrain: entry, field: field, center: entry.origin }, `island-${index}`))), water.map((ocean, index) => (_jsx(Water, { ocean: ocean }, `ocean-${index}`))), roads.map((entry, index) => (_jsx(RoadRibbons, { road: entry, field: field, exclusions: exclusions }, `road-${index}`))), _jsx(RoadJunctions, { junctions: junctions, field: field }), structures.map((entry, index) => (_jsx(Structures, { structures: entry, field: field }, `structures-${index}`))), pads.map((entry, index) => (_jsx(GroundPad, { pad: entry, field: field }, `pad-${index}`))), vegetation.map((grass, index) => (_jsx(Vegetation, { grass: grass, field: field, groundColor: vegetationGroundColor(feature.terrain, field, grass) }, `grass-${index}`))), feature.weather !== undefined && feature.weather.length > 0 ? (_jsx(Weather, { weather: feature.weather })) : null] }));
|
|
158
163
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RoadJunction } from "@jgengine/core/world/streets";
|
|
2
|
+
import type { TerrainField } from "@jgengine/core/world/terrain";
|
|
3
|
+
/**
|
|
4
|
+
* Renders welded road intersections: one asphalt disc patch per junction (covering the crossing
|
|
5
|
+
* ribbons so they read as one intersection instead of stacking/z-fighting), plus white stop lines
|
|
6
|
+
* and crosswalk bars at each approach. Fed the junctions the scene already computed from its roads.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function RoadJunctions({ junctions, field, }: {
|
|
10
|
+
junctions: readonly RoadJunction[];
|
|
11
|
+
field: TerrainField;
|
|
12
|
+
}): import("react").JSX.Element | null;
|