@jgengine/shell 0.14.0 → 0.15.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 +500 -1
- package/dist/GamePlayerShell.js +25 -5
- package/dist/Shell3dPresentation.d.ts +6 -6
- package/dist/Shell3dPresentation.js +4 -4
- package/dist/audio/synthEngine.js +12 -2
- package/dist/camera/ControllerCameraShake.d.ts +31 -0
- package/dist/camera/ControllerCameraShake.js +34 -0
- package/dist/camera/GameCameraRig.js +10 -5
- package/dist/camera/GameOrbitCamera.js +1 -10
- package/dist/camera/cameraRigs.js +12 -4
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +2 -0
- package/dist/camera/rigMath.js +7 -0
- package/dist/devtools/CollisionDebugWorld.js +2 -2
- package/dist/devtools/DevtoolsOverlay.js +2 -0
- package/dist/drivers/FrameDriver.d.ts +4 -1
- package/dist/drivers/FrameDriver.js +5 -3
- package/dist/environment/DayNightSky.d.ts +33 -0
- package/dist/environment/DayNightSky.js +51 -0
- package/dist/environment/Daylight.d.ts +3 -1
- package/dist/environment/Daylight.js +7 -3
- package/dist/environment/EnvironmentScene.js +6 -6
- package/dist/environment/RoadJunctions.d.ts +8 -4
- package/dist/environment/RoadJunctions.js +16 -15
- package/dist/environment/RoadRibbons.d.ts +42 -6
- package/dist/environment/RoadRibbons.js +150 -27
- package/dist/environment/VolumetricClouds.js +3 -3
- package/dist/environment/index.d.ts +1 -0
- package/dist/environment/index.js +1 -0
- package/dist/gameKit.d.ts +7 -4
- package/dist/gameKit.js +8 -5
- package/dist/inputSink.js +14 -0
- package/dist/materialOverride.js +30 -0
- package/dist/postfx/PostProcessing.d.ts +3 -2
- package/dist/postfx/PostProcessing.js +11 -3
- package/dist/postfx/ScreenEffectsOverlay.d.ts +32 -0
- package/dist/postfx/ScreenEffectsOverlay.js +70 -0
- package/dist/presentationResolve.d.ts +3 -2
- package/dist/presentationResolve.js +11 -6
- package/dist/render/CascadedShadows.d.ts +13 -0
- package/dist/render/CascadedShadows.js +96 -0
- package/dist/render/EnvironmentLighting.d.ts +23 -7
- package/dist/render/EnvironmentLighting.js +69 -10
- package/dist/render/PartMotion.js +4 -4
- package/dist/render/SceneLighting.js +7 -0
- package/dist/render/SceneModels.d.ts +37 -1
- package/dist/render/SceneModels.js +40 -11
- package/dist/render/modelLoad.d.ts +41 -5
- package/dist/render/modelLoad.js +85 -18
- package/dist/render/sceneCapture.d.ts +27 -0
- package/dist/render/sceneCapture.js +43 -0
- package/dist/render/useDisposable.d.ts +16 -0
- package/dist/render/useDisposable.js +24 -0
- package/dist/render/useModelAnimation.js +47 -4
- package/dist/scene/AuthoredScene.js +14 -24
- package/dist/scene/CityRenderer.js +103 -36
- package/dist/scene/GrassFieldRenderer.js +4 -1
- package/dist/scene/PoleLineRenderer.js +7 -16
- package/dist/settings/QuickControls.js +5 -3
- package/dist/settings/SettingsMenu.js +6 -3
- package/dist/settings/appliedSettings.d.ts +3 -1
- package/dist/settings/appliedSettings.js +17 -5
- package/dist/settings/settingsController.js +11 -5
- package/dist/shellMovement.d.ts +15 -1
- package/dist/shellMovement.js +15 -1
- package/dist/structures/GeneratedBuilding.js +4 -5
- package/dist/structures/TransformGizmo.js +6 -5
- package/dist/terrain/CarvedTerrain.js +3 -7
- package/dist/terrain/GrassField.js +2 -2
- package/dist/terrain/ProceduralGround.js +3 -5
- package/dist/terrain/grassMaterial.d.ts +8 -2
- package/dist/terrain/grassMaterial.js +24 -5
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/terrainDetailMaterial.js +11 -2
- package/dist/terrain/terrainMath.d.ts +38 -1
- package/dist/terrain/terrainMath.js +86 -64
- package/dist/touch/TouchControlsOverlay.d.ts +6 -2
- package/dist/touch/TouchControlsOverlay.js +136 -17
- package/dist/vfx/ParticleField.d.ts +32 -0
- package/dist/vfx/ParticleField.js +91 -0
- package/dist/vfx/WorldFloatingText.d.ts +32 -0
- package/dist/vfx/WorldFloatingText.js +32 -0
- package/dist/water/Ocean.js +3 -6
- package/dist/weather/LightningStrike.js +3 -9
- package/dist/weather/RainField.js +3 -7
- package/dist/weather/SnowField.js +3 -7
- package/dist/world/DataObjects.js +3 -6
- package/dist/world/GridWorldScene.js +3 -6
- package/dist/world/InstancedBodies.js +3 -6
- package/dist/world/InstancedJoints.js +2 -2
- package/dist/world/SpriteBatch.js +3 -6
- package/dist/world/WorldEntityFrames.d.ts +47 -0
- package/dist/world/WorldEntityFrames.js +75 -0
- package/dist/world/WorldHud.js +7 -5
- package/dist/world/WorldPings.d.ts +26 -0
- package/dist/world/WorldPings.js +68 -0
- package/dist/world/WorldScene.js +9 -4
- package/dist/world/pingPulse.d.ts +21 -0
- package/dist/world/pingPulse.js +17 -0
- package/package.json +5 -5
- package/skills/ce-handoff/SKILL.md +149 -0
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +3 -1
- package/skills/jgengine/api.md +17 -7
- package/skills/jgengine/capabilities.md +12 -0
- package/skills/jgengine/recipes/minimal-game.md +7 -4
- package/skills/jgengine-ui/SKILL.md +5 -3
- package/skills/jgengine-ui/api.md +708 -19
- package/skills/jgengine-ui/capabilities.md +362 -1
- package/skills/jgengine-ui/recipes/overhead-entity-frames.md +136 -0
- package/skills/jgengine-ui/reference.md +7 -4
- package/skills/jgengine-world/api.md +397 -11
- package/skills/jgengine-world/capabilities.md +123 -2
- package/skills/jgengine-world/reference.md +15 -1
|
@@ -16,7 +16,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
16
16
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
17
17
|
import { useFrame } from "@react-three/fiber";
|
|
18
18
|
import * as THREE from "three";
|
|
19
|
-
import { roundPathCorners, buildRoadRibbon, dashSegments } from "@jgengine/core/world/roads";
|
|
19
|
+
import { roundPathCorners, buildJunctionSurface, buildRoadRibbon, dashSegments, GROUND_DECAL_LAYERS, trimPathAtJunctions, } from "@jgengine/core/world/roads";
|
|
20
20
|
import { offsetPath } from "@jgengine/core/world/streets";
|
|
21
21
|
import { triangulatePolygon, triangulateRingBand } from "@jgengine/core/world/cityGeometry";
|
|
22
22
|
import { resolveCityObject, CITY_KIND, CITY_ZONE_KIND, } from "@jgengine/core/world/cityKind";
|
|
@@ -37,6 +37,27 @@ const CURB_COLOR = "#c4c0b6";
|
|
|
37
37
|
const HEDGE_COLOR = "#3d6631";
|
|
38
38
|
const PARK_COLORS = { plaza: "#b3ada0", green: "#4e7c41", meadow: "#6d8a4c", field: "#94805a", courtyard: "#5c8348", buffer: "#63734a" };
|
|
39
39
|
const CROP_COLORS = ["#5d7a35", "#7a6b41", "#6f7f3a"];
|
|
40
|
+
/**
|
|
41
|
+
* City ground-decal stack. The single-owner {@link GROUND_DECAL_LAYERS} table fixes the authored-road
|
|
42
|
+
* heights, but a generated district drapes many more surface classes (sidewalk/park/median/curb) and
|
|
43
|
+
* wants more clearance over relief, so it keeps its own baseline while mirroring the table's ordering
|
|
44
|
+
* (terrain < road == junction < marking) and its two hard contracts: the welded junction surface
|
|
45
|
+
* shares the road layer (seam-welded, never stacked), and markings ride above the road with
|
|
46
|
+
* `polygonOffset`/`renderOrder` on the material. Values below preserve the district's prior look.
|
|
47
|
+
*/
|
|
48
|
+
const SIDEWALK_LAYER = 0.1;
|
|
49
|
+
const PARK_LAYER = 0.12;
|
|
50
|
+
const DRIVEWAY_LAYER = 0.12;
|
|
51
|
+
const BRIDGE_DECK_LIFT = 0.12;
|
|
52
|
+
const PARKING_LAYER = 0.13;
|
|
53
|
+
const PLAZA_LAYER = 0.14;
|
|
54
|
+
const ROAD_LAYER = 0.16; // asphalt/gravel carriageway AND the welded junction surface (seam-shared)
|
|
55
|
+
const CROP_LAYER = 0.2;
|
|
56
|
+
const CURB_LAYER = 0.24;
|
|
57
|
+
const MARKING_LAYER = 0.26;
|
|
58
|
+
const MEDIAN_LAYER = 0.3;
|
|
59
|
+
/** Trim + weld tuning shared by {@link trimPathAtJunctions} and {@link buildJunctionSurface} for the district. */
|
|
60
|
+
const JUNCTION_OPTS = { curbReturnRadius: 3, apronMargin: 0.6, filletSegments: 6, elevation: ROAD_LAYER, maxSegmentLength: 2 };
|
|
40
61
|
const SPECIES_PROXY = { broadleaf: "oak", conifer: "pine", palm: "palm", cypress: "cypress" };
|
|
41
62
|
const SPECIES_SCALE = { broadleaf: 2.7, conifer: 2.5, palm: 2.6, cypress: 2.3 };
|
|
42
63
|
/** Accumulates draped ribbons, patches, discs, and quads into one merged (optionally colored) buffer. */
|
|
@@ -64,6 +85,17 @@ class MeshAccumulator {
|
|
|
64
85
|
this.indices.push(ribbon.indices[i] + offset);
|
|
65
86
|
this.pushColor(ribbon.positions.length / 3, color);
|
|
66
87
|
}
|
|
88
|
+
/** Append an already-draped ribbon/surface (absolute `[x,y,z]` positions) verbatim — e.g. a welded junction. */
|
|
89
|
+
addMesh(ribbon, color = null) {
|
|
90
|
+
if (ribbon.positions.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
const offset = this.positions.length / 3;
|
|
93
|
+
for (let i = 0; i < ribbon.positions.length; i += 1)
|
|
94
|
+
this.positions.push(ribbon.positions[i]);
|
|
95
|
+
for (let i = 0; i < ribbon.indices.length; i += 1)
|
|
96
|
+
this.indices.push(ribbon.indices[i] + offset);
|
|
97
|
+
this.pushColor(ribbon.positions.length / 3, color);
|
|
98
|
+
}
|
|
67
99
|
/** A ground-draped rotated rectangle, grid-subdivided so large patches follow the relief. */
|
|
68
100
|
addPatch(center, size, rotationY, sample, elevation, color = null) {
|
|
69
101
|
const cos = Math.cos(rotationY);
|
|
@@ -610,18 +642,43 @@ function OneCity({ object, context }) {
|
|
|
610
642
|
]) {
|
|
611
643
|
const x = center[0] + along[0] * length * sa + perp[0] * width * sp;
|
|
612
644
|
const z = center[1] + along[1] * length * sa + perp[1] * width * sp;
|
|
613
|
-
markings.positions.push(x, deckSampler(x, z) +
|
|
645
|
+
markings.positions.push(x, deckSampler(x, z) + MARKING_LAYER, z);
|
|
614
646
|
}
|
|
615
647
|
markings.indices.push(base, base + 2, base + 1, base + 1, base + 2, base + 3);
|
|
616
648
|
};
|
|
649
|
+
// Trim carriageways back to their junction boundaries and weld one surface per crossing (no
|
|
650
|
+
// overlapping ribbons hidden under a floating disc). Street endpoints coincide bitwise with
|
|
651
|
+
// junction nodes, so `trimPathAtJunctions` cuts each rounded ribbon exactly at the crossing; the
|
|
652
|
+
// terminal corners feed `buildJunctionSurface` per intersection. Any crossing that gets no trimmed
|
|
653
|
+
// approach (e.g. sliced past the intersection cap) falls back to the old rounded outline below.
|
|
654
|
+
const intersectionInputs = resolved.intersections.map((cross) => ({
|
|
655
|
+
x: cross.x,
|
|
656
|
+
z: cross.z,
|
|
657
|
+
arms: cross.arms.map((arm) => ({ angle: arm.angle, width: arm.width })),
|
|
658
|
+
}));
|
|
659
|
+
const approachesByCross = new Map();
|
|
617
660
|
for (const street of resolved.streets) {
|
|
618
661
|
if (street.points.length < 2)
|
|
619
662
|
continue;
|
|
620
663
|
const rounded = roundPathCorners(street.points, Math.max(2.5, street.width * 1.15), 9);
|
|
621
664
|
const surface = street.surface === "gravel" ? gravel : asphalt;
|
|
622
|
-
|
|
665
|
+
for (const sub of trimPathAtJunctions(rounded, street.width, intersectionInputs, JUNCTION_OPTS)) {
|
|
666
|
+
surface.addRibbon(sub.path, street.width, deckSampler, ROAD_LAYER);
|
|
667
|
+
for (const cut of sub.cuts) {
|
|
668
|
+
const approach = {
|
|
669
|
+
center: cut.center,
|
|
670
|
+
left: [cut.left[0], deckSampler(cut.left[0], cut.left[1]) + ROAD_LAYER, cut.left[1]],
|
|
671
|
+
right: [cut.right[0], deckSampler(cut.right[0], cut.right[1]) + ROAD_LAYER, cut.right[1]],
|
|
672
|
+
};
|
|
673
|
+
const list = approachesByCross.get(cut.junctionIndex);
|
|
674
|
+
if (list)
|
|
675
|
+
list.push(approach);
|
|
676
|
+
else
|
|
677
|
+
approachesByCross.set(cut.junctionIndex, [approach]);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
623
680
|
if (street.sidewalk && !useBlockSidewalks)
|
|
624
|
-
sidewalks.addRibbon(rounded, street.width + 3.6, sample,
|
|
681
|
+
sidewalks.addRibbon(rounded, street.width + 3.6, sample, SIDEWALK_LAYER);
|
|
625
682
|
// Curbs + painted edge/lane lines make a flat ribbon read as a real carriageway. Both break at
|
|
626
683
|
// junctions (the crossing owns its own pavement), so clip the centerline first.
|
|
627
684
|
if (street.surface === "asphalt") {
|
|
@@ -631,26 +688,26 @@ function OneCity({ object, context }) {
|
|
|
631
688
|
continue;
|
|
632
689
|
const left = offsetPath(run, half);
|
|
633
690
|
const right = offsetPath(run, -half);
|
|
634
|
-
curbs.addRibbon(left, 0.55, deckSampler,
|
|
635
|
-
curbs.addRibbon(right, 0.55, deckSampler,
|
|
691
|
+
curbs.addRibbon(left, 0.55, deckSampler, CURB_LAYER);
|
|
692
|
+
curbs.addRibbon(right, 0.55, deckSampler, CURB_LAYER);
|
|
636
693
|
if (street.level !== "lane") {
|
|
637
694
|
// White edge (fog) lines just inside the curb.
|
|
638
|
-
markings.addRibbon(offsetPath(run, half - 0.75), 0.16, deckSampler,
|
|
639
|
-
markings.addRibbon(offsetPath(run, -(half - 0.75)), 0.16, deckSampler,
|
|
695
|
+
markings.addRibbon(offsetPath(run, half - 0.75), 0.16, deckSampler, MARKING_LAYER);
|
|
696
|
+
markings.addRibbon(offsetPath(run, -(half - 0.75)), 0.16, deckSampler, MARKING_LAYER);
|
|
640
697
|
// Avenue+ get dashed lane dividers either side of the centerline.
|
|
641
698
|
if (street.level === "avenue") {
|
|
642
699
|
for (const dash of dashSegments(offsetPath(run, half * 0.5), 2.4, 3.2))
|
|
643
|
-
markings.addRibbon(dash, 0.13, deckSampler,
|
|
700
|
+
markings.addRibbon(dash, 0.13, deckSampler, MARKING_LAYER);
|
|
644
701
|
for (const dash of dashSegments(offsetPath(run, -half * 0.5), 2.4, 3.2))
|
|
645
|
-
markings.addRibbon(dash, 0.13, deckSampler,
|
|
702
|
+
markings.addRibbon(dash, 0.13, deckSampler, MARKING_LAYER);
|
|
646
703
|
}
|
|
647
704
|
}
|
|
648
705
|
}
|
|
649
706
|
}
|
|
650
707
|
if (street.bulb !== undefined) {
|
|
651
|
-
(street.surface === "gravel" ? gravel : asphalt).addDisc(street.bulb, street.width * 1.15, sample,
|
|
708
|
+
(street.surface === "gravel" ? gravel : asphalt).addDisc(street.bulb, street.width * 1.15, sample, ROAD_LAYER);
|
|
652
709
|
if (street.sidewalk)
|
|
653
|
-
sidewalks.addDisc(street.bulb, street.width * 1.15 + 1.8, sample,
|
|
710
|
+
sidewalks.addDisc(street.bulb, street.width * 1.15 + 1.8, sample, SIDEWALK_LAYER);
|
|
654
711
|
}
|
|
655
712
|
if (street.surface === "asphalt" && street.level !== "lane") {
|
|
656
713
|
if (street.level === "boulevard") {
|
|
@@ -664,9 +721,9 @@ function OneCity({ object, context }) {
|
|
|
664
721
|
if (runLength < 7)
|
|
665
722
|
continue;
|
|
666
723
|
const width = Math.max(1.3, street.width * 0.14);
|
|
667
|
-
medians.addRibbon(run, width, deckSampler,
|
|
668
|
-
medians.addDisc(run[0], width / 2, deckSampler,
|
|
669
|
-
medians.addDisc(run[run.length - 1], width / 2, deckSampler,
|
|
724
|
+
medians.addRibbon(run, width, deckSampler, MEDIAN_LAYER);
|
|
725
|
+
medians.addDisc(run[0], width / 2, deckSampler, MEDIAN_LAYER, null, 8);
|
|
726
|
+
medians.addDisc(run[run.length - 1], width / 2, deckSampler, MEDIAN_LAYER, null, 8);
|
|
670
727
|
}
|
|
671
728
|
}
|
|
672
729
|
else {
|
|
@@ -681,40 +738,50 @@ function OneCity({ object, context }) {
|
|
|
681
738
|
}
|
|
682
739
|
}
|
|
683
740
|
if (!inCross)
|
|
684
|
-
markings.addRibbon(dash, 0.18, deckSampler,
|
|
741
|
+
markings.addRibbon(dash, 0.18, deckSampler, MARKING_LAYER);
|
|
685
742
|
}
|
|
686
743
|
}
|
|
687
744
|
}
|
|
688
745
|
}
|
|
746
|
+
// NOTE: streetGenerator's `Street.sidewalks` offset polylines are NOT surfaced here — this renderer
|
|
747
|
+
// consumes `resolveCityObject`'s `CityStreet` (which carries only a `sidewalk` boolean), and its
|
|
748
|
+
// sidewalks come from richer block curb→land bands below. Wiring the raw offset polylines would be a
|
|
749
|
+
// second, redundant sidewalk source, so it is intentionally skipped for this component's data flow.
|
|
689
750
|
// Sidewalk band per block: the region between the curb ring (outer) and the land ring (inner).
|
|
690
751
|
// Blocks whose two rings coincide (no sidewalk) drop out — every band triangle is degenerate,
|
|
691
752
|
// so `addRingBand` emits nothing.
|
|
692
753
|
if (useBlockSidewalks) {
|
|
693
754
|
for (const block of resolved.blocks) {
|
|
694
|
-
sidewalks.addRingBand(block.curb, block.polygon, sample,
|
|
755
|
+
sidewalks.addRingBand(block.curb, block.polygon, sample, SIDEWALK_LAYER);
|
|
695
756
|
}
|
|
696
757
|
}
|
|
697
758
|
// Junctions: corner curb-return apron (sidewalk ring peeking out under the asphalt patch), the
|
|
698
759
|
// patch itself, and a zebra crossing on every REAL arm — but only where the crossing warrants
|
|
699
760
|
// it. Avenue-and-up arms (or a boulevard/avenue junction) get zebra stripes and the apron;
|
|
700
761
|
// small residential crossings get just the asphalt patch.
|
|
701
|
-
|
|
762
|
+
resolved.intersections.forEach((cross, crossIndex) => {
|
|
702
763
|
const bigLevel = cross.level === "boulevard" || cross.level === "avenue";
|
|
703
764
|
const armZebra = (width) => bigLevel || width >= resolved.rules.streetWidth * 1.4;
|
|
704
765
|
const paved = resolved.rules.sidewalks && cross.level !== "lane";
|
|
705
|
-
//
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
766
|
+
// A slightly larger apron underneath the crossing reads as the wrapped sidewalk/curb.
|
|
767
|
+
if (paved) {
|
|
768
|
+
const apron = junctionOutline(cross, 1.15, resolved.rules.sidewalkWidth + 0.4);
|
|
769
|
+
if (apron !== null)
|
|
770
|
+
sidewalks.addPolygon(apron, sample, SIDEWALK_LAYER);
|
|
771
|
+
}
|
|
772
|
+
// The junction pavement is welded onto the trimmed ribbon ends (seam-shared, no floating disc).
|
|
773
|
+
// Crossings that got no trimmed approach fall back to the rounded outline / disc, so coverage
|
|
774
|
+
// never regresses.
|
|
775
|
+
const approaches = approachesByCross.get(crossIndex);
|
|
776
|
+
if (approaches !== undefined && approaches.length > 0) {
|
|
777
|
+
patches.addMesh(buildJunctionSurface({ x: cross.x, z: cross.z }, approaches, deckSampler, JUNCTION_OPTS));
|
|
715
778
|
}
|
|
716
779
|
else {
|
|
717
|
-
|
|
780
|
+
const outline = junctionOutline(cross);
|
|
781
|
+
if (outline !== null)
|
|
782
|
+
patches.addPolygon(outline, deckSampler, ROAD_LAYER);
|
|
783
|
+
else
|
|
784
|
+
patches.addDisc([cross.x, cross.z], cross.radius, deckSampler, ROAD_LAYER);
|
|
718
785
|
}
|
|
719
786
|
if (paved) {
|
|
720
787
|
const maxHalf = Math.max(...cross.arms.map((arm) => arm.width / 2));
|
|
@@ -733,12 +800,12 @@ function OneCity({ object, context }) {
|
|
|
733
800
|
}
|
|
734
801
|
}
|
|
735
802
|
}
|
|
736
|
-
}
|
|
803
|
+
});
|
|
737
804
|
for (const drive of resolved.driveways) {
|
|
738
|
-
(drive.surface === "gravel" ? gravelDrives : pavementDrives).addRibbon(drive.points, drive.width, sample,
|
|
805
|
+
(drive.surface === "gravel" ? gravelDrives : pavementDrives).addRibbon(drive.points, drive.width, sample, DRIVEWAY_LAYER);
|
|
739
806
|
}
|
|
740
807
|
for (const pad of resolved.parkingLots) {
|
|
741
|
-
pavementDrives.addPatch(pad.center, pad.size, pad.rotationY, sample,
|
|
808
|
+
pavementDrives.addPatch(pad.center, pad.size, pad.rotationY, sample, PARKING_LAYER);
|
|
742
809
|
// Stall separators along the pad.
|
|
743
810
|
const cos = Math.cos(pad.rotationY);
|
|
744
811
|
const sin = Math.sin(pad.rotationY);
|
|
@@ -753,7 +820,7 @@ function OneCity({ object, context }) {
|
|
|
753
820
|
for (const park of resolved.parks) {
|
|
754
821
|
parkColor.set(PARK_COLORS[park.type]);
|
|
755
822
|
parkColor.multiplyScalar(0.92 + park.jitter * 0.16);
|
|
756
|
-
const elevation = park.type === "plaza" ?
|
|
823
|
+
const elevation = park.type === "plaza" ? PLAZA_LAYER : PARK_LAYER;
|
|
757
824
|
// Block-pipeline parks carry a real road-derived polygon; the rect is only a coarse proxy.
|
|
758
825
|
if (park.polygon !== undefined)
|
|
759
826
|
parks.addPolygon(park.polygon, sample, elevation, parkColor);
|
|
@@ -763,7 +830,7 @@ function OneCity({ object, context }) {
|
|
|
763
830
|
const crop = new THREE.Color(CROP_COLORS[Math.floor(park.jitter * CROP_COLORS.length) % CROP_COLORS.length]);
|
|
764
831
|
crop.multiplyScalar(0.9 + park.jitter * 0.2);
|
|
765
832
|
for (const row of park.rows)
|
|
766
|
-
crops.addRibbon(row, 1.05, sample,
|
|
833
|
+
crops.addRibbon(row, 1.05, sample, CROP_LAYER, crop);
|
|
767
834
|
}
|
|
768
835
|
}
|
|
769
836
|
return {
|
|
@@ -817,7 +884,7 @@ function OneCity({ object, context }) {
|
|
|
817
884
|
const arch = bridge.style === "arch" ? Math.sin(t * Math.PI) * Math.min(1.6, span * 0.03) : 0.25;
|
|
818
885
|
return hA + (hB - hA) * t + arch;
|
|
819
886
|
};
|
|
820
|
-
decks.addRibbon(bridge.points, bridge.width + 1.2, deckY,
|
|
887
|
+
decks.addRibbon(bridge.points, bridge.width + 1.2, deckY, BRIDGE_DECK_LIFT);
|
|
821
888
|
// Curb strips give the deck visible thickness from the side.
|
|
822
889
|
const tangent = [abx / span, abz / span];
|
|
823
890
|
const normal = [-tangent[1], tangent[0]];
|
|
@@ -1005,7 +1072,7 @@ function OneCity({ object, context }) {
|
|
|
1005
1072
|
}, [furniture]);
|
|
1006
1073
|
if (resolved === null)
|
|
1007
1074
|
return null;
|
|
1008
|
-
return (_jsxs("group", { children: [ground?.asphalt != null ? (_jsx("mesh", { geometry: ground.asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravel != null ? (_jsx("mesh", { geometry: ground.gravel, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.sidewalks != null ? (_jsx("mesh", { geometry: ground.sidewalks, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: SIDEWALK_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.patches != null ? (_jsx("mesh", { geometry: ground.patches, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.curbs != null ? (_jsx("mesh", { geometry: ground.curbs, receiveShadow: true, castShadow: true, children: _jsx("meshStandardMaterial", { color: CURB_COLOR, roughness: 0.9, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.markings != null ? (_jsx("mesh", { geometry: ground.markings, children: _jsx("meshStandardMaterial", { color: MARKING_COLOR, roughness: 0.8, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.medians != null ? (_jsx("mesh", { geometry: ground.medians, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: MEDIAN_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.pavementDrives != null ? (_jsx("mesh", { geometry: ground.pavementDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: PAVEMENT_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravelDrives != null ? (_jsx("mesh", { geometry: ground.gravelDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.parks != null ? (_jsx("mesh", { geometry: ground.parks, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.crops != null ? (_jsx("mesh", { geometry: ground.crops, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, bridges?.deck != null ? (_jsx("mesh", { geometry: bridges.deck, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: CONCRETE_COLOR, roughness: 0.85, metalness: 0.05, side: THREE.DoubleSide }) })) : null, bridges !== null && bridges.concrete.length > 0 ? (_jsx(InstancedBoxes, { matrices: bridges.concrete, color: CONCRETE_COLOR })) : null, bridges !== null && bridges.steel.length > 0 ? _jsx(InstancedBoxes, { matrices: bridges.steel, color: STEEL_COLOR }) : null, tunnels?.roof != null ? (_jsx("mesh", { geometry: tunnels.roof, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: TUNNEL_COLOR, roughness: 0.95, metalness: 0, side: THREE.DoubleSide }) })) : null, tunnels !== null && tunnels.stone.length > 0 ? _jsx(InstancedBoxes, { matrices: tunnels.stone, color: TUNNEL_COLOR }) : null, massing !== null ? massing.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `massing:${i}`)) : null, furniture !== null ? furniture.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `furniture:${i}`)) : null, _jsx(DetailBuildings, { buildings: detail.buildings, palette: palette })] }));
|
|
1075
|
+
return (_jsxs("group", { children: [ground?.asphalt != null ? (_jsx("mesh", { geometry: ground.asphalt, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravel != null ? (_jsx("mesh", { geometry: ground.gravel, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.sidewalks != null ? (_jsx("mesh", { geometry: ground.sidewalks, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: SIDEWALK_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.patches != null ? (_jsx("mesh", { geometry: ground.patches, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: ASPHALT_COLOR, roughness: 0.96, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.curbs != null ? (_jsx("mesh", { geometry: ground.curbs, receiveShadow: true, castShadow: true, children: _jsx("meshStandardMaterial", { color: CURB_COLOR, roughness: 0.9, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.markings != null ? (_jsx("mesh", { geometry: ground.markings, renderOrder: 1, children: _jsx("meshStandardMaterial", { color: MARKING_COLOR, roughness: 0.8, metalness: 0, side: THREE.DoubleSide, polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1 }) })) : null, ground?.medians != null ? (_jsx("mesh", { geometry: ground.medians, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: MEDIAN_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.pavementDrives != null ? (_jsx("mesh", { geometry: ground.pavementDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: PAVEMENT_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.gravelDrives != null ? (_jsx("mesh", { geometry: ground.gravelDrives, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: GRAVEL_COLOR, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.parks != null ? (_jsx("mesh", { geometry: ground.parks, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, ground?.crops != null ? (_jsx("mesh", { geometry: ground.crops, receiveShadow: true, children: _jsx("meshStandardMaterial", { vertexColors: true, roughness: 1, metalness: 0, side: THREE.DoubleSide }) })) : null, bridges?.deck != null ? (_jsx("mesh", { geometry: bridges.deck, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: CONCRETE_COLOR, roughness: 0.85, metalness: 0.05, side: THREE.DoubleSide }) })) : null, bridges !== null && bridges.concrete.length > 0 ? (_jsx(InstancedBoxes, { matrices: bridges.concrete, color: CONCRETE_COLOR })) : null, bridges !== null && bridges.steel.length > 0 ? _jsx(InstancedBoxes, { matrices: bridges.steel, color: STEEL_COLOR }) : null, tunnels?.roof != null ? (_jsx("mesh", { geometry: tunnels.roof, castShadow: true, receiveShadow: true, children: _jsx("meshStandardMaterial", { color: TUNNEL_COLOR, roughness: 0.95, metalness: 0, side: THREE.DoubleSide }) })) : null, tunnels !== null && tunnels.stone.length > 0 ? _jsx(InstancedBoxes, { matrices: tunnels.stone, color: TUNNEL_COLOR }) : null, massing !== null ? massing.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `massing:${i}`)) : null, furniture !== null ? furniture.meshes.map((mesh, i) => _jsx("primitive", { object: mesh }, `furniture:${i}`)) : null, _jsx(DetailBuildings, { buildings: detail.buildings, palette: palette })] }));
|
|
1009
1076
|
}
|
|
1010
1077
|
/** One instanced unit-box mesh from precomposed matrices (bridge parts). */
|
|
1011
1078
|
function InstancedBoxes({ matrices, color }) {
|
|
@@ -63,7 +63,10 @@ function OneGrass({ object, context }) {
|
|
|
63
63
|
return null;
|
|
64
64
|
const { rules, center } = resolved;
|
|
65
65
|
// Size the instance buffer to the authored patch (bounded): the default 1500-blade buffer reads
|
|
66
|
-
// as stubble on anything bigger than a lawn, silently ignoring the authored density.
|
|
66
|
+
// as stubble on anything bigger than a lawn, silently ignoring the authored density. The 250k cap
|
|
67
|
+
// is independent of the (now wider) camera distance fade — instances are placed across the patch
|
|
68
|
+
// footprint, and the fade only thins how many *draw* at range in the vertex shader — so a deeper
|
|
69
|
+
// meadow reads expansive without raising the instance ceiling.
|
|
67
70
|
const instanceCount = Math.max(1500, Math.min(250000, Math.ceil(rules.density * area[0] * area[1])));
|
|
68
71
|
return (_jsx(GrassField, { position: [center[0], center[1], center[2]], area: area, count: instanceCount, density: rules.density, bladeHeight: bladeHeight, colorBase: rules.colorBase, colorTip: rules.colorTip, colorGround: context.groundColorAt?.(center[0], center[2]), colorVariation: rules.colorVariation, roughness: rules.roughness, seed: hashSeed(rules.seed.length > 0 ? rules.seed : object.id), heightAt: heightAt, wind: wind, edgeFeather: 2.2, exclude: exclude }));
|
|
69
72
|
}
|
|
@@ -13,6 +13,7 @@ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
|
13
13
|
import { parseParams } from "@jgengine/core/scene/sceneKinds";
|
|
14
14
|
import { readNamedSockets } from "@jgengine/core/scene/modelSockets";
|
|
15
15
|
import { POLE_LINE_KIND, POLE_LINE_SCHEMA, resolvePoleLine } from "@jgengine/core/world/poleLineKind";
|
|
16
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
16
17
|
import { registerSceneKindRenderer } from "./sceneKindRenderers.js";
|
|
17
18
|
function Cables({ cables }) {
|
|
18
19
|
const geometry = useMemo(() => {
|
|
@@ -139,25 +140,15 @@ function ProxyPoles({ poles, height, wireCount, wireSpacing, }) {
|
|
|
139
140
|
const insulatorRef = useRef(null);
|
|
140
141
|
const armLength = wireCount > 1 ? (wireCount - 1) * wireSpacing + 0.6 : 0.8;
|
|
141
142
|
// Chunkier members than a real-scale pole: stylized worlds read thin cylinders as wire, not wood.
|
|
142
|
-
const trunkGeometry =
|
|
143
|
-
const armGeometry =
|
|
144
|
-
const braceGeometry =
|
|
143
|
+
const trunkGeometry = useDisposable(() => new THREE.CylinderGeometry(0.13, 0.22, height, 10), [height]);
|
|
144
|
+
const armGeometry = useDisposable(() => new THREE.BoxGeometry(armLength, 0.17, 0.14), [armLength]);
|
|
145
|
+
const braceGeometry = useDisposable(() => {
|
|
145
146
|
const braceSpan = Math.hypot(armLength * 0.32, 0.7);
|
|
146
147
|
return new THREE.BoxGeometry(0.07, braceSpan, 0.06);
|
|
147
148
|
}, [armLength]);
|
|
148
|
-
const insulatorGeometry =
|
|
149
|
-
const trunkMaterial =
|
|
150
|
-
const armMaterial =
|
|
151
|
-
useEffect(() => () => {
|
|
152
|
-
trunkGeometry.dispose();
|
|
153
|
-
armGeometry.dispose();
|
|
154
|
-
braceGeometry.dispose();
|
|
155
|
-
insulatorGeometry.dispose();
|
|
156
|
-
}, [trunkGeometry, armGeometry, braceGeometry, insulatorGeometry]);
|
|
157
|
-
useEffect(() => () => {
|
|
158
|
-
trunkMaterial.dispose();
|
|
159
|
-
armMaterial.dispose();
|
|
160
|
-
}, [trunkMaterial, armMaterial]);
|
|
149
|
+
const insulatorGeometry = useDisposable(() => new THREE.CylinderGeometry(0.06, 0.085, 0.24, 8), []);
|
|
150
|
+
const trunkMaterial = useDisposable(() => createWeatheredWoodMaterial("#6b533a"), []);
|
|
151
|
+
const armMaterial = useDisposable(() => createWeatheredWoodMaterial("#59452e", "x"), []);
|
|
161
152
|
useEffect(() => {
|
|
162
153
|
const trunk = trunkRef.current;
|
|
163
154
|
const arm = armRef.current;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { useDebouncedCommit } from "@jgengine/react/useDebouncedCommit";
|
|
3
4
|
function QuickSlider({ row }) {
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
5
|
+
// Live-mirror the thumb; debounce the persisting `set`, flush on release/blur (#1372).
|
|
6
|
+
const { value: local, onInput, flush } = useDebouncedCommit(Number(row.value), (next) => row.set(next));
|
|
7
|
+
const readout = row.format?.(local) ?? String(local);
|
|
8
|
+
return (_jsxs("label", { className: "flex items-center gap-2 text-xs text-neutral-300", children: [_jsx("span", { className: "w-16 shrink-0 truncate", children: row.label }), _jsx("input", { type: "range", min: row.min, max: row.max, step: row.step, value: local, "aria-label": row.label, className: "h-1.5 flex-1 cursor-pointer accent-emerald-400", onChange: (event) => onInput(Number(event.target.value)), onPointerUp: flush, onKeyUp: flush, onBlur: flush }), _jsx("span", { className: "w-9 shrink-0 text-right tabular-nums text-neutral-100", children: readout })] }));
|
|
7
9
|
}
|
|
8
10
|
function QuickToggle({ row }) {
|
|
9
11
|
const on = Boolean(row.value);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useState } from "react";
|
|
3
|
+
import { useDebouncedCommit } from "@jgengine/react/useDebouncedCommit";
|
|
3
4
|
import {} from "./settingsController.js";
|
|
4
5
|
const surface = "var(--jg-surface, #16161d)";
|
|
5
6
|
const surfaceDeep = "var(--jg-surface-deep, #0b0b11)";
|
|
@@ -26,9 +27,11 @@ function RowShell({ label, control }) {
|
|
|
26
27
|
return (_jsxs("div", { className: "flex items-center justify-between gap-4 rounded-xl px-3.5 py-3", style: rowStyle, children: [_jsx("span", { className: "text-sm", style: { color: text }, children: label }), control] }));
|
|
27
28
|
}
|
|
28
29
|
function SliderRow({ row }) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Mirror the thumb + readout locally; debounce the persisting `set` so a drag doesn't storm the
|
|
31
|
+
// settings store (and any graphics rebuild it drives), flushing on release/blur (#1372).
|
|
32
|
+
const { value: local, onInput, flush } = useDebouncedCommit(Number(row.value), (next) => row.set(next));
|
|
33
|
+
const readout = row.format?.(local) ?? String(local);
|
|
34
|
+
return (_jsx(RowShell, { label: row.label, control: _jsxs("div", { className: "flex flex-1 items-center gap-3 pl-4", children: [_jsx("input", { type: "range", min: row.min, max: row.max, step: row.step, value: local, "aria-label": row.label, className: "h-1.5 flex-1 cursor-pointer", style: { accentColor: accent }, onChange: (event) => onInput(Number(event.target.value)), onPointerUp: flush, onKeyUp: flush, onBlur: flush }), _jsx("span", { className: "w-12 shrink-0 text-right text-sm tabular-nums", style: { color: text }, children: readout })] }) }));
|
|
32
35
|
}
|
|
33
36
|
function ToggleRow({ row }) {
|
|
34
37
|
const on = Boolean(row.value);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { AudioBusDef } from "@jgengine/core/audio/audioFalloff";
|
|
2
2
|
import { type GraphicsQuality, type SettingsStore } from "@jgengine/core/settings/settingsModel";
|
|
3
|
-
import { type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
3
|
+
import { type TouchJoystickVariant, type TouchStyle } from "@jgengine/core/input/touchScheme";
|
|
4
4
|
import type { AudioEngine } from "../audio/audioEngine.js";
|
|
5
5
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
6
6
|
export declare const TOUCH_STYLE_AUTO = "auto";
|
|
7
7
|
/** Shell-internal: binds the SettingsStore touch-style choice to the touch renderer. @internal */
|
|
8
8
|
export declare function useTouchStyle(store: SettingsStore, fallback: TouchStyle): TouchStyle;
|
|
9
|
+
/** Shell-internal: binds the SettingsStore joystick-variant choice (fixed vs floating) to the touch renderer. @internal */
|
|
10
|
+
export declare function useTouchJoystickVariant(store: SettingsStore): TouchJoystickVariant;
|
|
9
11
|
export declare function useSettingsRevision(store: SettingsStore): number;
|
|
10
12
|
export declare function useGraphicsSettings(store: SettingsStore, shadowsDefault: boolean): {
|
|
11
13
|
shadows: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
2
2
|
import { busVolumeSettingId, DEFAULT_GRAPHICS_QUALITY, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_DPR, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
|
-
import { TOUCH_STYLES } from "@jgengine/core/input/touchScheme";
|
|
3
|
+
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANTS, TOUCH_STYLES, } from "@jgengine/core/input/touchScheme";
|
|
4
4
|
/** Sentinel Controls value meaning "defer to the game's suggested touch skin". */
|
|
5
5
|
export const TOUCH_STYLE_AUTO = "auto";
|
|
6
6
|
/** Shell-internal: binds the SettingsStore touch-style choice to the touch renderer. @internal */
|
|
@@ -9,10 +9,22 @@ export function useTouchStyle(store, fallback) {
|
|
|
9
9
|
const raw = store.get(SETTING_IDS.touchStyle, TOUCH_STYLE_AUTO);
|
|
10
10
|
return TOUCH_STYLES.includes(raw) ? raw : fallback;
|
|
11
11
|
}
|
|
12
|
+
/** Shell-internal: binds the SettingsStore joystick-variant choice (fixed vs floating) to the touch renderer. @internal */
|
|
13
|
+
export function useTouchJoystickVariant(store) {
|
|
14
|
+
useSettingsRevision(store);
|
|
15
|
+
const raw = store.get(SETTING_IDS.touchJoystick, DEFAULT_TOUCH_JOYSTICK_VARIANT);
|
|
16
|
+
return TOUCH_JOYSTICK_VARIANTS.includes(raw)
|
|
17
|
+
? raw
|
|
18
|
+
: DEFAULT_TOUCH_JOYSTICK_VARIANT;
|
|
19
|
+
}
|
|
12
20
|
export function useSettingsRevision(store) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
const versionRef = useRef(0);
|
|
22
|
+
const subscribe = useCallback((onStoreChange) => store.subscribe(() => {
|
|
23
|
+
versionRef.current += 1;
|
|
24
|
+
onStoreChange();
|
|
25
|
+
}), [store]);
|
|
26
|
+
const getVersion = useCallback(() => versionRef.current, []);
|
|
27
|
+
return useSyncExternalStore(subscribe, getVersion, getVersion);
|
|
16
28
|
}
|
|
17
29
|
export function useGraphicsSettings(store, shadowsDefault) {
|
|
18
30
|
useSettingsRevision(store);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { useEffect, useReducer } from "react";
|
|
2
1
|
import { actionLabel, bindingLabel } from "@jgengine/core/input/actionBindings";
|
|
3
2
|
import { BUILT_IN_SETTING_CATEGORIES, busVolumeSettingId, DEFAULT_GRAPHICS_QUALITY, DEFAULT_GRAPHICS_SHADOWS, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, GRAPHICS_QUALITY_OPTIONS, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
4
|
-
import { TOUCH_STYLE_OPTIONS } from "@jgengine/core/input/touchScheme";
|
|
5
|
-
import { TOUCH_STYLE_AUTO } from "./appliedSettings.js";
|
|
3
|
+
import { DEFAULT_TOUCH_JOYSTICK_VARIANT, TOUCH_JOYSTICK_VARIANT_OPTIONS, TOUCH_STYLE_OPTIONS, } from "@jgengine/core/input/touchScheme";
|
|
4
|
+
import { TOUCH_STYLE_AUTO, useSettingsRevision } from "./appliedSettings.js";
|
|
6
5
|
import { useSettingsStore, } from "@jgengine/react/settings";
|
|
7
6
|
import { usePlayerFov } from "../camera/PlayerFov.js";
|
|
8
7
|
const DEFAULT_CATEGORY_LABELS = {
|
|
@@ -19,8 +18,7 @@ const TOUCH_STYLE_SELECT_OPTIONS = [
|
|
|
19
18
|
export function useSettingsCategories(config) {
|
|
20
19
|
const store = useSettingsStore();
|
|
21
20
|
const fov = usePlayerFov();
|
|
22
|
-
|
|
23
|
-
useEffect(() => store.subscribe(() => force()), [store]);
|
|
21
|
+
useSettingsRevision(store);
|
|
24
22
|
const hidden = new Set(config.hide);
|
|
25
23
|
const extrasFor = (category) => config.extra
|
|
26
24
|
.filter((def) => def.category === category)
|
|
@@ -136,6 +134,14 @@ export function useSettingsCategories(config) {
|
|
|
136
134
|
options: TOUCH_STYLE_SELECT_OPTIONS,
|
|
137
135
|
set: (value) => store.set(SETTING_IDS.touchStyle, value),
|
|
138
136
|
},
|
|
137
|
+
{
|
|
138
|
+
id: SETTING_IDS.touchJoystick,
|
|
139
|
+
label: "Joystick",
|
|
140
|
+
kind: "select",
|
|
141
|
+
value: store.get(SETTING_IDS.touchJoystick, DEFAULT_TOUCH_JOYSTICK_VARIANT),
|
|
142
|
+
options: TOUCH_JOYSTICK_VARIANT_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
|
|
143
|
+
set: (value) => store.set(SETTING_IDS.touchJoystick, value),
|
|
144
|
+
},
|
|
139
145
|
]
|
|
140
146
|
: []),
|
|
141
147
|
...extrasFor("controls"),
|
package/dist/shellMovement.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
/** Movement action names the shell drives via the walk controller.
|
|
1
|
+
/** Movement action names the shell drives via the walk controller. */
|
|
2
2
|
export declare const SHELL_MOVEMENT_ACTIONS: readonly ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
3
|
+
/**
|
|
4
|
+
* Default keyboard codes for the shell walk controller (WASD + Space).
|
|
5
|
+
* Use as the base of `defineGame({ input: { ...DEFAULT_WALK_CODES, interact: ["KeyE"] } })`
|
|
6
|
+
* instead of re-typing the same five bindings in every game.
|
|
7
|
+
*
|
|
8
|
+
* @capability default-walk-codes stock WASD + jump key codes for the shell walk controller
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_WALK_CODES: {
|
|
11
|
+
readonly moveForward: readonly ["KeyW"];
|
|
12
|
+
readonly moveBack: readonly ["KeyS"];
|
|
13
|
+
readonly moveLeft: readonly ["KeyA"];
|
|
14
|
+
readonly moveRight: readonly ["KeyD"];
|
|
15
|
+
readonly jump: readonly ["Space"];
|
|
16
|
+
};
|
|
3
17
|
/**
|
|
4
18
|
* True when the game's input map binds any walk-controller action — the shell owns pose
|
|
5
19
|
* for those games. Games that omit all five drive pose themselves.
|
package/dist/shellMovement.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
/** Movement action names the shell drives via the walk controller.
|
|
1
|
+
/** Movement action names the shell drives via the walk controller. */
|
|
2
2
|
export const SHELL_MOVEMENT_ACTIONS = ["moveForward", "moveBack", "moveLeft", "moveRight", "jump"];
|
|
3
|
+
/**
|
|
4
|
+
* Default keyboard codes for the shell walk controller (WASD + Space).
|
|
5
|
+
* Use as the base of `defineGame({ input: { ...DEFAULT_WALK_CODES, interact: ["KeyE"] } })`
|
|
6
|
+
* instead of re-typing the same five bindings in every game.
|
|
7
|
+
*
|
|
8
|
+
* @capability default-walk-codes stock WASD + jump key codes for the shell walk controller
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_WALK_CODES = {
|
|
11
|
+
moveForward: ["KeyW"],
|
|
12
|
+
moveBack: ["KeyS"],
|
|
13
|
+
moveLeft: ["KeyA"],
|
|
14
|
+
moveRight: ["KeyD"],
|
|
15
|
+
jump: ["Space"],
|
|
16
|
+
};
|
|
3
17
|
/**
|
|
4
18
|
* True when the game's input map binds any walk-controller action — the shell owns pose
|
|
5
19
|
* for those games. Games that omit all five drive pose themselves.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
3
3
|
import * as THREE from "three";
|
|
4
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
4
5
|
const DEFAULT_PALETTE = {
|
|
5
6
|
wall: "#83766a",
|
|
6
7
|
window: "#8ecae6",
|
|
@@ -116,8 +117,7 @@ function bucketPartMatrices(buildings, visible) {
|
|
|
116
117
|
}
|
|
117
118
|
function BuildingKindBatch({ kind, matrices, palette, geometry, }) {
|
|
118
119
|
const meshRef = useRef(null);
|
|
119
|
-
const material =
|
|
120
|
-
useEffect(() => () => material.dispose(), [material]);
|
|
120
|
+
const material = useDisposable(() => batchMaterialFor(kind, palette), [kind, palette]);
|
|
121
121
|
useLayoutEffect(() => {
|
|
122
122
|
const mesh = meshRef.current;
|
|
123
123
|
if (mesh === null)
|
|
@@ -129,8 +129,7 @@ function BuildingKindBatch({ kind, matrices, palette, geometry, }) {
|
|
|
129
129
|
return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, matrices.length], castShadow: true, receiveShadow: true }, matrices.length));
|
|
130
130
|
}
|
|
131
131
|
export function InstancedBuildings({ buildings, palette, visibleKinds }) {
|
|
132
|
-
const geometry =
|
|
133
|
-
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
132
|
+
const geometry = useDisposable(() => new THREE.BoxGeometry(1, 1, 1), []);
|
|
134
133
|
const buckets = useMemo(() => {
|
|
135
134
|
const visible = visibleKinds === undefined ? null : new Set(visibleKinds);
|
|
136
135
|
return bucketPartMatrices(buildings, visible);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { TransformControls } from "@react-three/drei";
|
|
3
3
|
import { useThree } from "@react-three/fiber";
|
|
4
|
-
import { memo, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
/** Default rotation snap increment (radians) when callers don't configure one. */
|
|
6
6
|
export const DEFAULT_ROTATION_SNAP = Math.PI / 12;
|
|
7
7
|
/**
|
|
@@ -15,9 +15,10 @@ export const TransformGizmo = memo(function TransformGizmo({ position, rotationY
|
|
|
15
15
|
const draggingRef = useRef(false);
|
|
16
16
|
const controls = useThree((state) => state.controls);
|
|
17
17
|
const anchorKey = `${position.x}:${position.y}:${position.z}:${rotationY}:${mode}`;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const anchorRef = useCallback((node) => {
|
|
19
|
+
groupRef.current = node;
|
|
20
|
+
setObject(node);
|
|
21
|
+
}, []);
|
|
21
22
|
useEffect(() => {
|
|
22
23
|
const group = groupRef.current;
|
|
23
24
|
if (group === null || draggingRef.current)
|
|
@@ -50,7 +51,7 @@ export const TransformGizmo = memo(function TransformGizmo({ position, rotationY
|
|
|
50
51
|
scale: { x: current.scale.x, y: current.scale.y, z: current.scale.z },
|
|
51
52
|
});
|
|
52
53
|
};
|
|
53
|
-
return (_jsxs(_Fragment, { children: [_jsx("group", { ref:
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsx("group", { ref: anchorRef }), object !== null ? (_jsx(TransformControls, { object: object, mode: mode, size: size, space: space, ...snapProps, onMouseDown: () => {
|
|
54
55
|
draggingRef.current = true;
|
|
55
56
|
if (controls !== null && "enabled" in controls)
|
|
56
57
|
controls.enabled = false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import {} from "@react-three/fiber";
|
|
3
|
-
import { useEffect, useMemo } from "react";
|
|
4
3
|
import * as THREE from "three";
|
|
4
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
5
5
|
import { createFieldGroundGeometry } from "./terrainMath.js";
|
|
6
6
|
/**
|
|
7
7
|
* Renders a `TerrainField` as a deformed ground mesh — the crater/mound view for destructible terrain.
|
|
@@ -9,12 +9,8 @@ import { createFieldGroundGeometry } from "./terrainMath.js";
|
|
|
9
9
|
* once `epoch` changes. Pair with `InstancedBodies` to see debris resting in the crater it blasted.
|
|
10
10
|
*/
|
|
11
11
|
export function CarvedTerrain({ field, size, segments, center, colors, heightRange, paletteAt, roughness = 0.95, metalness = 0, surfaceMaterial, receiveShadow = true, epoch = 0, ...meshProps }) {
|
|
12
|
-
const geometry =
|
|
13
|
-
|
|
14
|
-
[field, size, segments, center, colors, heightRange, paletteAt, epoch]);
|
|
15
|
-
const defaultMaterial = useMemo(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
|
|
12
|
+
const geometry = useDisposable(() => createFieldGroundGeometry(field, { size, segments, center, colors, heightRange, paletteAt }), [field, size, segments, center, colors, heightRange, paletteAt, epoch]);
|
|
13
|
+
const defaultMaterial = useDisposable(() => new THREE.MeshStandardMaterial({ color: "#ffffff", roughness, metalness, vertexColors: true }), [metalness, roughness]);
|
|
16
14
|
const material = surfaceMaterial ?? defaultMaterial;
|
|
17
|
-
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
18
|
-
useEffect(() => () => defaultMaterial.dispose(), [defaultMaterial]);
|
|
19
15
|
return _jsx("mesh", { ...meshProps, geometry: geometry, material: material, receiveShadow: receiveShadow });
|
|
20
16
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useFrame } from "@react-three/fiber";
|
|
3
3
|
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { useDisposable } from "../render/useDisposable.js";
|
|
4
5
|
import { createGrassBladeGeometry, grassTuftCount, GRASS_TUFT_BLADES, } from "./grassGeometry.js";
|
|
5
6
|
import { createGrassMaterial, } from "./grassMaterial.js";
|
|
6
7
|
import { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget, } from "./grassBudget.js";
|
|
7
8
|
export { DEFAULT_GRASS_COUNT, DEFAULT_GRASS_DENSITY, resolveGrassInstanceBudget } from "./grassBudget.js";
|
|
8
9
|
export { GRASS_TUFT_BLADES } from "./grassGeometry.js";
|
|
9
10
|
export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRASS_DENSITY, budget, area = 40, seed = 1, segments = 4, bladeHeight, bladeWidth, bladeBend, tuftBlades = GRASS_TUFT_BLADES, tuftRadius, edgeFeather, exclude, heightAt, colorBase, colorTip, colorGround, colorVariation, wind, distanceFade, normalLift, roughness, castShadow = false, receiveShadow = true, frustumCulled = true, ...meshProps }) {
|
|
10
|
-
const geometry =
|
|
11
|
+
const geometry = useDisposable(() => createGrassBladeGeometry({
|
|
11
12
|
count,
|
|
12
13
|
area,
|
|
13
14
|
seed,
|
|
@@ -37,7 +38,6 @@ export function GrassField({ count = DEFAULT_GRASS_COUNT, density = DEFAULT_GRAS
|
|
|
37
38
|
useFrame((state) => {
|
|
38
39
|
handle.uniforms.uTime.value = state.clock.elapsedTime;
|
|
39
40
|
});
|
|
40
|
-
useEffect(() => () => geometry.dispose(), [geometry]);
|
|
41
41
|
useEffect(() => () => handle.material.dispose(), [handle]);
|
|
42
42
|
return (_jsx("mesh", { ...meshProps, geometry: geometry, material: handle.material, castShadow: castShadow, receiveShadow: receiveShadow, frustumCulled: frustumCulled }));
|
|
43
43
|
}
|