@hology/core 0.0.260 → 0.0.262

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 (62) hide show
  1. package/dist/gameplay/actors/actor.d.ts +4 -0
  2. package/dist/gameplay/actors/actor.js +1 -1
  3. package/dist/gameplay/actors/builtin/components/actor-transform-persistence.d.ts +29 -0
  4. package/dist/gameplay/actors/builtin/components/actor-transform-persistence.js +4 -0
  5. package/dist/gameplay/ai/navigation.js +1 -1
  6. package/dist/gameplay/index.d.ts +3 -0
  7. package/dist/gameplay/index.js +1 -1
  8. package/dist/gameplay/initiate.d.ts +4 -0
  9. package/dist/gameplay/initiate.js +1 -1
  10. package/dist/gameplay/inject.d.ts +8 -0
  11. package/dist/gameplay/inject.js +1 -1
  12. package/dist/gameplay/net/service/net-decorator.d.ts +9 -5
  13. package/dist/gameplay/net/service/net-decorator.js +1 -1
  14. package/dist/gameplay/net/service/net-service.js +1 -1
  15. package/dist/gameplay/services/actor-persistence.d.ts +69 -0
  16. package/dist/gameplay/services/actor-persistence.js +4 -0
  17. package/dist/gameplay/services/asset-loader.d.ts +3 -0
  18. package/dist/gameplay/services/asset-loader.js +1 -1
  19. package/dist/gameplay/services/render.d.ts +1 -1
  20. package/dist/gameplay/services/scene-transition.js +1 -1
  21. package/dist/gameplay/services/world.d.ts +10 -1
  22. package/dist/gameplay/services/world.js +1 -1
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -1
  25. package/dist/rendering/graphics-runtime.d.ts +1 -1
  26. package/dist/rendering/graphics-runtime.js +1 -1
  27. package/dist/rendering/node-material-webgpu-resolver.js +1 -1
  28. package/dist/rendering/webgpu-experimental-backend.js +1 -1
  29. package/dist/rendering.d.ts +6 -1
  30. package/dist/rendering.js +1 -1
  31. package/dist/scene/asset-resource-loader.d.ts +3 -0
  32. package/dist/scene/asset-resource-loader.js +1 -1
  33. package/dist/scene/bootstrap.d.ts +2 -0
  34. package/dist/scene/bootstrap.js +1 -1
  35. package/dist/scene/custom-param-deserialize.js +1 -1
  36. package/dist/scene/custom-param-runtime-types.js +1 -1
  37. package/dist/scene/lattice-deformation.d.ts +28 -0
  38. package/dist/scene/lattice-deformation.js +4 -0
  39. package/dist/scene/materializer.d.ts +22 -3
  40. package/dist/scene/materializer.js +1 -1
  41. package/dist/scene/model-ref.d.ts +7 -0
  42. package/dist/scene/model-ref.js +4 -0
  43. package/dist/scene/model.d.ts +2 -1
  44. package/dist/scene/model.js +1 -1
  45. package/dist/shader/parameter.d.ts +2 -1
  46. package/dist/shader-nodes/pom.js +1 -1
  47. package/dist/test/actor-persistence-runtime.test.d.ts +2 -0
  48. package/dist/test/actor-persistence-runtime.test.js +4 -0
  49. package/dist/test/actor-persistence.test.d.ts +2 -0
  50. package/dist/test/actor-persistence.test.js +4 -0
  51. package/dist/test/actor-transform-persistence.test.d.ts +2 -0
  52. package/dist/test/actor-transform-persistence.test.js +4 -0
  53. package/dist/test/graphics-runtime.test.js +1 -1
  54. package/dist/test/lattice-deformation.test.d.ts +2 -0
  55. package/dist/test/lattice-deformation.test.js +4 -0
  56. package/dist/test/materializer-prefetch.test.js +1 -1
  57. package/dist/test/prefab-instance-params.test.js +1 -1
  58. package/dist/test/world-lifecycle.test.js +1 -1
  59. package/dist/test/world-prefab-spawn.test.js +1 -1
  60. package/docs/actor-persistence.md +87 -0
  61. package/package.json +1 -1
  62. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,7 @@
1
+ import type { AssetId } from './model.js';
2
+ /** Stable identity of a model asset; load an instance when it is needed. */
3
+ export declare class ModelRef {
4
+ readonly id: AssetId;
5
+ constructor(id: AssetId);
6
+ }
7
+ //# sourceMappingURL=model-ref.d.ts.map
@@ -0,0 +1,4 @@
1
+ export class ModelRef{constructor(o){this.id=o}}/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -51,7 +51,8 @@ export declare enum SerializedParamType {
51
51
  Struct = 29,
52
52
  DataAsset = 30,
53
53
  SequenceData = 31,
54
- Scene = 32
54
+ Scene = 32,
55
+ ModelRef = 33
55
56
  }
