@woosh/meep-engine 2.168.0 → 2.169.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/README.md +2 -2
- package/build/bundle-worker-image-decoder.js +1 -1
- package/package.json +1 -1
- package/src/REVIEW_2026_08_06/adv-core.md +511 -0
- package/src/REVIEW_2026_08_06/adv-gfx.md +480 -0
- package/src/REVIEW_2026_08_06/adv-view.md +446 -0
- package/src/REVIEW_2026_08_06/api-core.md +296 -0
- package/src/REVIEW_2026_08_06/api-engine.md +195 -0
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +507 -0
- package/src/REVIEW_2026_08_06/core-binary.md +348 -0
- package/src/REVIEW_2026_08_06/core-collection.md +376 -0
- package/src/REVIEW_2026_08_06/core-math.md +221 -0
- package/src/REVIEW_2026_08_06/core-process.md +266 -0
- package/src/REVIEW_2026_08_06/core-science.md +223 -0
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +279 -0
- package/src/REVIEW_2026_08_06/engine-animation.md +167 -0
- package/src/REVIEW_2026_08_06/engine-asset.md +166 -0
- package/src/REVIEW_2026_08_06/engine-ecs.md +920 -0
- package/src/REVIEW_2026_08_06/engine-input.md +197 -0
- package/src/REVIEW_2026_08_06/engine-misc.md +248 -0
- package/src/REVIEW_2026_08_06/engine-terrain.md +52 -0
- package/src/REVIEW_2026_08_06/generation.md +176 -0
- package/src/REVIEW_2026_08_06/geom-2d.md +434 -0
- package/src/REVIEW_2026_08_06/geom-3d.md +117 -0
- package/src/REVIEW_2026_08_06/gfx-gi.md +143 -0
- package/src/REVIEW_2026_08_06/gfx-render.md +740 -0
- package/src/REVIEW_2026_08_06/gfx-texture.md +690 -0
- package/src/REVIEW_2026_08_06/prior-criticals.md +49 -0
- package/src/REVIEW_2026_08_06/prior-highs.md +138 -0
- package/src/REVIEW_2026_08_06/test-gaps.md +364 -0
- package/src/REVIEW_2026_08_06/view.md +667 -0
- package/src/REVIEW_2026_08_06.md +571 -0
- package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
- package/src/core/binary/BinaryBuffer.js +12 -1
- package/src/core/binary/BitImage2.d.ts +5 -1
- package/src/core/binary/BitImage2.d.ts.map +1 -1
- package/src/core/binary/BitImage2.js +5 -1
- package/src/core/bvh8/build/NodeProxy.d.ts.map +1 -1
- package/src/core/bvh8/build/NodeProxy.js +314 -308
- package/src/core/collection/list/List.d.ts +15 -0
- package/src/core/collection/list/List.d.ts.map +1 -1
- package/src/core/collection/list/List.js +34 -0
- package/src/core/collection/list/SortedListProjection.d.ts.map +1 -1
- package/src/core/collection/list/SortedListProjection.js +4 -1
- package/src/core/collection/map/BiMap.d.ts +25 -2
- package/src/core/collection/map/BiMap.d.ts.map +1 -1
- package/src/core/collection/map/BiMap.js +57 -3
- package/src/core/debug/matchers/IsAnything.d.ts.map +1 -1
- package/src/core/debug/matchers/IsAnything.js +5 -1
- package/src/core/events/signal/Signal.d.ts.map +1 -1
- package/src/core/events/signal/Signal.js +3 -1
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +433 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts +31 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_is_counter_clockwise.js +84 -0
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts +24 -2
- package/src/core/geom/2d/polygon/polygon2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/polygon/polygon2_signed_area.js +52 -34
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.corpus.js +86 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts +40 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulate.js +1280 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts +28 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.d.ts.map +1 -0
- package/src/core/geom/2d/polygon/polygon2_triangulation_deviation.js +77 -0
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts +6 -3
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_rasterize_conservative.js +26 -13
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts +17 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.d.ts.map +1 -1
- package/src/core/geom/2d/triangle/tri2_signed_area.js +35 -23
- package/src/core/geom/2d/v2_morton_encode.d.ts +20 -0
- package/src/core/geom/2d/v2_morton_encode.d.ts.map +1 -0
- package/src/core/geom/2d/v2_morton_encode.js +23 -0
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.d.ts.map +1 -1
- package/src/core/geom/3d/atlas/segment/atlas_merge_charts.js +4 -1
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts +27 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_compute_normal.js +60 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts +34 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.d.ts.map +1 -0
- package/src/core/geom/3d/polygon/polygon3_triangulate.js +94 -0
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts +8 -2
- package/src/core/geom/3d/quaternion/quat3_multiply.d.ts.map +1 -1
- package/src/core/geom/3d/quaternion/quat3_multiply.js +8 -2
- package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/ConvexHullShape3D.js +32 -2
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts +23 -6
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.d.ts.map +1 -1
- package/src/core/geom/3d/topology/struct/binary/io/face/bt_face_triangulate.js +268 -124
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts +17 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_assert_valid.js +44 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts +20 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_count_edge_faces.js +40 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts +19 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_closed.js +34 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts +22 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_is_manifold.js +37 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts +87 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.d.ts.map +1 -0
- package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_validate.js +599 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts +32 -0
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_compute_signed_volume.js +66 -7
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts +22 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.d.ts.map +1 -1
- package/src/core/geom/3d/triangle/tri3_mesh_compute_signed_volume.js +83 -38
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +554 -0
- package/src/core/geom/vec3/v3_cotangent.d.ts +6 -2
- package/src/core/geom/vec3/v3_cotangent.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cotangent.js +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts +6 -2
- package/src/core/geom/vec3/v3_cross.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_cross.js +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts +6 -2
- package/src/core/geom/vec3/v3_multiply.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_multiply.js +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts +6 -2
- package/src/core/geom/vec3/v3_subtract.d.ts.map +1 -1
- package/src/core/geom/vec3/v3_subtract.js +6 -2
- package/src/core/localization/Localization.js +1 -1
- package/src/core/math/random/randomSeed.d.ts +13 -0
- package/src/core/math/random/randomSeed.d.ts.map +1 -0
- package/src/core/math/random/randomSeed.js +16 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts +50 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_apply_transform.js +107 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts +34 -0
- package/src/core/math/spline/spline3_hermite_reverse.d.ts.map +1 -0
- package/src/core/math/spline/spline3_hermite_reverse.js +49 -0
- package/src/core/math/statistics/hammersley_sequence.d.ts +17 -4
- package/src/core/math/statistics/hammersley_sequence.d.ts.map +1 -1
- package/src/core/math/statistics/hammersley_sequence.js +25 -16
- package/src/core/process/Future.d.ts.map +1 -1
- package/src/core/process/Future.js +12 -0
- package/src/core/process/PromiseWatcher.d.ts.map +1 -1
- package/src/core/process/PromiseWatcher.js +9 -2
- package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
- package/src/core/process/executor/ConcurrentExecutor.js +12 -2
- package/src/core/process/worker/WorkerProxy.d.ts.map +1 -1
- package/src/core/process/worker/WorkerProxy.js +5 -2
- package/src/core/process/worker/extractTransferables.d.ts.map +1 -1
- package/src/core/process/worker/extractTransferables.js +4 -1
- package/src/engine/Engine.d.ts +12 -12
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +149 -10
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +4 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.d.ts.map +1 -1
- package/src/engine/asset/loaders/ArrayBufferLoader.js +7 -0
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.d.ts.map +1 -1
- package/src/engine/ecs/speaker/lines/sets/LineSetDescription.js +3 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.d.ts.map +1 -1
- package/src/engine/ecs/terrain/ecs/TerrainSystem.js +4 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts +29 -0
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.d.ts.map +1 -1
- package/src/engine/ecs/terrain/tiles/TerrainTileManager.js +84 -23
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +446 -0
- package/src/engine/graphics/GraphicsEngine.d.ts +56 -7
- package/src/engine/graphics/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics/GraphicsEngine.js +196 -24
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts +9 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextFailureReason.js +17 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts +106 -0
- package/src/engine/graphics/context/WebGLContextMonitor.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextMonitor.js +341 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts +14 -0
- package/src/engine/graphics/context/WebGLContextState.d.ts.map +1 -0
- package/src/engine/graphics/context/WebGLContextState.js +24 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts +2 -0
- package/src/engine/graphics/context/testWebGLContextLoss.d.ts.map +1 -0
- package/src/engine/graphics/context/testWebGLContextLoss.js +696 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationAdapter.js +68 -32
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts +14 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/graphics/ecs/camera/serialization/CameraSerializationUpgrader_0_1.js +41 -0
- package/src/engine/graphics/ecs/decal/v2/FPDecalSystem.js +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js +5 -0
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +5 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2D.js +15 -0
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail2d/Trail2DSystem.js +2 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +4 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3D.js +488 -476
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/trail3d/Trail3DSystem.js +280 -253
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/BuildNeighborsFast.js +3 -2
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.d.ts.map +1 -1
- package/src/engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js +3 -1
- package/src/engine/graphics/geometry/computeMeshSurfaceArea.js +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts +5 -0
- package/src/engine/graphics/load_and_set_cubemap_v0.d.ts.map +1 -1
- package/src/engine/graphics/load_and_set_cubemap_v0.js +26 -21
- package/src/engine/graphics/particles/particular/group/ParticleGroup.d.ts.map +1 -1
- package/src/engine/graphics/particles/particular/group/ParticleGroup.js +19 -8
- package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/AtlasLookupTexture.js +10 -3
- package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/TextureAtlas.js +4 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.d.ts.map +1 -1
- package/src/engine/graphics/texture/atlas/gpu/WebGLTextureAtlas.js +11 -0
- package/src/engine/graphics/texture/cubemap/load_environment_map.d.ts.map +1 -1
- package/src/engine/graphics/texture/cubemap/load_environment_map.js +5 -2
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/distance/computeSignedDistanceField_Chamfer.js +26 -18
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts +15 -2
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/filter/sampler2d_blur_gaussian.js +29 -3
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts +11 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.d.ts.map +1 -1
- package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js +158 -171
- package/src/engine/graphics/texture/sampler/sampler2d_paint.js +1 -1
- package/src/engine/graphics/texture/sampler/util/bitSet2Sampler2D.js +2 -2
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.d.ts.map +1 -1
- package/src/engine/input/ecs/util/TopDownCameraControllerHelper.js +10 -3
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts +14 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.d.ts.map +1 -0
- package/src/engine/navigation/ecs/components/PathSerializationUpgrader_2_3.js +38 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +433 -425
- package/src/engine/physics/constraint/solve_constraints.d.ts +4 -1
- package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
- package/src/engine/physics/constraint/solve_constraints.js +38 -13
- package/src/engine/physics/contact/ManifoldStore.d.ts +15 -3
- package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
- package/src/engine/physics/contact/ManifoldStore.js +15 -3
- package/src/engine/physics/ecs/Joint.d.ts +7 -4
- package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
- package/src/engine/physics/ecs/Joint.js +7 -4
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +14 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +16 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/physics/fluid/ecs/FluidSystem.d.ts +3 -3
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +15 -7
- package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/convex_convex_manifold.js +33 -10
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_concave.js +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts +6 -2
- package/src/engine/physics/narrowphase/refine_ray_hit.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/refine_ray_hit.js +6 -2
- package/src/engine/physics/queries/raycast.d.ts.map +1 -1
- package/src/engine/physics/queries/raycast.js +11 -4
- package/src/engine/save/storage/IndexedDBStorage.d.ts.map +1 -1
- package/src/engine/save/storage/IndexedDBStorage.js +21 -3
- package/src/engine/simulation/Ticker.d.ts +12 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +18 -0
- package/src/engine/sound/simulation/AcousticSimulator.d.ts.map +1 -1
- package/src/engine/sound/simulation/AcousticSimulator.js +9 -1
- package/src/engine/sound/simulation/core/VolumeField.d.ts +6 -2
- package/src/engine/sound/simulation/core/VolumeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/core/VolumeField.js +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts +6 -2
- package/src/engine/sound/simulation/probe/AcousticProbeField.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/AcousticProbeField.js +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts +6 -2
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/acoustic_probe_transfer.js +6 -2
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts +3 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.d.ts.map +1 -1
- package/src/engine/sound/simulation/probe/bakeProbeReflectors.js +3 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +8 -1
- package/src/generation/theme/TerrainTheme.d.ts.map +1 -1
- package/src/generation/theme/TerrainTheme.js +2 -1
- package/src/generation/theme/ThemeEngine.d.ts.map +1 -1
- package/src/generation/theme/ThemeEngine.js +6 -5
- package/src/view/ViewGroup.d.ts.map +1 -1
- package/src/view/ViewGroup.js +9 -7
- package/src/view/graphics/WebGLContextFailureView.d.ts +19 -0
- package/src/view/graphics/WebGLContextFailureView.d.ts.map +1 -0
- package/src/view/graphics/WebGLContextFailureView.js +76 -0
- package/src/view/minimap/Minimap.d.ts +8 -0
- package/src/view/minimap/Minimap.d.ts.map +1 -1
- package/src/view/minimap/Minimap.js +16 -3
- package/src/view/minimap/dom/MinimapMarkerView.d.ts.map +1 -1
- package/src/view/minimap/dom/MinimapMarkerView.js +3 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# REVIEW_2026_07_03 §3 "Critical findings" — re-verification against current source
|
|
2
|
+
|
|
3
|
+
Source root: `H:/git/moh/app/src/mir-engine/meep/src/`
|
|
4
|
+
Verified: 2026-08-06. Line numbers below are **current**, not the ones in the review.
|
|
5
|
+
|
|
6
|
+
C1/C3/C4/C10/C22 (FIXED) and C5 (OPEN) carried forward from the lead reviewer; C5 re-confirmed with a current line.
|
|
7
|
+
C2 does not exist (intentionally absent from the review).
|
|
8
|
+
|
|
9
|
+
| ID | Status | Current file:line | Evidence | Spec exists? |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| C1 | **FIXED** | `engine/ecs/EntityObserver.js` | carried forward from lead reviewer | — |
|
|
12
|
+
| C3 | **FIXED** | `engine/grid/transform2grid/Transform2GridPositionSystem.js` | carried forward from lead reviewer | — |
|
|
13
|
+
| C4 | **FIXED** | `engine/ecs/attachment/AttachmentSystem.js` | carried forward from lead reviewer | — |
|
|
14
|
+
| C5 | **OPEN** | `engine/graphics/ecs/decal/v2/FPDecalSystem.js:216` | in `unlink()`, after `this.__fp_decal = null;`: `transform.subscribe(this.__update_transform, this);` — identical call to the one in `link()` at :162; no `unsubscribe` anywhere in the file | **No** — `graphics/ecs/decal/v2/Decal.spec.js` exists but does not exercise unlink/transform-change |
|
|
15
|
+
| C6 | **FIXED** | `engine/control/first-person/sensors/FirstPersonSensorsSystem.js:135-136` | `const rx = -Math.cos(yaw);` / `const rz = Math.sin(yaw);` — matches the prescribed fix; comment at :132 now documents the controller convention | **Yes** — `FirstPersonSensorsSystem.spec.js:117` `"side wall on the player's right (world -X at yaw 0) fires wallRight (C6)"`; also referenced from `control/first-person/WallRunScenarios.spec.js:32` |
|
|
16
|
+
| C7 | **FIXED** | `engine/input/ecs/systems/InputControllerSystem.js:106` | `return b.priority - a.priority;` (typo `priotity` gone). Field is now real: `input/ecs/components/InputBinding.js:11` `constructor({ path, listener, exclusive = false, priority = 0 })` and `:36` `this.priority = priority;` | **Yes** — `InputControllerSystem.spec.js:5` `"bindings on the same path dispatch in descending priority order (C7)"` |
|
|
17
|
+
| C8 | **FIXED** | `engine/graphics/particles/particular/group/ParticleGroup.js:467` | `indices.sort((a, b) => a - b);` with comment "sort indices ascending numerically (the compaction below walks the deleted tail block and assumes ascending numeric order)". (File moved: `particular/…` → `graphics/particles/particular/…`) | **Yes** — `ParticleGroup.spec.js:114` `"deleteIndices() compacts correctly when a deleted index >= 10 outranks lower ones (C8)"` |
|
|
18
|
+
| C9 | **FIXED** | `engine/graphics/particles/particular/engine/emitter/ParticlePool.js:507` | `attribute.updateRange.count = n * itemSize;` — and the sibling site at `:177` `attribute.updateRange.count = activeParticleLimit * attribute.itemSize;` | **Yes** — `ParticlePool.spec.js:227` `"writeAttributeForAllArray marks updateRange.count in elements, not particles (C9)"` |
|
|
19
|
+
| C10 | **FIXED** | `engine/graphics/texture/atlas/AtlasLookupTexture.js` | carried forward from lead reviewer | — |
|
|
20
|
+
| C11 | **FIXED** | `engine/graphics/texture/sampler/resize/sampler2d_scale_down_lanczos.js:93-94` | `const rcp_ratio_y = 2 / ratio_y;` / `const rcp_ratio_x = 2 / ratio_x;` | **Yes** — `sampler2d_scale_down_lanczos.spec.js:33` `"non-uniform downscale is transpose-symmetric (C11)"`, written **outside** the still-present `describe.skip("currently broken")` block at :4 (those three legacy tests remain skipped) |
|
|
21
|
+
| C12 | **FIXED** | `engine/ecs/fow/FogOfWar.js:707-713` | `const samplerWidth = this.size.x + 2;` / `const samplerHeight = this.size.y + 2;` / `new Sampler2D(uint8Array, 1, samplerWidth, samplerHeight);` — no transposition | **Yes** — `ecs/fow/FogOfWar.spec.js:16` `"resize builds a sampler with non-transposed dimensions (C12)"` |
|
|
22
|
+
| C13 | **OPEN** | `engine/ecs/terrain/tiles/TerrainTileManager.js:566` | `stitchTile()` still floors: `const gridSize = this.totalSize.clone(); gridSize.divide(this.tileSize); gridSize.floor();` while `initializeTiles()` at `:352-353` creates tiles with `gridSize.divide(time_size); gridSize.ceil();`. The floored `gridSize` then gates every neighbour lookup (`if (x < gridSize.x - 1)` at :577/:586/:593, `if (y < gridSize.y - 1)` at :581), so the partial edge tiles that `ceil` created are never given top/right/bottom neighbours. `computeTileIndex` (:405) and `getRaw` (:422) already use `Math.ceil` — `stitchTile` is the lone floor. | **No** — no spec anywhere in `engine/` matches `stitch`/C13; `ecs/terrain/tiles/TerrainTile.spec.js` exists but does not cover manager-level seam stitching |
|
|
23
|
+
| C14 | **FIXED** | `engine/asset/preloader/AssetPreloader.js:297-301` | `assetLoadFailed` now has the completion check: `//monitor completion (a failure counts toward the level batch, and //may be the last completion in it — see assetLoadSuccess)` then `if ((batchElementLoadedCount + batchElementFailedCount) >= batch_size) { this.on.levelFinished.dispatch(level); }` — mirrors `assetLoadSuccess` at :284. (File moved: `preloader/` → `asset/preloader/`) | **Yes** — `AssetPreloader.spec.js:89` `"a failing asset in an earlier level still advances the preload (C14)"` |
|
|
24
|
+
| C15 | **FIXED** | `engine/navigation/ecs/path_following/PathFollower.js:160-162` | `if (typeof json.loop === "boolean") { this.writeFlag(PathFollowerFlags.Loop, json.loop); }` — and `json.active` is separately routed to `PathFollowerFlags.Active` at :156-158 | **Yes** — `PathFollower.spec.js:8` `"fromJSON reads the loop flag from json.loop, not json.active (C15)"` |
|
|
25
|
+
| C16 | **FIXED** | `engine/animation/curve/actionProcessorOperations/curveActions.js:118-133` | `MoveKeyframeAction.apply` now copies in place: `this.actionContext.refCurve.keys[i].copy(this.curveValueEnd.keys[i]);` with comment "do NOT reassign the live curve's keyframe to the snapshot object … (C16)"; `revert` mirrors it with `.copy(this.curveValueStart.keys[i])`. Snapshots are taken by `.clone()` in the constructor (:110, :113). | **Yes** — `curveActions.spec.js:13` `"MoveKeyframeAction applies end values without aliasing live keyframes to its snapshot (C16)"` |
|
|
26
|
+
| C17 | **OPEN** | `engine/graphics/geometry/MikkT/BuildNeighborsFast.js:116-118` | The B-side edge unpack is still wrong: `const i0_B = temp_edge_B[1];` / `const i1_B = temp_edge_B[2];` / `const edgenum_B = temp_edge_B[2];` — `i1_B` and `edgenum_B` read the **same slot**, and the "flip" comment at :115 is still there. `GetEdge` (`MikkT/GetEdge.js:22-42`) documents the layout as `out[0]=i0, out[1]=i1, out[2]=edgenum`, and the A-side at :85-87 reads it correctly (`temp_edge_A[0]/[1]/[2]`). So the pairing test `if (i0_A === i0_B && i1_A === i1_B …)` at :123 compares a vertex index against an edge number and effectively never matches. | **No** — `MikkT/MikkTSpace.spec.js` only has `'empty geometry'` (:3) and `'single triangle'` (:12); a single triangle has no neighbours, so the pairing path is untested |
|
|
27
|
+
| C18 | **OPEN** | `engine/graphics/geometry/MikkT/GenerateSharedVerticesIndexList.js:79` | Insertion pass still has the precedence bug: `const bin_index = pass * 257 + (_comp_0[i] >> shift) & 0xff;` — `+` binds tighter than `&`, so this masks the whole sum, not just the byte. The **counting** pass at :59 was written correctly (`const index = pass * 257 + ((key >> shift) & 0xff) + 1;`), so counts and insertion now disagree about bin identity. | **No** — no spec references radix sorting or shared-vertex welding; `MikkTSpace.spec.js` does not reach multi-vertex welding |
|
|
28
|
+
| C19 | **FIXED** | `engine/logging/elastic/ElasticSearchLogger.js:61, 82, 141-142` | Field names unified: the config is `this.__flush_timeout = 2000;` (:61) and the handle is `this.__timeout = -1;` (:82). `__prod()` arms it correctly: `} else if (this.__timeout === -1) { this.__timeout = setTimeout(this.__bound_flush, this.__flush_timeout); }`, and `flush()` clears the same field (:95-97). No `__timeout_ms` remains. | **Yes** — `ElasticSearchLogger.spec.js:4` `"logging fewer than flush_size records arms a deferred flush timer (C19)"` |
|
|
29
|
+
| C20 | **FIXED** | `engine/ecs/EntityComponentDataset.js:1881-1882` | `dropData()` now clears both: `this.__entityEventListeners = []; this.__entityAnyEventListeners = [];`. `removeEntity` also purges both (:980-981). | **Yes** — `EntityComponentDataset.spec.js:1019` `"dropData clears any-event listeners so a recycled entity id does not inherit them (C20)"` |
|
|
30
|
+
| C21 | **FIXED** | `engine/ecs/EntityManager.js:357` | Inside `detachDataset()` (:334): `system.handleDatasetDetached(dataset);`. The `handleDatasetAttached` calls remain only on the genuine attach paths (:316 in attach, :714 on system startup). | No dedicated spec found (behaviour is exercised indirectly by the ECS lifecycle suite) |
|
|
31
|
+
| C22 | **FIXED** | `engine/sound/material/concrete/json/serializeSoundMaterialToJSON.js` | carried forward from lead reviewer | — |
|
|
32
|
+
|
|
33
|
+
## Tally
|
|
34
|
+
|
|
35
|
+
- **FIXED: 17** — C1, C3, C4, C6, C7, C8, C9, C10, C11, C12, C14, C15, C16, C19, C20, C21, C22
|
|
36
|
+
- **OPEN: 4** — C5, C13, C17, C18
|
|
37
|
+
- **MOVED/UNCLEAR: 0**
|
|
38
|
+
|
|
39
|
+
Three files moved since the review but were located and verified (no ambiguity about identity):
|
|
40
|
+
|
|
41
|
+
- `particular/group/ParticleGroup.js` → `graphics/particles/particular/group/ParticleGroup.js`
|
|
42
|
+
- `particular/engine/emitter/ParticlePool.js` → `graphics/particles/particular/engine/emitter/ParticlePool.js`
|
|
43
|
+
- `preloader/AssetPreloader.js` → `asset/preloader/AssetPreloader.js`
|
|
44
|
+
|
|
45
|
+
## Notes on the OPEN items
|
|
46
|
+
|
|
47
|
+
- **None of the four OPEN items has a regression spec.** Every FIXED item except C21 shipped with a spec that names its finding ID, so the absence of one is a reliable signal that C5/C13/C17/C18 were never worked.
|
|
48
|
+
- **C17 and C18 are both in `graphics/geometry/MikkT/`** and both sit on the tangent-generation path. The only spec in that directory covers empty geometry and a single triangle — neither case reaches edge pairing or vertex welding.
|
|
49
|
+
- No fix for any of the four is implemented as an `assert.*` (which would not count, since asserts are stripped in production) — the defective lines are plain unguarded logic.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# REVIEW_2026_07_03 §2 + §4 — re-verification against current source (2026-08-06)
|
|
2
|
+
|
|
3
|
+
Root: `H:/git/moh/app/src/mir-engine/meep/src/`
|
|
4
|
+
Rule applied: an `assert.*` is stripped in production, so an assert-only guard is **not** a fix.
|
|
5
|
+
|
|
6
|
+
**Tally: 34 FIXED / 32 OPEN / 1 MOVED-UNCLEAR — 67 items checked.**
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## §4 — ECS core & lifecycle
|
|
11
|
+
|
|
12
|
+
| Item | Status | Current file:line | Evidence |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| `Entity.promiseEvent` uses `removeEventListener` instead of `add` | **FIXED** | `engine/ecs/Entity.js:327` | `this.addEventListener(EventType.EntityRemoved, reject);` — matching `removeEventListener` cleanup at :320 |
|
|
15
|
+
| `BinaryBufferSerializer` entity-id delta ordering guarded only by a stripped assert | **OPEN** | `engine/ecs/storage/BinaryBufferSerializer.js:180` | `assert.ok(entity >= lastEntity, ...)` is still the only guard on `const step = entity - lastEntity;` |
|
|
16
|
+
| `EntityComponentDataset` event dispatch: no `try/finally` around the handler loop → throwing handler skips the proxy-pointer restore (§8 LOW) | **OPEN** | `engine/ecs/EntityComponentDataset.js:2161` (and :2203) | `entityListenersProxyPointer = pointerOffset;` restored after a bare loop in both `dispatchEventListenersByHash` and `dispatchAnyEventListenersByHash` |
|
|
17
|
+
|
|
18
|
+
## §4 — Graphics: ECS bindings
|
|
19
|
+
|
|
20
|
+
| Item | Status | Current file:line | Evidence |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `LightBinding` scale handler bound to `rotation.onChanged` | **FIXED** | `engine/graphics/ecs/light/binding/LightBinding.js:51` | `this.__c_transform.scale.onChanged.add(this.__apply_scale, this);` (unlink mirrors at :89) |
|
|
23
|
+
| `WaterSystem.shutdown` dereferences `graphicsEngine.size`; preRender hook leaked | **FIXED** | `engine/graphics/ecs/water/WaterSystem.js:68` | `if (this.__preRenderHook !== null) { this.graphicsEngine.on.preRender.remove(this.__preRenderHook);` — no `.size` deref remains |
|
|
24
|
+
| `auto_set_camera_clipping_planes` reads `old_near/far` after overwriting | **FIXED** | `engine/graphics/ecs/camera/auto_set_camera_clipping_planes.js:45` | `const old_near = camera.near;` captured **before** `camera.near = c.clip_near` (:51) |
|
|
25
|
+
| `set_camera_aspect_ratio` ortho `h = bottom - top` | **FIXED** | `engine/graphics/ecs/camera/set_camera_aspect_ratio.js:25` | `const h = c.top - c.bottom;` |
|
|
26
|
+
| `ForwardPlusRenderingPlugin.shutdown` removes from the wrong signal | **FIXED** | `engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.js:81` | `graphics.main_view.on.preRender.remove(this.__prepare_for_render, this);` matches the add at :65 |
|
|
27
|
+
| `sg_hierarchy_compute_bounding_box_via_parent_entity` ignores `entity` | **FIXED** | `engine/graphics/ecs/mesh-v2/sg_hierarchy_compute_bounding_box_via_parent_entity.js:19` | `const sg = ecd.getComponent(entity, ShadedGeometry);` — unioned into `result` at :24 |
|
|
28
|
+
| `SGMeshSystem.update` splices its wait-queue while modulo-indexing | **OPEN** | `engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js:283` | `const index = pointer % l;` with `waiting.splice(index, 1);` at :295 and `l` captured before the loop, never decremented |
|
|
29
|
+
|
|
30
|
+
## §4 — Graphics: texture / particles / geometry
|
|
31
|
+
|
|
32
|
+
| Item | Status | Current file:line | Evidence |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| `ManagedTextureAtlas` serves stale atlas-evicted patches from a TTL-∞ loader cache | **OPEN** | `engine/graphics/texture/atlas/ManagedTextureAtlas.js:148` | `patch = await this.#patch_loader.get(url);` — `#remove_from_lookup` (:91) never invalidates the loader cache (`DEFAULT_TIME_TO_LIVE = Infinity`) |
|
|
35
|
+
| `CachingTextureAtlas.__evict_all` leaves index 0 | **FIXED** | `engine/graphics/texture/atlas/CachingTextureAtlas.js:153` | `for (let i = this.__cached_patches.length - 1; i >= 0; i--) {` |
|
|
36
|
+
| `sampler2d_scale_down_linear` mis-indexes on non-integer ratios | **OPEN** | `engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.js:54` | `const row_index = (sampleOffsetY + j) * iW;` with fractional `sampleOffsetY = y * sH`; only `assert.ok(sW >= 1, …)` guards it |
|
|
37
|
+
| `sampler2d_copy_with_margins` uses `width` for a vertical bound | **FIXED** | `engine/graphics/texture/sampler/sampler2d_copy_with_margins.js:138` | `for (y = destinationY + height, yMax = Math.min(destination.height, y + marginBottom); ...` (commit `90bcf672a`) |
|
|
38
|
+
| `SimulationStepCurlNoiseVelocity` shared `this.time` accumulates per emitter | **OPEN** | `engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseVelocity.js:22` | `this.time += this.timeDelta;` on the single shared instance held by `ParticularEngine.steps` |
|
|
39
|
+
| `ParticleEmitter` hash/equals disagree on flags | **FIXED** | `engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js:694` | `&& (this.flags & SERIALIZABLE_FLAGS) === (other.flags & SERIALIZABLE_FLAGS);` — same mask `#computeHash()` folds in at :664 |
|
|
40
|
+
| `ShaderManager` leaks a disposed emitter after a graveyard round-trip | **OPEN** | `engine/graphics/particles/particular/engine/shader/ShaderManager.js:399` | `this.recordGraveyard.set(emitter, entry);` — the `entry.count <= 0` branch never splices the emitter out of `entry.emitters` |
|
|
41
|
+
| `equal_attribute_structure` returns truthy `-1` on name mismatch | **FIXED** | `engine/graphics/geometry/optimization/merge/merge_geometry_hierarchy.js:91` | `if (b_attribute_index === -1) { return false; }`; caller uses `if (!equal_attribute_structure(...)) continue;` |
|
|
42
|
+
| `InstancedMeshGroup.build` stale per-instance colors on shrink+regrow | **FIXED** | `engine/graphics/geometry/instancing/InstancedMeshGroup.js:575` | `newColorArray.fill(255, this.count * 4);` |
|
|
43
|
+
| `bvh32_geometry_nearest` duplicated denormalize block | **FIXED** | `engine/graphics/geometry/buffered/query/bvh32_geometry_nearest.js:189` | `if (vertex_data_normalized) { ax = typed_array_value_denormalize(ax, vertices);` — block appears exactly once |
|
|
44
|
+
| `ensureGeometryBoundingSphere` returns stale `null` on first call | **FIXED** | `engine/graphics/geometry/buffered/ensureGeometryBoundingSphere.js:15` | `boundingSphere = geometry.boundingSphere = new ThreeSphere(...)` — chained assignment before `return boundingSphere;` |
|
|
45
|
+
|
|
46
|
+
## §4 — Water (broken three ways)
|
|
47
|
+
|
|
48
|
+
| Item | Status | Current file:line | Evidence |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `WaterShader.js` references r136-removed ShaderChunk symbols | **OPEN** | `engine/graphics/shaders/WaterShader.js:22` | `THREE.ShaderChunk["lights_pars"],` — plus `BRDF_Diffuse_Lambert` (:112, :124) and `linear_to_gamma_fragment` (:142) |
|
|
51
|
+
| `WaterSystem.shutdown` TypeError | **FIXED** | (see bindings table) | — |
|
|
52
|
+
| `Water`/`WaterSystem` never dispose height `DataTexture` / geometry / material | **OPEN** | `engine/graphics/ecs/water/WaterSystem.js:215` | `unlink(component, entity) { component.bvh.unlink();` — no `dispose()` on the `PlaneBufferGeometry` (:187), material (:165), or the `DataTexture` in `Water.updateShaderForTerrain` |
|
|
53
|
+
|
|
54
|
+
## §4 — Control / input
|
|
55
|
+
|
|
56
|
+
| Item | Status | Current file:line | Evidence |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `phaseCrossed` misses exactly-on-boundary landings | **FIXED** | `engine/control/first-person/FirstPersonPlayerControllerSystem.js:2022` | `return prev < boundary && next >= boundary;` (inclusive in both branches) |
|
|
59
|
+
| `PointerDevice.observeTap` shared move handler under multi-touch | **OPEN** | `engine/input/devices/PointerDevice.js:121` | `up.addOne(handleUp);` / `move.add(handleMove);` — one closure for all pointer ids; `reset(id)` removes the other pointer's registration |
|
|
60
|
+
| `InputControllerSystem.remove` splices index `-1` | **OPEN** | `engine/input/ecs/systems/InputControllerSystem.js:140` | `this.deferred.splice(i_d, 1);` with no `i_d !== -1` guard |
|
|
61
|
+
| `TouchDevice` never attaches its handlers | **OPEN** | `engine/input/devices/touch/TouchDevice.js:53` | `#eventHandlerGlobalTouchEnd = (event) => {` — no `addEventListener`/attach anywhere in the file |
|
|
62
|
+
|
|
63
|
+
## §4 — ECS gui / terrain
|
|
64
|
+
|
|
65
|
+
| Item | Status | Current file:line | Evidence |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `HeadsUpDisplaySystem` `z *= scale.y` | **FIXED** | `engine/ecs/gui/hud/HeadsUpDisplaySystem.js:88` | `z *= transform_scale.z;` |
|
|
68
|
+
| `obtainTileAtWorldPosition2D` dereferences an out-of-bounds tile | **OPEN** | `engine/ecs/terrain/tiles/TerrainTileManager.js:489` | `return this.obtain(tile.gridPosition.x, tile.gridPosition.y);` — `getTileByWorldPosition2D` returns `undefined` out of bounds and there is no guard |
|
|
69
|
+
|
|
70
|
+
## §4 — Asset loaders
|
|
71
|
+
|
|
72
|
+
| Item | Status | Current file:line | Evidence |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| 1-bit / sub-8-bit palette PNGs decode to garbage | **OPEN** | `engine/asset/loaders/image/png/PNG.js:199` | `const i = this.colors * this.bitDepth / 8 * (y * this.width + x);` — fractional byte index, no shift/mask unpack (also `getUint8Data_case3` :346) |
|
|
75
|
+
| `SVGAssetLoader` hangs on non-200 | **OPEN** | `engine/asset/loaders/SVGAssetLoader.js:10` | `if (xhr.readyState === xhr.DONE && xhr.status === 200) {` — no `else`, so a 404/500 settles nothing |
|
|
76
|
+
| Worker image decoder detaches a cached `ArrayBuffer` | **FIXED** | `engine/asset/loaders/image/codec/ThreadedImageDecoder.js:52` | `return this.worker.request('decode', [data.buffer]);` — `WorkerProxy.trySendMessage` posts with **no** transfer list, so the buffer is cloned |
|
|
77
|
+
|
|
78
|
+
## §4 — Animation / navigation / intelligence
|
|
79
|
+
|
|
80
|
+
| Item | Status | Current file:line | Evidence |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| `AnimationTrackPlayback.advance` `%= timeEnd` wrap + repeated `on.ended` | **OPEN** (partly fixed) | `engine/animation/keyed2/AnimationTrackPlayback.js:171` | `newPosition %= this.track.timeEnd;` still ignores `timeStart`. The repeated-`ended` half **is** fixed: `else if (this.position < this.track.timeEnd)` latch at :172 |
|
|
83
|
+
| `estimatePathViaIterativeIntegral` infinite loop | **FIXED** | `engine/graphics/ecs/path/tube/build/estimatePathViaIterativeIntegral.js:28` | `if (!on_path) { break; }` |
|
|
84
|
+
| GLTF CUBICSPLINE tangents missing `/Δt` | **OPEN** | `engine/animation/clip/curve_from_track_data_cubic_tangents.js:30` | `curve.add(Keyframe.from(time, value, inTangent, outTangent));` — no division by the keyframe delta anywhere in the file |
|
|
85
|
+
| Grid A* inadmissible squared-distance heuristic | **FIXED** | `engine/navigation/grid/find_path_on_grid_astar.js:185` | `return Math.abs(dx) + Math.abs(dy);` with a comment naming the old squared-euclidean estimate |
|
|
86
|
+
| `OverrideContextBehavior` double init | **FIXED** | `engine/intelligence/behavior/decorator/OverrideContextBehavior.js:37` | `Behavior.prototype.initialize.call(this, context);` with a comment explaining the deliberate skip of `super.initialize()` |
|
|
87
|
+
| `ConditionalBehavior` abandons a running source without finalizing | **OPEN** | `engine/intelligence/behavior/util/ConditionalBehavior.js:41` | `} else if (s === BehaviorStatus.Failed) { condition.finalize(); return s;` — `this.__source` is never finalized on that branch; file unchanged since before the review |
|
|
88
|
+
|
|
89
|
+
## §4 — Root engine / UI / logging
|
|
90
|
+
|
|
91
|
+
| Item | Status | Current file:line | Evidence |
|
|
92
|
+
|---|---|---|---|
|
|
93
|
+
| `Ticker.pause()` doesn't pause the clock | **FIXED** | `engine/simulation/Ticker.js:269` | `this.clock.pause();` (commit `53a45db88`) |
|
|
94
|
+
| `Ticker.start()` discards the initial rAF handle | **OPEN** | `engine/simulation/Ticker.js:250` | `requestAnimationFrame(animationFrameCallback);` — handle not stored, so `#isUpdateLoopActive()` is false right after start and `stop()` cannot cancel it (`animate()` then re-arms rAF forever) |
|
|
95
|
+
| `SceneManager.clear()` leaves clock speed-modifiers | **FIXED** | `engine/scene/SceneManager.js:159` | `this.deactivateScene(this.#current_scene);` with a comment on removing the modifiers |
|
|
96
|
+
| ElasticSearchLogger POSTs a JSON array, not NDJSON | **FIXED** | `engine/logging/elastic/ElasticSearchLogger.js:123` | `const body = lines.join('\n') + '\n';` with action lines and a `_bulk` endpoint |
|
|
97
|
+
| ElasticSearchLogger `@timestamp = performance.now()` | **OPEN** | `engine/logging/elastic/ElasticSearchLogger.js:151` | `record.time = performance.now();` written as `'@timestamp': record.time` (:117) |
|
|
98
|
+
| `GraphicsEngine.stop()` leaks framebuffers / listeners | **OPEN** | `engine/graphics/GraphicsEngine.js:642` | `stop() { this.context.detach(); ... }` — no `frameBuffers` teardown and the three `context_monitor.on.lost/restored/failed.add(...)` from `start()` (:521-523) are never removed; `initializeFrameBuffers()` re-`add`s buffers on every start |
|
|
99
|
+
| `GUIEngine.shutdown()` leaks tickers / viewport listeners | **OPEN** | `engine/ui/GUIEngine.js:501` | `shutdown() { this.windows.reset(); ... }` — never calls `this.ticker.stop()` (started at :470) and leaves `engine.gameView.size.process(...)` / `this.cursor.process(...)` bound. Localization listener removal **is** now present (:508) |
|
|
100
|
+
| `set_environment_texture` leaks the PMREM target | **FIXED** | `engine/graphics/GraphicsEngine.js:415` | `if (previous_target !== null) { previous_target.dispose(); }` |
|
|
101
|
+
| `DomElementProcessorManager.unlink` leak | **OPEN** | `engine/ui/bindings/DomElementProcessorManager.js:303` | `unlink() { this.__mutation_observer.disconnect(); ... }` — every `DomElementBinding` in `__element_to_bindings` stays bound (no `binding.unbind()`); file unchanged since 2023. The `preRender` listener removal (:309) **is** paired |
|
|
102
|
+
|
|
103
|
+
## §4 — Graphics GI (sh3)
|
|
104
|
+
|
|
105
|
+
| Item | Status | Current file:line | Evidence |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| LPV GI vertex shader walks the tet mesh in object space | **OPEN** | `engine/graphics/sh3/gi/material/space_vertex/build_vertex_shader.js:18` | `lpv_walk_to_tetrahedron(position, nearest_tet, tet, barycentric_coordinates);` — `position` is object space while :12/:14 use `worldPosition.xyz` |
|
|
108
|
+
| Path tracer: texture wrap mode never copied | **FIXED** | `engine/graphics/sh3/path_tracer/material/MaterialConverter.js:59` | `st.wrapS = texture.wrapS;` / `st.wrapT = texture.wrapT;` |
|
|
109
|
+
| Path tracer: tint linearized twice | **OPEN** | `engine/graphics/sh3/path_tracer/material/MaterialConverter.js:111` | `sRGB_to_linear(tint, 0, tint, 0);` — the linear tint is multiplied into the still-sRGB-encoded sampler, whose clone keeps `decode_sRGB = true` (`StandardTexture.js:39, :97`), so it is decoded again at sample time |
|
|
110
|
+
| Path tracer: emissive color / intensity dropped | **OPEN** | `engine/graphics/sh3/path_tracer/material/MaterialConverter.js:96` | `} else if (name === "emissive") { result.emissive = sampler;` — only the map; `material.emissive` / `emissiveIntensity` are never read and `StandardMaterial` has no such field |
|
|
111
|
+
|
|
112
|
+
## §4 — Network
|
|
113
|
+
|
|
114
|
+
| Item | Status | Current file:line | Evidence |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| `ServerAuthoritativeServer` default-null `authorize` | **FIXED** | `engine/network/orchestrator/ServerAuthoritativeServer.js:130` | `this.executor.authorize = make_owner_authorization({ world, identity_class, server_peer_id });` — secure by default |
|
|
117
|
+
| Receiver reassembly cap 64 KiB vs sender ~302 KiB | **OPEN** | `engine/network/transport/fragments/FragmentAssembler.js:59` | `max_message_size = 65536,` and `NetworkPeer.js:457` uses `new FragmentAssembler()` with no override, while `packet_size.js:59` gives `MAX_LOGICAL_MESSAGE_BYTES = 1186 * 255 ≈ 302 KiB` |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## §2 — Recurring patterns
|
|
122
|
+
|
|
123
|
+
| Item | Status | Current file:line | Evidence |
|
|
124
|
+
|---|---|---|---|
|
|
125
|
+
| §2.1 `Camera.copy` drops fov / clip planes | **FIXED** | `engine/graphics/ecs/camera/Camera.js:127` | `this.fov.copy(other.fov); this.clip_near = other.clip_near; this.clip_far = other.clip_far;` |
|
|
126
|
+
| §2.1 `GridPosition2Transform.copy` drops flags | **OPEN** | `engine/grid/grid2transform/GridPosition2Transform.js:80` | `copy(other) { this.offset.copy(other.offset); return this; }` — `flags` (:18) is never copied |
|
|
127
|
+
| §2.1 `Transform.copy` propagates a cleared auto-update flag | **OPEN** | `engine/ecs/transform/Transform.js:278` | `this.flags = other.flags;` after `this.clearFlag(TransformFlags.AutomaticChangeDetection)` — unchanged since 2023 |
|
|
128
|
+
| §2.2 AnimationGraph JSON state `name` / `tags` dropped | **FIXED** | `writeAnimationGraphDefinitionToJSON.js:73-74`, `readAnimationGraphDefinitionFromJSON.js:89-90` | `name: s.name, tags: s.tags,` written; `state.name = name; state.tags = tags;` read |
|
|
129
|
+
| §2.2 `startingSate` typo | **FIXED** (as a divergence) | `writeAnimationGraphDefinitionToJSON.js:100`, `readAnimationGraphDefinitionFromJSON.js:25` | `startingSate: states.indexOf(graph.startingSate)` / `startingSate = 0` — symmetric write/read plus a round-trip spec; the misspelling survives as the field name only |
|
|
130
|
+
| §2.2 POINT-light `distance` binary-only | **FIXED** | `engine/graphics/ecs/light/Light.js:174` | `if (actual_type === LightType.SPOT \|\| actual_type === LightType.POINT) { result.distance = this.distance.toJSON(); }` with the matching read at :141 |
|
|
131
|
+
| §2.2 `SerializationMetadata` flags truncated to 8 bits | **OPEN** | `engine/ecs/components/SerializationMetadataSerializationAdapter.js:15` | `buffer.writeUint8(value.flags);` / `value.flags = buffer.readUint8();` |
|
|
132
|
+
| §2.2 `Team` sentinel aliasing | **OPEN** | `engine/ecs/team/TeamSerializationAdapter.js:22` | `//TODO it's possible to write encoded Infinity values by accident` — `2147483647` round-trips as `Infinity` (same in `ObservedInteger.toBinaryBuffer`) |
|
|
133
|
+
| §2.2 `ViewportPosition.resolveGuiCollisions` dropped | **FIXED** | `engine/ecs/gui/position/ViewportPosition.js:114` | `resolveGuiCollisions: this.resolveGuiCollisions,` in `toJSON`, read at :103, binary at `ViewportPositionSerializationAdapter.js:21/38` |
|
|
134
|
+
| §2.9 `AnimationClip.repeatCount` 1 vs ∞ | **FIXED** | `engine/ecs/animation/AnimationClip.js:9` / `:86` | field `repeatCount = new ObservedInteger(1);`; `fromJSON` else-branch `this.repeatCount.set(1);` |
|
|
135
|
+
| §2.9 `screenEdgeWidth` 10 vs 0 | **FIXED** | `engine/ecs/gui/position/ViewportPosition.js:46` / `:95` | field `screenEdgeWidth = 10;`; `fromJSON` default `screenEdgeWidth = 10,` |
|
|
136
|
+
| §2.8 `SoundControllerSystem` missing `return` after error | **OPEN** | `engine/sound/ecs/SoundControllerSystem.js:82` | `console.error("Entity " + entity + " has no SoundEmitter to control");` (:68) then `emitter.tracks.add(track);` (:82) |
|
|
137
|
+
| §2.8 `SoundEmitterSystem` missing `return` after error | **MOVED-UNCLEAR** | `engine/sound/ecs/emitter/SoundEmitterSystem.js` | The file now contains no `console.*`/logger call and no `=== undefined` fall-through; the described log-and-fall-through site no longer exists, so the item cannot be mapped to current code |
|
|
138
|
+
| §2.8 `ParticleVolume.fromJSON` missing `return` after error | **FIXED** | `engine/graphics/particles/particular/engine/utils/volume/ParticleVolume.js:336` | `throw new Error(\`Unsupported sampling function type '${sampling_function}', ...\`);` — a real throw, not a log |
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# meep — functional test-gap analysis
|
|
2
|
+
|
|
3
|
+
Question answered: **where would a real defect ship undetected because no test would fail?**
|
|
4
|
+
|
|
5
|
+
Baseline taken as given (1542 suites green, 9337 tests). Everything below is evidence-backed by reading
|
|
6
|
+
the source *and* the specs that exist. Raw coverage counts are used only to size a gap, never as the argument.
|
|
7
|
+
|
|
8
|
+
Ranked by blast radius.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
### TG-1 Save/load durability layer has no test of any kind
|
|
13
|
+
- **Severity**: CRITICAL
|
|
14
|
+
- **Area**: `H:/git/moh/app/src/mir-engine/meep/src/engine/save/**` (6 files, 0 specs)
|
|
15
|
+
- **Gap**: `Storage.js` (the abstract contract + all `promise*` wrappers + `contains` implemented on top of `list`),
|
|
16
|
+
`storage/IndexedDBStorage.js` (the only real browser backend), `storage/InMemoryStorage.js`,
|
|
17
|
+
`StorageBackedList.js` (exports class `StorageBackedSet` — the serialized read-modify-write queue behind
|
|
18
|
+
achievements and options), and `GameStateLoader.js` (the `save`/`load`/`exists`/`count` engine plugin with a
|
|
19
|
+
legacy-format fallback) are collectively untested. Nothing asserts the `Storage` contract for *any*
|
|
20
|
+
implementation, and nothing asserts that a backend failure produces a rejection rather than a hang.
|
|
21
|
+
- **What could ship broken**:
|
|
22
|
+
- Every `IndexedDBStorage` method settles purely from DOM events (`success`/`error` listeners). Drop or
|
|
23
|
+
mis-wire one `error` listener and a save/load promise **never settles** — the game sits on a spinner
|
|
24
|
+
forever instead of surfacing an error. No test would notice.
|
|
25
|
+
- The file already carries the divergence this gap predicts: `list()` builds its transaction with the legacy
|
|
26
|
+
`IDBTransaction.READ_ONLY` constant (removed from the spec, evaluates to `undefined`) while every other
|
|
27
|
+
method uses `IndexedDBTransactionMode.ReadOnly`. It works today only because `transaction(name, undefined)`
|
|
28
|
+
defaults to `readonly`. A latent inconsistency survived in the one class that touches player saves.
|
|
29
|
+
- `QuotaExceededError` handling is an explicit in-file `TODO`: a full disk silently loses the save.
|
|
30
|
+
- `StorageBackedSet.remove()` calls `storage.store(key, list, resolve, reject)` with **no** `progress`
|
|
31
|
+
argument while `add()` passes `noop`. A backend that invokes `progress(...)` unconditionally throws on
|
|
32
|
+
remove only. Its `lastPromise` serialization chain (interleaved `add`/`remove`/`contains`) is never
|
|
33
|
+
exercised, so a lost-update race would be invisible.
|
|
34
|
+
- `GameStateLoader.count()` never calls `reject`; `load()` silently falls through to the deprecated
|
|
35
|
+
`legacyLoad` path when no metadata matches — a metadata regression degrades to the legacy path silently
|
|
36
|
+
instead of failing.
|
|
37
|
+
- **Cheapest closing test**: one `Storage`-contract suite parameterised over `InMemoryStorage` and a
|
|
38
|
+
deliberately-failing stub backend (store/load/list/contains/remove/`promise*`, plus "backend error rejects,
|
|
39
|
+
never hangs"), and one `StorageBackedSet` spec for interleaved mutation ordering. **~6 files**, and it gives
|
|
40
|
+
`IndexedDBStorage` a contract to be checked against the day a fake-IDB backend is added.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
### TG-2 Only one component in the engine has ever round-tripped through the binary save format
|
|
45
|
+
- **Severity**: CRITICAL
|
|
46
|
+
- **Area**: `engine/ecs/storage/**` + all `*SerializationAdapter.js`
|
|
47
|
+
- **Gap**: The sole end-to-end serializer test is `engine/ecs/storage/BinaryBufferSerializer.spec.js` — 44 lines,
|
|
48
|
+
one entity, one component (`Name`, a single string), one adapter. There are **77** `BinaryClassSerializationAdapter`
|
|
49
|
+
subclasses; **46 have no spec**, including every graphics ECS component (`Mesh`, `Light`, `Camera`,
|
|
50
|
+
`TopDownCameraController`, `Decal`, `Water`, `Highlight`, `AnimationController`, `AnimationGraph`,
|
|
51
|
+
`ParticleEmitter`, `Sampler2D`), `FogOfWar`/`FogOfWarRevealer`, `Terrain`, `GUIElement`, `HeadsUpDisplay`,
|
|
52
|
+
`ViewportPosition`, `Team`, `Tooltip`, `InverseKinematics`, `PropertySet`, `Animation`, `TransformPose`,
|
|
53
|
+
`GridPosition2Transform`, `GridObstacle`, all six acoustic-simulation adapters, and all eight behaviour-tree
|
|
54
|
+
node adapters. (The `sopra` audio adapters are the exception — all 13 are specced.)
|
|
55
|
+
- **What could ship broken**: any field silently dropped, reordered, or read with the wrong width. This is not
|
|
56
|
+
hypothetical — reading the code turned one up immediately:
|
|
57
|
+
`engine/graphics/ecs/light/LightSerializationAdapter.js` writes `distance` **only when `type === SPOT`**,
|
|
58
|
+
but `Light.js` documents and `Light.toJSON()` persists `distance` for **SPOT and POINT**, and neither the
|
|
59
|
+
binary path nor `Light.toJSON` for a POINT light carries `maxShadowDistance`. Consequence: **every point
|
|
60
|
+
light loaded from a binary save has its radius reset to `DEFAULT_DISTANCE = 1`**, and every directional
|
|
61
|
+
light loses its shadow-range tuning. The suite is green.
|
|
62
|
+
Worse than a wrong value: because adapters are read back-to-back out of one collection buffer, a
|
|
63
|
+
read/write length mismatch does not fail locally — it desynchronises the cursor and corrupts every
|
|
64
|
+
subsequent component of that type in the file.
|
|
65
|
+
- **Cheapest closing test**: one table-driven harness — for each adapter, construct an instance with every
|
|
66
|
+
field set to a non-default value, `serialize` → rewind → `deserialize` into a fresh instance, assert field
|
|
67
|
+
equality, and assert the buffer cursor advanced by exactly the bytes written. **~77 adapter classes in a
|
|
68
|
+
single spec file.**
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### TG-3 The JSON authoring path and the binary save path disagree about the field set, and nothing compares them
|
|
73
|
+
- **Severity**: CRITICAL
|
|
74
|
+
- **Area**: cross-cutting — 121 classes implement both `toJSON` and `fromJSON`; ~77 of those also have a binary adapter
|
|
75
|
+
- **Gap**: No test anywhere asserts the three-way contract `field-init == fromJSON(toJSON(x)) == fromBinary(toBinary(x))`.
|
|
76
|
+
The two formats are maintained by hand in separate files and drift independently. This is distinct from TG-2:
|
|
77
|
+
even a perfect binary round-trip test would not catch it.
|
|
78
|
+
- **What could ship broken**: a component authored in a JSON blueprint and the same component restored from a
|
|
79
|
+
save file end up with **different state**. Confirmed live example:
|
|
80
|
+
`engine/graphics/ecs/water/Water.js` — `toJSON()` emits only `level` and `color`, and `fromJSON` reads only
|
|
81
|
+
those two. `WaterSerializationAdapter` persists **nine** fields (`level`, `color`, `shoreDepthTransition.min/max`,
|
|
82
|
+
`shoreColor`, `waveSpeed`, `waveAmplitude`, `waveFrequency`, `scattering`). So a water body authored in a
|
|
83
|
+
blueprint silently gets default waves and shore colour, while the same entity saved and reloaded keeps them —
|
|
84
|
+
meaning the water looks *different after a save/load cycle than it did on first load of the level*.
|
|
85
|
+
`Light` diverges the same way (TG-2). Nothing distinguishes "field deliberately runtime-only" from
|
|
86
|
+
"field forgotten in one of the two writers".
|
|
87
|
+
- **Cheapest closing test**: one parameterised triple-round-trip harness driven by the registered-class list,
|
|
88
|
+
with an explicit opt-out list for genuinely transient fields. **~121 classes**, and it permanently pins the
|
|
89
|
+
two formats to each other.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### TG-4 21 binary format upgraders, zero specs — the backward-compatibility path for old saves is unverified
|
|
94
|
+
- **Severity**: CRITICAL
|
|
95
|
+
- **Area**: all `*SerializationUpgrader_*.js` / `*SerializationUpdater_*.js`
|
|
96
|
+
- **Gap**: `Tag_0_1`, `ClingToTerrain_0_1`, `Terrain_0_1`, `Terrain_1_2`, `Transform_0_1`,
|
|
97
|
+
`TopDownCameraController_0_1`, `Highlight_0_1`, `Water_0_1`, `ParticleEmitter_0_1`,
|
|
98
|
+
`GridPosition2Transform_0_1`, `GridPosition_0_1`, `Path_0_1`, `Path_1_2`, `PathFollower_0_1`, `_1_2`, `_2_3`,
|
|
99
|
+
`_3_4`, `RigidBody_0_1`, `SoundEmitter_0_1`, `_1_2` — none has a spec.
|
|
100
|
+
The *mechanism* is covered: `BinarySerializationRegistry.spec.js` tests chain search/shortest-path and
|
|
101
|
+
`BinaryCollectionSerialization.spec.js` runs a two-step chain, both with synthetic upgraders. **Not one real
|
|
102
|
+
upgrader is ever fed real old-version bytes.**
|
|
103
|
+
- **What could ship broken**: an upgrader's only contract is "read exactly the old layout, write exactly what
|
|
104
|
+
the *current* adapter's `deserialize` expects". That contract spans two files that are edited months apart.
|
|
105
|
+
Example: `GridPosition2TransformSerializationUpgrader_0_1` reads two `Float64`s and re-encodes them into the
|
|
106
|
+
v1 header+`Float32` form. The day the v1 adapter layout changes, the upgrader keeps emitting the old shape;
|
|
107
|
+
the adapter mis-parses it, the collection cursor desynchronises, and **every remaining component in the save
|
|
108
|
+
file is garbage**. Players with pre-existing saves get corruption or a load crash; CI is green because CI
|
|
109
|
+
only ever writes and reads the current version.
|
|
110
|
+
- **Cheapest closing test**: per-upgrader golden-bytes test — hand-construct the old-version buffer, run the
|
|
111
|
+
registered chain, deserialize with the current adapter, assert fields. **21 files, one table-driven spec.**
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### TG-5 Transient-entity and non-serializable-component exclusion is structurally unreachable by the suite
|
|
116
|
+
- **Severity**: CRITICAL
|
|
117
|
+
- **Area**: `engine/ecs/storage/BinaryBufferSerializer.js`, `engine/ecs/components/SerializationMetadata.js`
|
|
118
|
+
- **Gap**: `SerializationMetadata` appears in **zero** spec files anywhere in the tree. The serializer has two
|
|
119
|
+
gates: `isEntityTransient()` (skips entities flagged `SerializationFlags.Transient`, and components carrying
|
|
120
|
+
the `@serialization_transient` magic field) and `isComponentClassSerializable()` (`componentClass.serializable !== false`,
|
|
121
|
+
relied on by **15** classes: `Renderable`, `Input`, `InputController`, `Script`, `Timer`, `Attachment`,
|
|
122
|
+
`AttachmentSockets`, `ParentEntity`, `DynamicActor`, `Voice`, `PathDisplay`, `Trail2D`, `Trail3D`,
|
|
123
|
+
`ShadedGeometry`, `Transform2GridPosition`). The single end-to-end spec has neither a `SerializationMetadata`
|
|
124
|
+
component nor a `serializable = false` component, so **both predicates are constant over the entire suite**.
|
|
125
|
+
- **What could ship broken**: invert or short-circuit either gate and you get one of two silent catastrophes —
|
|
126
|
+
(a) every save file comes out empty or missing most entities, or (b) transient UI/preview/effect entities and
|
|
127
|
+
runtime-only components get baked into every save and **resurrect on load** (ghost HUD entities, orphaned
|
|
128
|
+
trails, duplicated input controllers), with save files growing every cycle. Neither turns a test red.
|
|
129
|
+
Also unreachable in the same 44-line spec: the multi-type path. With exactly one component type, the
|
|
130
|
+
"rewind and skip a type that wrote 0 components" branch and the `numTypesWritten` header patch-up at the end
|
|
131
|
+
of `process()` never execute, and the entity-delta encoding (`step = entity - lastEntity`) is only ever
|
|
132
|
+
exercised with a single entity, so any regression in gap encoding across sparse entity ids is invisible.
|
|
133
|
+
- **Cheapest closing test**: grow the existing end-to-end spec to ~6 cases — transient entity excluded,
|
|
134
|
+
`@serialization_transient` component excluded, `serializable = false` type excluded, a type with zero
|
|
135
|
+
surviving instances (header rewind), multiple types, and sparse entity ids. **1 file**, closes the whole
|
|
136
|
+
save-file-shape contract.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### TG-6 Engine startup/shutdown: the only test is "constructor does not throw"
|
|
141
|
+
- **Severity**: CRITICAL
|
|
142
|
+
- **Area**: `engine/Engine.js` (682 lines) / `engine/Engine.spec.js` (15 lines), `engine/plugin/EnginePluginManager.js` (319 lines, no spec), `engine/EngineHarness.js` (548 lines, referenced by zero specs)
|
|
143
|
+
- **Gap**: `Engine.start()` and `Engine.stop()` are **never invoked by any test**. `EngineHarness` — the
|
|
144
|
+
in-repo helper built for exactly this — is used only by `editor/prototype*` files and `LevelGenerator.js`,
|
|
145
|
+
never by a spec. `EnginePluginManager` (dependency-ordered plugin startup/shutdown, recursive dependency
|
|
146
|
+
transition, reference counting) has no spec at all; only the `EnginePlugin` base class does.
|
|
147
|
+
- **What could ship broken**:
|
|
148
|
+
- `start()` fans out four concurrent branches (`sound.start().then(entityManager.startup)`,
|
|
149
|
+
`staticKnowledge.load`, `gui.startup`, `plugins.startup`) and its rejection handler is
|
|
150
|
+
`function (e) { logger.error('Engine Failed to start: ' + e); }` — **the returned promise resolves on
|
|
151
|
+
failure**. Any branch regressing to a rejection leaves a half-started engine while every caller proceeds
|
|
152
|
+
as though startup succeeded. Nothing asserts that `start()` rejects on failure.
|
|
153
|
+
- `stop()` never cancels the `requestAnimationFrame` loop that `start()` begins, so rendering continues
|
|
154
|
+
after shutdown against torn-down state. Nothing asserts otherwise.
|
|
155
|
+
- The shutdown *ordering* (entityManager → plugins → gui → assetManager, and ticker paused first) is pure
|
|
156
|
+
convention. Reordering it — e.g. shutting down `assetManager` before plugins that still hold assets — is
|
|
157
|
+
a green change.
|
|
158
|
+
- `EnginePluginManager` carries an in-code `TODO address cyclic dependencies, these can cause a deadlock`.
|
|
159
|
+
A cyclic plugin graph hangs startup forever, and because `start()` swallows, it hangs *silently* with no
|
|
160
|
+
error anywhere.
|
|
161
|
+
- **Cheapest closing test**: one spec on the already-imported `InMemoryEnginePlatform` with stub plugins:
|
|
162
|
+
`start()` resolves only once every plugin reaches Running; `start()` **rejects** when a plugin's startup
|
|
163
|
+
throws; `stop()` transitions plugins in reverse and stops the frame loop; a cyclic plugin dependency fails
|
|
164
|
+
rather than hangs. **~5 files** (`Engine`, `EnginePluginManager`, `PluginReferenceContext`, `EngineBootstrapper`,
|
|
165
|
+
`EngineConfiguration`), and it makes `EngineHarness` a tested entry point for everything downstream.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### TG-7 `link`/`unlink` symmetry is never asserted for any System, despite the base class documenting it as the contract
|
|
170
|
+
- **Severity**: MAJOR
|
|
171
|
+
- **Area**: `engine/ecs/System.js` and 70 `*System.js` implementations (28 have an adjacent spec)
|
|
172
|
+
- **Gap**: `System.js`'s class JSDoc spells the contract out with a worked example — whatever `link()` attaches,
|
|
173
|
+
`unlink()` must detach. No test anywhere enforces it. The 42 systems with no spec include precisely the ones
|
|
174
|
+
that mutate the three.js scene graph, the DOM, or long-lived signal lists on link:
|
|
175
|
+
`engine/ecs/renderable/RenderSystem.js`, `engine/graphics/ecs/mesh/MeshSystem.js`,
|
|
176
|
+
`engine/graphics/ecs/mesh-v2/aggregate/SGMeshSystem.js`, `engine/graphics/ecs/light/LightSystem.js`,
|
|
177
|
+
`engine/graphics/ecs/camera/CameraSystem.js` and `topdown/TopDownCameraControllerSystem.js`,
|
|
178
|
+
`engine/graphics/ecs/water/WaterSystem.js`, `engine/ecs/gui/GUIElementSystem.js`,
|
|
179
|
+
`engine/ecs/terrain/ecs/TerrainSystem.js`, `engine/ecs/fow/FogOfWarSystem.js`,
|
|
180
|
+
`engine/ecs/transform-attachment/TransformAttachmentSystem.js`, `engine/ecs/systems/AnimationSystem.js`,
|
|
181
|
+
`engine/graphics/ecs/animation/AnimationControllerSystem.js` and `animator/AnimationGraphSystem.js`,
|
|
182
|
+
`engine/ecs/tooltip/TooltipComponentSystem.js`, `engine/ecs/ik/InverseKinematicsSystem.js`,
|
|
183
|
+
`engine/input/ecs/systems/InputSystem.js`, `engine/sound/ecs/SoundListenerSystem.js`,
|
|
184
|
+
`engine/grid/position/GridPositionSystem.js`, `engine/grid/grid2transform/GridPosition2TransformSystem.js`.
|
|
185
|
+
- **What could ship broken**: delete a single `signal.remove(...)`, `scene.remove(object3d)`, or
|
|
186
|
+
`map.delete(entity)` from any `unlink` and the suite stays green while the game leaks one listener or one
|
|
187
|
+
scene-graph node **per entity per spawn**. That is the classic long-session symptom set — memory growth,
|
|
188
|
+
ghost geometry that keeps rendering after the entity dies, and stale callbacks firing against destroyed
|
|
189
|
+
components — and it is exactly the failure mode a unit suite is supposed to catch cheaply. Currently
|
|
190
|
+
nothing does; it only becomes visible in a soak.
|
|
191
|
+
- **Cheapest closing test**: one generic harness — for a given system, build a real `EntityComponentDataset`,
|
|
192
|
+
create an entity carrying the system's declared `dependencies` tuple, snapshot `hasHandlers()` on every
|
|
193
|
+
`Signal` reachable from the component instances plus the sizes of the system's own containers, link, then
|
|
194
|
+
remove a component and assert the snapshot is restored. One table entry per system.
|
|
195
|
+
**Covers the 42 currently unspecced systems**, and pins the contract for the 28 that do have specs.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### TG-8 `AbstractContextSystem`'s pooled per-entity context is never recycled under test — and it does not reset
|
|
200
|
+
- **Severity**: MAJOR
|
|
201
|
+
- **Area**: `engine/ecs/system/AbstractContextSystem.js`, `engine/ecs/system/SystemEntityContext.js` (both 0 specs)
|
|
202
|
+
- **Gap**: These two classes implement the pooled per-entity context used by 7 systems (`LightSystem`,
|
|
203
|
+
`ParentEntitySystem`, `DynamicActorSystem`, `VoiceSystem`, `FPDecalSystem`, `PathDisplaySystem`,
|
|
204
|
+
`SpriteSystemPE`). No test links an entity, unlinks it, and links a *second* entity on one of these systems,
|
|
205
|
+
so the pool never actually recycles anywhere in the suite — the reuse path is structurally unreachable.
|
|
206
|
+
- **What could ship broken**: the `ObjectPoolFactory` reset hook is an empty `// TODO` and the destroy hook is
|
|
207
|
+
`(ctx) => { //ctx.unlink(); }`. `ctx.components` is never truncated on release, so a context recycled from a
|
|
208
|
+
3-component tuple onto a 2-component tuple still holds the **previous entity's** third component at
|
|
209
|
+
`components[2]`. Any context that reads a positional component defensively operates on a live object
|
|
210
|
+
belonging to a dead entity — mis-attributed lights, decals bound to the wrong transform, voices playing at
|
|
211
|
+
a stale position. `SystemEntityContext.link()` is also guarded by `__is_linked`; if a release path ever
|
|
212
|
+
skips `unlink()`, the recycled context silently **never links at all** and the entity is invisible to the
|
|
213
|
+
system with no error.
|
|
214
|
+
- **Cheapest closing test**: one spec with a trivial context class — link A, unlink A, link B with a shorter
|
|
215
|
+
tuple, assert the recycled context carries no state from A and is properly linked. **Base class + 7 subclasses.**
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### TG-9 `view/View.spec.js` is a 6-line smoke test standing in for the entire view lifecycle
|
|
220
|
+
- **Severity**: MAJOR
|
|
221
|
+
- **Area**: `view/View.js` (804 lines) and 54 `View` subclasses
|
|
222
|
+
- **Gap**: The whole spec is `test("constructor does not throw", ...)`. `View.js` implements a real lifecycle:
|
|
223
|
+
`link()`/`unlink()` walk both `bindings` and `children`; `addChild` links eagerly if the parent is already
|
|
224
|
+
linked; `removeChild` detaches the element **and** unlinks the child; `destroy()` asserts not-linked and
|
|
225
|
+
cascades to children; `addBinding`/`removeBinding` link and unlink `SignalBinding`s. None of it is asserted.
|
|
226
|
+
Zero specs across `view/elements` (34 files), `view/minimap` (12), `view/controller` (10), `view/common` (9 of 10),
|
|
227
|
+
`view/currency` (6), `view/interaction` (5).
|
|
228
|
+
This is **not** a "the DOM isn't testable here" situation: `jest-environment-jsdom@29.7.0` is installed and
|
|
229
|
+
**34 specs already opt in** via the `@jest-environment jsdom` docblock — including `engine/Engine.spec.js` itself.
|
|
230
|
+
- **What could ship broken**: break the unlink cascade over `children` (an early return, a wrong loop bound,
|
|
231
|
+
an exception in one child aborting the rest) and every nested view keeps its signal subscriptions alive
|
|
232
|
+
after being removed from the DOM. That is the "HUD updates a panel that no longer exists" crash and a leak
|
|
233
|
+
of one subscription tree per screen transition. Equally invisible: `addChild` on an already-linked parent
|
|
234
|
+
failing to link the child, so a view renders but never updates.
|
|
235
|
+
- **Cheapest closing test**: one jsdom spec on the base `View` — link/unlink symmetry over bindings and
|
|
236
|
+
children, addChild/removeChild while linked and while unlinked, binding add/remove, destroy-while-linked
|
|
237
|
+
assertion. **Pins the contract for all 54 subclasses** in one file.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### TG-10 Eleven of twelve asset loaders are untested; the AssetManager contract is only ever proven against stubs
|
|
242
|
+
- **Severity**: MAJOR
|
|
243
|
+
- **Area**: `engine/asset/loaders/**`
|
|
244
|
+
- **Gap**: `engine/asset/AssetManager.spec.js` is a genuine strength — 33 tests covering loader failure and
|
|
245
|
+
retry, missing loader type, throwing requester, transformers (throwing / invalid result), nested loads,
|
|
246
|
+
dependency recording, cycles, `load_concurrency=1` deadlock, `skip_queue`, priority, insert-during-in-flight,
|
|
247
|
+
alias resolution, idle executor, and graceful vs immediate shutdown. **Every one of those tests uses a stub
|
|
248
|
+
loader.** No real loader is ever checked against that contract: `ArrayBufferLoader`, `GLTFAssetLoader`,
|
|
249
|
+
`JsonAssetLoader`, `TextAssetLoader`, `SVGAssetLoader`, `SoundAssetLoader`, `JavascriptAssetLoader`,
|
|
250
|
+
`FontAssetLoader`, `TextureAssetLoader`, `AttachmentSocketsAssetLoader`,
|
|
251
|
+
`AnimationGraphDefinitionAssetLoader` (only `ImageRGBADataLoader` has a spec).
|
|
252
|
+
- **What could ship broken**: the contract AssetManager relies on is "call exactly one of `callback`/`failure`,
|
|
253
|
+
exactly once". `TextAssetLoader` passes `failure` straight through to `xhr`; `JsonAssetLoader` catches
|
|
254
|
+
`JSON.parse` errors and calls `failure`. Drop the `failure` argument in either — a one-token change — and a
|
|
255
|
+
404 or a truncated JSON file produces a request that **never settles**: the loading screen hangs at N% with
|
|
256
|
+
no error, forever. AssetManager's excellent hang-detection tests cannot see it because they never run a real
|
|
257
|
+
loader. `resolvePath` / rootPath-applied-once is likewise only asserted for the stub.
|
|
258
|
+
- **Cheapest closing test**: one spec per loader with a mocked `xhr`/`fetch` asserting exactly one settlement
|
|
259
|
+
for each of success, HTTP error, and malformed payload. **11 files.**
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### TG-11 Signal has nine hand-copied dispatch implementations; only `dispatch()` is behaviourally tested
|
|
264
|
+
- **Severity**: MAJOR
|
|
265
|
+
- **Area**: `core/events/signal/Signal.js` (818 lines) / `Signal.spec.js`
|
|
266
|
+
- **Gap**: `dispatch`, `send0`, `send1`, `send2`, `send3`, `send4`, `send6`, `send8` (and the internal variant)
|
|
267
|
+
each **independently re-implement** the same three behaviours: the `SignalFlags.Silent` early-out (13
|
|
268
|
+
occurrences), the `generation` guard that skips handlers added during the current dispatch (9 occurrences),
|
|
269
|
+
and the `RemoveAfterExecution` removal that makes `addOne` fire once (10 occurrences).
|
|
270
|
+
`Signal.spec.js` tests once-semantics, mute/unmute, and the add-during-dispatch guard **exclusively through
|
|
271
|
+
`dispatch()`**. `send0`–`send8` are covered only for argument forwarding (`expect(call_param_list).toEqual([...])`).
|
|
272
|
+
- **What could ship broken**: delete the `RemoveAfterExecution` branch from `send1` and every `addOne` handler
|
|
273
|
+
on a one-argument signal fires forever — that includes the engine tick path (`engine.ticker.onTick.send1(dt)`),
|
|
274
|
+
so a "run once next frame" callback becomes a permanent per-frame callback. Delete the `Silent` check from
|
|
275
|
+
`send2` and muting stops working for two-argument signals. Both are green.
|
|
276
|
+
Two further contracts are asserted nowhere at all, for any variant:
|
|
277
|
+
(a) **removal during dispatch** — each loop caches `const next = _h.next` *before* invoking the handler, so a
|
|
278
|
+
handler that removes the next entry of a same-function/different-context chain still causes it to be invoked;
|
|
279
|
+
(b) **error isolation** — every loop wraps the handler call in `try { … } catch (e) { console.error(…) }`.
|
|
280
|
+
"One throwing listener must not stop the remaining listeners" is load-bearing engine-wide (Signal is *the*
|
|
281
|
+
eventing primitive) and no test pins it, so removing or narrowing that catch would let a single bad listener
|
|
282
|
+
silently kill every downstream listener for that frame.
|
|
283
|
+
- **Cheapest closing test**: one parameterised spec running the same five behavioural assertions across all
|
|
284
|
+
nine dispatch entry points. **1 file, 9 divergent code paths.**
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### TG-12 Grid↔world conversion: only one direction is tested, and the round trip never is
|
|
289
|
+
- **Severity**: MAJOR
|
|
290
|
+
- **Area**: `engine/grid/**` (19 files, 4 specs)
|
|
291
|
+
- **Gap**: `transform2grid/Transform2GridPositionSystem.spec.js` covers world→grid. The inverse —
|
|
292
|
+
`grid2transform/GridPosition2Transform.js` and `GridPosition2TransformSystem.js` — has no spec, and no test
|
|
293
|
+
anywhere composes the two. `area/AreaTrigger.js`, `TriggerAction.js` and `TriggerActionType.js` have none either.
|
|
294
|
+
- **What could ship broken**: a half-cell offset, an axis swap, or a sign error in `GridPosition2TransformSystem`
|
|
295
|
+
places every unit's mesh away from the cell the simulation believes it occupies. The logical grid stays
|
|
296
|
+
perfectly self-consistent, so pathfinding, obstacle and combat tests all still pass — but click-selection,
|
|
297
|
+
move targets and range indicators all address the wrong tile. It is visible only on screen.
|
|
298
|
+
`AreaTrigger` is a `BitSet` mask plus a `size` and an action list: a trigger that fires every frame instead
|
|
299
|
+
of once on entry, or that misses the last row/column because of an off-by-one in the mask index, ships green.
|
|
300
|
+
- **Cheapest closing test**: a `grid → world → grid` identity spec over a **non-square** grid with a non-zero
|
|
301
|
+
`offset` (1 file), plus an `AreaTrigger` enter/exit edge + mask-bounds spec (2 files).
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
### TG-13 `ObservedValue` and `ObservedEnum` have no spec while their narrower siblings do
|
|
306
|
+
- **Severity**: MAJOR
|
|
307
|
+
- **Area**: `core/model/`
|
|
308
|
+
- **Gap**: `ObservedBoolean`, `ObservedInteger`, `ObservedString` and `BooleanVector3` all have specs.
|
|
309
|
+
The generic `ObservedValue.js` — used for `System.state`, plugin process state, and pervasively across
|
|
310
|
+
graphics and UI — plus `ObservedEnum.js` (e.g. `Light.type`) and `DebouncedObservedBoolean.js` have none.
|
|
311
|
+
They are separate classes, not subclasses, so the sibling specs give them no coverage.
|
|
312
|
+
- **What could ship broken**: `ObservedValue.set` guards on `this.__value !== value` before
|
|
313
|
+
`onChanged.send2(value, oldValue)`. Remove the guard and every no-op write re-dispatches — systems that
|
|
314
|
+
react to state changes by relinking (the plugin state machine; `LightSystem` rebuilding a three.js light
|
|
315
|
+
when `Light.type` changes) start thrashing every frame, a pure-performance regression with no functional
|
|
316
|
+
symptom to trip a test. Drop `oldValue` from the dispatch and every two-argument observer silently receives
|
|
317
|
+
`undefined` for the previous value — the "changed from X to Y" branches all take the wrong path.
|
|
318
|
+
`setSilent`'s no-dispatch guarantee, and `process()`'s fire-immediately-then-on-change behaviour, are
|
|
319
|
+
likewise unasserted.
|
|
320
|
+
- **Cheapest closing test**: one shared behavioural suite (equal-value set does not dispatch; set dispatches
|
|
321
|
+
`(new, old)`; `setSilent` does not dispatch; `process` fires immediately and on change; unsubscribe stops
|
|
322
|
+
delivery) run over all six observed types. **6 files.**
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
### TG-14 Sampler rectangle/blit operations behind the texture atlas and terrain overlay have no tests
|
|
327
|
+
- **Severity**: MAJOR
|
|
328
|
+
- **Area**: `engine/graphics/texture/sampler/`
|
|
329
|
+
- **Gap**: Worth stating what is *not* wrong here first: the resize family is well covered and its specs
|
|
330
|
+
deliberately use asymmetric `3×1` and `1×3` inputs (`resize/sampler2d_scale.spec.js`,
|
|
331
|
+
`sampler2d_scale_down_linear.spec.js`), so width/height transpositions in the scaling path **would** be caught.
|
|
332
|
+
The rectangle-copy family has no spec at all: `sampler2d_sub_copy_same_item_size.js` (used by
|
|
333
|
+
`engine/graphics/texture/atlas/TextureAtlas.js` and `engine/graphics/material/optimization/MaterialOptimizationContext.js`),
|
|
334
|
+
`sampler2d_copy_channel_data.js` (same optimizer), `sampler2d_paint.js` (used by
|
|
335
|
+
`engine/ecs/terrain/overlay/TerrainOverlay.js`), `sampler2d_copy_rectangle.js`, and `sampler2d_combine.js`.
|
|
336
|
+
- **What could ship broken**: an x/y or width/height transposition, or an off-by-one in the destination stride,
|
|
337
|
+
in `sampler2d_sub_copy_same_item_size` writes every atlas tile rotated, offset, or bleeding into its
|
|
338
|
+
neighbour. Every material that passes through atlas optimisation then renders with wrong texture content —
|
|
339
|
+
and the only thing that would notice is a human looking at the screen. The same class of defect in
|
|
340
|
+
`sampler2d_paint` corrupts terrain overlay brushes.
|
|
341
|
+
- **Cheapest closing test**: one spec per op using a **non-square** source, a differently-shaped destination,
|
|
342
|
+
and an off-origin destination rectangle. **5 files.**
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Considered and deliberately not reported
|
|
347
|
+
|
|
348
|
+
- **`engine/metrics` (0/6)** — `GlobalMetrics`/`ProxyMetricsGateway`/`GoogleAnalyticsMetrics`/`NullMetricsGateway`
|
|
349
|
+
are analytics plumbing. A break loses telemetry, not gameplay or data. Low blast radius; not worth a test.
|
|
350
|
+
- **`core/debug` (2/23), `core/function` (2/15)** — matcher/description helpers and one-line combinators
|
|
351
|
+
(`noop`, `returnTrue`, `passThrough`). Trivial pure helpers, explicitly out of scope.
|
|
352
|
+
- **Behaviour trees (`engine/intelligence/behavior`)** — looks thin by package count but is actually well
|
|
353
|
+
covered: `Behavior`, `SelectorBehavior`, `ParallelBehavior`, `SequenceBehavior`, `RepeatBehavior`,
|
|
354
|
+
`OverrideContextBehavior`, `ActionBehavior`, `BranchBehavior`, `ConditionBehavior`, `WeightedRandomBehavior`,
|
|
355
|
+
`DieBehavior`, `SendEventBehavior`, `BehaviorComponent` and `BehaviorSystem` all have specs. Status
|
|
356
|
+
propagation is genuinely exercised. Not a gap.
|
|
357
|
+
- **`engine/animation` curve/clip layer** — 17 specs covering curves, keyframes, tracks, clip bindings,
|
|
358
|
+
fitting, subdivision, optimisation, compression and quaternion hemisphere alignment. Solid.
|
|
359
|
+
- **`AssetManager` itself** — see TG-10; the manager is one of the best-tested things in the tree. The gap is
|
|
360
|
+
strictly the concrete loaders underneath it.
|
|
361
|
+
- **`EntityManager` / `EntityComponentDataset` / `EntityObserver`** — 31, 55 and 9 tests respectively, covering
|
|
362
|
+
startup/shutdown states, dataset attach/detach, observer link/unlink, exception handling in update, fixed-step
|
|
363
|
+
catch-up capping, and dependency-less systems. Genuinely strong; the ECS *core* is not the gap — the systems
|
|
364
|
+
built on it are (TG-7, TG-8).
|