@aura3d/engine 1.4.0 → 1.4.1

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.
Files changed (33) hide show
  1. package/README.md +30 -14
  2. package/dist/aura3d-cli/asset-core-types.d.ts +18 -2
  3. package/dist/aura3d-cli/asset-core-types.d.ts.map +1 -1
  4. package/dist/create-aura3d/index.js +1 -1
  5. package/dist/create-aura3d/showcase-spec-artifacts.d.ts +1 -0
  6. package/dist/create-aura3d/showcase-spec-artifacts.d.ts.map +1 -1
  7. package/dist/create-aura3d/showcase-spec-artifacts.js +1 -0
  8. package/dist/create-aura3d/showcase-spec-artifacts.js.map +1 -1
  9. package/dist/create-aura3d/showcase-spec-evidence.d.ts.map +1 -1
  10. package/dist/create-aura3d/showcase-spec-evidence.js +66 -0
  11. package/dist/create-aura3d/showcase-spec-evidence.js.map +1 -1
  12. package/dist/create-aura3d/showcase-spec-parser.d.ts.map +1 -1
  13. package/dist/create-aura3d/showcase-spec-parser.js +28 -0
  14. package/dist/create-aura3d/showcase-spec-parser.js.map +1 -1
  15. package/dist/create-aura3d/showcase-spec-platformer-artifacts.d.ts.map +1 -1
  16. package/dist/create-aura3d/showcase-spec-platformer-artifacts.js +2 -4
  17. package/dist/create-aura3d/showcase-spec-platformer-artifacts.js.map +1 -1
  18. package/dist/create-aura3d/showcase-spec-types.d.ts +15 -0
  19. package/dist/create-aura3d/showcase-spec-types.d.ts.map +1 -1
  20. package/dist/engine/agent-api/GameSceneGeometryBindings.d.ts +1 -0
  21. package/dist/engine/agent-api/GameSceneGeometryBindings.d.ts.map +1 -1
  22. package/dist/engine/agent-api/GameSceneGeometryBindings.js +1 -1
  23. package/dist/engine/agent-api/GameSceneGeometryBindings.js.map +1 -1
  24. package/dist/engine/agent-api/PublicGameGeometry.d.ts +117 -0
  25. package/dist/engine/agent-api/PublicGameGeometry.d.ts.map +1 -0
  26. package/dist/engine/agent-api/PublicGameGeometry.js +118 -0
  27. package/dist/engine/agent-api/PublicGameGeometry.js.map +1 -0
  28. package/dist/engine/agent-api/index.d.ts +119 -2
  29. package/dist/engine/agent-api/index.d.ts.map +1 -1
  30. package/dist/engine/agent-api/index.js +608 -56
  31. package/dist/engine/agent-api/index.js.map +1 -1
  32. package/dist/engine/agent-api/product-viewer-runtime.d.ts.map +1 -1
  33. package/package.json +29 -31
@@ -54,8 +54,10 @@ import { createRuntimeNodeSpec } from "./GameSceneBridge.js";
54
54
  import { createFightingGameKit, fighting as fightingGameKit } from "./game-kits/fighting.js";
55
55
  import { GAME_FALLING_BLOCK_PIECES, createGameAssetBoundPlatformerLevel, createGameAssetBoundRacingRoute, createGameFallingBlocksKit, createGameLocomotionKit, createGamePlatformerKit, createGameRacingKit } from "./GameGenreKits.js";
56
56
  import { createGamePlatformerSceneBinding, createGamePlatformerPresentationCamera, createGameRacingPresentationCamera, createGameRacingSceneBinding } from "./GameSceneGeometryBindings.js";
57
+ import { certifyPublicPlatformerGeometry, certifyPublicRacingGeometry } from "./PublicGameGeometry.js";
57
58
  export { GAME_FALLING_BLOCK_PIECES, createGameAssetBoundPlatformerLevel, createGameAssetBoundRacingRoute, createGameFallingBlocksKit, createGameLocomotionKit, createGamePlatformerKit, createGameRacingKit } from "./GameGenreKits.js";
58
59
  export { createGamePlatformerSceneBinding, createGamePlatformerPresentationCamera, createGameRacingPresentationCamera, createGameRacingSceneBinding } from "./GameSceneGeometryBindings.js";
60
+ export { certifyPublicPlatformerGeometry, certifyPublicRacingGeometry } from "./PublicGameGeometry.js";
59
61
  import { createPromptAnimationEpisodePlan, createPromptAnimationStoryBible, definePromptAnimationStoryboard } from "./PromptAnimationContract.js";
60
62
  import { applyShotPlaybackFrame, createShotPlaybackPlan, createShotTimeline, installShotPlayback, sampleShotPlaybackPlan } from "./ShotTimeline.js";
61
63
  import { captionCueAtTime, createCaptionTimingProof, deriveCaptionTrackFromDialogue } from "./DialoguePerformance.js";
@@ -3626,19 +3628,19 @@ export function createAuraGameRuntime(options = {}) {
3626
3628
  bodies: []
3627
3629
  };
3628
3630
  }