56
57
  export type VfxInputBinding = {
57
58
  type: 'vfxInput';
@@ -1,4 +1,4 @@
1
- export var DetailTier;!function(e){e[e.low=0]="low",e[e.medium=1]="medium",e[e.high=2]="high"}(DetailTier||(DetailTier={}));export var SerializedParamType;!function(e){e[e.FloatNode=0]="FloatNode",e[e.Number=1]="Number",e[e.Texture=2]="Texture",e[e.Sampler2DNode=3]="Sampler2DNode",e[e.Boolean=4]="Boolean",e[e.BooleanNode=5]="BooleanNode",e[e.Vector2=6]="Vector2",e[e.Vec2Node=7]="Vec2Node",e[e.Vector3=8]="Vector3",e[e.Vec3Node=9]="Vec3Node",e[e.Color=10]="Color",e[e.RgbNode=11]="RgbNode",e[e.String=12]="String",e[e.BaseActor=13]="BaseActor",e[e.Euler=14]="Euler",e[e.Object3D=15]="Object3D",e[e.Material=16]="Material",e[e.AudioBuffer=17]="AudioBuffer",e[e.Vector4=18]="Vector4",e[e.Vec4Node=19]="Vec4Node",e[e.VisualEffect=20]="VisualEffect",e[e.Array=21]="Array",e[e.Curve=22]="Curve",e[e.ColorLayer=23]="ColorLayer",e[e.MaskLayer=24]="MaskLayer",e[e.Prefab=25]="Prefab",e[e.PrefabActor=26]="PrefabActor",e[e.AnimationClip=27]="AnimationClip",e[e.Sequence=28]="Sequence",e[e.Struct=29]="Struct",e[e.DataAsset=30]="DataAsset",e[e.SequenceData=31]="SequenceData",e[e.Scene=32]="Scene"}(SerializedParamType||(SerializedParamType={}));/*
1
+ export var DetailTier;!function(e){e[e.low=0]="low",e[e.medium=1]="medium",e[e.high=2]="high"}(DetailTier||(DetailTier={}));export var SerializedParamType;!function(e){e[e.FloatNode=0]="FloatNode",e[e.Number=1]="Number",e[e.Texture=2]="Texture",e[e.Sampler2DNode=3]="Sampler2DNode",e[e.Boolean=4]="Boolean",e[e.BooleanNode=5]="BooleanNode",e[e.Vector2=6]="Vector2",e[e.Vec2Node=7]="Vec2Node",e[e.Vector3=8]="Vector3",e[e.Vec3Node=9]="Vec3Node",e[e.Color=10]="Color",e[e.RgbNode=11]="RgbNode",e[e.String=12]="String",e[e.BaseActor=13]="BaseActor",e[e.Euler=14]="Euler",e[e.Object3D=15]="Object3D",e[e.Material=16]="Material",e[e.AudioBuffer=17]="AudioBuffer",e[e.Vector4=18]="Vector4",e[e.Vec4Node=19]="Vec4Node",e[e.VisualEffect=20]="VisualEffect",e[e.Array=21]="Array",e[e.Curve=22]="Curve",e[e.ColorLayer=23]="ColorLayer",e[e.MaskLayer=24]="MaskLayer",e[e.Prefab=25]="Prefab",e[e.PrefabActor=26]="PrefabActor",e[e.AnimationClip=27]="AnimationClip",e[e.Sequence=28]="Sequence",e[e.Struct=29]="Struct",e[e.DataAsset=30]="DataAsset",e[e.SequenceData=31]="SequenceData",e[e.Scene=32]="Scene",e[e.ModelRef=33]="ModelRef"}(SerializedParamType||(SerializedParamType={}));/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -7,11 +7,12 @@ import type { VisualEffect } from '../effects/vfx/vfx-param.js';
7
7
  import type { Prefab, PrefabOf } from '../scene/objects/prefab.js';
8
8
  import type { DataAssetRef } from '../scene/objects/data-asset.js';
9
9
  import type { SceneRef } from '../scene/scene-ref.js';
10
+ import type { ModelRef } from '../scene/model-ref.js';
10
11
  import type { Curve2 } from '../utils/curve.js';
11
12
  import type { ColorLayer, MaskLayer } from './color-layer.js';
12
13
  import type { Sequence } from '../effects/sequence/sequence-data.js';
13
14
  import type { ShaderGraphTarget } from './graph/model.js';
14
- export type KnownParameterType = Type<FloatNode | Number | Texture | Sampler2DNode | Boolean | BooleanNode | Vector2 | Vec2Node | Vector3 | Vec3Node | Vector4 | Vec4Node | Color | RgbNode | String | BaseActor | Euler | Object3D | Material | AnimationClip | VisualEffect | Sequence | Prefab | PrefabOf<BaseActor> | DataAssetRef | SceneRef | Curve2 | ColorLayer | MaskLayer>;
15
+ export type KnownParameterType = Type<FloatNode | Number | Texture | Sampler2DNode | Boolean | BooleanNode | Vector2 | Vec2Node | Vector3 | Vec3Node | Vector4 | Vec4Node | Color | RgbNode | String | BaseActor | Euler | Object3D | Material | AnimationClip | VisualEffect | Sequence | Prefab | PrefabOf<BaseActor> | DataAssetRef | SceneRef | ModelRef | Curve2 | ColorLayer | MaskLayer>;
15
16
  export type ParameterType = Type<any> | AbstractType<any>;
16
17
  export type ParameterTypeResolver = () => ParameterType;
17
18
  export type ParameterOption<T> = {
@@ -1,4 +1,4 @@
1
- import{abs as e,attributes as t,cross as n,dFdx as r,dFdy as a,float as i,normalize as l,select as s,transformed as p,UniformSampler2dArraySlice as f,uniforms as $,varying as o,Vec2Node as v,vec3 as u,vec4 as m}from"three-shader-graph";import{isWgslCompiler as h,isWgslFragmentCompiler as d}from"three-shader-graph";import{glslFunction as c}from"./glsl-node.js";export function computeTangent(e,t,i){const s=r(t),p=a(t),f=r(i),$=a(i),o=n(p,e),v=n(e,s),u=o.multiplyScalar(f.x).add(v.multiplyScalar($.x));return m(l(u),1)}export function fallbackTangent(t){const r=e(t.y),a=s(r.lt(i(.999)),u(0,1,0),u(1,0,0));return l(n(a,t))}const g=t.normal,S=o(l($.normalMatrix.multiplyVec(g))),x=o(p.mvPosition.xyz.multiplyScalar(-1));export function parallaxOcclusionMapping(e,t,n,r=8,a=24){return new y(e,t,n,r,a)}class y extends v{constructor(e,t,n,r,a){super(),this.uv=e,this.heightMap=t,this.heightScale=n,this.minLayers=r,this.maxLayers=a}compile(e){if(!h(e))return{out:e.get(this.compileGlsl())};if(!d(e))throw new Error("Parallax occlusion mapping requires fragment-stage derivatives in WGSL.");const t=`pom_${e.variable()}`,n=e.get(this.uv),r=e.get(this.heightMap),a=e.get(this.heightScale),l=e.get(S),s=e.get(x),p=e.get(i(this.minLayers)),f=e.get(i(this.maxLayers)),$=C(this.heightMap)?e.get(L(this.heightMap)):null;return{chunk:`\n let ${t}Uv = ${n};\n let ${t}Normal = normalize(${l});\n let ${t}ViewPosition = ${s};\n let ${t}ViewDirection = normalize(${t}ViewPosition);\n let ${t}Dp1 = dpdx(-${t}ViewPosition);\n // WGSL fragment coordinates have the opposite Y orientation from GLSL.\n // Negate both Y derivatives so the reconstructed cotangent frame keeps\n // the same handedness as the GLSL implementation.\n let ${t}Dp2 = -dpdy(-${t}ViewPosition);\n let ${t}Duv1 = dpdx(${t}Uv);\n let ${t}Duv2 = -dpdy(${t}Uv);\n let ${t}Dp2Perp = cross(${t}Dp2, ${t}Normal);\n let ${t}Dp1Perp = cross(${t}Normal, ${t}Dp1);\n let ${t}T = ${t}Dp2Perp * ${t}Duv1.x + ${t}Dp1Perp * ${t}Duv2.x;\n let ${t}B = ${t}Dp2Perp * ${t}Duv1.y + ${t}Dp1Perp * ${t}Duv2.y;\n let ${t}InvMax = inverseSqrt(max(dot(${t}T, ${t}T), dot(${t}B, ${t}B)));\n let ${t}Tbn = mat3x3<f32>(\n ${t}T * ${t}InvMax,\n ${t}B * ${t}InvMax,\n ${t}Normal\n );\n let ${t}Vv = -(transpose(${t}Tbn) * ${t}ViewDirection);\n let ${t}SafeVvZ = select(\n ${t}Vv.z,\n select(-0.0001, 0.0001, ${t}Vv.z >= 0.0),\n abs(${t}Vv.z) < 0.0001\n );\n let ${t}ParallaxLimit = (-length(${t}Vv.xy) / ${t}SafeVvZ) * ${a};\n let ${t}OffsetDirection = normalize(${t}Vv.xy);\n let ${t}MaxOffset = ${t}OffsetDirection * ${t}ParallaxLimit;\n let ${t}NumSamples = max(\n 1.0,\n mix(${f}, ${p}, dot(-${t}ViewDirection, ${t}Normal))\n );\n let ${t}StepSize = 1.0 / ${t}NumSamples;\n var ${t}CurrentRayHeight = 1.0;\n var ${t}CurrentOffset = vec2<f32>(0.0);\n var ${t}LastOffset = vec2<f32>(0.0);\n var ${t}LastSampledHeight = 1.0;\n var ${t}CurrentSampledHeight = 1.0;\n for (var ${t}SampleIndex: i32 = 0; ${t}SampleIndex < 24; ${t}SampleIndex = ${t}SampleIndex + 1) {\n if (f32(${t}SampleIndex) > ${t}NumSamples) {\n break;\n }\n ${t}CurrentSampledHeight = ${null==$?`textureSampleLevel(${r}Texture, ${r}Sampler, ${t}Uv + ${t}CurrentOffset, 0.0).r`:`textureSampleLevel(${r}Texture, ${r}Sampler, ${t}Uv + ${t}CurrentOffset, i32(${$}), 0.0).r`};\n if (${t}CurrentSampledHeight > ${t}CurrentRayHeight) {\n let ${t}Delta1 = ${t}CurrentSampledHeight - ${t}CurrentRayHeight;\n let ${t}Delta2 = (${t}CurrentRayHeight + ${t}StepSize) - ${t}LastSampledHeight;\n let ${t}Ratio = ${t}Delta1 / (${t}Delta1 + ${t}Delta2);\n ${t}CurrentOffset = ${t}Ratio * ${t}LastOffset + (1.0 - ${t}Ratio) * ${t}CurrentOffset;\n break;\n }\n ${t}CurrentRayHeight = ${t}CurrentRayHeight - ${t}StepSize;\n ${t}LastOffset = ${t}CurrentOffset;\n ${t}CurrentOffset = ${t}CurrentOffset + ${t}StepSize * ${t}MaxOffset;\n ${t}LastSampledHeight = ${t}CurrentSampledHeight;\n }\n let ${t}UvWithOffset = ${t}Uv + ${t}CurrentOffset;\n `,out:`${t}UvWithOffset`}}compileGlsl(){const e=C(this.heightMap)?L(this.heightMap):null,t=null==e?"texture2D(heightMap, uv + vCurrOffset).r":"texture(heightMap, vec3(uv + vCurrOffset, heightMapLayer)).r",n={uv:this.uv,heightScale:this.heightScale,heightMap:this.heightMap,minLayers:i(this.minLayers),maxLayers:i(this.maxLayers),vN:S,vViewPosition:x,...null==e?{}:{heightMapLayer:e}};return c(v,n,`\n vec3 N = normalize(vN);\n vec3 viewDir = normalize(vViewPosition);\n vec3 dp1 = dFdx(-vViewPosition);\n vec3 dp2 = dFdy(-vViewPosition);\n vec2 duv1 = dFdx(uv);\n vec2 duv2 = dFdy(uv);\n vec3 dp2perp = cross(dp2, N);\n vec3 dp1perp = cross(N, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n float invmax = inversesqrt(max(dot(T, T), dot(B, B)));\n mat3 tbn = mat3(T * invmax, B * invmax, N);\n vec3 vv = -viewDir * tbn;\n float parallaxLimit = -length(vv.xy) / vv.z;\n parallaxLimit *= heightScale;\n vec2 vOffsetDir = normalize(vv.xy);\n vec2 vMaxOffset = vOffsetDir * parallaxLimit;\n float nNumSamples = mix(float(maxLayers), float(minLayers), dot(-viewDir, N));\n float fStepSize = 1.0 / nNumSamples;\n float fCurrRayHeight = 1.0;\n vec2 vCurrOffset = vec2(0.0);\n vec2 vLastOffset = vec2(0.0);\n float fLastSampledHeight = 1.0;\n float fCurrSampledHeight = 1.0;\n for (int nCurrSample = 0; nCurrSample < 24; nCurrSample++) {\n if (float(nCurrSample) > nNumSamples) break;\n fCurrSampledHeight = ${t};\n if (fCurrSampledHeight > fCurrRayHeight) {\n float delta1 = fCurrSampledHeight - fCurrRayHeight;\n float delta2 = (fCurrRayHeight + fStepSize) - fLastSampledHeight;\n float ratio = delta1 / (delta1 + delta2);\n vCurrOffset = ratio * vLastOffset + (1.0 - ratio) * vCurrOffset;\n break;\n } else {\n fCurrRayHeight -= fStepSize;\n vLastOffset = vCurrOffset;\n vCurrOffset += fStepSize * vMaxOffset;\n fLastSampledHeight = fCurrSampledHeight;\n }\n }\n return uv + vCurrOffset;\n `)}}function C(e){return e instanceof f||"sampler2DArray"===e.constructor.typeName}function L(e){return e.index??i(0)}/*
1
+ import{abs as e,attributes as t,cross as n,dFdx as a,dFdy as r,float as i,normalize as l,select as s,transformed as p,UniformSampler2dArraySlice as f,uniforms as o,varying as $,Vec2Node as v,vec3 as u,vec4 as m}from"three-shader-graph";import{isWgslCompiler as h,isWgslFragmentCompiler as d}from"three-shader-graph";import{glslFunction as c}from"./glsl-node.js";export function computeTangent(e,t,i){const s=a(t),p=r(t),f=a(i),o=r(i),$=n(p,e),v=n(e,s),u=$.multiplyScalar(f.x).add(v.multiplyScalar(o.x));return m(l(u),1)}export function fallbackTangent(t){const a=e(t.y),r=s(a.lt(i(.999)),u(0,1,0),u(1,0,0));return l(n(r,t))}const g=t.normal,S=$(l(o.normalMatrix.multiplyVec(g))),x=$(p.mvPosition.xyz.multiplyScalar(-1));export function parallaxOcclusionMapping(e,t,n,a=8,r=24){return new y(e,t,n,a,r)}class y extends v{constructor(e,t,n,a,r){super(),this.uv=e,this.heightMap=t,this.heightScale=n,this.minLayers=a,this.maxLayers=r}compile(e){if(!h(e))return{out:e.get(this.compileGlsl())};if(!d(e))throw new Error("Parallax occlusion mapping requires fragment-stage derivatives in WGSL.");const t=`pom_${e.variable()}`,n=e.get(this.uv),a=e.get(this.heightMap),r=e.get(this.heightScale),l=e.get(S),s=e.get(x),p=e.get(i(this.minLayers)),f=e.get(i(this.maxLayers)),o=C(this.heightMap)?e.get(L(this.heightMap)):null;return{chunk:`\n let ${t}Uv = ${n};\n let ${t}Normal = normalize(${l});\n let ${t}ViewPosition = ${s};\n let ${t}ViewDirection = normalize(${t}ViewPosition);\n let ${t}Dp1 = dpdx(-${t}ViewPosition);\n // WGSL fragment coordinates have the opposite Y orientation from GLSL.\n // Negate both Y derivatives so the reconstructed cotangent frame keeps\n // the same handedness as the GLSL implementation.\n let ${t}Dp2 = -dpdy(-${t}ViewPosition);\n let ${t}Duv1 = dpdx(${t}Uv);\n let ${t}Duv2 = -dpdy(${t}Uv);\n let ${t}Dp2Perp = cross(${t}Dp2, ${t}Normal);\n let ${t}Dp1Perp = cross(${t}Normal, ${t}Dp1);\n let ${t}T = ${t}Dp2Perp * ${t}Duv1.x + ${t}Dp1Perp * ${t}Duv2.x;\n let ${t}B = ${t}Dp2Perp * ${t}Duv1.y + ${t}Dp1Perp * ${t}Duv2.y;\n let ${t}InvMax = inverseSqrt(max(dot(${t}T, ${t}T), dot(${t}B, ${t}B)));\n let ${t}Tbn = mat3x3<f32>(\n ${t}T * ${t}InvMax,\n ${t}B * ${t}InvMax,\n ${t}Normal\n );\n let ${t}Vv = -(transpose(${t}Tbn) * ${t}ViewDirection);\n let ${t}SafeVvZ = select(\n ${t}Vv.z,\n select(-0.0001, 0.0001, ${t}Vv.z >= 0.0),\n abs(${t}Vv.z) < 0.0001\n );\n let ${t}ParallaxLimit = (-length(${t}Vv.xy) / ${t}SafeVvZ) * ${r};\n let ${t}OffsetDirection = normalize(${t}Vv.xy);\n let ${t}MaxOffset = ${t}OffsetDirection * ${t}ParallaxLimit;\n // Fade when the full parallax displacement approaches a pixel. This\n // follows screen footprint, so distant and heavily tiled surfaces\n // naturally stop paying for height-map samples.\n let ${t}UvFootprint = max(length(${t}Duv1), length(${t}Duv2));\n let ${t}Fade = smoothstep(\n 0.5, 2.0, length(${t}MaxOffset) / max(${t}UvFootprint, 0.000001)\n );\n let ${t}NumSamples = max(\n 1.0,\n mix(${f}, ${p}, dot(-${t}ViewDirection, ${t}Normal))\n );\n let ${t}StepSize = 1.0 / ${t}NumSamples;\n var ${t}CurrentRayHeight = 1.0;\n var ${t}CurrentOffset = vec2<f32>(0.0);\n var ${t}LastOffset = vec2<f32>(0.0);\n var ${t}LastSampledHeight = 1.0;\n var ${t}CurrentSampledHeight = 1.0;\n if (${t}Fade > 0.0) {\n for (var ${t}SampleIndex: i32 = 0; ${t}SampleIndex < 24; ${t}SampleIndex = ${t}SampleIndex + 1) {\n if (f32(${t}SampleIndex) > ${t}NumSamples) {\n break;\n }\n ${t}CurrentSampledHeight = ${null==o?`textureSampleLevel(${a}Texture, ${a}Sampler, ${t}Uv + ${t}CurrentOffset, 0.0).r`:`textureSampleLevel(${a}Texture, ${a}Sampler, ${t}Uv + ${t}CurrentOffset, i32(${o}), 0.0).r`};\n if (${t}CurrentSampledHeight > ${t}CurrentRayHeight) {\n let ${t}Delta1 = ${t}CurrentSampledHeight - ${t}CurrentRayHeight;\n let ${t}Delta2 = (${t}CurrentRayHeight + ${t}StepSize) - ${t}LastSampledHeight;\n let ${t}Ratio = ${t}Delta1 / (${t}Delta1 + ${t}Delta2);\n ${t}CurrentOffset = ${t}Ratio * ${t}LastOffset + (1.0 - ${t}Ratio) * ${t}CurrentOffset;\n break;\n }\n ${t}CurrentRayHeight = ${t}CurrentRayHeight - ${t}StepSize;\n ${t}LastOffset = ${t}CurrentOffset;\n ${t}CurrentOffset = ${t}CurrentOffset + ${t}StepSize * ${t}MaxOffset;\n ${t}LastSampledHeight = ${t}CurrentSampledHeight;\n }\n }\n let ${t}UvWithOffset = ${t}Uv + ${t}CurrentOffset * ${t}Fade;\n `,out:`${t}UvWithOffset`}}compileGlsl(){const e=C(this.heightMap)?L(this.heightMap):null,t=null==e?"texture2D(heightMap, uv + vCurrOffset).r":"texture(heightMap, vec3(uv + vCurrOffset, heightMapLayer)).r",n={uv:this.uv,heightScale:this.heightScale,heightMap:this.heightMap,minLayers:i(this.minLayers),maxLayers:i(this.maxLayers),vN:S,vViewPosition:x,...null==e?{}:{heightMapLayer:e}};return c(v,n,`\n vec3 N = normalize(vN);\n vec3 viewDir = normalize(vViewPosition);\n vec3 dp1 = dFdx(-vViewPosition);\n vec3 dp2 = dFdy(-vViewPosition);\n vec2 duv1 = dFdx(uv);\n vec2 duv2 = dFdy(uv);\n vec3 dp2perp = cross(dp2, N);\n vec3 dp1perp = cross(N, dp1);\n vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;\n vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;\n float invmax = inversesqrt(max(dot(T, T), dot(B, B)));\n mat3 tbn = mat3(T * invmax, B * invmax, N);\n vec3 vv = -viewDir * tbn;\n float parallaxLimit = -length(vv.xy) / vv.z;\n parallaxLimit *= heightScale;\n vec2 vOffsetDir = normalize(vv.xy);\n vec2 vMaxOffset = vOffsetDir * parallaxLimit;\n float nNumSamples = mix(float(maxLayers), float(minLayers), dot(-viewDir, N));\n float fStepSize = 1.0 / nNumSamples;\n float fCurrRayHeight = 1.0;\n vec2 vCurrOffset = vec2(0.0);\n vec2 vLastOffset = vec2(0.0);\n float fLastSampledHeight = 1.0;\n float fCurrSampledHeight = 1.0;\n for (int nCurrSample = 0; nCurrSample < 24; nCurrSample++) {\n if (float(nCurrSample) > nNumSamples) break;\n fCurrSampledHeight = ${t};\n if (fCurrSampledHeight > fCurrRayHeight) {\n float delta1 = fCurrSampledHeight - fCurrRayHeight;\n float delta2 = (fCurrRayHeight + fStepSize) - fLastSampledHeight;\n float ratio = delta1 / (delta1 + delta2);\n vCurrOffset = ratio * vLastOffset + (1.0 - ratio) * vCurrOffset;\n break;\n } else {\n fCurrRayHeight -= fStepSize;\n vLastOffset = vCurrOffset;\n vCurrOffset += fStepSize * vMaxOffset;\n fLastSampledHeight = fCurrSampledHeight;\n }\n }\n return uv + vCurrOffset;\n `)}}function C(e){return e instanceof f||"sampler2DArray"===e.constructor.typeName}function L(e){return e.index??i(0)}/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,2 @@
1
+ import 'reflect-metadata';
2
+ //# sourceMappingURL=actor-persistence-runtime.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import"reflect-metadata";import{expect as e,test as t,vi as n}from"vitest";n.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??(e[t]=n.fn())});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),n.mock("../scene/bootstrap.js",()=>({loadScene:n.fn()})),n.mock("../rendering.js",()=>({RenderingView:class{},GraphicsRuntime:class{}}));import{loadScene as r}from"../scene/bootstrap.js";import{HologyApplicationRuntime as s}from"../gameplay/initiate.js";import{NetMode as o}from"../gameplay/net/net-session.js";import{SceneRef as i}from"../scene/scene-ref.js";function a(){const e=function(){let e,t;return{promise:new Promise((n,r)=>{e=n,t=r}),resolve:e,reject:t}}(),t={},i=[],a={registerPersistentActors:()=>[t]},c={sceneName:"old",netMode:o.none,sceneRoots:[],components:{},materializer:{dispose:n.fn()},container:{set:n.fn()},viewController:{paused:!1},renderingView:{compileAsync:n.fn(),initTexturesAsync:n.fn()},physics:{removeRemoved:n.fn(),addFromScene:n.fn()},sceneTransition:{publish:n.fn()},world:{actors:[],stopSceneEventGraphs:n.fn(),removeSceneLifetimeActors:n.fn(),persistence:{captureAll:n.fn(()=>i.push("capture")),restore:n.fn(()=>e.promise)},addActor:n.fn(function(e){this.actors.push(e),i.push("register")}),beginActorPlay:n.fn(()=>i.push("begin")),startSceneEventGraphs:n.fn(()=>i.push("graphs"))}},l={},p=Object.create(s.prototype);return Object.assign(p,{config:{shaders:{},actors:{},dataDir:""},getApplicationService:n.fn(),loading:n.fn(),checkIntent:n.fn(),setState:n.fn(),renderingHost:{applicationView:l,activate:n.fn(e=>i.push(e===l?"hide":"present"))}}),n.mocked(r).mockResolvedValue({materializer:a,actors:[t],sceneRoots:[],scene:{}}),{runtime:p,session:c,restore:e,order:i}}t("travel suspends presentation and simulation until restore, begin-play and graphs complete",async()=>{const{runtime:t,session:r,restore:s,order:o}=a(),c=t.performSceneTransition(r,{scene:new i("next")},1);await n.waitFor(()=>e(r.world.persistence.restore).toHaveBeenCalled()),e(r.viewController.paused).toBe(!0),e(o).toEqual(["hide","capture","register"]),s.resolve(),await c,e(o).toEqual(["hide","capture","register","begin","graphs","present"]),e(r.viewController.paused).toBe(!1)}),t("failed restore leaves the scene suspended without begin-play or event graphs",async()=>{const{runtime:t,session:r,restore:s,order:o}=a(),c=t.performSceneTransition(r,{scene:new i("next")},1),l=e(c).rejects.toThrow("invalid saved actor");await n.waitFor(()=>e(r.world.persistence.restore).toHaveBeenCalled()),s.reject(new Error("invalid saved actor")),await l,e(r.viewController.paused).toBe(!0),e(o).toEqual(["hide","capture","register"])}),t("cancelled loading cannot begin-play or present a restored scene",async()=>{const{runtime:t,session:r,restore:s,order:o}=a(),c=t.performSceneTransition(r,{scene:new i("next")},1),l=e(c).rejects.toThrow("cancelled");await n.waitFor(()=>e(r.world.persistence.restore).toHaveBeenCalled()),t.checkIntent.mockImplementation(()=>{throw new Error("cancelled")}),s.resolve(),await l,e(o).not.toContain("begin"),e(o).not.toContain("present")}),t("network persistence is rejected before creating a session",async()=>{const{runtime:t}=a();t.createSession=n.fn(),await e(t.performLoad(class{},{persistence:{},multi:{session:{mode:o.client}}},1)).rejects.toThrow("single-player"),e(t.createSession).not.toHaveBeenCalled()});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,2 @@
1
+ import 'reflect-metadata';
2
+ //# sourceMappingURL=actor-persistence.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import"reflect-metadata";import{describe as e,expect as t,test as s,vi as n}from"vitest";import{ActorPersistence as o,assertActorPersistenceData as r}from"../gameplay/services/actor-persistence.js";class a{constructor(e="health"){this.persistenceKey=e,this.persistenceVersion=1,this.value=10}onCapturePersistence(){return{value:this.value}}onRestorePersistence(e){this.value=e.value}}function c(e=!0){const s=new a;return{persistenceEnabled:e,attachedComponents:[s],component:s,persistenceVersion:2,value:"closed",onCapturePersistence(){return{value:this.value}},onRestorePersistence(e,s){t(s.version).toBe(2),this.value=e.value}}}function i(e){const t=new o;return t.configure({data:e}),t}a.__isActorComponent=!0;const p={sceneId:"stable-scene",path:"prefab/enemy"};e("ActorPersistence",()=>{s("captures independent actor and component state without aliasing snapshots",async()=>{const e=i(),s=c();e.register(s,p),s.component.value=31,s.value="open";const n=e.snapshot();s.component.value=80,t(n.scenes["stable-scene"]["prefab/enemy"].components.health.data).toEqual({value:31});const o=i(n),r=c();o.register(r,p),await o.restore([r]),t(r.component.value).toBe(31),t(r.value).toBe("open")}),s("deduplicates aliased components but rejects duplicate stable keys",()=>{const e=i(),s=c();t(e.register(s,p)).toBe(!0),t(Object.keys(e.snapshot().scenes["stable-scene"]["prefab/enemy"].components)).toEqual(["health"]);const n=c();n.attachedComponents.push(new a),t(()=>i().register(n,p)).toThrow(/Duplicate/)}),s("restores all participants before post-restore and supports cyclic identity lookups",async()=>{const e=i(),s=c(),o=c(),r={sceneId:"stable-scene",path:"other"};e.register(s,p),e.register(o,r),o.component.value=42;const a=i(e.snapshot()),l=c(),u=c();a.register(l,p),a.register(u,r);const d=n.fn(()=>{t(a.resolve(r)).toBe(u),t(u.component.value).toBe(42),t(a.resolve(p)).toBe(l)});Object.assign(l,{onPersistenceRestored:d}),await a.restore([l,u]),t(d).toHaveBeenCalledOnce()}),s("preserves unloaded scenes and unknown component entries",async()=>{const e=i({version:1,scenes:{elsewhere:{chest:{actor:{version:5,data:"untouched"}}},"stable-scene":{"prefab/enemy":{components:{future:{version:3,data:[1,2]}}}}},dynamic:{}}),s=c();e.register(s,p),await e.restore([s]);const n=e.snapshot();t(n.scenes.elsewhere.chest.actor.version).toBe(5),t(n.scenes["stable-scene"]["prefab/enemy"].components.future).toEqual({version:3,data:[1,2]})}),s("records whole-prefab removal immediately and never resurrects it on capture or teardown",()=>{const e=i(),s=c();e.register(s,p,{...p,path:"prefab"}),t(e.markRemoved(s)).toBe(!0),t(e.resolve(p)).toBeUndefined(),t(e.isRemoved({...p,path:"prefab/other"})).toBe(!0),e.unregister(s),t(e.snapshot().scenes["stable-scene"]).toEqual({prefab:{removed:!0}}),t(()=>e.register(c(),p)).toThrow(/removed/)}),s("ordinary unregister captures state without marking removal and permits scene revisit",async()=>{const e=i(),s=c();e.register(s,p),s.component.value=17,e.unregister(s),t(e.isRemoved(p)).toBe(!1);const n=c();e.register(n,p),await e.restore([n]),t(n.component.value).toBe(17)}),s("disabled sessions and nonparticipating actors do not inspect component state",()=>{const e=c(!1);Object.defineProperty(e,"component",{get(){throw new Error("must not inspect")}}),t(i().register(e,p)).toBe(!1),t((new o).register(c(),p)).toBe(!1)}),s("dynamic identities survive new sessions and reject duplicate registration",async()=>{const e=i(),s=c(),n={dynamicId:"quest-companion",path:""};e.register(s,n),t(()=>e.register(c(),n)).toThrow(/Duplicate/);const o=i(e.snapshot()),r=c();o.register(r,n),await o.restore([r]),t(o.getIdentity(r)).toEqual(n)}),s("failed or cancelled async restores do not overwrite good saved state",async()=>{const e=i(),s=c();e.register(s,p);const n=e.snapshot(),o=i(n),r=c();Object.assign(r.component,{async onRestorePersistence(){throw this.value=-100,new Error("bad version")}}),o.register(r,p),await t(o.restore([r])).rejects.toThrow(/health/),o.unregister(r),t(o.snapshot()).toEqual(n);const a=i(n),l=c();a.register(l,p),await t(a.restore([l],()=>{throw new Error("cancelled")})).rejects.toThrow("cancelled")}),s("validates document versions and JSON payloads",()=>{t(()=>r({version:2,scenes:{},dynamic:{}})).toThrow();const e=i(),s=c();e.register(s,p),Object.assign(s,{onCapturePersistence:()=>({value:1/0})}),t(()=>e.snapshot()).toThrow(/capture/),t(()=>i({version:1,scenes:{a:{b:{components:[]}}},dynamic:{}})).toThrow()})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -0,0 +1,2 @@
1
+ import 'reflect-metadata';
2
+ //# sourceMappingURL=actor-transform-persistence.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{__setFunctionName as o}from"tslib";import"reflect-metadata";import{describe as t,expect as e,test as n,vi as s}from"vitest";import{Group as r,Vector3 as a}from"three";import{ActorPersistence as i}from"../gameplay/services/actor-persistence.js";import{ActorTransformPersistenceComponent as c,captureActorTransform as p,restoreActorTransform as m}from"../gameplay/actors/builtin/components/actor-transform-persistence.js";function y(){const o=new r,t={object:o,position:o.position,rotation:o.rotation,persistenceEnabled:!0,attachedComponents:[]},e=Object.create(c.prototype);return Object.assign(e,{actor:t,persistenceKey:"transform",persistenceVersion:1,restored:!1,physics:{setPosition:s.fn(),setRotation:s.fn(),flushModifiedBodyPositionsToColliders:s.fn()},worldPosition:new a,worldRotation:o.quaternion.clone()}),t.attachedComponents.push(e),{actor:t,component:e}}s.mock("../gameplay/actors/component.js",()=>{var t;return{ActorComponent:(t=class{},o(t,"ActorComponent"),t.__isActorComponent=!0,t),Component:()=>o=>o}}),s.mock("../gameplay/inject.js",()=>({inject:()=>({})})),s.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{}})),t("actor transform persistence",()=>{n("round trips position and yaw independently of pitch and roll",async()=>{const o=y();o.actor.position.set(3,4,5),o.actor.rotation.set(.25,1.2,.5);const t=new i;t.configure({}),t.register(o.actor,{sceneId:"scene",path:"enemy"});const n=t.snapshot();e(n.scenes.scene.enemy.components?.transform).toEqual({version:1,data:{position:{x:3,y:4,z:5},yaw:1.2}});const s=y();s.actor.rotation.set(.4,0,.6);const r=new i;r.configure({data:n}),r.register(s.actor,{sceneId:"scene",path:"enemy"}),await r.restore([s.actor]),e(s.actor.position.toArray()).toEqual([3,4,5]),e(s.actor.rotation.y).toBe(1.2),e(s.actor.rotation.x).toBe(.4),e(s.actor.rotation.z).toBe(.6),e(s.component.restored).toBe(!0)}),n("rejects invalid payload without partially changing the actor",()=>{const{actor:o}=y();o.position.set(7,8,9),e(()=>m(o,{position:{x:1,y:2,z:"bad"},yaw:1})).toThrow(/Invalid actor transform/),e(o.position.toArray()).toEqual([7,8,9]),e(p(o).position).toEqual({x:7,y:8,z:9})}),n("old saves without a transform retain the authored position",async()=>{const{actor:o,component:t}=y();o.position.set(2,0,-3);const n=new i;n.configure({data:{version:1,scenes:{},dynamic:{}}}),n.register(o,{sceneId:"scene",path:"enemy"}),await n.restore([o]),e(o.position.toArray()).toEqual([2,0,-3]),e(t.restored).toBe(!1)})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as a,it as t,vi as i}from"vitest";import{GraphicsRuntime as n}from"../rendering/graphics-runtime.js";e("GraphicsRuntime",()=>{t("shares a backend until the final view releases it",async()=>{const e=new n,t={id:1},l=i.fn(async()=>t),r=i.fn(),s=await e.acquire("webgpu","default",l),u=await e.acquire("webgpu","default",l);a(s).toBe(t),a(u).toBe(t),a(l).toHaveBeenCalledOnce(),e.release("webgpu","default",r),a(r).not.toHaveBeenCalled(),e.release("webgpu","default",r),a(r).toHaveBeenCalledWith(t)})});/*
1
+ import{describe as e,expect as a,it as i,vi as n}from"vitest";import{GraphicsRuntime as t}from"../rendering/graphics-runtime.js";e("GraphicsRuntime",()=>{i("shares a backend until the final view releases it",async()=>{const e=new t,i={id:1},s=n.fn(async()=>i),r=n.fn(),l=await e.acquire("webgpu","default",s),u=await e.acquire("webgpu","default",s);a(l).toBe(i),a(u).toBe(i),a(s).toHaveBeenCalledOnce(),e.release("webgpu","default",r),a(r).not.toHaveBeenCalled(),e.release("webgpu","default",r),a(r).toHaveBeenCalledWith(i)}),i("reuses an idle backend until its retention period expires",async()=>{n.useFakeTimers();try{const e=new t,i={id:1},s=n.fn(async()=>i),r=n.fn();await e.acquire("webgpu","default",s),e.release("webgpu","default",r,12e4),await n.advanceTimersByTimeAsync(6e4),a(await e.acquire("webgpu","default",s)).toBe(i),a(s).toHaveBeenCalledOnce(),e.release("webgpu","default",r,12e4),await n.advanceTimersByTimeAsync(12e4),a(r).toHaveBeenCalledOnce()}finally{n.useRealTimers()}}),i("evicts an idle backend when a different configuration is acquired",async()=>{const e=new t,i=n.fn();await e.acquire("webgpu","first",async()=>({id:1})),e.release("webgpu","first",i,12e4),await e.acquire("webgpu","second",async()=>({id:2})),a(i).toHaveBeenCalledWith({id:1}),e.release("webgpu","second",i)})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lattice-deformation.test.d.ts.map
@@ -0,0 +1,4 @@
1
+ import{describe as e,expect as t,it as o,vi as r}from"vitest";import{BoxGeometry as s,BufferGeometry as n,Float32BufferAttribute as a,Group as i,Matrix4 as m,Mesh as f,MeshBasicMaterial as d,PlaneGeometry as l,Vector3 as y}from"three";import{createLatticeDeformation as p,deformLatticeGeometry as c,isValidLattice as g,latticePoint as u,LatticeGeometryCache as w,setLatticeGeometrySourceIdentity as h}from"../scene/lattice-deformation.js";function B(e=new s(2,2,2)){const t=new i,o=new f(e,new d);return t.add(o),{root:t,mesh:o}}function b(e,o){t(e.x).toBeCloseTo(o.x,5),t(e.y).toBeCloseTo(o.y,5),t(e.z).toBeCloseTo(o.z,5)}e("lattice deformation",()=>{o("preserves undeformed geometry, normals and hard edges",()=>{const{root:e,mesh:o}=B(),r=p(e,[3,2,1]),s=c(o.geometry,new m,r);t(Array.from(s.getAttribute("position").array)).toEqual(Array.from(o.geometry.getAttribute("position").array)),t(Array.from(s.getAttribute("normal").array)).toEqual(Array.from(o.geometry.getAttribute("normal").array)),t(s).not.toBe(o.geometry)}),o("transforms positions, normals and tangents under a shear without modifying source buffers",()=>{const e=new n;e.setAttribute("position",new a([0,0,0,1,0,0,0,1,0],3)),e.setAttribute("normal",new a([1,0,0,1,0,0,1,0,0],3)),e.setAttribute("tangent",new a([0,1,0,1,0,1,0,1,0,1,0,1],4));const o=p(new f(new s(2,2,2)),[2,2,2]),r=new y;o.offsets.forEach((e,t)=>{e[0]=.5*u(o,t,r).y});const i=c(e,new m,o);b((new y).fromBufferAttribute(i.getAttribute("position"),2),new y(.5,1,0)),b((new y).fromBufferAttribute(i.getAttribute("normal"),0),new y(1,-.5,0).normalize()),b((new y).fromBufferAttribute(i.getAttribute("tangent"),0),new y(.5,1,0).normalize()),t(Array.from(e.getAttribute("position").array)).toEqual([0,0,0,1,0,0,0,1,0]),t(i.boundingBox.max.x).toBe(1)}),o("uses model local space for transformed child meshes and ignores scene transforms",()=>{const{root:e,mesh:t}=B();t.position.set(3,2,-1),t.rotation.z=Math.PI/2,t.scale.set(2,1,.5),e.position.set(100,200,300),e.scale.setScalar(4);const o=p(e,[1,1,1]);b((new y).fromArray(o.min),new y(2,0,-1.5)),b((new y).fromArray(o.max),new y(4,4,-.5)),o.offsets.forEach(e=>{e[0]=2});const r=c(t.geometry,t.matrix,o);b((new y).fromBufferAttribute(r.getAttribute("position"),0),(new y).fromBufferAttribute(t.geometry.getAttribute("position"),0).add(new y(0,-2,0)))}),o("supports planar models and rejects malformed and oversized cages",()=>{const e=p(new f(new l(2,2)),[1,1,1]);t(g(e)).toBe(!0),t(e.max[2]).toBeGreaterThan(e.min[2]),t(g({...e,offsets:[]})).toBe(!1),t(g({...e,segments:[9,1,1]})).toBe(!1),t(g({...e,min:[NaN,0,0]})).toBe(!1),t(()=>p(new i,[1e4,1e4,1e4])).toThrow("Lattice segments")}),o("reuses identical buffers across copies and restores the original when disabled",()=>{const{root:e,mesh:o}=B(),r=e.clone(),s=r.children[0];r.position.set(9,0,0);const n=o.geometry,a=p(e),i=new w;a.offsets[0][1]=.5,i.apply(e,a),i.apply(r,structuredClone(a)),t(o.geometry).toBe(s.geometry),t(o.geometry).not.toBe(n);const m=o.geometry;i.apply(e,{...a,offsets:a.offsets.map(e=>[e[0],e[1]+1,e[2]])}),t(o.geometry).not.toBe(s.geometry),i.apply(e,a),t(o.geometry).toBe(m),i.apply(e,{...a,enabled:!1}),t(o.geometry).toBe(n),t(s.geometry).toBe(m),i.dispose()}),o("shares independently frozen copies with identical source and pose identities",()=>{const{root:e,mesh:o}=B(),{root:r,mesh:s}=B(o.geometry.clone());h(o.geometry,"same-source-and-pose"),h(s.geometry,"same-source-and-pose");const n=p(e),a=new w;n.offsets[0][0]=.25,a.apply(e,n),a.apply(r,{offsets:n.offsets,max:n.max,min:n.min,segments:n.segments}),t(o.geometry).toBe(s.geometry),a.dispose()}),o("leaves independently authored child objects and editor helpers undeformed",()=>{const{root:e,mesh:o}=B(),r=p(e),s=new i,n=new f(o.geometry);s.userData.src={type:"asset_mesh"},s.add(n),e.add(s);const a=new f(o.geometry);a.name="UCX_model",e.add(a);const m=new f(o.geometry);m.userData.isEditorVisualization=!0,e.add(m);const d=n.geometry,l=new w;r.offsets.forEach(e=>{e[0]=1}),l.apply(e,r),t(o.geometry).not.toBe(d),t(n.geometry).toBe(d),t(m.geometry).toBe(d),t(a.geometry).toBe(d),l.dispose()}),o("isolates geometry variants and disposes idle buffers without disposing live copies",()=>{const{root:e,mesh:o}=B(),s=e.clone(),n=s.children[0],a=p(e),i=new w;a.offsets[0][0]=.25,i.apply(s,a);const m=r.spyOn(n.geometry,"dispose");i.apply(e,a,"painted"),t(o.geometry).not.toBe(n.geometry);for(let t=1;t<20;t++){const o=structuredClone(a);o.offsets[0][0]=t,i.apply(e,o)}t(m).not.toHaveBeenCalled(),i.release(s),i.dispose(),t(m).toHaveBeenCalledOnce()})});/*
2
+ * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
+ * See the LICENSE.md file for details.
4
+ */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as t,it as a,vi as s}from"vitest";import{Scene as i,Texture as r}from"three";import{Subject as n}from"rxjs";s.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&s.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),s.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),s.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),s.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()}));import{SceneMaterializer as d}from"../scene/materializer.js";import{DetailTier as p,SerializedParamType as o}from"../scene/model.js";function m(e,t){const a=s.fn(async(e,t)=>({scene:{},animations:[]})),p=s.fn(async e=>new r),o=s.fn(async e=>({texture:new r,layerIndex:0})),m=s.fn(),f={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},l={onCreate:new n,onDelete:new n,onUpdate:new n,getAsset:s.fn(async e=>t.get(e)??null),getAssets:s.fn(async()=>Array.from(t.values()))};return{materializer:new d(new i,f,l,{getMesh:a,getTexture:p,getTextureArray:o},{renderer:{initTexture:m},onLoop:s.fn()},[],[],{create:async()=>null,initActor:async()=>{}}),getMesh:a,getTexture:p,getTextureArray:o,initTexture:m}}function f(e){return{name:e.id,...e}}function l(e,t){return{id:e,name:e,type:"mesh",fileKey:`${e}.glb`,fileFormat:"glb",mesh:t}}function c(e,t){return{id:e,name:e,type:"prefab",prefab:{objects:t}}}function u(e,t){return{id:e,name:e,type:"material",material:{type:"shader",side:0,params:{},shaderParams:{map:{type:o.Texture,value:t}}}}}function h(e){return{id:e,name:e,type:"texture",fileKey:`${e}.png`,fileFormat:"png"}}function y(e){return{color:"#ffffff",materialId:e}}e("SceneMaterializer asset prefetch",()=>{a("prefetches unique materialized asset meshes with the same options used by createFromAsset",async()=>{const e=l("mesh-a"),a=l("mesh-b"),s=l("mesh-c"),i=l("mesh-disabled"),r=l("mesh-high",{detailTier:p.high}),n=c("prefab-a",[f({id:"prefab-mesh-c",type:"asset_mesh",assetId:s.id}),f({id:"prefab-duplicate-mesh-a",type:"asset_mesh",assetId:e.id}),f({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),d=new Map([e,a,s,i,r,n].map(e=>[e.id,e])),o=[f({id:"root",type:"group",children:[f({id:"mesh-a-1",type:"asset_mesh",assetId:e.id}),f({id:"mesh-a-2",type:"asset_mesh",assetId:e.id}),f({id:"disabled-group",type:"group",enabled:!1,children:[f({id:"disabled-mesh",type:"asset_mesh",assetId:i.id})]}),f({id:"prefab-instance",type:"prefab",assetId:n.id}),f({id:"high-detail-mesh",type:"asset_mesh",assetId:r.id})]}),f({id:"mesh-b",type:"asset_mesh",assetId:a.id})],{materializer:u,getMesh:h}=m(o,d);u.detailTier=p.low,await u.preInit(),await u.prefetchAssets(),t(h).toHaveBeenCalledTimes(3),t(new Set(h.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(h.mock.calls.every(([,e])=>!0===e?.mergeGeomtries)).toBe(!0)}),a("initializes textures from nested groups and prefab assets",async()=>{const e=h("texture-root"),a=h("texture-group"),s=h("texture-prefab"),i=h("texture-skipped"),r=u("material-root",e.id),n=u("material-group",a.id),d=u("material-prefab",s.id),p=u("material-skipped",i.id),o=c("prefab-a",[f({id:"prefab-group",type:"group",children:[f({id:"prefab-shape",type:"shape_mesh",materialAssignments:[y(d.id)]})]}),f({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),l=new Map([e,a,s,i,r,n,d,p,o].map(e=>[e.id,e])),g=[f({id:"root-group",type:"group",children:[f({id:"nested-mesh",type:"asset_mesh",materialAssignments:[y(n.id)]}),f({id:"disabled-group",type:"group",enabled:!1,children:[f({id:"skipped-mesh",type:"asset_mesh",materialAssignments:[y(p.id)]})]}),f({id:"prefab-instance",type:"prefab",assetId:o.id})]}),f({id:"root-shape",type:"shape_mesh",materialAssignments:[y(r.id)]})],{materializer:b,getTexture:x,initTexture:w}=m(g,l);await b.preInit(),await b.initTextures(),t(x).toHaveBeenCalledTimes(3),t(new Set(x.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(w).toHaveBeenCalledTimes(3)})});/*
1
+ import{describe as e,expect as t,it as a,vi as s}from"vitest";import{Scene as i,Texture as r}from"three";import{Subject as n}from"rxjs";s.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&s.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),s.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),s.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),s.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),s.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:s.fn()}));import{SceneMaterializer as d}from"../scene/materializer.js";import{DetailTier as p,SerializedParamType as o}from"../scene/model.js";function m(e,t){const a=s.fn(async(e,t)=>({scene:{},animations:[]})),p=s.fn(async e=>new r),o=s.fn(async e=>({texture:new r,layerIndex:0})),m=s.fn(),l={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},f={onCreate:new n,onDelete:new n,onUpdate:new n,getAsset:s.fn(async e=>t.get(e)??null),getAssets:s.fn(async()=>Array.from(t.values()))};return{materializer:new d(new i,l,f,{getMesh:a,getTexture:p,getTextureArray:o},{renderer:{initTexture:m},initTexture:m,onLoop:s.fn()},[],[],{create:async()=>null,initActor:async()=>{}}),getMesh:a,getTexture:p,getTextureArray:o,initTexture:m}}function l(e){return{name:e.id,...e}}function f(e,t){return{id:e,name:e,type:"mesh",fileKey:`${e}.glb`,fileFormat:"glb",mesh:t}}function c(e,t){return{id:e,name:e,type:"prefab",prefab:{objects:t}}}function u(e,t){return{id:e,name:e,type:"material",material:{type:"shader",side:0,params:{},shaderParams:{map:{type:o.Texture,value:t}}}}}function h(e){return{id:e,name:e,type:"texture",fileKey:`${e}.png`,fileFormat:"png"}}function y(e){return{color:"#ffffff",materialId:e}}e("SceneMaterializer asset prefetch",()=>{a("prefetches unique materialized asset meshes without creating discarded instances",async()=>{const e=f("mesh-a"),a=f("mesh-b"),s=f("mesh-c"),i=f("mesh-disabled"),r=f("mesh-high",{detailTier:p.high}),n=c("prefab-a",[l({id:"prefab-mesh-c",type:"asset_mesh",assetId:s.id}),l({id:"prefab-duplicate-mesh-a",type:"asset_mesh",assetId:e.id}),l({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),d=new Map([e,a,s,i,r,n].map(e=>[e.id,e])),o=[l({id:"root",type:"group",children:[l({id:"mesh-a-1",type:"asset_mesh",assetId:e.id}),l({id:"mesh-a-2",type:"asset_mesh",assetId:e.id}),l({id:"disabled-group",type:"group",enabled:!1,children:[l({id:"disabled-mesh",type:"asset_mesh",assetId:i.id})]}),l({id:"prefab-instance",type:"prefab",assetId:n.id}),l({id:"high-detail-mesh",type:"asset_mesh",assetId:r.id})]}),l({id:"mesh-b",type:"asset_mesh",assetId:a.id})],{materializer:u,getMesh:h}=m(o,d);u.detailTier=p.low,await u.preInit(),await u.prefetchAssets(),t(h).toHaveBeenCalledTimes(3),t(new Set(h.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(h.mock.calls.every(([,e])=>!0===e?.mergeGeomtries)).toBe(!0),t(h.mock.calls.every(([,e])=>!1===e?.clone)).toBe(!0)}),a("initializes textures from nested groups and prefab assets",async()=>{const e=h("texture-root"),a=h("texture-group"),s=h("texture-prefab"),i=h("texture-skipped"),r=u("material-root",e.id),n=u("material-group",a.id),d=u("material-prefab",s.id),p=u("material-skipped",i.id),o=c("prefab-a",[l({id:"prefab-group",type:"group",children:[l({id:"prefab-shape",type:"shape_mesh",materialAssignments:[y(d.id)]})]}),l({id:"prefab-cycle",type:"prefab",assetId:"prefab-a"})]),f=new Map([e,a,s,i,r,n,d,p,o].map(e=>[e.id,e])),g=[l({id:"root-group",type:"group",children:[l({id:"nested-mesh",type:"asset_mesh",materialAssignments:[y(n.id)]}),l({id:"disabled-group",type:"group",enabled:!1,children:[l({id:"skipped-mesh",type:"asset_mesh",materialAssignments:[y(p.id)]})]}),l({id:"prefab-instance",type:"prefab",assetId:o.id})]}),l({id:"root-shape",type:"shape_mesh",materialAssignments:[y(r.id)]})],{materializer:b,getTexture:x,initTexture:w}=m(g,f);await b.preInit(),await b.initTextures(),t(x).toHaveBeenCalledTimes(3),t(new Set(x.mock.calls.map(([e])=>e.id))).toEqual(new Set([e.id,a.id,s.id])),t(w).toHaveBeenCalledTimes(3)})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{__decorate as e,__metadata as a}from"tslib";import{describe as t,expect as r,it as n,vi as o}from"vitest";import{Scene as i}from"three";import{Subject as s}from"rxjs";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,a)=>(a in e||(e[a]=("string"!=typeof a||!a.startsWith("is"))&&o.fn()),e[a]),set:(e,a,t)=>(e[a]=t,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),o.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),o.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),o.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:o.fn()}));import{BaseActor as c}from"../gameplay/actors/actor.js";import{ActorComponent as p}from"../gameplay/actors/component.js";import{Parameter as m}from"../shader/parameter.js";import{SceneMaterializer as l}from"../scene/materializer.js";import{SerializedParamType as u}from"../scene/model.js";class f extends p{constructor(){super(...arguments),this.animationPreset="mixamo"}}e([m(),a("design:type",Object)],f.prototype,"animationPreset",void 0);class d extends c{constructor(){super(...arguments),this.animation=new f,this.animationPreset="mixamo"}}e([m(),a("design:type",Object)],d.prototype,"animationPreset",void 0);class v extends c{constructor(){super(...arguments),this.value="before"}applySceneParameters(e,a){return!(1!==a.size||!a.has("value"))&&(this.value=e.value,!0)}}e([m(),a("design:type",Object)],v.prototype,"value",void 0);class y extends p{constructor(){super(...arguments),this.value="before"}applySceneParameters(e,a){return!(1!==a.size||!a.has("value"))&&(this.value=e.value,!0)}}e([m(),a("design:type",Object)],y.prototype,"value",void 0);class b extends c{constructor(){super(...arguments),this.component=new y}}class P extends c{}async function g(e){return(await w(e)).actor}async function w(e){const a=x("prefab-asset",[A({id:"prefab-actor",type:"actor",actor:{type:"PrefabParamActor",params:{animationPreset:{type:u.String,value:e.prefabActorValue}},innerParams:null==e.prefabComponentValue?[]:[{path:["animation"],params:{animationPreset:{type:u.String,value:e.prefabComponentValue}}}],components:[]}})],"prefab-actor"),t=A({id:"prefab-instance",type:"prefab",assetId:a.id,prefab:{mainActorId:"prefab-actor",params:null==e.instanceParam?{}:{animationPreset:e.instanceParam},innerParams:null==e.instanceInnerParam?[]:[{path:["animation"],params:{animationPreset:e.instanceInnerParam}}]}}),{materializer:r}=h([t],new Map([[a.id,a]]));return await r.materialize(t),await r.initActorsPostInit(),{actor:r.materializedActors.get("prefab-instance/prefab-actor"),materializer:r}}function h(e,a,t=!1){const r={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},n={supportsLivePrefabParameters:t,onCreate:new s,onDelete:new s,onUpdate:new s,getAsset:o.fn(async e=>a.get(e)??null),getAssets:o.fn(async()=>Array.from(a.values()))},c={create:o.fn(async(e,a)=>{const t=new e;return null!=a&&t.position.copy(a),t}),initActor:o.fn(async e=>{e.__isInitialised=!0})};return{materializer:new l(new i,r,n,{},{renderer:{shadowMap:{needsUpdate:!1}}},[],[{name:"PrefabParamActor",type:d},{name:"LiveParamActor",type:v},{name:"SceneAttachedComponentActor",type:P}],c,[{name:"LiveParamComponent",type:y}]),assetsProvider:n}}function A(e){return{name:e.id,position:[0,0,0],rotation:[0,0,0,"XYZ"],scale:[1,1,1],...e}}function x(e,a,t){return{id:e,name:e,type:"prefab",prefab:{objects:a,mainActorId:t}}}t("prefab instance actor params",()=>{n("does not let generated prefab instance defaults override the prefab actor value",async()=>{const e=await g({prefabActorValue:"basic",instanceParam:{type:u.String,value:"mixamo"}});r(e.animationPreset).toBe("basic")}),n("applies prefab instance params when the override is explicit",async()=>{const e=await g({prefabActorValue:"basic",instanceParam:{type:u.String,value:"mixamo",override:!0}});r(e.animationPreset).toBe("mixamo")}),n("does not let generated prefab instance component defaults override prefab component values",async()=>{const e=await g({prefabActorValue:"basic",prefabComponentValue:"basic",instanceInnerParam:{type:u.String,value:"mixamo"}});r(e.animation.animationPreset).toBe("basic")}),n("applies prefab instance component params when the override is explicit",async()=>{const e=await g({prefabActorValue:"basic",prefabComponentValue:"basic",instanceInnerParam:{type:u.String,value:"mixamo",override:!0}});r(e.animation.animationPreset).toBe("mixamo")}),n("resolves direct scene actors and exposed prefab actors by scene object id",async()=>{const{actor:e,materializer:a}=await w({prefabActorValue:"basic"}),t=new d;a.materializedActors.set("direct-actor",t),r(a.resolveSceneActor("direct-actor")).toBe(t),r(a.resolveSceneActor("prefab-instance")).toBe(e)}),n("applies supported scene parameter changes without recreating the actor",async()=>{const e=A({id:"live-actor",type:"actor",actor:{type:"LiveParamActor",params:{value:{type:u.String,value:"after"}},innerParams:[],components:[]}}),a={...e.actor,params:{value:{type:u.String,value:"before"}}},{materializer:t}=h([e],new Map),n=new v,o=await t.tryApplyLiveActorParameterUpdate(n,e,JSON.stringify(a));r(o).toBe(!0),r(n.value).toBe("after")}),n("applies supported component parameter changes without recreating the actor",async()=>{const e=A({id:"live-component",type:"actor",actor:{type:"LiveComponentParamActor",params:{},innerParams:[{path:["component"],params:{value:{type:u.String,value:"after"}}}],components:[]}}),a={...e.actor,innerParams:[{path:["component"],params:{value:{type:u.String,value:"before"}}}]},{materializer:t}=h([e],new Map),n=new b,o=await t.tryApplyLiveActorParameterUpdate(n,e,JSON.stringify(a));r(o).toBe(!0),r(n.component.value).toBe("after")}),n("applies supported scene-attached component parameter changes without recreating the actor",async()=>{const e=A({id:"scene-attached-component",type:"actor",actor:{type:"SceneAttachedComponentActor",params:{},innerParams:[],components:[{type:"LiveParamComponent",params:{value:{type:u.String,value:"before"}},innerParams:[]}]}}),{materializer:a}=h([e],new Map);await a.materialize(e),await a.initActorsPostInit();const t=a.materializedActors.get(e.id),n=t.attachedComponents[0],o=JSON.stringify(e.actor);e.actor.components[0].params.value={type:u.String,value:"after"};const i=await a.tryApplyLiveActorParameterUpdate(t,e,o);r(i).toBe(!0),r(n.value).toBe("after"),r(t.attachedComponents[0]).toBe(n)}),n("applies saved prefab parameters to a runtime-spawned prefab instance",async()=>{const e=x("live-prefab",[A({id:"live-actor",type:"actor",actor:{type:"LiveParamActor",params:{value:{type:u.String,value:"before"}},components:[]}})],"live-actor"),a=structuredClone(e);a.prefab.objects[0].actor.params.value={type:u.String,value:"after"};const{materializer:t,assetsProvider:n}=h([],new Map([[e.id,e]]),!0),i=await t.createFromPrefabAsset(e,{sceneObjectChain:["runtime-instance"],runtimeSpawn:!0},void 0,void 0,!1),s=i.mainActor;n.onUpdate.next(a),await o.waitFor(()=>r(s.value).toBe("after")),r(i.mainActor).toBe(s)}),n("applies supported component changes when the actor rejects an independent change",async()=>{const e=x("partially-live-prefab",[A({id:"component-actor",type:"actor",actor:{type:"SceneAttachedComponentActor",params:{unsupported:{type:u.String,value:"before"}},components:[{type:"LiveParamComponent",params:{value:{type:u.String,value:"before"}}}]}})],"component-actor"),a=structuredClone(e);a.prefab.objects[0].actor.params.unsupported.value="after",a.prefab.objects[0].actor.components[0].params.value.value="after";const{materializer:t,assetsProvider:n}=h([],new Map([[e.id,e]]),!0),i=await t.createFromPrefabAsset(e,{sceneObjectChain:["runtime-component-instance"],runtimeSpawn:!0},void 0,void 0,!1),s=i.mainActor,c=s.attachedComponents[0],p=o.spyOn(console,"warn").mockImplementation(()=>{});n.onUpdate.next(a),await o.waitFor(()=>r(c.value).toBe("after")),r(i.mainActor).toBe(s),r(p).toHaveBeenCalledWith(r.stringContaining("were not applied")),p.mockRestore()})});/*
1
+ import{__decorate as e,__metadata as t}from"tslib";import{describe as a,expect as r,it as n,vi as i}from"vitest";import{Scene as o}from"three";import{Subject as s}from"rxjs";i.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&i.fn()),e[t]),set:(e,t,a)=>(e[t]=a,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),i.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),i.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),i.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),i.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:i.fn()}));import{BaseActor as c}from"../gameplay/actors/actor.js";import{ActorComponent as p}from"../gameplay/actors/component.js";import{Parameter as m}from"../shader/parameter.js";import{SceneMaterializer as l}from"../scene/materializer.js";import{SerializedParamType as d}from"../scene/model.js";import{ActorPersistence as f}from"../gameplay/services/actor-persistence.js";class u extends p{constructor(){super(...arguments),this.animationPreset="mixamo"}}e([m(),t("design:type",Object)],u.prototype,"animationPreset",void 0);class v extends c{constructor(){super(...arguments),this.target=null,this.targets=[],this.animation=new u,this.animationPreset="mixamo"}get persistenceEnabled(){return!0}}e([m(),t("design:type",c)],v.prototype,"target",void 0),e([m({type:c,array:!0}),t("design:type",Array)],v.prototype,"targets",void 0),e([m(),t("design:type",Object)],v.prototype,"animationPreset",void 0);class y extends c{constructor(){super(...arguments),this.value="before"}applySceneParameters(e,t){return!(1!==t.size||!t.has("value"))&&(this.value=e.value,!0)}}e([m(),t("design:type",Object)],y.prototype,"value",void 0);class b extends p{constructor(){super(...arguments),this.value="before"}applySceneParameters(e,t){return!(1!==t.size||!t.has("value"))&&(this.value=e.value,!0)}}e([m(),t("design:type",Object)],b.prototype,"value",void 0);class w extends c{constructor(){super(...arguments),this.component=new b}}class P extends c{}async function g(e){return(await A(e)).actor}async function A(e){const t=B("prefab-asset",[z({id:"prefab-actor",type:"actor",actor:{type:"PrefabParamActor",params:{animationPreset:{type:d.String,value:e.prefabActorValue}},innerParams:null==e.prefabComponentValue?[]:[{path:["animation"],params:{animationPreset:{type:d.String,value:e.prefabComponentValue}}}],components:[]}})],"prefab-actor"),a=z({id:"prefab-instance",type:"prefab",assetId:t.id,prefab:{mainActorId:"prefab-actor",params:null==e.instanceParam?{}:{animationPreset:e.instanceParam},innerParams:null==e.instanceInnerParam?[]:[{path:["animation"],params:{animationPreset:e.instanceInnerParam}}]}}),{materializer:r}=h([a],new Map([[t.id,t]]));return await r.materialize(a),await r.initActorsPostInit(),{actor:r.materializedActors.get("prefab-instance/prefab-actor"),materializer:r}}function h(e,t,a=!1){const r={getObjects:()=>e,onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},n={supportsLivePrefabParameters:a,onCreate:new s,onDelete:new s,onUpdate:new s,getAsset:i.fn(async e=>t.get(e)??null),getAssets:i.fn(async()=>Array.from(t.values()))},c={create:i.fn(async(e,t)=>{const a=new e;return null!=t&&a.position.copy(t),a}),initActor:i.fn(async e=>{e.__isInitialised=!0})},p={invalidateShadows:i.fn(),renderer:{shadowMap:{needsUpdate:!1}}};return{materializer:new l(new o,r,n,{},p,[],[{name:"PrefabParamActor",type:v},{name:"LiveParamActor",type:y},{name:"SceneAttachedComponentActor",type:P}],c,[{name:"LiveParamComponent",type:b}]),assetsProvider:n,actorProvider:c}}function z(e){return{name:e.id,position:[0,0,0],rotation:[0,0,0,"XYZ"],scale:[1,1,1],...e}}function B(e,t,a){return{id:e,name:e,type:"prefab",prefab:{objects:t,mainActorId:a}}}a("prefab instance actor params",()=>{n("does not let generated prefab instance defaults override the prefab actor value",async()=>{const e=await g({prefabActorValue:"basic",instanceParam:{type:d.String,value:"mixamo"}});r(e.animationPreset).toBe("basic")}),n("applies prefab instance params when the override is explicit",async()=>{const e=await g({prefabActorValue:"basic",instanceParam:{type:d.String,value:"mixamo",override:!0}});r(e.animationPreset).toBe("mixamo")}),n("does not let generated prefab instance component defaults override prefab component values",async()=>{const e=await g({prefabActorValue:"basic",prefabComponentValue:"basic",instanceInnerParam:{type:d.String,value:"mixamo"}});r(e.animation.animationPreset).toBe("basic")}),n("applies prefab instance component params when the override is explicit",async()=>{const e=await g({prefabActorValue:"basic",prefabComponentValue:"basic",instanceInnerParam:{type:d.String,value:"mixamo",override:!0}});r(e.animation.animationPreset).toBe("mixamo")}),n("resolves direct scene actors and exposed prefab actors by scene object id",async()=>{const{actor:e,materializer:t}=await A({prefabActorValue:"basic"}),a=new v;t.materializedActors.set("direct-actor",a),r(t.resolveSceneActor("direct-actor")).toBe(a),r(t.resolveSceneActor("prefab-instance")).toBe(e)}),n("applies supported scene parameter changes without recreating the actor",async()=>{const e=z({id:"live-actor",type:"actor",actor:{type:"LiveParamActor",params:{value:{type:d.String,value:"after"}},innerParams:[],components:[]}}),t={...e.actor,params:{value:{type:d.String,value:"before"}}},{materializer:a}=h([e],new Map),n=new y,i=await a.tryApplyLiveActorParameterUpdate(n,e,JSON.stringify(t));r(i).toBe(!0),r(n.value).toBe("after")}),n("applies supported component parameter changes without recreating the actor",async()=>{const e=z({id:"live-component",type:"actor",actor:{type:"LiveComponentParamActor",params:{},innerParams:[{path:["component"],params:{value:{type:d.String,value:"after"}}}],components:[]}}),t={...e.actor,innerParams:[{path:["component"],params:{value:{type:d.String,value:"before"}}}]},{materializer:a}=h([e],new Map),n=new w,i=await a.tryApplyLiveActorParameterUpdate(n,e,JSON.stringify(t));r(i).toBe(!0),r(n.component.value).toBe("after")}),n("applies supported scene-attached component parameter changes without recreating the actor",async()=>{const e=z({id:"scene-attached-component",type:"actor",actor:{type:"SceneAttachedComponentActor",params:{},innerParams:[],components:[{type:"LiveParamComponent",params:{value:{type:d.String,value:"before"}},innerParams:[]}]}}),{materializer:t}=h([e],new Map);await t.materialize(e),await t.initActorsPostInit();const a=t.materializedActors.get(e.id),n=a.attachedComponents[0],i=JSON.stringify(e.actor);e.actor.components[0].params.value={type:d.String,value:"after"};const o=await t.tryApplyLiveActorParameterUpdate(a,e,i);r(o).toBe(!0),r(n.value).toBe("after"),r(a.attachedComponents[0]).toBe(n)}),n("applies saved prefab parameters to a runtime-spawned prefab instance",async()=>{const e=B("live-prefab",[z({id:"live-actor",type:"actor",actor:{type:"LiveParamActor",params:{value:{type:d.String,value:"before"}},components:[]}})],"live-actor"),t=structuredClone(e);t.prefab.objects[0].actor.params.value={type:d.String,value:"after"};const{materializer:a,assetsProvider:n}=h([],new Map([[e.id,e]]),!0),o=await a.createFromPrefabAsset(e,{sceneObjectChain:["runtime-instance"],runtimeSpawn:!0},void 0,void 0,!1),s=o.mainActor;n.onUpdate.next(t),await i.waitFor(()=>r(s.value).toBe("after")),r(o.mainActor).toBe(s)}),n("applies supported component changes when the actor rejects an independent change",async()=>{const e=B("partially-live-prefab",[z({id:"component-actor",type:"actor",actor:{type:"SceneAttachedComponentActor",params:{unsupported:{type:d.String,value:"before"}},components:[{type:"LiveParamComponent",params:{value:{type:d.String,value:"before"}}}]}})],"component-actor"),t=structuredClone(e);t.prefab.objects[0].actor.params.unsupported.value="after",t.prefab.objects[0].actor.components[0].params.value.value="after";const{materializer:a,assetsProvider:n}=h([],new Map([[e.id,e]]),!0),o=await a.createFromPrefabAsset(e,{sceneObjectChain:["runtime-component-instance"],runtimeSpawn:!0},void 0,void 0,!1),s=o.mainActor,c=s.attachedComponents[0],p=i.spyOn(console,"warn").mockImplementation(()=>{});n.onUpdate.next(t),await i.waitFor(()=>r(c.value).toBe("after")),r(o.mainActor).toBe(s),r(p).toHaveBeenCalledWith(r.stringContaining("were not applied")),p.mockRestore()})}),a("authored persistence materialization",()=>{const e=()=>z({id:"enemy",type:"actor",actor:{type:"PrefabParamActor"}}),t=(e,t)=>z({id:e,type:"prefab",assetId:t}),a=()=>{const e=new f;return e.configure({}),e};n("skips removed prefab contents before constructors and preserves other instances",async()=>{const n=B("enemy-prefab",[e()],"enemy"),i=t("first",n.id),o=t("second",n.id),s=new Map([[n.id,n]]),c=h([i,o],s),p=a();await c.materializer.preparePersistence(p,"scene-id",[i,o]),await c.materializer.materialize(i),await c.materializer.materialize(o),await c.materializer.initActorsPostInit(),c.materializer.registerPersistentActors(),p.markRemoved(c.materializer.resolveSceneActor("first"));const m=p.snapshot(),l=new f;l.configure({data:m});const d=h([i,o],s),u=await d.materializer.preparePersistence(l,"scene-id",[i,o]);r(u.map(e=>e.id)).toEqual(["second"]),r(await d.materializer.materialize(i)).toBeUndefined(),r(d.actorProvider.create).not.toHaveBeenCalled(),await d.materializer.materialize(o),r(d.actorProvider.create).toHaveBeenCalledTimes(1),r(d.materializer.resolveSceneActor("first")).toBeUndefined(),r(l.isRemoved({sceneId:"scene-id",path:"first"})).toBe(!0)}),n("filters an actor subtree before static instancing can visit its children",async()=>{const t=e();t.children=[z({id:"collider",type:"shape_mesh"})];const a=new f;a.configure({data:{version:1,scenes:{scene:{enemy:{removed:!0}}},dynamic:{}}});const{materializer:n,actorProvider:i}=h([t],new Map);r(await n.preparePersistence(a,"scene",[t])).toEqual([]),r(await n.materialize(t.children[0])).toBeUndefined(),r(i.create).not.toHaveBeenCalled()}),n("keeps nested prefab identities distinct and removes the outer exposed placement",async()=>{const n=B("inner",[e()],"enemy"),i=B("outer",[t("nested",n.id)],"nested"),o=[t("one",i.id),t("two",i.id)],{materializer:s}=h(o,new Map([[n.id,n],[i.id,i]])),c=a();await s.preparePersistence(c,"scene",o);for(const e of o)await s.materialize(e);await s.initActorsPostInit(),s.registerPersistentActors();const p=s.resolveSceneActor("one"),m=s.resolveSceneActor("two");r(c.getIdentity(p)).toEqual({sceneId:"scene",path:"one/nested/enemy"}),r(c.getIdentity(m)).toEqual({sceneId:"scene",path:"two/nested/enemy"}),c.markRemoved(p),r(c.snapshot().scenes.scene.one).toEqual({removed:!0}),r(c.isRemoved(c.getIdentity(m))).toBe(!1)}),n("does not replace a removed exposed actor with a remaining prefab sibling",async()=>{const a=z({...e(),id:"sibling"}),n=B("prefab",[e(),a],"enemy"),i=t("placed",n.id),o=z({id:"reference",type:"actor",actor:{type:"PrefabParamActor",params:{target:{type:d.BaseActor,value:{type:"prefab",id:"placed"}}}}}),s=new f;s.configure({data:{version:1,scenes:{scene:{"placed/enemy":{removed:!0}}},dynamic:{}}});const{materializer:c}=h([i,o],new Map([[n.id,n]]));await c.preparePersistence(s,"scene",[i,o]),await c.materialize(i),await c.materialize(o),await c.initActorsPostInit(),r(c.materializedActors.size).toBe(2),r(c.resolveSceneActor("placed")).toBeUndefined(),r(c.resolveSceneActor("reference").target).toBeNull()}),n("binds circular actor references and resolves direct removed references to null",async()=>{const e=(e,t)=>z({id:e,type:"actor",actor:{type:"PrefabParamActor",params:{target:{type:d.BaseActor,value:{type:"actor",id:t}},targets:{type:d.Array,element:d.BaseActor,value:[{type:"actor",id:t}]}}}}),t=[e("a","b"),e("b","a")],{materializer:n}=h(t,new Map),i=a();await n.preparePersistence(i,"scene",t);for(const e of t)await n.materialize(e);await n.initActorsPostInit();const o=n.resolveSceneActor("a"),s=n.resolveSceneActor("b");r(o.target).toBe(s),r(s.target).toBe(o),r(o.targets).toEqual([s]),n.registerPersistentActors(),i.markRemoved(s);const c=new f;c.configure({data:i.snapshot()});const p=h(t,new Map).materializer;await p.preparePersistence(c,"scene",t);for(const e of t)await p.materialize(e);await p.initActorsPostInit(),r(p.resolveSceneActor("a").target).toBeNull(),r(p.resolveSceneActor("a").targets).toEqual([null])}),n("reuses retained authored actors without another initialization or restore",async()=>{const t=e(),n=a(),i=h([t],new Map);await i.materializer.preparePersistence(n,"scene",[t]),await i.materializer.materialize(t),await i.materializer.initActorsPostInit();const[o]=i.materializer.registerPersistentActors(),s=h([t],new Map);await s.materializer.preparePersistence(n,"scene",[t]),await s.materializer.materialize(t),await s.materializer.initActorsPostInit(),r(s.actorProvider.create).not.toHaveBeenCalled(),r(s.actorProvider.initActor).not.toHaveBeenCalled(),r(s.materializer.resolveSceneActor("enemy")).toBe(o),r(s.materializer.registerPersistentActors()).toEqual([])}),n("does not apply a scene-local tombstone to an identically named prefab-local ID",async()=>{const a=B("prefab",[e()],"enemy"),n=t("placed",a.id),i=new f;i.configure({data:{version:1,scenes:{scene:{enemy:{removed:!0}}},dynamic:{}}});const{materializer:o}=h([e(),n],new Map([[a.id,a]])),s=await o.preparePersistence(i,"scene",[e(),n]);for(const e of s)await o.materialize(e);r(o.materializedActors.has("placed/enemy")).toBe(!0),r(o.materializedActors.has("enemy")).toBe(!1)})});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{__decorate as t}from"tslib";import"reflect-metadata";import{expect as e,test as n,vi as o}from"vitest";o.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),o.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{}}));import{Scene as r}from"three";import{Subject as i}from"rxjs";import{Container as s}from"typedi";import{Actor as a,BaseActor as c}from"../gameplay/actors/actor.js";import{ActorComponent as d,Component as l}from"../gameplay/actors/component.js";import{ActorFactory as m}from"../gameplay/actors/factory.js";import{World as y}from"../gameplay/services/world.js";function u(t){const e=Object.create(y.prototype);return e.actorFactory=t,e.physics={removeActor(){}},e.actors=[],e.actorLifetimes=new Map,e.actorPrefabInstances=new Map,e.prefabInstances=new Set,e.sceneEventGraphs=new Map,e.sceneEventGraphSubscriptions=[],e.actorAdded=new i,e.actorRemoved=new i,e.scene=new r,e}n("world begin and end play skip runtime-only components in editor",async()=>{const t=new m(s.of("world-lifecycle-editor"),{inEditor:!0}),n=await t.create(p),o=u(t);o.addActor(n),o.removeActor(n),e(n.beginCount).toBe(1),e(n.endCount).toBe(1),e(n.runtimeOnly.beginCount).toBe(0),e(n.runtimeOnly.endCount).toBe(0),e(n.runtimeOnly.actor).toBeUndefined(),e(n.inEditor.beginCount).toBe(1),e(n.inEditor.endCount).toBe(1),e(n.inEditor.actor).toBe(n),e(n.editorOnly.beginCount).toBe(1),e(n.editorOnly.endCount).toBe(1),e(n.editorOnly.actor).toBe(n)}),n("world begin and end play skip editor-only components at runtime",async()=>{const t=new m(s.of("world-lifecycle-runtime"),{inEditor:!1}),n=await t.create(p),o=u(t);o.addActor(n),o.removeActor(n),e(n.beginCount).toBe(1),e(n.endCount).toBe(1),e(n.runtimeOnly.beginCount).toBe(1),e(n.runtimeOnly.endCount).toBe(1),e(n.runtimeOnly.actor).toBe(n),e(n.inEditor.beginCount).toBe(1),e(n.inEditor.endCount).toBe(1),e(n.inEditor.actor).toBe(n),e(n.editorOnly.beginCount).toBe(0),e(n.editorOnly.endCount).toBe(0),e(n.editorOnly.actor).toBeUndefined()});let p=class extends c{constructor(){super(...arguments),this.beginCount=0,this.endCount=0,this.runtimeOnly=this.attach(g),this.inEditor=this.attach(h),this.editorOnly=this.attach(B)}onBeginPlay(){this.beginCount++}onEndPlay(){this.endCount++}};p=t([a()],p);class C extends d{constructor(){super(...arguments),this.beginCount=0,this.endCount=0}onBeginPlay(){this.beginCount++}onEndPlay(){this.endCount++}}let g=class extends C{};g=t([l()],g);let h=class extends C{};h=t([l({inEditor:!0})],h);let B=class extends C{};B=t([l({inEditor:!0,editorOnly:!0})],B);/*
1
+ import{__decorate as e}from"tslib";import"reflect-metadata";import{expect as t,test as n,vi as o}from"vitest";o.hoisted(()=>{const e=new Proxy({},{get:(e,t)=>e[t]??(e[t]=o.fn())});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),o.mock("../gameplay/services/render.js",()=>({ViewController:class{}})),o.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{}})),o.mock("../rendering.js",()=>({RenderingView:class{}}));import{Scene as r}from"three";import{Subject as i}from"rxjs";import{Container as s}from"typedi";import{Actor as a,BaseActor as c}from"../gameplay/actors/actor.js";import{ActorComponent as d,Component as l}from"../gameplay/actors/component.js";import{ActorFactory as p}from"../gameplay/actors/factory.js";import{World as m}from"../gameplay/services/world.js";import{ActorPersistence as y}from"../gameplay/services/actor-persistence.js";function u(e){const t=Object.create(m.prototype);return t.actorFactory=e,t.physics={removeActor(){}},t.actors=[],t.actorLifetimes=new Map,t.actorPrefabInstances=new Map,t.prefabInstances=new Set,t.sceneEventGraphs=new Map,t.sceneEventGraphSubscriptions=[],t.actorAdded=new i,t.actorRemoved=new i,t.scene=new r,t}n("persistent batch is visible for lookup before restore and begin-play",async()=>{const e=new p(s.of("persistent-world-lifecycle"),{inEditor:!1}),n=u(e),o=new y;o.configure({}),n.persistence=o;const r=await e.create(h),i=await e.create(h);Object.defineProperty(r,"persistenceEnabled",{value:!0}),Object.defineProperty(i,"persistenceEnabled",{value:!0}),o.register(r,{sceneId:"scene",path:"first"}),o.register(i,{sceneId:"scene",path:"second"}),n.addActor(r,void 0,void 0,void 0,!0),n.addActor(i,void 0,void 0,void 0,!0),Object.assign(r,{onPersistenceRestored(){t(n.actors).toEqual([r,i]),t(r.beginCount).toBe(0),t(i.beginCount).toBe(0)}}),await o.restore([r,i]),n.beginActorPlay(r),n.beginActorPlay(i),t(r.beginCount).toBe(1),t(i.beginCount).toBe(1),n.removeActor(r),t(o.isRemoved({sceneId:"scene",path:"first"})).toBe(!1)}),n("explicit dynamic spawns restore before begin-play and reject removed IDs before construction",async()=>{const n=new p(s.of("persistent-dynamic-spawn"),{inEditor:!1}),r=u(n),i=new y;i.configure({data:{version:1,scenes:{},dynamic:{companion:{"":{actor:{version:1,data:{health:23}}}},removed:{"":{removed:!0}}}}}),r.persistence=i;let d=class extends c{constructor(){super(...arguments),this.health=100,this.begunHealth=0,this.persistenceVersion=1}onCapturePersistence(){return{health:this.health}}async onRestorePersistence(e){this.health=e.health}onBeginPlay(){this.begunHealth=this.health}};d=e([a({persistent:!0})],d);const l=await r.spawnActor(d,void 0,void 0,{persistenceId:"companion"});t(l.begunHealth).toBe(23);const m=o.spyOn(n,"create");await t(r.spawnActor(d,void 0,void 0,{persistenceId:"removed"})).rejects.toThrow("removed"),t(m).not.toHaveBeenCalled(),t(()=>r.spawnActorSync(d,void 0,void 0,{persistenceId:"sync"})).toThrow("spawnActorSync")}),n("world begin and end play skip runtime-only components in editor",async()=>{const e=new p(s.of("world-lifecycle-editor"),{inEditor:!0}),n=await e.create(h),o=u(e);o.addActor(n),o.removeActor(n),t(n.beginCount).toBe(1),t(n.endCount).toBe(1),t(n.runtimeOnly.beginCount).toBe(0),t(n.runtimeOnly.endCount).toBe(0),t(n.runtimeOnly.actor).toBe(n),t(n.inEditor.beginCount).toBe(1),t(n.inEditor.endCount).toBe(1),t(n.inEditor.actor).toBe(n),t(n.editorOnly.beginCount).toBe(1),t(n.editorOnly.endCount).toBe(1),t(n.editorOnly.actor).toBe(n)}),n("world begin and end play skip editor-only components at runtime",async()=>{const e=new p(s.of("world-lifecycle-runtime"),{inEditor:!1}),n=await e.create(h),o=u(e);o.addActor(n),o.removeActor(n),t(n.beginCount).toBe(1),t(n.endCount).toBe(1),t(n.runtimeOnly.beginCount).toBe(1),t(n.runtimeOnly.endCount).toBe(1),t(n.runtimeOnly.actor).toBe(n),t(n.inEditor.beginCount).toBe(1),t(n.inEditor.endCount).toBe(1),t(n.inEditor.actor).toBe(n),t(n.editorOnly.beginCount).toBe(0),t(n.editorOnly.endCount).toBe(0),t(n.editorOnly.actor).toBe(n)});let h=class extends c{constructor(){super(...arguments),this.beginCount=0,this.endCount=0,this.runtimeOnly=this.attach(b),this.inEditor=this.attach(v),this.editorOnly=this.attach(w)}onBeginPlay(){this.beginCount++}onEndPlay(){this.endCount++}};h=e([a()],h);class g extends d{constructor(){super(...arguments),this.beginCount=0,this.endCount=0}onBeginPlay(){this.beginCount++}onEndPlay(){this.endCount++}}let b=class extends g{};b=e([l()],b);let v=class extends g{};v=e([l({inEditor:!0})],v);let w=class extends g{};w=e([l({inEditor:!0,editorOnly:!0})],w);/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -1,4 +1,4 @@
1
- import{describe as e,expect as t,test as n,vi as o}from"vitest";import{Euler as a,Group as r,Scene as s,Vector3 as c}from"three";import{Subject as i}from"rxjs";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=o.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),o.mock("../rendering.js",()=>({RenderingView:class{}})),o.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{}}));import{World as d}from"../gameplay/services/world.js";import{ActorLifetime as p}from"../gameplay/actors/actor.js";e("World.spawnPrefab",()=>{n("makes the requested transform available while prefab actors initialize",async()=>{const e=new s,n=new s,f=new r;f.position.set(1,0,0);const m={object:f,disposed:new i,attachedComponents:[],onBeginPlay:o.fn(),onEndPlay:o.fn(),constructor:Object},l=new r;let w,y=n;const b={id:"prefab-a",type:"prefab",prefab:{objects:[]},revision:2},v={inEditor:!0,setScene:o.fn(e=>{y=e}),createFromPrefabAsset:o.fn(async(e,t,n)=>(n.add(l),l.add(f),v.inEditor||y.attach(f),w=f.position.clone(),{object:l,actors:[m],mainActor:m,instanceId:"runtime-instance"})),unregisterLivePrefabInstance:o.fn()},j=Object.create(d.prototype);j.scene=e,j.materializer=v,j.actorFactory={inEditor:!1},j.physics={addFromScene:o.fn(),removeRemoved:o.fn(),removeActor:o.fn()},j.assetsProvider={getAsset:o.fn(async()=>b)},j.actorLifetimes=new Map,j.actorPrefabInstances=new Map,j.prefabInstances=new Set,j.sceneEventGraphs=new Map,j.actorAdded=new i,j.actorRemoved=new i,j.actors=[];const P=new c(10,2,5),g=new a(0,Math.PI/2,0),h=new r,B=new r;h.position.copy(P),h.rotation.copy(g),B.position.set(1,0,0),h.add(B),h.updateWorldMatrix(!0,!0);const u=B.getWorldPosition(new c),E=await j.spawnPrefab({asset:{id:"prefab-a",type:"prefab",prefab:{objects:[]}}},P,g);t(w).toEqual(u),t(v.setScene).toHaveBeenCalledWith(e),t(v.inEditor).toBe(!1),t(f.parent).toBe(e),t(E?.object.parent).toBe(e),t(E?.object.position).toEqual(P),t(E?.object.rotation.y).toBeCloseTo(g.y),t(E?.lifetime).toBe(p.scene),t(j.getActorLifetime(m)).toBe(E?.lifetime),t(v.createFromPrefabAsset).toHaveBeenCalledWith(b,t.anything(),t.any(r),void 0,!1),j.removePrefab(E),t(v.unregisterLivePrefabInstance).toHaveBeenCalledWith("runtime-instance")})}),n("scene cleanup uses explicit actor lifetime instead of spawn origin",()=>{const e=Object.create(d.prototype);e.scene=new s,e.actors=[],e.actorLifetimes=new Map,e.actorPrefabInstances=new Map,e.prefabInstances=new Set,e.sceneEventGraphs=new Map,e.physics={removeActor:o.fn()},e.actorFactory={inEditor:!1},e.actorAdded=new i,e.actorRemoved=new i;const n=new r;e.scene.add(n);const a=()=>({object:new r,disposed:new i,attachedComponents:[],onBeginPlay:o.fn(),onEndPlay:o.fn(),constructor:Object}),c=a(),f=a();n.add(c.object,f.object),e.addActor(c),e.addActor(f,void 0,void 0,p.game);const m=e.removeSceneLifetimeActors([n]);t(e.actors).toEqual([f]),t(m.has(f)).toBe(!0),t(f.object.parent).toBe(e.scene),t(c.onEndPlay).toHaveBeenCalledOnce(),t(f.onEndPlay).not.toHaveBeenCalled()}),n("spawnActorSync applies explicit owner and lifetime",()=>{const e=Object.create(d.prototype),n={id:1},a={object:new r,disposed:new i,attachedComponents:[],onBeginPlay:o.fn(),onEndPlay:o.fn(),constructor:Object};e.actorFactory={inEditor:!1,createSync:o.fn(()=>a)},e.actors=[],e.actorLifetimes=new Map,e.actorPrefabInstances=new Map,e.actorAdded=new i,e.scene=new s;const c=e.spawnActorSync(Object,void 0,void 0,{owner:n,lifetime:p.game});t(c).toBe(a),t(a.owner).toBe(n),t(e.getActorLifetime(a)).toBe(p.game)});/*
1
+ import{describe as e,expect as t,test as n,vi as o}from"vitest";import{Euler as a,Group as r,Scene as s,Vector3 as c}from"three";import{Subject as i}from"rxjs";o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=o.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e})}),o.mock("../rendering.js",()=>({RenderingView:class{}})),o.mock("../gameplay/services/physics/physics-system.js",()=>({PhysicsSystem:class{}}));import{World as d}from"../gameplay/services/world.js";import{ActorLifetime as p}from"../gameplay/actors/actor.js";import{ActorPersistence as m}from"../gameplay/services/actor-persistence.js";n("a dynamic persistent prefab restores every actor before any begin-play",async()=>{const e=Object.create(d.prototype),n=[],a=o=>({name:o,object:new r,disposed:new i,attachedComponents:[],constructor:Object,persistenceEnabled:!0,persistenceVersion:1,value:0,onCapturePersistence(){return this.value},async onRestorePersistence(a){t(e.actors).toHaveLength(2),t(n.every(e=>e.startsWith("restore"))).toBe(!0),await Promise.resolve(),this.value=a,n.push("restore-"+o)},onBeginPlay(){n.push("begin-"+o)},onEndPlay(){}}),c=a("main"),p=a("child"),f=new r,l=new m;l.configure({data:{version:1,scenes:{},dynamic:{drop:{"":{actor:{version:1,data:7}},child:{actor:{version:1,data:9}}}}}}),e.persistence=l,e.scene=new s,Object.assign(e,{actors:[],actorLifetimes:new Map,actorPrefabInstances:new Map,prefabInstances:new Set,actorAdded:new i,actorRemoved:new i,actorFactory:{inEditor:!1},physics:{addFromScene:o.fn()},materializer:{setScene:o.fn(),createFromPrefabAsset:async()=>({object:f,actors:[c,p],actorsById:new Map([["main",c],["child",p]]),mainActor:c,instanceId:"runtime"})}}),await e.spawnPrefab({asset:{id:"asset",prefab:{}}},void 0,void 0,{persistenceId:"drop"}),t(n).toEqual(["restore-main","restore-child","begin-main","begin-child"]),t(c.value).toBe(7),t(p.value).toBe(9)}),e("World.spawnPrefab",()=>{n("makes the requested transform available while prefab actors initialize",async()=>{const e=new s,n=new s,m=new r;m.position.set(1,0,0);const f={object:m,disposed:new i,attachedComponents:[],onBeginPlay:o.fn(),onEndPlay:o.fn(),constructor:Object},l=new r;let w,b=n;const y={id:"prefab-a",type:"prefab",prefab:{objects:[]},revision:2},v={inEditor:!0,setScene:o.fn(e=>{b=e}),createFromPrefabAsset:o.fn(async(e,t,n)=>(n.add(l),l.add(m),v.inEditor||b.attach(m),w=m.position.clone(),{object:l,actors:[f],mainActor:f,instanceId:"runtime-instance"})),unregisterLivePrefabInstance:o.fn()},h=Object.create(d.prototype);h.scene=e,h.materializer=v,h.actorFactory={inEditor:!1},h.physics={addFromScene:o.fn(),removeRemoved:o.fn(),removeActor:o.fn()},h.assetsProvider={getAsset:o.fn(async()=>y)},h.actorLifetimes=new Map,h.actorPrefabInstances=new Map,h.prefabInstances=new Set,h.sceneEventGraphs=new Map,h.actorAdded=new i,h.actorRemoved=new i,h.actors=[];const P=new c(10,2,5),g=new a(0,Math.PI/2,0),j=new r,u=new r;j.position.copy(P),j.rotation.copy(g),u.position.set(1,0,0),j.add(u),j.updateWorldMatrix(!0,!0);const B=u.getWorldPosition(new c),E=await h.spawnPrefab({asset:{id:"prefab-a",type:"prefab",prefab:{objects:[]}}},P,g);t(w).toEqual(B),t(v.setScene).toHaveBeenCalledWith(e),t(v.inEditor).toBe(!1),t(m.parent).toBe(e),t(E?.object.parent).toBe(e),t(E?.object.position).toEqual(P),t(E?.object.rotation.y).toBeCloseTo(g.y),t(E?.lifetime).toBe(p.scene),t(h.getActorLifetime(f)).toBe(E?.lifetime),t(v.createFromPrefabAsset).toHaveBeenCalledWith(y,t.anything(),t.any(r),void 0,!1),h.removePrefab(E),t(v.unregisterLivePrefabInstance).toHaveBeenCalledWith("runtime-instance")})}),n("scene cleanup uses explicit actor lifetime instead of spawn origin",()=>{const e=Object.create(d.prototype);e.scene=new s,e.actors=[],e.actorLifetimes=new Map,e.actorPrefabInstances=new Map,e.prefabInstances=new Set,e.sceneEventGraphs=new Map,e.physics={removeActor:o.fn()},e.actorFactory={inEditor:!1},e.actorAdded=new i,e.actorRemoved=new i;const n=new r;e.scene.add(n);const a=()=>({object:new r,disposed:new i,attachedComponents:[],onBeginPlay:o.fn(),onEndPlay:o.fn(),constructor:Object}),c=a(),m=a();n.add(c.object,m.object),e.addActor(c),e.addActor(m,void 0,void 0,p.game);const f=e.removeSceneLifetimeActors([n]);t(e.actors).toEqual([m]),t(f.has(m)).toBe(!0),t(m.object.parent).toBe(e.scene),t(c.onEndPlay).toHaveBeenCalledOnce(),t(m.onEndPlay).not.toHaveBeenCalled()}),n("spawnActorSync applies explicit owner and lifetime",()=>{const e=Object.create(d.prototype),n={id:1},a={object:new r,disposed:new i,attachedComponents:[],onBeginPlay:o.fn(),onEndPlay:o.fn(),constructor:Object};e.actorFactory={inEditor:!1,createSync:o.fn(()=>a)},e.actors=[],e.actorLifetimes=new Map,e.actorPrefabInstances=new Map,e.actorAdded=new i,e.scene=new s;const c=e.spawnActorSync(Object,void 0,void 0,{owner:n,lifetime:p.game});t(c).toBe(a),t(a.owner).toBe(n),t(e.getActorLifetime(a)).toBe(p.game)});/*
2
2
  * Copyright (©) 2026 Hology Interactive AB. All rights reserved.
3
3
  * See the LICENSE.md file for details.
4
4
  */
@@ -0,0 +1,87 @@
1
+ # Actor persistence
2
+
3
+ Persistence is opt-in and storage-independent. Enable it when loading a single-player session:
4
+
5
+ ```ts
6
+ await application.loadGame(Game, {
7
+ sceneName: save.sceneName,
8
+ persistence: { data: save.persistence }, // undefined data starts an empty document
9
+ })
10
+ ```
11
+
12
+ `initiateGame()` and React's `HologyScene` accept the same `persistence` option. Games load and migrate their save envelope before loading the scene, then write `inject(ActorPersistence).snapshot()` alongside their other save data. Capture is synchronous and returns detached JSON data; do it before awaiting storage. Sessions without this option do not register actors. Network sessions reject the option.
13
+
14
+ ## Actor and component hooks
15
+
16
+ ```ts
17
+ @Actor({ persistent: true })
18
+ class Chest extends BaseActor implements PersistentState<{ opened: boolean }> {
19
+ readonly persistenceVersion = 1
20
+ opened = false
21
+
22
+ onCapturePersistence() { return { opened: this.opened } }
23
+ onRestorePersistence(state: { opened: boolean }, context: PersistenceContext) {
24
+ if (context.version !== 1 || typeof state?.opened !== 'boolean') {
25
+ throw new Error('Unsupported chest state')
26
+ }
27
+ this.opened = state.opened
28
+ }
29
+ onPersistenceRestored() { this.updateAppearance() }
30
+ }
31
+
32
+ @Component()
33
+ class Cooldown extends ActorComponent implements PersistentState<{ remaining: number }> {
34
+ // Stable within the owning actor, including nested components. Two cooldowns
35
+ // need distinct keys, e.g. 'primary-attack' and 'dodge'. Never use array indices.
36
+ persistenceKey = 'primary-attack'
37
+ readonly persistenceVersion = 1
38
+ remaining = 0
39
+
40
+ onCapturePersistence() { return { remaining: this.remaining } }
41
+ onRestorePersistence(state: { remaining: number }, context: PersistenceContext) {
42
+ if (context.version !== 1 || !Number.isFinite(state?.remaining)) {
43
+ throw new Error('Unsupported cooldown state')
44
+ }
45
+ this.remaining = Math.max(0, state.remaining)
46
+ }
47
+ }
48
+ ```
49
+
50
+ Override `get persistenceEnabled()` for a policy based on authored parameters (for example, enemy faction). It is evaluated after initialization. A component never opts its actor in. An opted-in actor may have only component hooks, or no hooks if it only needs permanent removal.
51
+
52
+ For the common position-and-yaw case, attach `ActorTransformPersistenceComponent` to an opted-in actor. It uses the stable component key `transform` and stores local position and yaw, leaving pitch and roll at their authored values. It restores before begin-play and synchronizes an existing physics body after the restore batch. Attach it only once per actor. `captureActorTransform(actor)` and `restoreActorTransform(actor, state)` provide the same JSON shape for custom hooks; the latter validates the payload and sets the object transform, so custom movement systems must also synchronize their own state and physics. A character movement component, for example, can call `movement.teleport(actor.position, actor.rotation.y)` in the actor's `onPersistenceRestored()` when a saved transform was applied (`transformComponent.restored`).
53
+
54
+ Initialize defaults in `onInit()`. Restore runs after all actors/components initialize and references bind, before `onBeginPlay()`, event graphs, game preload and presentation. Do not reset persisted fields in begin-play. Components restore before their actor. `onPersistenceRestored()` runs after the entire batch restores, including actors with no saved entry. Ordinary restore hooks may resolve other actors, but cannot assume those actors have restored yet. Hooks should not spawn participants or mutate the batch.
55
+
56
+ Save finite JSON values, not actors, vectors, signals, functions, dates or cyclic objects. Validate or migrate component/actor payload versions in the restore hook. Unknown component entries are retained for later saves. Restore failures stop loading and include identity and component key in the error. Restore by assigning state, without replaying damage, death, item rewards or other gameplay events. Save remaining durations rather than wall-clock deadlines unless the game intentionally advances them while closed.
57
+
58
+ ## Identity and removal
59
+
60
+ Authored identities are `{ sceneId, path }`: the stable scene metadata ID and the existing object/prefab instance path. Names are not IDs. Renaming does not change identity; recreating an object or moving it across prefab boundaries may require a game save migration.
61
+
62
+ Call `persistence.markRemoved(actor)` at the permanent gameplay outcome, such as enemy death, before delayed presentation cleanup. It records removal without destroying the live object. The materializer skips that authored subtree on the next load. For a prefab's exposed actor, this removes the whole exposed placement, including nested prefabs, meshes, collisions and event graphs. Ordinary `world.removeActor()` and unloading capture state but never mark it removed.
63
+
64
+ References to removed actors become `null`. Gameplay must handle that possibility. `getIdentity(actor)`, `resolve(identity)` and `isRemoved(identity)` support game save references. Scene parameters can use direct actor references, including arrays; a removed actor resolves to `null` in the corresponding field.
65
+
66
+ Scene travel captures outgoing actors and retains unloaded scene records. Game-lifetime actors remain registered and are not restored again when their authored scene is revisited.
67
+
68
+ ## Dynamic actors
69
+
70
+ The game saves its own spawn descriptors and recreates actors/prefabs explicitly:
71
+
72
+ ```ts
73
+ await world.spawnActor(Companion, position, rotation, { persistenceId: 'quest-companion' })
74
+ await world.spawnPrefab(droppedItemPrefab, position, rotation, { persistenceId: savedDrop.id })
75
+ ```
76
+
77
+ The actor must still opt in. Dynamic identities are `{ dynamicId, path }`, with an empty path for the main actor and prefab-local paths for other actors. IDs are unique across the session, independent of scene. The game decides which scene to spawn them in. A removed or already active ID is an error; use `isRemoved({ dynamicId, path: '' })` before recreating it. All actors in a prefab restore before any begin-play. References across separately spawned dynamic actors require game-controlled creation order. Synchronous spawning cannot restore persistent state.
78
+
79
+ The engine does not recreate dynamically attached components. The game must attach them with stable keys before restoration. Dynamic prefabs are removed as a unit when any persistent member is marked removed.
80
+
81
+ ## Cost and testing
82
+
83
+ Persistence has no frame callback. Nonparticipating actors receive no persistence record or subscription. Discovery occurs at initialization/capture/restore boundaries, and snapshots traverse registered actors only. Snapshot size and capture frequency remain under game control.
84
+
85
+ Core checks: `npm test -- --run src/test/actor-persistence.test.ts src/test/actor-persistence-runtime.test.ts src/test/world-lifecycle.test.ts src/test/world-prefab-spawn.test.ts src/test/prefab-instance-params.test.ts src/test/rendering-host.test.ts` from `packages/core`.
86
+
87
+ Maran integration checks, with both repositories side by side: `npm exec -- vitest run --config ../../../legend-of-maran/tests/persistence.config.mts` from `packages/core`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hology/core",
3
- "version": "0.0.260",
3
+ "version": "0.0.262",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",