3629
- export function createGameRacingPresentationTrackNodes(options) {
3631
+ export function createGameRacingRoadMeshNodes(options) {
3630
3632
  const mode = options.mode ?? "standalone";
3631
- const routePoints = options.route.points.map((point) => options.sceneBinding.toScenePoint(point, options.roadY ?? 0.012));
3633
+ const routePoints = racingPresentationRoutePoints(options.sceneBinding, options.route, options.roadY);
3632
3634
  if (routePoints.length < 2)
3633
3635
  return [];
3634
- const guideVisibility = options.guideVisibility ?? (mode === "asset-overlay" ? "evidence" : "full");
3635
- if (mode === "asset-overlay" && guideVisibility === "public")
3636
- return [];
3637
3636
  const isCircuitStage = mode === "game-circuit";
3638
- const roadWidth = Math.max((options.route.width ?? 0.18) * options.sceneBinding.transform.scale * (isCircuitStage ? 3.05 : 1.5), isCircuitStage ? 0.42 : 0.18);
3637
+ const roadWidth = racingPresentationRoadWidth(options.sceneBinding, options.route, mode);
3638
+ const markingVisibility = options.markingVisibility ?? "full";
3639
3639
  const nodes = [];
3640
- if (mode === "standalone" || mode === "game-circuit") {
3641
- const bounds = boundsForPresentationPoints(routePoints, roadWidth * (isCircuitStage ? 4.5 : 3.2));
3640
+ const includeTerrain = options.includeTerrain ?? (mode === "standalone" || isCircuitStage);
3641
+ if (includeTerrain && mode !== "asset-overlay") {
3642
+ const terrainPaddingScale = options.terrainPaddingScale ?? (isCircuitStage ? 4.5 : 3.2);
3643
+ const bounds = boundsForPresentationPoints(routePoints, roadWidth * terrainPaddingScale);
3642
3644
  nodes.push(primitives.box({
3643
3645
  name: "scene-bound racing terrain pad",
3644
3646
  material: material.pbr({ color: options.terrainColor ?? (isCircuitStage ? "#1f342e" : "#263b35"), roughness: 0.92, metallic: 0 })
@@ -3649,7 +3651,7 @@ export function createGameRacingPresentationTrackNodes(options) {
3649
3651
  if (isCircuitStage) {
3650
3652
  nodes.push(primitives.box({
3651
3653
  name: "scene-bound racing pit lane apron",
3652
- material: material.pbr({ color: "#27323a", roughness: 0.86, metallic: 0 })
3654
+ material: material.pbr({ color: options.pitLaneColor ?? "#121c1e", roughness: 0.86, metallic: 0 })
3653
3655
  })
3654
3656
  .position(roundGamePresentation(bounds.centerX - bounds.width * 0.12), 0.002, roundGamePresentation(bounds.centerZ + bounds.depth * 0.23))
3655
3657
  .rotate(0, -0.16, 0)
@@ -3686,69 +3688,484 @@ export function createGameRacingPresentationTrackNodes(options) {
3686
3688
  const y = segment.midY;
3687
3689
  nodes.push(primitives.box({
3688
3690
  name: `scene-bound racing road segment ${index + 1}`,
3689
- material: material.pbr({ color: options.roadColor ?? "#343b3f", roughness: 0.76, metallic: 0 })
3691
+ material: material.pbr({ color: options.roadColor ?? (isCircuitStage ? "#11191d" : "#343b3f"), roughness: 0.82, metallic: 0 })
3690
3692
  })
3691
3693
  .position(segment.midX, y, segment.midZ)
3692
3694
  .rotate(0, -segment.angle, 0)
3693
- .scale([segment.length + roadWidth * 0.6, 0.03, roadWidth])
3694
- .toJSON(), primitives.box({
3695
- name: `scene-bound racing center stripe ${index + 1}`,
3695
+ .scale([segment.length + roadWidth * 0.44, 0.028, roadWidth])
3696
+ .toJSON());
3697
+ if (markingVisibility === "none")
3698
+ continue;
3699
+ const isSubtleMark = markingVisibility === "subtle";
3700
+ if (!isSubtleMark || index % 3 === 0) {
3701
+ nodes.push(primitives.box({
3702
+ name: `scene-bound racing center stripe ${index + 1}`,
3703
+ material: material.emissive({
3704
+ color: options.laneColor ?? "#d8f6ff",
3705
+ emissive: options.laneColor ?? "#8df4ff",
3706
+ emissiveIntensity: isSubtleMark ? 0.34 : 0.7,
3707
+ roughness: 0.4
3708
+ })
3709
+ })
3710
+ .position(segment.midX, y + 0.022, segment.midZ)
3711
+ .rotate(0, -segment.angle, 0)
3712
+ .scale([Math.max(0.08, segment.length * (isCircuitStage ? 0.28 : 0.72)), 0.009, 0.01])
3713
+ .toJSON());
3714
+ }
3715
+ if (!isSubtleMark || index % 4 === 0) {
3716
+ nodes.push(primitives.box({
3717
+ name: `scene-bound racing left curb ${index + 1}`,
3718
+ material: material.pbr({ color: index % 2 === 0 ? "#d4ded8" : options.curbColor ?? "#df3550", roughness: 0.66 })
3719
+ })
3720
+ .position(roundGamePresentation(segment.midX + sideX * roadWidth * 0.54), y + 0.024, roundGamePresentation(segment.midZ + sideZ * roadWidth * 0.54))
3721
+ .rotate(0, -segment.angle, 0)
3722
+ .scale([Math.max(0.08, segment.length * 0.38), 0.012, 0.016])
3723
+ .toJSON(), primitives.box({
3724
+ name: `scene-bound racing right curb ${index + 1}`,
3725
+ material: material.pbr({ color: index % 2 === 0 ? options.curbColor ?? "#df3550" : "#d4ded8", roughness: 0.66 })
3726
+ })
3727
+ .position(roundGamePresentation(segment.midX - sideX * roadWidth * 0.54), y + 0.024, roundGamePresentation(segment.midZ - sideZ * roadWidth * 0.54))
3728
+ .rotate(0, -segment.angle, 0)
3729
+ .scale([Math.max(0.08, segment.length * 0.38), 0.012, 0.016])
3730
+ .toJSON());
3731
+ }
3732
+ if (isCircuitStage && (!isSubtleMark || index % 2 === 0)) {
3733
+ const railLength = Math.max(0.1, segment.length * (isSubtleMark ? 0.5 : 0.64));
3734
+ nodes.push(primitives.box({
3735
+ name: `scene-bound racing left guardrail ${index + 1}`,
3736
+ material: material.pbr({ color: "#141d20", roughness: 0.74, metallic: 0 })
3737
+ })
3738
+ .position(roundGamePresentation(segment.midX + sideX * roadWidth * 0.62), y + 0.034, roundGamePresentation(segment.midZ + sideZ * roadWidth * 0.62))
3739
+ .rotate(0, -segment.angle, 0)
3740
+ .scale([railLength, 0.018, 0.014])
3741
+ .toJSON(), primitives.box({
3742
+ name: `scene-bound racing right guardrail ${index + 1}`,
3743
+ material: material.pbr({ color: "#141d20", roughness: 0.74, metallic: 0 })
3744
+ })
3745
+ .position(roundGamePresentation(segment.midX - sideX * roadWidth * 0.62), y + 0.034, roundGamePresentation(segment.midZ - sideZ * roadWidth * 0.62))
3746
+ .rotate(0, -segment.angle, 0)
3747
+ .scale([railLength, 0.018, 0.014])
3748
+ .toJSON());
3749
+ }
3750
+ }
3751
+ return nodes;
3752
+ }
3753
+ export function createGameRacingCheckpointGateNodes(options) {
3754
+ const mode = options.mode ?? "scene-bound";
3755
+ const sample = racingPresentationSceneSample(options.sceneBinding, options.route, options.progress, options.roadY);
3756
+ if (!sample)
3757
+ return [];
3758
+ const roadWidth = options.roadWidth ?? racingPresentationRoadWidth(options.sceneBinding, options.route, "game-circuit");
3759
+ const label = options.index === undefined ? "racing checkpoint gate" : `racing checkpoint gate ${options.index + 1}`;
3760
+ const sideX = -Math.sin(sample.angle);
3761
+ const sideZ = Math.cos(sample.angle);
3762
+ const crossAngle = -sample.angle + Math.PI / 2;
3763
+ const prefix = mode === "asset-bound" ? "asset-bound" : "scene-bound";
3764
+ if (mode === "asset-bound") {
3765
+ return [
3766
+ primitives.box({
3767
+ name: `${prefix} ${label}`,
3768
+ material: material.emissive({ color: options.gateColor ?? "#f8f1bf", emissive: options.accentColor ?? "#f6e27a", emissiveIntensity: 0.38, roughness: 0.46 })
3769
+ })
3770
+ .position(sample.point[0], sample.point[1] + 0.045, sample.point[2])
3771
+ .rotate(0, crossAngle, 0)
3772
+ .scale([roadWidth * 0.9, 0.01, 0.026])
3773
+ .toJSON()
3774
+ ];
3775
+ }
3776
+ const postOffset = roadWidth * 0.72;
3777
+ const bannerY = sample.point[1] + 0.096;
3778
+ const lightY = sample.point[1] + 0.072;
3779
+ const leftX = roundGamePresentation(sample.point[0] + sideX * postOffset);
3780
+ const leftZ = roundGamePresentation(sample.point[2] + sideZ * postOffset);
3781
+ const rightX = roundGamePresentation(sample.point[0] - sideX * postOffset);
3782
+ const rightZ = roundGamePresentation(sample.point[2] - sideZ * postOffset);
3783
+ const bannerX = roundGamePresentation(sample.point[0] + sideX * roadWidth * 0.76);
3784
+ const bannerZ = roundGamePresentation(sample.point[2] + sideZ * roadWidth * 0.76);
3785
+ return [
3786
+ primitives.box({
3787
+ name: `${prefix} ${label} left post`,
3788
+ material: material.metal({ color: options.gateColor ?? "#26373a", roughness: 0.5, metallic: 0.28 })
3789
+ })
3790
+ .position(leftX, sample.point[1] + 0.041, leftZ)
3791
+ .scale([0.028, 0.082, 0.028])
3792
+ .toJSON(),
3793
+ primitives.box({
3794
+ name: `${prefix} ${label} right post`,
3795
+ material: material.metal({ color: options.gateColor ?? "#26373a", roughness: 0.5, metallic: 0.28 })
3796
+ })
3797
+ .position(rightX, sample.point[1] + 0.041, rightZ)
3798
+ .scale([0.028, 0.082, 0.028])
3799
+ .toJSON(),
3800
+ primitives.box({
3801
+ name: `${prefix} ${label} overhead banner`,
3696
3802
  material: material.emissive({
3697
- color: options.laneColor ?? "#d8f6ff",
3698
- emissive: options.laneColor ?? "#8df4ff",
3699
- emissiveIntensity: 0.7,
3700
- roughness: 0.4
3803
+ color: options.accentColor ?? "#b66d33",
3804
+ emissive: options.accentColor ?? "#d88f4a",
3805
+ emissiveIntensity: 0.16,
3806
+ roughness: 0.42
3701
3807
  })
3702
3808
  })
3703
- .position(segment.midX, y + 0.022, segment.midZ)
3704
- .rotate(0, -segment.angle, 0)
3705
- .scale([Math.max(0.08, segment.length * (isCircuitStage ? 0.44 : 0.72)), 0.012, 0.014])
3706
- .toJSON(), primitives.box({
3707
- name: `scene-bound racing left curb ${index + 1}`,
3708
- material: material.pbr({ color: index % 2 === 0 ? "#f4f7fb" : options.curbColor ?? "#df3550", roughness: 0.62 })
3809
+ .position(bannerX, bannerY, bannerZ)
3810
+ .rotate(0, crossAngle, 0)
3811
+ .scale([roadWidth * 0.26, 0.024, 0.024])
3812
+ .toJSON(),
3813
+ primitives.sphere({
3814
+ name: `${prefix} ${label} left signal light`,
3815
+ material: material.emissive({ color: options.lightColor ?? "#7ee8c4", emissive: options.lightColor ?? "#7ee8c4", emissiveIntensity: 0.64, roughness: 0.32 })
3709
3816
  })
3710
- .position(roundGamePresentation(segment.midX + sideX * roadWidth * 0.54), y + 0.024, roundGamePresentation(segment.midZ + sideZ * roadWidth * 0.54))
3711
- .rotate(0, -segment.angle, 0)
3712
- .scale([Math.max(0.08, segment.length * 0.82), 0.015, 0.02])
3713
- .toJSON(), primitives.box({
3714
- name: `scene-bound racing right curb ${index + 1}`,
3715
- material: material.pbr({ color: index % 2 === 0 ? options.curbColor ?? "#df3550" : "#f4f7fb", roughness: 0.62 })
3817
+ .position(roundGamePresentation(sample.point[0] + sideX * roadWidth * 0.2), lightY, roundGamePresentation(sample.point[2] + sideZ * roadWidth * 0.2))
3818
+ .scale(0.018)
3819
+ .toJSON(),
3820
+ primitives.sphere({
3821
+ name: `${prefix} ${label} right signal light`,
3822
+ material: material.emissive({ color: options.lightColor ?? "#7ee8c4", emissive: options.lightColor ?? "#7ee8c4", emissiveIntensity: 0.64, roughness: 0.32 })
3716
3823
  })
3717
- .position(roundGamePresentation(segment.midX - sideX * roadWidth * 0.54), y + 0.024, roundGamePresentation(segment.midZ - sideZ * roadWidth * 0.54))
3718
- .rotate(0, -segment.angle, 0)
3719
- .scale([Math.max(0.08, segment.length * 0.82), 0.015, 0.02])
3720
- .toJSON());
3824
+ .position(roundGamePresentation(sample.point[0] - sideX * roadWidth * 0.2), lightY, roundGamePresentation(sample.point[2] - sideZ * roadWidth * 0.2))
3825
+ .scale(0.018)
3826
+ .toJSON(),
3827
+ primitives.cylinder({
3828
+ name: `${prefix} ${label} left cone marker`,
3829
+ material: material.emissive({ color: options.accentColor ?? "#c78344", emissive: options.accentColor ?? "#c78344", emissiveIntensity: 0.26, roughness: 0.52 })
3830
+ })
3831
+ .position(roundGamePresentation(sample.point[0] + sideX * roadWidth * 0.78), sample.point[1] + 0.034, roundGamePresentation(sample.point[2] + sideZ * roadWidth * 0.78))
3832
+ .scale([0.038, 0.068, 0.038])
3833
+ .toJSON(),
3834
+ primitives.cylinder({
3835
+ name: `${prefix} ${label} right cone marker`,
3836
+ material: material.emissive({ color: options.accentColor ?? "#c78344", emissive: options.accentColor ?? "#c78344", emissiveIntensity: 0.26, roughness: 0.52 })
3837
+ })
3838
+ .position(roundGamePresentation(sample.point[0] - sideX * roadWidth * 0.78), sample.point[1] + 0.034, roundGamePresentation(sample.point[2] - sideZ * roadWidth * 0.78))
3839
+ .scale([0.038, 0.068, 0.038])
3840
+ .toJSON()
3841
+ ];
3842
+ }
3843
+ export function createGameRacingStartFinishNodes(options) {
3844
+ const mode = options.mode ?? "scene-bound";
3845
+ const sample = racingPresentationSceneSample(options.sceneBinding, options.route, 0, options.roadY);
3846
+ if (!sample)
3847
+ return [];
3848
+ const roadWidth = options.roadWidth ?? racingPresentationRoadWidth(options.sceneBinding, options.route, "game-circuit");
3849
+ const crossAngle = -sample.angle + Math.PI / 2;
3850
+ const prefix = mode === "asset-bound" ? "asset-bound" : "scene-bound";
3851
+ if (mode === "asset-bound") {
3852
+ return [
3853
+ primitives.box({
3854
+ name: `${prefix} racing start finish band`,
3855
+ material: material.emissive({ color: "#ffffff", emissive: "#ffffff", emissiveIntensity: 0.36, roughness: 0.3 })
3856
+ })
3857
+ .position(sample.point[0], sample.point[1] + 0.052, sample.point[2])
3858
+ .rotate(0, crossAngle, 0)
3859
+ .scale([roadWidth * 1.05, 0.01, 0.035])
3860
+ .toJSON()
3861
+ ];
3721
3862
  }
3722
- options.sceneBinding.checkpointScenePoints.forEach((point, index) => {
3863
+ const nodes = [];
3864
+ const sideX = -Math.sin(sample.angle);
3865
+ const sideZ = Math.cos(sample.angle);
3866
+ const forwardX = Math.cos(sample.angle);
3867
+ const forwardZ = Math.sin(sample.angle);
3868
+ nodes.push(primitives.box({
3869
+ name: "scene-bound racing start grid vehicle pad",
3870
+ material: material.pbr({ color: options.checkerColorB ?? "#0d1416", roughness: 0.78, metallic: 0 })
3871
+ })
3872
+ .position(roundGamePresentation(sample.point[0] - forwardX * roadWidth * 0.1), sample.point[1] + 0.028, roundGamePresentation(sample.point[2] - forwardZ * roadWidth * 0.1))
3873
+ .rotate(0, crossAngle, 0)
3874
+ .scale([roadWidth * 1.08, 0.01, roadWidth * 0.78])
3875
+ .toJSON(), primitives.box({
3876
+ name: "scene-bound racing start finish light strip",
3877
+ material: material.emissive({ color: options.lightColor ?? "#86e391", emissive: options.lightColor ?? "#86e391", emissiveIntensity: 0.48, roughness: 0.38 })
3878
+ })
3879
+ .position(sample.point[0], sample.point[1] + 0.047, sample.point[2])
3880
+ .rotate(0, crossAngle, 0)
3881
+ .scale([roadWidth * 0.86, 0.012, 0.028])
3882
+ .toJSON());
3883
+ const tileCount = 10;
3884
+ for (let tile = 0; tile < tileCount; tile += 1) {
3885
+ const laneOffset = ((tile + 0.5) / tileCount - 0.5) * roadWidth * 1.18;
3723
3886
  nodes.push(primitives.box({
3724
- name: `${mode === "asset-overlay" ? "asset-bound" : "scene-bound"} racing checkpoint gate ${index + 1}`,
3725
- material: material.emissive({
3726
- color: "#f8f1bf",
3727
- emissive: "#f6e27a",
3728
- emissiveIntensity: mode === "asset-overlay" ? 0.38 : 0.56,
3729
- roughness: 0.46
3730
- })
3887
+ name: `scene-bound racing start finish checker ${tile + 1}`,
3888
+ material: material.pbr({ color: tile % 2 === 0 ? options.checkerColorA ?? "#d4ded8" : options.checkerColorB ?? "#0d1416", roughness: 0.52, metallic: 0 })
3731
3889
  })
3732
- .position(point[0], point[1] + (mode === "asset-overlay" ? 0.045 : 0.026), point[2])
3733
- .scale([roadWidth * (mode === "asset-overlay" ? 0.9 : 1.25), mode === "asset-overlay" ? 0.01 : 0.014, 0.026])
3890
+ .position(roundGamePresentation(sample.point[0] + sideX * laneOffset), sample.point[1] + 0.04, roundGamePresentation(sample.point[2] + sideZ * laneOffset))
3891
+ .rotate(0, crossAngle, 0)
3892
+ .scale([roadWidth / tileCount, 0.014, 0.07])
3734
3893
  .toJSON());
3894
+ }
3895
+ const postOffset = roadWidth * 0.68;
3896
+ const leftX = roundGamePresentation(sample.point[0] + sideX * postOffset);
3897
+ const leftZ = roundGamePresentation(sample.point[2] + sideZ * postOffset);
3898
+ const rightX = roundGamePresentation(sample.point[0] - sideX * postOffset);
3899
+ const rightZ = roundGamePresentation(sample.point[2] - sideZ * postOffset);
3900
+ const bannerX = roundGamePresentation(sample.point[0] + sideX * roadWidth * 0.84);
3901
+ const bannerZ = roundGamePresentation(sample.point[2] + sideZ * roadWidth * 0.84);
3902
+ nodes.push(primitives.box({
3903
+ name: "scene-bound racing launch grid bright marker",
3904
+ material: material.emissive({ color: options.checkerColorA ?? "#718980", emissive: options.lightColor ?? "#86e391", emissiveIntensity: 0.16, roughness: 0.44 })
3905
+ })
3906
+ .position(roundGamePresentation(sample.point[0] - forwardX * roadWidth * 0.28), sample.point[1] + 0.038, roundGamePresentation(sample.point[2] - forwardZ * roadWidth * 0.28))
3907
+ .rotate(0, crossAngle, 0)
3908
+ .scale([roadWidth * 0.44, 0.012, 0.05])
3909
+ .toJSON(), primitives.box({
3910
+ name: "scene-bound racing launch grid dark marker",
3911
+ material: material.pbr({ color: options.checkerColorB ?? "#0d1416", roughness: 0.52, metallic: 0 })
3912
+ })
3913
+ .position(roundGamePresentation(sample.point[0] - forwardX * roadWidth * 0.42), sample.point[1] + 0.039, roundGamePresentation(sample.point[2] - forwardZ * roadWidth * 0.42))
3914
+ .rotate(0, crossAngle, 0)
3915
+ .scale([roadWidth * 0.44, 0.012, 0.05])
3916
+ .toJSON(), primitives.sphere({
3917
+ name: "scene-bound racing start left lane signal",
3918
+ material: material.emissive({ color: options.lightColor ?? "#86e391", emissive: options.lightColor ?? "#86e391", emissiveIntensity: 0.72, roughness: 0.32 })
3919
+ })
3920
+ .position(roundGamePresentation(sample.point[0] + sideX * roadWidth * 0.34 - forwardX * roadWidth * 0.2), sample.point[1] + 0.06, roundGamePresentation(sample.point[2] + sideZ * roadWidth * 0.34 - forwardZ * roadWidth * 0.2))
3921
+ .scale(0.036)
3922
+ .toJSON(), primitives.sphere({
3923
+ name: "scene-bound racing start right lane signal",
3924
+ material: material.emissive({ color: options.lightColor ?? "#86e391", emissive: options.lightColor ?? "#86e391", emissiveIntensity: 0.72, roughness: 0.32 })
3925
+ })
3926
+ .position(roundGamePresentation(sample.point[0] - sideX * roadWidth * 0.34 - forwardX * roadWidth * 0.2), sample.point[1] + 0.06, roundGamePresentation(sample.point[2] - sideZ * roadWidth * 0.34 - forwardZ * roadWidth * 0.2))
3927
+ .scale(0.036)
3928
+ .toJSON(), primitives.box({
3929
+ name: "scene-bound racing start gantry left post",
3930
+ material: material.metal({ color: options.gantryColor ?? "#26373a", roughness: 0.5, metallic: 0.28 })
3931
+ })
3932
+ .position(leftX, sample.point[1] + 0.064, leftZ)
3933
+ .scale([0.032, 0.128, 0.032])
3934
+ .toJSON(), primitives.box({
3935
+ name: "scene-bound racing start gantry right post",
3936
+ material: material.metal({ color: options.gantryColor ?? "#26373a", roughness: 0.5, metallic: 0.28 })
3937
+ })
3938
+ .position(rightX, sample.point[1] + 0.064, rightZ)
3939
+ .scale([0.032, 0.128, 0.032])
3940
+ .toJSON(), primitives.box({
3941
+ name: "scene-bound racing start finish gantry banner",
3942
+ material: material.emissive({ color: options.gantryColor ?? "#2c4243", emissive: options.lightColor ?? "#86e391", emissiveIntensity: 0.12, roughness: 0.42 })
3943
+ })
3944
+ .position(bannerX, sample.point[1] + 0.138, bannerZ)
3945
+ .rotate(0, crossAngle, 0)
3946
+ .scale([roadWidth * 0.34, 0.034, 0.034])
3947
+ .toJSON(), primitives.sphere({
3948
+ name: "scene-bound racing start green light",
3949
+ material: material.emissive({ color: options.lightColor ?? "#86e391", emissive: options.lightColor ?? "#86e391", emissiveIntensity: 0.72, roughness: 0.32 })
3950
+ })
3951
+ .position(roundGamePresentation(sample.point[0] - sideX * roadWidth * 0.84), sample.point[1] + 0.156, roundGamePresentation(sample.point[2] - sideZ * roadWidth * 0.84))
3952
+ .scale(0.023)
3953
+ .toJSON());
3954
+ return nodes;
3955
+ }
3956
+ export function createGamePublicRacingPresentationNodes(options) {
3957
+ const mode = "game-circuit";
3958
+ const roadWidth = racingPresentationRoadWidth(options.sceneBinding, options.route, mode);
3959
+ const nodes = [
3960
+ ...createGameRacingRoadMeshNodes({ ...options, mode, includeTerrain: options.includeTerrain ?? true })
3961
+ ];
3962
+ for (const [index, progress] of (options.route.checkpoints ?? []).entries()) {
3963
+ nodes.push(...createGameRacingCheckpointGateNodes({
3964
+ sceneBinding: options.sceneBinding,
3965
+ route: options.route,
3966
+ progress,
3967
+ index,
3968
+ roadY: options.roadY,
3969
+ roadWidth,
3970
+ gateColor: options.checkpointColor,
3971
+ accentColor: options.checkpointAccentColor,
3972
+ lightColor: options.startLightColor
3973
+ }));
3974
+ }
3975
+ nodes.push(...createGameRacingStartFinishNodes({
3976
+ sceneBinding: options.sceneBinding,
3977
+ route: options.route,
3978
+ roadY: options.roadY,
3979
+ roadWidth,
3980
+ lightColor: options.startLightColor
3981
+ }));
3982
+ return nodes;
3983
+ }
3984
+ export function createGameRacingCameraRig(options) {
3985
+ return createGameRacingPresentationCamera(options);
3986
+ }
3987
+ export function certifyPublicRacingPresentation(input) {
3988
+ const base = certifyPublicRacingGeometry(input);
3989
+ const blockers = [...base.blockers];
3990
+ const presentation = input.presentation;
3991
+ if (!presentation) {
3992
+ blockers.push("racing:presentation-missing");
3993
+ }
3994
+ else {
3995
+ if ((presentation.roadMeshNodes ?? 0) < 8)
3996
+ blockers.push(`racing:presentation-road-mesh-too-small:${presentation.roadMeshNodes ?? 0}`);
3997
+ if ((presentation.checkpointGateNodes ?? 0) < input.checkpoints.length) {
3998
+ blockers.push(`racing:presentation-checkpoint-gates-too-few:${presentation.checkpointGateNodes ?? 0}`);
3999
+ }
4000
+ if ((presentation.startFinishNodes ?? 0) < 4)
4001
+ blockers.push(`racing:presentation-start-finish-too-small:${presentation.startFinishNodes ?? 0}`);
4002
+ if (presentation.cameraMode !== "follow" && presentation.cameraMode !== "perspective") {
4003
+ blockers.push(`racing:presentation-camera-not-racing:${presentation.cameraMode ?? "missing"}`);
4004
+ }
4005
+ if ((presentation.debugMarkerCount ?? 0) > 0)
4006
+ blockers.push(`racing:presentation-debug-markers:${presentation.debugMarkerCount}`);
4007
+ }
4008
+ return {
4009
+ ...base,
4010
+ publicReady: blockers.length === 0,
4011
+ blockers
4012
+ };
4013
+ }
4014
+ export function createGameRacingPresentationTrackNodes(options) {
4015
+ const mode = options.mode ?? "standalone";
4016
+ const guideVisibility = options.guideVisibility ?? (mode === "asset-overlay" ? "evidence" : "full");
4017
+ if (mode === "asset-overlay" && guideVisibility === "public")
4018
+ return [];
4019
+ const roadWidth = racingPresentationRoadWidth(options.sceneBinding, options.route, mode);
4020
+ const nodes = [...createGameRacingRoadMeshNodes(options)];
4021
+ if (mode === "asset-overlay") {
4022
+ for (const [index, progress] of (options.route.checkpoints ?? []).entries()) {
4023
+ nodes.push(...createGameRacingCheckpointGateNodes({
4024
+ sceneBinding: options.sceneBinding,
4025
+ route: options.route,
4026
+ progress,
4027
+ index,
4028
+ mode: "asset-bound",
4029
+ roadY: options.roadY,
4030
+ roadWidth,
4031
+ gateColor: options.laneColor,
4032
+ accentColor: options.laneColor
4033
+ }));
4034
+ }
4035
+ nodes.push(...createGameRacingStartFinishNodes({
4036
+ sceneBinding: options.sceneBinding,
4037
+ route: options.route,
4038
+ mode: "asset-bound",
4039
+ roadY: options.roadY,
4040
+ roadWidth
4041
+ }));
4042
+ return nodes;
4043
+ }
4044
+ if (guideVisibility === "evidence" || guideVisibility === "full") {
4045
+ for (const [index, progress] of (options.route.checkpoints ?? []).entries()) {
4046
+ nodes.push(...createGameRacingCheckpointGateNodes({
4047
+ sceneBinding: options.sceneBinding,
4048
+ route: options.route,
4049
+ progress,
4050
+ index,
4051
+ roadY: options.roadY,
4052
+ roadWidth,
4053
+ gateColor: options.laneColor,
4054
+ accentColor: options.laneColor
4055
+ }));
4056
+ }
4057
+ nodes.push(...createGameRacingStartFinishNodes({
4058
+ sceneBinding: options.sceneBinding,
4059
+ route: options.route,
4060
+ roadY: options.roadY,
4061
+ roadWidth,
4062
+ lightColor: options.laneColor
4063
+ }));
4064
+ }
4065
+ return nodes;
4066
+ }
4067
+ export function createGamePublicPlatformerPresentationNodes(options) {
4068
+ return createGamePlatformerPresentationSurfaceNodes({
4069
+ ...options,
4070
+ mode: "game-level",
4071
+ guideVisibility: "public"
3735
4072
  });
3736
- const start = routePoints[0];
3737
- if (start) {
3738
- nodes.push(primitives.box({
3739
- name: `${mode === "asset-overlay" ? "asset-bound" : "scene-bound"} racing start finish band`,
4073
+ }
4074
+ export function createGamePlatformerGroundMeshNodes(options) {
4075
+ return createSinglePlatformerSurfaceMeshNodes("ground", options);
4076
+ }
4077
+ export function createGamePlatformerPlatformMeshNodes(options) {
4078
+ return createSinglePlatformerSurfaceMeshNodes("platform", options);
4079
+ }
4080
+ export function createGamePlatformerHazardNodes(options) {
4081
+ const mode = options.mode ?? "game-level";
4082
+ const isGameLevel = mode === "game-level";
4083
+ const rect = options.sceneBinding.surfaceToSceneRect(options.hazard);
4084
+ return [
4085
+ primitives.box({
4086
+ name: `scene-bound platformer hazard ${options.hazard.id}`,
3740
4087
  material: material.emissive({
3741
- color: "#ffffff",
3742
- emissive: "#ffffff",
3743
- emissiveIntensity: mode === "asset-overlay" ? 0.36 : 0.5,
3744
- roughness: 0.3
4088
+ color: options.color ?? "#ff6b6b",
4089
+ emissive: options.color ?? "#ff6b6b",
4090
+ emissiveIntensity: isGameLevel ? 0.7 : 0.54,
4091
+ roughness: 0.36
3745
4092
  })
3746
4093
  })
3747
- .position(start[0], start[1] + (mode === "asset-overlay" ? 0.052 : 0.03), start[2])
3748
- .scale([roadWidth * (mode === "asset-overlay" ? 1.05 : 1.45), mode === "asset-overlay" ? 0.01 : 0.014, 0.035])
3749
- .toJSON());
4094
+ .position(rect.center[0], rect.center[1], rect.center[2] + (isGameLevel ? 0.39 : 0.31))
4095
+ .scale([Math.max(rect.size[0], 0.045), Math.max(rect.size[1], 0.045), 0.055])
4096
+ .toJSON()
4097
+ ];
4098
+ }
4099
+ export function createGamePlatformerCheckpointNodes(options) {
4100
+ const point = options.sceneBinding.toScenePoint(options.checkpoint, 0.02);
4101
+ return [
4102
+ primitives.box({
4103
+ name: `scene-bound platformer checkpoint ${options.checkpoint.id}`,
4104
+ material: material.emissive({
4105
+ color: options.color ?? "#d7f9ff",
4106
+ emissive: options.color ?? "#a6f4ff",
4107
+ emissiveIntensity: 0.48,
4108
+ roughness: 0.4
4109
+ })
4110
+ })
4111
+ .position(point[0], point[1], point[2] + 0.29)
4112
+ .scale([0.026, 0.22, 0.034])
4113
+ .toJSON()
4114
+ ];
4115
+ }
4116
+ export function createGamePlatformerFinishNodes(options) {
4117
+ const point = options.sceneBinding.toScenePoint(options.finish, 0.04);
4118
+ return [
4119
+ primitives.box({
4120
+ name: `scene-bound platformer finish marker${options.finish.id ? ` ${options.finish.id}` : ""}`,
4121
+ material: material.emissive({
4122
+ color: options.color ?? "#b6ffbd",
4123
+ emissive: options.color ?? "#83f58f",
4124
+ emissiveIntensity: 0.6,
4125
+ roughness: 0.42
4126
+ })
4127
+ })
4128
+ .position(point[0], point[1], point[2] + 0.34)
4129
+ .scale([0.06, 0.34, 0.05])
4130
+ .toJSON()
4131
+ ];
4132
+ }
4133
+ export function createGamePlatformerCameraRig(options) {
4134
+ return createGamePlatformerPresentationCamera(options);
4135
+ }
4136
+ export function certifyPublicPlatformerPresentation(input) {
4137
+ const base = certifyPublicPlatformerGeometry(input);
4138
+ const blockers = [...base.blockers];
4139
+ const presentation = input.presentation;
4140
+ if (!presentation) {
4141
+ blockers.push("platformer:presentation-missing");
3750
4142
  }
3751
- return nodes;
4143
+ else {
4144
+ if ((presentation.groundMeshNodes ?? 0) < 2)
4145
+ blockers.push(`platformer:presentation-ground-mesh-too-small:${presentation.groundMeshNodes ?? 0}`);
4146
+ if ((presentation.platformMeshNodes ?? 0) < 6)
4147
+ blockers.push(`platformer:presentation-platform-mesh-too-small:${presentation.platformMeshNodes ?? 0}`);
4148
+ if ((presentation.hazardNodes ?? 0) < input.hazards.length) {
4149
+ blockers.push(`platformer:presentation-hazards-too-few:${presentation.hazardNodes ?? 0}`);
4150
+ }
4151
+ if ((presentation.checkpointNodes ?? 0) < input.checkpoints.length) {
4152
+ blockers.push(`platformer:presentation-checkpoints-too-few:${presentation.checkpointNodes ?? 0}`);
4153
+ }
4154
+ if ((presentation.finishNodes ?? 0) < 1)
4155
+ blockers.push(`platformer:presentation-finish-missing:${presentation.finishNodes ?? 0}`);
4156
+ if (presentation.cameraMode !== "follow" && presentation.cameraMode !== "perspective") {
4157
+ blockers.push(`platformer:presentation-camera-not-side-scroller:${presentation.cameraMode ?? "missing"}`);
4158
+ }
4159
+ if ((presentation.debugMarkerCount ?? 0) > 0)
4160
+ blockers.push(`platformer:presentation-debug-markers:${presentation.debugMarkerCount}`);
4161
+ if (presentation.characterGrounded !== true)
4162
+ blockers.push("platformer:presentation-character-not-grounded");
4163
+ }
4164
+ return {
4165
+ ...base,
4166
+ publicReady: blockers.length === 0,
4167
+ blockers
4168
+ };
3752
4169
  }
3753
4170
  export function createGamePlatformerPresentationSurfaceNodes(options) {
3754
4171
  const mode = options.mode ?? "standalone";
@@ -3758,7 +4175,7 @@ export function createGamePlatformerPresentationSurfaceNodes(options) {
3758
4175
  return [];
3759
4176
  const isGameLevel = mode === "game-level";
3760
4177
  const nodes = [];
3761
- if ((mode === "standalone" || mode === "game-level") && platforms.length > 0) {
4178
+ if ((mode === "standalone" || mode === "game-level") && platforms.length > 0 && options.includeBackdrop !== false) {
3762
4179
  const rects = platforms.map((surface) => options.sceneBinding.surfaceToSceneRect(surface));
3763
4180
  const bounds = boundsForPresentationRects(rects, isGameLevel ? 0.78 : 0.42);
3764
4181
  nodes.push(primitives.box({
@@ -3865,6 +4282,54 @@ export function createGamePlatformerPresentationSurfaceNodes(options) {
3865
4282
  }
3866
4283
  return nodes;
3867
4284
  }
4285
+ function createSinglePlatformerSurfaceMeshNodes(surfaceRole, options) {
4286
+ const mode = options.mode ?? "game-level";
4287
+ const isGameLevel = mode === "game-level";
4288
+ const rect = options.sceneBinding.surfaceToSceneRect(options.surface);
4289
+ const width = Math.max(rect.size[0], 0.08);
4290
+ const height = Math.max(rect.size[1], 0.055);
4291
+ if (mode === "asset-bound") {
4292
+ return [
4293
+ primitives.box({
4294
+ name: `asset-bound platformer ${surfaceRole} contact strip ${options.surface.id}`,
4295
+ material: material.emissive({
4296
+ color: options.trimColor ?? "#c9f7ff",
4297
+ emissive: options.trimColor ?? "#a8f4ff",
4298
+ emissiveIntensity: 0.28,
4299
+ roughness: 0.44
4300
+ })
4301
+ })
4302
+ .position(rect.center[0], roundGamePresentation(rect.center[1] + height / 2 + 0.026), rect.center[2] + 0.24)
4303
+ .scale([width * 0.78, 0.012, 0.024])
4304
+ .toJSON()
4305
+ ];
4306
+ }
4307
+ return [
4308
+ primitives.box({
4309
+ name: `scene-bound platformer ${surfaceRole} mesh ${options.surface.id}`,
4310
+ material: material.pbr({
4311
+ color: options.color ?? (isGameLevel ? "#526972" : "#718994"),
4312
+ roughness: 0.72,
4313
+ metallic: 0
4314
+ })
4315
+ })
4316
+ .position(rect.center[0], rect.center[1], rect.center[2])
4317
+ .scale([width, height, isGameLevel ? 0.64 : 0.5])
4318
+ .toJSON(),
4319
+ primitives.box({
4320
+ name: `scene-bound platformer ${surfaceRole} trim ${options.surface.id}`,
4321
+ material: material.emissive({
4322
+ color: options.trimColor ?? "#c9f7ff",
4323
+ emissive: options.trimColor ?? "#a8f4ff",
4324
+ emissiveIntensity: 0.36,
4325
+ roughness: 0.38
4326
+ })
4327
+ })
4328
+ .position(rect.center[0], roundGamePresentation(rect.center[1] + height / 2 + 0.012), rect.center[2] + (isGameLevel ? 0.334 : 0.262))
4329
+ .scale([width * 0.92, 0.016, 0.035])
4330
+ .toJSON()
4331
+ ];
4332
+ }
3868
4333
  function boundsForPresentationPoints(points, padding) {
3869
4334
  const xs = points.map((point) => point[0]);
3870
4335
  const zs = points.map((point) => point[2]);
@@ -3904,6 +4369,77 @@ function segmentPresentation(start, end) {
3904
4369
  angle: Math.atan2(dz, dx)
3905
4370
  };
3906
4371
  }
4372
+ function racingPresentationRoutePoints(sceneBinding, route, roadY) {
4373
+ return route.points.map((point) => sceneBinding.toScenePoint(point, roadY ?? 0.012));
4374
+ }
4375
+ function racingPresentationRoadWidth(sceneBinding, route, mode) {
4376
+ const isCircuitStage = mode === "game-circuit";
4377
+ return Math.max((route.width ?? 0.18) * sceneBinding.transform.scale * (isCircuitStage ? 1.55 : 1.5), isCircuitStage ? 0.34 : 0.18);
4378
+ }
4379
+ function racingPresentationSceneSample(sceneBinding, route, progress, roadY) {
4380
+ const sample = racingPresentationRouteSample(route, progress);
4381
+ if (!sample)
4382
+ return undefined;
4383
+ return {
4384
+ point: sceneBinding.toScenePoint(sample.point, roadY ?? 0.012),
4385
+ angle: sample.angle
4386
+ };
4387
+ }
4388
+ function racingPresentationRouteSample(route, progress) {
4389
+ const points = route.points;
4390
+ if (points.length < 2)
4391
+ return undefined;
4392
+ const clamped = Number.isFinite(progress) ? Math.min(1, Math.max(0, progress)) : 0;
4393
+ let totalLength = 0;
4394
+ const segmentLengths = [];
4395
+ for (let index = 0; index < points.length - 1; index += 1) {
4396
+ const start = points[index];
4397
+ const end = points[index + 1];
4398
+ if (!start || !end)
4399
+ continue;
4400
+ const length = Math.hypot(end.x - start.x, end.y - start.y);
4401
+ segmentLengths[index] = length;
4402
+ totalLength += length;
4403
+ }
4404
+ if (totalLength <= 0) {
4405
+ const start = points[0];
4406
+ const end = points[1] ?? start;
4407
+ if (!start || !end)
4408
+ return undefined;
4409
+ return {
4410
+ point: start,
4411
+ angle: Math.atan2(end.y - start.y, end.x - start.x)
4412
+ };
4413
+ }
4414
+ const targetLength = clamped * totalLength;
4415
+ let traversed = 0;
4416
+ for (let index = 0; index < points.length - 1; index += 1) {
4417
+ const start = points[index];
4418
+ const end = points[index + 1];
4419
+ const length = segmentLengths[index] ?? 0;
4420
+ if (!start || !end || length <= 0)
4421
+ continue;
4422
+ if (targetLength <= traversed + length || index === points.length - 2) {
4423
+ const localT = Math.min(1, Math.max(0, (targetLength - traversed) / length));
4424
+ return {
4425
+ point: {
4426
+ x: start.x + (end.x - start.x) * localT,
4427
+ y: start.y + (end.y - start.y) * localT
4428
+ },
4429
+ angle: Math.atan2(end.y - start.y, end.x - start.x)
4430
+ };
4431
+ }
4432
+ traversed += length;
4433
+ }
4434
+ const last = points[points.length - 1];
4435
+ const previous = points[points.length - 2] ?? last;
4436
+ if (!last || !previous)
4437
+ return undefined;
4438
+ return {
4439
+ point: last,
4440
+ angle: Math.atan2(last.y - previous.y, last.x - previous.x)
4441
+ };
4442
+ }
3907
4443
  function roundGamePresentation(value) {
3908
4444
  return Math.round(value * 1000) / 1000;
3909
4445
  }
@@ -3932,11 +4468,27 @@ export const game = {
3932
4468
  platformerSceneBinding: createGamePlatformerSceneBinding,
3933
4469
  platformerPresentationCamera: createGamePlatformerPresentationCamera,
3934
4470
  platformerPresentationSurfaces: createGamePlatformerPresentationSurfaceNodes,
4471
+ publicPlatformerPresentation: createGamePublicPlatformerPresentationNodes,
4472
+ platformerGroundMesh: createGamePlatformerGroundMeshNodes,
4473
+ platformerPlatformMesh: createGamePlatformerPlatformMeshNodes,
4474
+ platformerHazard: createGamePlatformerHazardNodes,
4475
+ platformerCheckpoint: createGamePlatformerCheckpointNodes,
4476
+ platformerFinish: createGamePlatformerFinishNodes,
4477
+ platformerCameraRig: createGamePlatformerCameraRig,
4478
+ certifyPlatformerGeometry: certifyPublicPlatformerGeometry,
4479
+ certifyPlatformerPresentation: certifyPublicPlatformerPresentation,
3935
4480
  platformer: createGamePlatformerKit,
3936
4481
  assetBoundRacingRoute: createGameAssetBoundRacingRoute,
3937
4482
  racingSceneBinding: createGameRacingSceneBinding,
3938
4483
  racingPresentationCamera: createGameRacingPresentationCamera,
4484
+ racingCameraRig: createGameRacingCameraRig,
4485
+ racingRoadMesh: createGameRacingRoadMeshNodes,
4486
+ racingCheckpointGate: createGameRacingCheckpointGateNodes,
4487
+ racingStartFinish: createGameRacingStartFinishNodes,
3939
4488
  racingPresentationTrack: createGameRacingPresentationTrackNodes,
4489
+ publicRacingPresentation: createGamePublicRacingPresentationNodes,
4490
+ certifyRacingGeometry: certifyPublicRacingGeometry,
4491
+ certifyRacingPresentation: certifyPublicRacingPresentation,
3940
4492
  racing: createGameRacingKit,
3941
4493
  fallingBlocks: createGameFallingBlocksKit,
3942
4494
  fallingBlockPieces: GAME_FALLING_BLOCK_PIECES,