Merge cuts loot-1-present and loot-2 (Soul verdicts cut-loot-1-present.s1, cut-loot-2.s2)

Conflicts: Behaviors.cs keeps Union(48, PickupData) and Union(49, LaunchSuppressorData);
Zone.cs keeps the departure record on _entities with master's Visibility (emissions-heard);
ContactRevealTests.cs keeps master's removal of the emission-departure test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 08:27:47 +02:00
commit fbe05c7992
84 changed files with 1881 additions and 657 deletions

View file

@ -23,6 +23,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4094535758000554889}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
@ -30,7 +31,6 @@ Transform:
m_Children:
- {fileID: 9133920759869389018}
m_Father: {fileID: 8489385781787202308}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &5284461483464579928
GameObject:
@ -44,7 +44,6 @@ GameObject:
- component: {fileID: 2667259809042191123}
- component: {fileID: 2093307059618178533}
- component: {fileID: 8815906420840494942}
- component: {fileID: 8842807112802394818}
- component: {fileID: 8689774408369415534}
- component: {fileID: 5705448000916375656}
- component: {fileID: -5496574226213385675}
@ -62,6 +61,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5284461483464579928}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 2.76403, y: 0, z: 0}
m_LocalScale: {x: 3, y: 3, z: 3}
@ -69,7 +69,6 @@ Transform:
m_Children:
- {fileID: 6463386671854692106}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &2667259809042191123
MeshFilter:
@ -96,6 +95,11 @@ MeshRenderer:
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@ -118,9 +122,11 @@ MeshRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!135 &8815906420840494942
SphereCollider:
@ -130,29 +136,19 @@ SphereCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5284461483464579928}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
serializedVersion: 3
m_Radius: 0.91187096
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &8842807112802394818
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5284461483464579928}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 737f47c158214f0995ba2835f040ed96, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
GridAttraction: 10
Gravity: 1
Drag: 0.05
LaunchDrag: 0.2
--- !u!54 &8689774408369415534
Rigidbody:
m_ObjectHideFlags: 0
@ -160,10 +156,21 @@ Rigidbody:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5284461483464579928}
serializedVersion: 2
serializedVersion: 5
m_Mass: 1
m_Drag: 0
m_AngularDrag: 0.05
m_LinearDamping: 0
m_AngularDamping: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 0
m_IsKinematic: 1
m_Interpolate: 0
@ -181,6 +188,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0c18f67d85af44c2b5afc8ecd15ce960, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
LabelFadeDuration: 0.5
LabelPersistDuration: 3
LabelDisplayAngle: 15
@ -204,6 +213,7 @@ PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 6463386671854692106}
m_Modifications:
- target: {fileID: 2945231343692618236, guid: e7877477dc889804ea9d04f7ffd08e9b,
@ -327,6 +337,9 @@ PrefabInstance:
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: e7877477dc889804ea9d04f7ffd08e9b, type: 3}
--- !u!114 &3355762817961458026 stripped
MonoBehaviour:

View file

@ -23,6 +23,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3193517496555256095}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
@ -30,7 +31,6 @@ Transform:
m_Children:
- {fileID: 7638477100053754924}
m_Father: {fileID: 3419130779256825702}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &7643266616531142933
GameObject:
@ -44,7 +44,6 @@ GameObject:
- component: {fileID: 3269639735116279291}
- component: {fileID: 7427774873968414741}
- component: {fileID: 674324171579366365}
- component: {fileID: 7449398549621940518}
- component: {fileID: 4507103896627202189}
- component: {fileID: 648083621841515580}
- component: {fileID: -5443764149998352794}
@ -62,6 +61,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7643266616531142933}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 3, y: 3, z: 3}
@ -69,7 +69,6 @@ Transform:
m_Children:
- {fileID: 3601651121922051457}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &3269639735116279291
MeshFilter:
@ -96,6 +95,11 @@ MeshRenderer:
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@ -118,9 +122,11 @@ MeshRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!135 &674324171579366365
SphereCollider:
@ -130,29 +136,19 @@ SphereCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7643266616531142933}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
serializedVersion: 3
m_Radius: 0.8
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &7449398549621940518
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7643266616531142933}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 737f47c158214f0995ba2835f040ed96, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
GridAttraction: 10
Gravity: 1
Drag: 0.05
LaunchDrag: 0.2
--- !u!114 &4507103896627202189
MonoBehaviour:
m_ObjectHideFlags: 0
@ -165,6 +161,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0c18f67d85af44c2b5afc8ecd15ce960, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
LabelFadeDuration: 0.5
LabelPersistDuration: 3
LabelDisplayAngle: 15
@ -178,10 +176,21 @@ Rigidbody:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7643266616531142933}
serializedVersion: 2
serializedVersion: 5
m_Mass: 1
m_Drag: 0
m_AngularDrag: 0.05
m_LinearDamping: 0
m_AngularDamping: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 0
m_IsKinematic: 1
m_Interpolate: 0
@ -204,6 +213,7 @@ PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 3601651121922051457}
m_Modifications:
- target: {fileID: 2945231343692618236, guid: e7877477dc889804ea9d04f7ffd08e9b,
@ -327,6 +337,9 @@ PrefabInstance:
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: e7877477dc889804ea9d04f7ffd08e9b, type: 3}
--- !u!114 &4202270197902620060 stripped
MonoBehaviour:

View file

@ -23,6 +23,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6430764275721745537}
serializedVersion: 2
m_LocalRotation: {x: 0.7069834, y: -0, z: -0, w: 0.7072302}
m_LocalPosition: {x: 0, y: -0.9858858, z: 0.000344038}
m_LocalScale: {x: 1, y: 1, z: 1}
@ -30,7 +31,6 @@ Transform:
m_Children:
- {fileID: 5373456011921669618}
m_Father: {fileID: 7548484292876150345}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &7167547457198401877
GameObject:
@ -44,7 +44,6 @@ GameObject:
- component: {fileID: 7390838957595368880}
- component: {fileID: 6960669510133879758}
- component: {fileID: 3507902894264904200}
- component: {fileID: 8747099621875196907}
- component: {fileID: 1545255963217739444}
- component: {fileID: -5409019571952974707}
- component: {fileID: -2501508196100734606}
@ -62,6 +61,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7167547457198401877}
serializedVersion: 2
m_LocalRotation: {x: -0.7069834, y: 0, z: 0, w: 0.7072302}
m_LocalPosition: {x: -0, y: 0, z: -2.9576573}
m_LocalScale: {x: 3, y: 3, z: 3}
@ -69,7 +69,6 @@ Transform:
m_Children:
- {fileID: 4002800527770282731}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: -89.98, y: 0, z: 0}
--- !u!33 &7390838957595368880
MeshFilter:
@ -96,6 +95,11 @@ MeshRenderer:
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@ -118,9 +122,11 @@ MeshRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!135 &3507902894264904200
SphereCollider:
@ -130,29 +136,19 @@ SphereCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7167547457198401877}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
serializedVersion: 3
m_Radius: 0.8303433
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &8747099621875196907
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7167547457198401877}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 737f47c158214f0995ba2835f040ed96, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
GridAttraction: 10
Gravity: 1
Drag: 0.05
LaunchDrag: 0.2
--- !u!114 &1545255963217739444
MonoBehaviour:
m_ObjectHideFlags: 0
@ -165,6 +161,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0c18f67d85af44c2b5afc8ecd15ce960, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
LabelFadeDuration: 0.5
LabelPersistDuration: 3
LabelDisplayAngle: 15
@ -178,10 +176,21 @@ Rigidbody:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7167547457198401877}
serializedVersion: 2
serializedVersion: 5
m_Mass: 1
m_Drag: 0
m_AngularDrag: 0.05
m_LinearDamping: 0
m_AngularDamping: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 0
m_IsKinematic: 1
m_Interpolate: 0
@ -204,6 +213,7 @@ PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 4002800527770282731}
m_Modifications:
- target: {fileID: 2945231343692618236, guid: e7877477dc889804ea9d04f7ffd08e9b,
@ -327,6 +337,9 @@ PrefabInstance:
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: e7877477dc889804ea9d04f7ffd08e9b, type: 3}
--- !u!114 &1928241908402748482 stripped
MonoBehaviour:

View file

@ -23,6 +23,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3932408193141279893}
serializedVersion: 2
m_LocalRotation: {x: 0.7069834, y: -0, z: -0, w: 0.7072302}
m_LocalPosition: {x: 0, y: 0.9264834, z: -0.0003232956}
m_LocalScale: {x: 1, y: 1, z: 1}
@ -30,7 +31,6 @@ Transform:
m_Children:
- {fileID: 7372978312502119311}
m_Father: {fileID: 5807113421007930924}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &8555821447211356933
GameObject:
@ -44,7 +44,6 @@ GameObject:
- component: {fileID: 4899765339662518078}
- component: {fileID: 1691511938472729356}
- component: {fileID: 3657818610818556929}
- component: {fileID: 946329779588188460}
- component: {fileID: 7390748043139687254}
- component: {fileID: -4611973708976329780}
- component: {fileID: -8908388773057984787}
@ -62,6 +61,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8555821447211356933}
serializedVersion: 2
m_LocalRotation: {x: -0.7069834, y: 0, z: 0, w: 0.7072302}
m_LocalPosition: {x: -0, y: 0, z: 2.7794502}
m_LocalScale: {x: 3, y: 3, z: 3}
@ -69,7 +69,6 @@ Transform:
m_Children:
- {fileID: 2388751103296260666}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: -89.98, y: 0, z: 0}
--- !u!33 &4899765339662518078
MeshFilter:
@ -96,6 +95,11 @@ MeshRenderer:
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@ -118,9 +122,11 @@ MeshRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!135 &3657818610818556929
SphereCollider:
@ -130,29 +136,19 @@ SphereCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8555821447211356933}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
serializedVersion: 3
m_Radius: 0.75
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &946329779588188460
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8555821447211356933}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 737f47c158214f0995ba2835f040ed96, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
GridAttraction: 10
Gravity: 1
Drag: 0.05
LaunchDrag: 0.2
--- !u!114 &7390748043139687254
MonoBehaviour:
m_ObjectHideFlags: 0
@ -165,6 +161,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0c18f67d85af44c2b5afc8ecd15ce960, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
LabelFadeDuration: 0.5
LabelPersistDuration: 3
LabelDisplayAngle: 15
@ -178,10 +176,21 @@ Rigidbody:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8555821447211356933}
serializedVersion: 2
serializedVersion: 5
m_Mass: 1
m_Drag: 0
m_AngularDrag: 0.05
m_LinearDamping: 0
m_AngularDamping: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 0
m_IsKinematic: 1
m_Interpolate: 0
@ -204,6 +213,7 @@ PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 2388751103296260666}
m_Modifications:
- target: {fileID: 2945231343692618236, guid: e7877477dc889804ea9d04f7ffd08e9b,
@ -327,6 +337,9 @@ PrefabInstance:
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: e7877477dc889804ea9d04f7ffd08e9b, type: 3}
--- !u!114 &3891726626588969535 stripped
MonoBehaviour:

View file

@ -23,6 +23,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2238948151922859802}
serializedVersion: 2
m_LocalRotation: {x: 0.7069834, y: -0, z: -0, w: 0.7072302}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
@ -30,7 +31,6 @@ Transform:
m_Children:
- {fileID: 3300919539099916088}
m_Father: {fileID: 8414659055235772867}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &7767746557817481743
GameObject:
@ -44,7 +44,6 @@ GameObject:
- component: {fileID: 2868629223358676395}
- component: {fileID: 6959931328227918623}
- component: {fileID: 4540815661693075899}
- component: {fileID: 4008709970769624704}
- component: {fileID: 7436534270673427838}
- component: {fileID: -6589342969031261525}
- component: {fileID: 5724758014884646323}
@ -62,6 +61,7 @@ Transform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7767746557817481743}
serializedVersion: 2
m_LocalRotation: {x: -0.7069834, y: 0, z: 0, w: 0.7072302}
m_LocalPosition: {x: -0, y: 0, z: 0}
m_LocalScale: {x: 3, y: 3, z: 3}
@ -69,7 +69,6 @@ Transform:
m_Children:
- {fileID: 7682789761269032435}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: -89.98, y: 0, z: 0}
--- !u!33 &2868629223358676395
MeshFilter:
@ -96,6 +95,11 @@ MeshRenderer:
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
@ -118,9 +122,11 @@ MeshRenderer:
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!135 &4540815661693075899
SphereCollider:
@ -130,29 +136,19 @@ SphereCollider:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7767746557817481743}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
serializedVersion: 3
m_Radius: 0.60371214
m_Center: {x: 0, y: 0, z: 0}
--- !u!114 &4008709970769624704
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7767746557817481743}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 737f47c158214f0995ba2835f040ed96, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
GridAttraction: 10
Gravity: 1
Drag: 0.05
LaunchDrag: 0.2
--- !u!114 &7436534270673427838
MonoBehaviour:
m_ObjectHideFlags: 0
@ -165,6 +161,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0c18f67d85af44c2b5afc8ecd15ce960, type: 3}
m_Name:
m_EditorClassIdentifier:
RotationSpeed: 1
GridOffset: 0
LabelFadeDuration: 0.5
LabelPersistDuration: 3
LabelDisplayAngle: 15
@ -178,10 +176,21 @@ Rigidbody:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7767746557817481743}
serializedVersion: 2
serializedVersion: 5
m_Mass: 1
m_Drag: 0
m_AngularDrag: 0.05
m_LinearDamping: 0
m_AngularDamping: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 0
m_IsKinematic: 1
m_Interpolate: 0
@ -204,6 +213,7 @@ PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 7682789761269032435}
m_Modifications:
- target: {fileID: 2945231343692618236, guid: e7877477dc889804ea9d04f7ffd08e9b,
@ -327,6 +337,9 @@ PrefabInstance:
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: e7877477dc889804ea9d04f7ffd08e9b, type: 3}
--- !u!224 &3300919539099916088 stripped
RectTransform:

View file

@ -16404,13 +16404,13 @@ MonoBehaviour:
LODHandler: {fileID: 1009503736}
SlimeRenderer: {fileID: 1254851254}
SlimeGravityCamera: {fileID: 1066823975}
SimpleCommodityPickup: {fileID: 4008709970769624704, guid: 521bfeece7dd5fd4e8f166d38493f18c,
SimpleCommodityPickup: {fileID: 7436534270673427838, guid: 521bfeece7dd5fd4e8f166d38493f18c,
type: 3}
CompoundCommodityPickup: {fileID: 7449398549621940518, guid: 366909b6a01d7254d8eef01c333f8e00,
CompoundCommodityPickup: {fileID: 4507103896627202189, guid: 366909b6a01d7254d8eef01c333f8e00,
type: 3}
GearPickup: {fileID: 946329779588188460, guid: d2b7e9a9cd7733041b0553682e4b61af,
GearPickup: {fileID: 7390748043139687254, guid: d2b7e9a9cd7733041b0553682e4b61af,
type: 3}
WeaponPickup: {fileID: 8842807112802394818, guid: d2cec1d8c3d94074e9d2e6088e1c02d7,
WeaponPickup: {fileID: 5705448000916375656, guid: d2cec1d8c3d94074e9d2e6088e1c02d7,
type: 3}
MinePrefab: {fileID: 7283754118921506074, guid: b04728b2b65e057429eb6e8529299c8a,
type: 3}

View file

@ -0,0 +1,227 @@
using System;
using System.Collections;
using System.IO;
using System.Linq;
using GameCult.Caching;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using float2 = CultMath.float2;
// Batch proof that ZoneRenderer draws Zone.FloatingItems and nothing else, in a play session with no scene of its own:
// the pickup prefabs are the ones ARPG.unity's ZoneRenderer names (so the wiring is read here too), a zone is loaded
// into a fresh ZoneRenderer, and the zone's own Release, step, expiry and TryPickUp are what move and remove the
// pickups. Zone.Release adds exactly one ItemPickup, placed at the body's position; stepping the zone moves it with the
// body; expiry and TryPickUp each destroy it; an item already in the zone when the renderer loads it is drawn too.
//
// Unity -batchmode -nographics -projectPath <project> -executeMethod FloatingItemsDrawnSmoke.Run -logFile <log>
//
// Do not pass -quit: Run enters play mode (a domain reload), the check runs there, and the Editor exits itself, 0 on
// success and 1 on any failure. Destroy only takes effect in a play session, which is why this is not an edit-mode check.
[InitializeOnLoad]
public static class FloatingItemsDrawnSmoke
{
private const string Flag = "FloatingItemsDrawnSmoke.running";
private const string Wiring = "FloatingItemsDrawnSmoke.wiring";
private static IEnumerator _check;
private static int _frames;
private static string _work;
static FloatingItemsDrawnSmoke()
{
if (SessionState.GetBool(Flag, false)) EditorApplication.playModeStateChanged += OnPlayMode;
}
public static void Run()
{
try
{
SessionState.SetString(Wiring, ReadWiring());
SessionState.SetBool(Flag, true);
EditorApplication.EnterPlaymode();
}
catch (Exception exception)
{
Debug.LogException(exception);
EditorApplication.Exit(1);
}
}
private static void OnPlayMode(PlayModeStateChange change)
{
if (change != PlayModeStateChange.EnteredPlayMode) return;
_work = Path.Combine(Path.GetTempPath(), $"floating-items-smoke-{Guid.NewGuid():N}");
Directory.CreateDirectory(_work);
_check = Check();
EditorApplication.update += Step;
}
private static void Step()
{
try
{
if (++_frames > 3000) throw new InvalidOperationException("The check did not finish in 3000 frames.");
if (_check.MoveNext()) return;
Finish(0);
}
catch (Exception exception)
{
Debug.LogException(exception);
Finish(1);
}
}
private static void Finish(int code)
{
EditorApplication.update -= Step;
SessionState.SetBool(Flag, false);
try { if (Directory.Exists(_work)) Directory.Delete(_work, true); } catch (Exception) { /* a temp directory */ }
EditorApplication.Exit(code);
}
private static void Require(bool condition, string what)
{
if (!condition) throw new InvalidOperationException(what);
}
private static IEnumerator Check()
{
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")?.GameplaySettings
?? throw new InvalidOperationException("No Settings.asset.");
// The wiring: the scene's ZoneRenderer names an ItemPickup prefab in each of the four fields, and none of those
// prefabs keeps a script that no longer exists.
var wired = SessionState.GetString(Wiring, "").Split('|').Select(entry => entry.Split('=')).Select(pair =>
(Field: pair[0], Pickup: pair[1] == "" ? null : AssetDatabase.LoadAssetAtPath<GameObject>(pair[1])?.GetComponent<ItemPickup>())).ToArray();
Require(wired.Length == 4, "The wiring was not carried into the play session.");
foreach (var (name, pickup) in wired)
{
Require(pickup != null, $"ZoneRenderer.{name} is not an ItemPickup in ARPG.unity.");
Require(pickup.ScanLabel != null && pickup.ScanLabelContainer != null, $"{name}'s prefab has no scan label.");
Require(!pickup.GetComponentsInChildren<Component>(true).Any(c => c == null), $"{name}'s prefab has a missing script.");
}
var shape = new Shape(5, 5);
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
var cargoShape = new Shape(4, 4);
foreach (var cell in cargoShape.AllCoordinates) cargoShape[cell] = true;
var oneCell = new Shape(1, 1);
oneCell[new CultMath.int2(0, 0)] = true;
var cache = AetheriaStores.Open(Path.Combine(_work, "smoke.cc"), catalogWritable: true);
cache.Upsert(new VerseGrammar { Revision = 1 });
cache.Upsert(new HullData { Name = "Smoke Hub", HullType = HullType.Station, Shape = shape, Durability = 1000, Mass = 1000 });
cache.Upsert(new CargoBayData { Name = "Smoke Crate", Shape = new Shape(), InteriorShape = cargoShape, Durability = 4 });
cache.Upsert(new GearData
{
Name = "Smoke Spare", Hardpoint = HardpointType.Tool, Shape = oneCell, Durability = 1,
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000
});
cache.Upsert(new SimpleCommodityData { Name = "Smoke Ore", Shape = oneCell, MaxStack = 100 });
cache.FlushAsync().Wait();
var ledger = new ProvenanceLedger();
for (var i = 1; i <= 8; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
var items = new ItemManager(cache, ledger, settings, _ => { });
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), null, null);
EquippableItem Make(string name, int lot) => new EquippableItem
{
Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<EquippableItemData>(name)), Durability = 1, Lot = lot
};
// A station with a crate takes the picked-up stack. It is not admitted: the zone holds no entity for the renderer to draw.
var hubRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("Smoke Hub"));
var taker = new OrbitalEntity(items, zone, new EquippableItem { Data = hubRef, Durability = 1000, Lot = 1 }, default, new EntitySettings());
Require(taker.TryEquip(Make("Smoke Crate", 2), new CultMath.int2(1, 1)), "The smoke station took no crate.");
// An item already in the zone when the renderer loads it: drawn from the load, not only from the add.
var early = zone.Release(Make("Smoke Spare", 3), new float2(5, 6), float2.zero);
// The renderer's side. Inactive while it is configured, so it runs no Awake or Start of the scene's own.
var rootObject = new GameObject("Smoke Zone Root");
var rendererObject = new GameObject("Smoke Zone Renderer");
rendererObject.SetActive(false);
var renderer = rendererObject.AddComponent<ZoneRenderer>();
renderer.ZoneRoot = rootObject.transform;
foreach (var (name, pickup) in wired)
typeof(ZoneRenderer).GetField(name).SetValue(renderer, pickup);
renderer.ItemManager = items;
renderer.SectorBrushes = new GameObject("Smoke Brushes").transform;
renderer.SlimeGravityCamera = new GameObject("Smoke Gravity Camera").AddComponent<Camera>();
var slimeObject = new GameObject("Smoke Slime");
slimeObject.SetActive(false); // the real Slime needs its compute shader; LoadZone only sets its radius
renderer.SlimeRenderer = slimeObject.AddComponent<Slime>();
renderer.LoadZone(zone);
yield return null;
ItemPickup[] Pickups() => rootObject.GetComponentsInChildren<ItemPickup>(true).Where(p => p != null).ToArray();
void At(ItemPickup pickup, FloatingItem floating, string what)
{
var at = floating.Body.Position;
var want = new Vector3(at.x, zone.GetHeight(at), at.y);
Require(Vector3.Distance(pickup.transform.position, want) < 1e-3f,
$"{what}: the pickup is at {pickup.transform.position}, the body at {want}.");
}
Require(Pickups().Length == 1, $"load: {Pickups().Length} pickups for one existing floating item.");
At(Pickups()[0], early, "load");
// Release adds exactly one pickup, at the body's position, bound to its item.
var ore = new SimpleCommodity { Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<SimpleCommodityData>("Smoke Ore")), Quantity = 7 };
var released = zone.Release(ore, new float2(3, 4), new float2(10, 0));
yield return null;
Require(Pickups().Length == 2, $"release: {Pickups().Length} pickups for two floating items.");
var drawn = Pickups().Single(p => p.Item == ore);
At(drawn, released, "release");
// Stepping the zone moves the sim body; the pickup follows it on its next update, with nothing of its own.
var before = drawn.transform.position;
zone.Update(.5f);
yield return null;
Require(released.Body.Position.x > 3.5f, "the sim body did not drift.");
At(drawn, released, "step");
Require(drawn.transform.position != before, "the pickup did not move with the body.");
// Pickup destroys exactly the picked-up item's pickup.
Require(zone.TryPickUp(taker, released.Id), "the station refused a one-cell stack.");
yield return null;
yield return null;
Require(drawn == null, "pickup: the pickup is still there.");
Require(Pickups().Length == 1 && Pickups()[0].Item == early.Item, "pickup: the wrong pickup went.");
// Expiry destroys the pickup of an item nobody took.
zone.Update(3600f); // far past any lifetime
yield return null;
yield return null;
Require(zone.FloatingItems.Count == 0, "expiry: the item did not expire.");
Require(Pickups().Length == 0, $"expiry: {Pickups().Length} pickups left.");
// A later release is drawn again, and clearing the renderer takes what is left.
zone.Release(Make("Smoke Spare", 4), new float2(1, 1), float2.zero);
yield return null;
Require(Pickups().Length == 1, "re-release: not drawn.");
renderer.ClearZone();
yield return null;
yield return null;
Require(Pickups().Length == 0, "clear: pickups left after ClearZone.");
Debug.Log("FLOATING_ITEMS_SMOKE ok");
}
// What ARPG.unity's ZoneRenderer names in each pickup field, read from the saved scene before the play session (a
// scene cannot be opened inside one) and carried across the domain reload as the prefabs' asset paths.
private static string ReadWiring()
{
var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Additive);
try
{
var found = UnityEngine.Object.FindObjectsByType<ZoneRenderer>(FindObjectsInactive.Include, FindObjectsSortMode.None)
.Where(r => r.gameObject.scene == scene).ToArray();
Require(found.Length == 1, $"ARPG.unity has {found.Length} ZoneRenderers.");
var wired = found[0];
return string.Join("|", new[]
{
("SimpleCommodityPickup", wired.SimpleCommodityPickup), ("CompoundCommodityPickup", wired.CompoundCommodityPickup),
("GearPickup", wired.GearPickup), ("WeaponPickup", wired.WeaponPickup)
}.Select(f => f.Item1 + "=" + (f.Item2 == null ? "" : AssetDatabase.GetAssetPath(f.Item2))));
}
finally { EditorSceneManager.CloseScene(scene, true); }
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 67544a932f794a2458741c731ef04d11

View file

@ -64,10 +64,8 @@ public static class ImpactPresenterSmoke
var hullRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("Smoke Hull"));
var ship = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 1 }, new EntitySettings());
var other = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 2 }, new EntitySettings());
zone.Entities.Add(ship);
zone.Entities.Add(other);
ship.Activate();
other.Activate();
zone.Admit(ship, piloted: false);
zone.Admit(other, piloted: false);
// The scene's side: the tuning object with a pooled effect, and the game manager that names the player's ship.
var feedbackObject = new GameObject("Smoke Impact Feedback");

View file

@ -0,0 +1,75 @@
using System;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
// Strips the deleted integrator component from the pickup prefabs and points ZoneRenderer's four pickup fields in ARPG.unity at
// each prefab's ItemPickup, then saves the prefabs and the scene. Idempotent: a prefab with no missing script is left as
// it is, and the fields are set to the same values again.
//
// Unity -batchmode -projectPath <project> -executeMethod WireFloatingItems.Run -logFile <log>
//
// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure. The menu item runs the same wiring.
public static class WireFloatingItems
{
private const string ScenePath = "Assets/Scenes/ARPG.unity";
private const string PickupFolder = "Assets/Prefabs/RPG/Pickups/";
// The field each prefab filled before this cut (read from ARPG.unity at the cut's base). Icosahedron fills none.
private static readonly (string Field, string Prefab)[] Fields =
{
("SimpleCommodityPickup", "Tetrahedron"),
("CompoundCommodityPickup", "Hexahedron"),
("GearPickup", "Octahedron"),
("WeaponPickup", "Dodecahedron"),
};
private static readonly string[] Prefabs = { "Tetrahedron", "Hexahedron", "Octahedron", "Dodecahedron", "Icosahedron" };
[MenuItem("Aetheria/Wire Floating Items")]
public static void Menu() => Wire();
public static void Run()
{
try
{
Wire();
EditorApplication.Exit(0);
}
catch (Exception exception)
{
Debug.LogException(exception);
EditorApplication.Exit(1);
}
}
private static void Wire()
{
foreach (var name in Prefabs)
{
var path = PickupFolder + name + ".prefab";
var contents = PrefabUtility.LoadPrefabContents(path);
try
{
var removed = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(contents);
if (contents.GetComponent<ItemPickup>() == null) throw new InvalidOperationException($"{path} has no ItemPickup.");
if (removed > 0) PrefabUtility.SaveAsPrefabAsset(contents, path);
Debug.Log($"WIRE_FLOATING_ITEMS {name} removed={removed}");
}
finally { PrefabUtility.UnloadPrefabContents(contents); }
}
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
var renderer = UnityEngine.Object.FindFirstObjectByType<ZoneRenderer>();
if (renderer == null) throw new InvalidOperationException("ARPG.unity has no ZoneRenderer.");
var serialized = new SerializedObject(renderer);
foreach (var (field, prefab) in Fields)
{
var pickup = AssetDatabase.LoadAssetAtPath<GameObject>(PickupFolder + prefab + ".prefab").GetComponent<ItemPickup>();
var property = serialized.FindProperty(field) ?? throw new InvalidOperationException($"ZoneRenderer has no {field}.");
property.objectReferenceValue = pickup;
}
serialized.ApplyModifiedProperties();
EditorSceneManager.MarkSceneDirty(scene);
if (!EditorSceneManager.SaveScene(scene)) throw new InvalidOperationException("The scene did not save.");
Debug.Log("WIRE_FLOATING_ITEMS ok");
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9a56d7dc8471d224f9bed434b39f7c87

View file

@ -739,7 +739,7 @@ public class ActionGameManager : MonoBehaviour
if (CurrentEntity != null)
{
CurrentEntity.Deactivate();
CurrentEntity.Zone.Entities.Remove(CurrentEntity);
CurrentEntity.Zone.Remove(CurrentEntity);
CurrentEntity.Zone = Zone;
Zone.Admit(CurrentEntity, piloted: false);
}
@ -1332,9 +1332,7 @@ public class ActionGameManager : MonoBehaviour
TargetShieldsFill.fillAmount = target.Shield == null ? 0 : lerp(.25f, .75f, target.Shield.Progress);
}
var tractorPower = Input.Player.TractorBeam.ReadValue<float>();
CurrentEntity.TractorPower =
saturate(CurrentEntity.TractorPower + sign(tractorPower - CurrentEntity.TractorPower) * Time.deltaTime * 2);
CurrentEntity.TractorCommand = Input.Player.TractorBeam.ReadValue<float>();
}
}
// One clock steps the zone, paused or not: a paused frame feeds it no real time.

View file

@ -13,15 +13,15 @@ using UnityEngine;
public class CapabilityPresentationBinder : MonoBehaviour
{
private readonly List<IDisposable> _subscriptions = new List<IDisposable>();
private Func<int, Transform> _grabTargetResolver;
private Func<int, Vector3> _grabInitialVelocityResolver;
private Func<FloatingBodyId, Transform> _grabTargetResolver;
private Func<FloatingBodyId, Vector3> _grabInitialVelocityResolver;
// Subscribes every presenter interface implemented by components on this object or its children to
// the matching stream of `events`. `grabTargetResolver`/`grabInitialVelocityResolver` resolve a
// GrabEvent's plain target handle to the live Transform/velocity a grab presenter needs; the owner
// GrabEvent's target id to the live Transform/velocity a grab presenter needs; the owner
// of `events` supplies them (the test scene today; the real pickup capability's caller later).
public void Bind(CapabilityEvents events, Func<int, Transform> grabTargetResolver = null,
Func<int, Vector3> grabInitialVelocityResolver = null)
public void Bind(CapabilityEvents events, Func<FloatingBodyId, Transform> grabTargetResolver = null,
Func<FloatingBodyId, Vector3> grabInitialVelocityResolver = null)
{
Unbind();
if (events == null) return;
@ -40,8 +40,8 @@ public class CapabilityPresentationBinder : MonoBehaviour
var p = presenter;
_subscriptions.Add(events.Grab.Subscribe(e =>
{
var target = _grabTargetResolver?.Invoke(e.TargetHandle);
var velocity = _grabInitialVelocityResolver?.Invoke(e.TargetHandle) ?? Vector3.zero;
var target = _grabTargetResolver?.Invoke(e.Target);
var velocity = _grabInitialVelocityResolver?.Invoke(e.Target) ?? Vector3.zero;
p.Grab(e, target, velocity);
}));
}

View file

@ -11,7 +11,7 @@ public interface IAbsorbPresenter
}
// Grab needs a live Transform to animate, which the engine-free GrabEvent cannot carry (its
// TargetHandle is a plain identity, not a Unity reference). The binder resolves the handle through a
// Target is a plain identity, not a Unity reference). The binder resolves the id through a
// delegate supplied by the event source's owner and hands the presenter the result, along with the
// initial velocity the target was already moving with (also owner-supplied, for the same reason).
public interface IGrabPresenter

View file

@ -301,27 +301,6 @@ public class EntityInstance : MonoBehaviour
{
if (!this) return;
_destroyed = true;
foreach (var gear in Entity.Equipment)
{
if (gear != Entity.EquippedHull && Random.value < ZoneRenderer.Settings.LootDropProbability)
{
ZoneRenderer.DropItem(
Entity.Position.ToUnity(),
Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity,
gear.EquippableItem);
}
}
foreach (var cargo in Entity.CargoBays)
{
foreach (var item in cargo.Cargo.Keys)
{
ZoneRenderer.DropItem(
Entity.Position.ToUnity(),
Random.onUnitSphere * ZoneRenderer.Settings.LootDropVelocity,
item);
}
}
if (DestroyEffect != null)
{
var t = Instantiate(DestroyEffect).transform;

View file

@ -11,9 +11,6 @@ using UnityEngine;
public class GameSettings : ScriptableObject
{
public string StartingHullName = "Longinus";
public float PickupLifetime = 30;
public float LootDropProbability = .25f;
public float LootDropVelocity = 25;
public float HeatstrokePhasingFloor = 0;
public float HeatstrokePhasingFrequency = 5;
public float WormholeDistanceRatio;

View file

@ -1,51 +0,0 @@
using System;
using CultMath;
using CultMath.UnityBridge;
using UnityEngine;
using static CultMath.math;
using Random = UnityEngine.Random;
public class GridObject : MonoBehaviour
{
public float RotationSpeed;
public float GridOffset;
public float GridAttraction;
public float Gravity;
public float Drag = .1f;
public float LaunchDrag;
private float2 _selfVelocity;
private float _timeOffset;
public Zone Zone { get; set; }
public Vector3 Velocity { get; set; }
private void Start()
{
_timeOffset = Random.value * 100;
}
void Update()
{
var t = transform;
t.localRotation = Quaternion.Euler(sin(Time.time - _timeOffset * RotationSpeed) * 90, 0, cos(Time.time - _timeOffset * RotationSpeed) * 90);
if (Zone == null) return;
var position = t.position;
var gridHeight = Zone.GetHeight(position.Flatland().ToCultMath()) + GridOffset;
Velocity += Vector3.up * (sign(gridHeight - position.y) * GridAttraction * Time.deltaTime);
Velocity *= max(0, 1 - LaunchDrag * Time.deltaTime);
var normal = Zone.GetNormal(position.Flatland().ToCultMath());
var force = new float2(normal.x, normal.z);
var forceMagnitude = lengthsq(force);
if (forceMagnitude > .001f)
{
var fa = 1 / (1 - forceMagnitude) - 1;
_selfVelocity += normalize(force) * Zone.Settings.GravityStrength * fa * Gravity;
}
_selfVelocity *= max(0, 1 - Drag * Time.deltaTime);
t.position = position + (Velocity + new Vector3(_selfVelocity.x, 0, _selfVelocity.y)) * Time.deltaTime;
}
}

View file

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 737f47c158214f0995ba2835f040ed96
timeCreated: 1618441439

View file

@ -4,9 +4,15 @@ using CultMath;
using CultMath.UnityBridge;
using UnityEngine;
using static CultMath.math;
using Random = UnityEngine.Random;
// How one floating item looks. Zone owns the item's every fact (existence, position, expiry, pickup); once bound,
// this reads the latest sim body and draws, spun for show, and writes nothing back (rulings unity-presents-only-r3
// and sim-speed-presentation). An unbound pickup (FieldTester's test scene) leaves its transform alone.
public class ItemPickup : MonoBehaviour
{
public float RotationSpeed = 1f;
public float GridOffset;
public float LabelFadeDuration = .5f;
public float LabelPersistDuration = 3;
public float LabelDisplayAngle = 15;
@ -15,14 +21,38 @@ public class ItemPickup : MonoBehaviour
public TextMeshPro ScanLabel;
public ItemInstance Item { get; set; }
public ZoneRenderer ZoneRenderer { get; set; }
public float3 ViewOrigin { get; set; }
public float3 ViewDirection { get; set; }
private float _displayTime;
private float _timeOffset;
private Zone _zone;
private FloatingItem _floating;
public void Bind(Zone zone, FloatingItem floating)
{
_zone = zone;
_floating = floating;
Item = floating.Item;
_timeOffset = Random.value * 100;
Place();
}
private void Place()
{
var p = _floating.Body.Position;
transform.position = new Vector3(p.x, _zone.GetHeight(p) + GridOffset, p.y);
}
private void Update()
{
if (_floating != null)
{
Place();
transform.localRotation = Quaternion.Euler(
sin(Time.time - _timeOffset * RotationSpeed) * 90, 0, cos(Time.time - _timeOffset * RotationSpeed) * 90);
}
var diff = transform.position.ToCultMath() - ViewOrigin;
var toThis = normalize(diff);
var viewAngle = acos(dot(toThis, ViewDirection)) * Mathf.Rad2Deg;
@ -34,9 +64,4 @@ public class ItemPickup : MonoBehaviour
ScanLabel.color = c;
ScanLabelContainer.rotation = Quaternion.LookRotation((-toThis).ToUnity());
}
private void OnDestroy()
{
ZoneRenderer?.DestroyLoot(this);
}
}
}

View file

@ -52,40 +52,6 @@ public class ShieldManager : MonoBehaviour, IAbsorbPresenter
Debug.LogWarning("ShieldManager has no ShieldEnvelope; falling back to its own transform for the hit direction.", this);
}
private void OnCollisionEnter(Collision other)
{
var otherShield = other.collider.GetComponent<ShieldManager>();
if (!otherShield)
{
var gridObject = other.collider.GetComponent<GridObject>();
if (gridObject)
{
var itemPickup = other.collider.GetComponent<ItemPickup>();
if (itemPickup)
{
if (Entity.CargoBays.Any(c => c.TryStore(itemPickup.Item)))
{
// TODO: Pickup notification!
Destroy(itemPickup.gameObject);
}
else
{
// TODO: Pickup failed notification!
var cp = other.GetContact(0);
gridObject.Velocity += cp.normal * 25;
Debug.Log("Attempted item pickup, but no space in cargo bay!");
}
}
else
{
Debug.Log("Shield collision occurred with grid object, but other collider isn't an item pickup!");
}
}
else
Debug.Log("Shield collision occurred, but other collider isn't a shield or a grid object!");
}
}
// A shot the shield took ripples it where the shot landed, drawn on the hull as it is drawn. The hull layer is
// ImpactPresenter's.
public void Absorb(AbsorbEvent e)

View file

@ -1,19 +0,0 @@
using System;
using UnityEngine;
public class TimedDestroy : MonoBehaviour
{
public float Duration;
private float _startTime;
private void Start()
{
_startTime = Time.time;
}
public void Update()
{
if(Time.time - _startTime > Duration) Destroy(gameObject);
}
}

View file

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 5d5539a23b814254b038a16f32f1d8a9
timeCreated: 1618453298

View file

@ -14,20 +14,9 @@ public class TractorBeam : MonoBehaviour
public void Update()
{
transform.rotation = Quaternion.LookRotation(Direction);
// The beam draws nothing until it presents a sim grab (loot-3): no particles, and no pull; the simulation
// decides every floating item's motion.
var emission = ParticleSystem.emission;
if (Power > .01f)
{
var objectFound = false;
foreach (var o in Physics.SphereCastAll(transform.position, Radius, Direction, Distance, 1 << 21))
{
var gridObject = o.collider.GetComponent<GridObject>();
if (gridObject == null) continue;
objectFound = true;
gridObject.Velocity += Vector3.Normalize(transform.position - gridObject.transform.position) * (Traction * Time.deltaTime);
}
emission.rateOverTimeMultiplier = objectFound ? Power : 0;
}
else emission.rateOverTimeMultiplier = 0;
emission.rateOverTimeMultiplier = 0;
}
}

View file

@ -189,6 +189,7 @@ public interface IPowerConsumer
Union(45, typeof(MunitionSeekerData)),
Union(46, typeof(MunitionControlData)),
Union(47, typeof(MunitionFuelData)),
Union(48, typeof(PickupData)),
Union(49, typeof(LaunchSuppressorData)),
JsonObject(MemberSerialization.OptIn)]
public abstract class BehaviorData

View file

@ -0,0 +1,171 @@
using System;
using System.Linq;
using CultMath;
using MessagePack;
using Newtonsoft.Json;
using static CultMath.math;
using float2 = CultMath.float2;
// The tractor beam: the capability that lets a ship collect floating items. It spins its beam up in sim time toward
// the pilot's command, and once the beam is up claims the free item nearest its axis and runs one timed grab
// (extend, envelop, pull) that ends in Zone.TryPickUp, the one commit of a pickup. Its stats are flat
// (Min == Max), as the other behaviours that carry their own timings.
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
public class PickupData : BehaviorData
{
// How far along the beam's axis an item can be claimed, and how far off the axis.
[Inspectable, JsonProperty("range"), Key(1), RuntimeInspectable]
public PerformanceStat Range = new PerformanceStat();
[Inspectable, JsonProperty("beamRadius"), Key(2), RuntimeInspectable]
public PerformanceStat BeamRadius = new PerformanceStat();
// The spin-up (0..1) needed to claim an item, and to keep a grab going.
[Inspectable, JsonProperty("grabPower"), Key(3), RuntimeInspectable]
public PerformanceStat GrabPower = new PerformanceStat();
// Seconds of each phase of a grab.
[Inspectable, JsonProperty("extendTime"), Key(4), RuntimeInspectable]
public PerformanceStat ExtendTime = new PerformanceStat();
[Inspectable, JsonProperty("envelopTime"), Key(5), RuntimeInspectable]
public PerformanceStat EnvelopTime = new PerformanceStat();
[Inspectable, JsonProperty("pullTime"), Key(6), RuntimeInspectable]
public PerformanceStat PullTime = new PerformanceStat();
// Spin-up and spin-down, in power per sim second.
[Inspectable, JsonProperty("spinRate"), Key(7), RuntimeInspectable]
public PerformanceStat SpinRate = new PerformanceStat();
public override Behavior CreateInstance(EquippedItem item)
{
return new Pickup(this, item);
}
// A beam is a piece of gear the pilot holds on; no consumable carries one.
public override Behavior CreateInstance(ConsumableItemEffect item) =>
throw new NotSupportedException("pickup: gear only");
}
// Owns this ship's beam power and its one grab. Power is written only here; Zone owns the held item's pose.
public class Pickup : Behavior, IAlwaysUpdatedBehavior
{
private readonly PickupData _data;
// Power and the grab clock are kept in double so a fixed tick count lands on the same side of a boundary
// whatever dt the zone steps by.
private double _power;
private bool _grabbing;
private FloatingBodyId _target;
private double _elapsed;
// How many of Envelop and Pull have been published so far (0, 1 or 2).
private int _published;
public Pickup(PickupData data, EquippedItem item) : base(data, item)
{
_data = data;
}
// The beam's spun-up state in [0, 1]: what the pilot's command has driven it to.
public float Power => (float) _power;
public void Update(float dt)
{
var active = Item.Active.Value;
var command = active ? Math.Clamp(Entity.TractorCommand, 0f, 1f) : 0f;
var step = (double) Evaluate(_data.SpinRate) * dt;
_power = Math.Clamp(_power + Math.Clamp(command - _power, -step, step), 0.0, 1.0);
var zone = Entity.Zone;
var grabPower = Evaluate(_data.GrabPower);
var extend = (double) Evaluate(_data.ExtendTime);
var envelop = (double) Evaluate(_data.EnvelopTime);
var pull = (double) Evaluate(_data.PullTime);
var range = Evaluate(_data.Range);
var radius = Evaluate(_data.BeamRadius);
var origin = Entity.Position.xz;
if (!_grabbing)
{
if (!active || _power < grabPower) return;
var item = Select(zone, origin, range, radius);
if (item == null || !zone.TryHold(Entity, item.Id)) return;
_grabbing = true;
_target = item.Id;
_elapsed = 0;
_published = 0;
Publish(GrabPhase.Started, item, 0);
Publish(GrabPhase.Extend, item, (float) extend);
return;
}
var held = zone.FloatingItems.FirstOrDefault(f => f.Id.Equals(_target));
if (held == null || !ReferenceEquals(held.Holder, Entity))
{
_grabbing = false;
Entity.Capabilities.PublishGrab(new GrabEvent(_target, GrabPhase.Cancelled, float3(0), 0));
return;
}
var pulling = _elapsed >= extend + envelop;
if (!active || _power < grabPower || (!pulling && !InBeam(held.Body.Position, origin, range, radius, out _)))
{
_grabbing = false;
zone.Free(_target);
Publish(GrabPhase.Cancelled, held, 0);
return;
}
_elapsed += dt;
if (_published < 1 && _elapsed >= extend) { _published = 1; Publish(GrabPhase.Envelop, held, (float) envelop); }
if (_published < 2 && _elapsed >= extend + envelop) { _published = 2; Publish(GrabPhase.Pull, held, (float) pull); }
if (_published == 2)
zone.SetHeldProgress(_target, pull > 0 ? (float) ((_elapsed - extend - envelop) / pull) : 1f);
if (_elapsed < extend + envelop + pull) return;
_grabbing = false;
if (zone.TryPickUp(Entity, _target)) Publish(GrabPhase.Completed, held, 0);
else
{
zone.Free(_target);
Publish(GrabPhase.Cancelled, held, 0);
}
}
// The one beam region: within Range along the aim axis (never behind) and BeamRadius off it. Selection claims
// from it and the grab holds on to its item only while the item stays in it. A zero aim has no region.
private bool InBeam(float2 position, float2 origin, float range, float radius, out float off)
{
off = 0;
var aim = Entity.Aim.xz;
if (lengthsq(aim) <= 0) return false;
var axis = normalize(aim);
var to = position - origin;
var along = dot(to, axis);
off = length(to - axis * along);
return along >= 0 && along <= range && off <= radius;
}
// The free item nearest the beam axis within the beam region that some cargo bay can store whole (the bay's own
// TryFindSpace, the test TryStore opens with); ties go to the lowest id. A zero aim selects nothing.
private FloatingItem Select(Zone zone, float2 origin, float range, float radius)
{
FloatingItem best = null;
var bestOff = float.MaxValue;
foreach (var floating in zone.FloatingItems)
{
if (floating.Holder != null) continue;
if (!InBeam(floating.Body.Position, origin, range, radius, out var off)) continue;
if (!Entity.CargoBays.Any(bay => bay.TryFindSpace(floating.Item))) continue;
if (off < bestOff || (off == bestOff && floating.Id.Value < best.Id.Value))
{
best = floating;
bestOff = off;
}
}
return best;
}
private void Publish(GrabPhase phase, FloatingItem item, float duration) =>
Entity.Capabilities.PublishGrab(new GrabEvent(item.Id, phase, float3(item.Body.Position.x, Entity.Position.y, item.Body.Position.y), duration));
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ac7678d240564331be7486588d28c5ee
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -52,19 +52,18 @@ public enum GrabPhase
Cancelled
}
// The target is identified by a plain handle, not a Unity type: the simulation will own the real
// target identity (a loot drop or item instance id) once the pickup capability exists. Today's only
// publisher (the FieldShieldTest scene) hands out its own handles; nothing here interprets them.
// One phase of a grab. The target is the floating item the Pickup behaviour holds (the FieldShieldTest scene
// publishes its own handles as ids); nothing here interprets it.
public readonly struct GrabEvent
{
public readonly int TargetHandle;
public readonly FloatingBodyId Target;
public readonly GrabPhase Phase;
public readonly float3 TargetPosition;
public readonly float PhaseDuration;
public GrabEvent(int targetHandle, GrabPhase phase, float3 targetPosition, float phaseDuration)
public GrabEvent(FloatingBodyId target, GrabPhase phase, float3 targetPosition, float phaseDuration)
{
TargetHandle = targetHandle;
Target = target;
Phase = phase;
TargetPosition = targetPosition;
PhaseDuration = phaseDuration;

View file

@ -153,7 +153,11 @@ public abstract class Entity
public bool OverrideShutdown { get; set; }
public float TractorPower { get; set; }
// The pilot's tractor command (0..1); the Pickup behaviour spins its beam toward it.
public float TractorCommand;
// The beam's spun-up state: Pickup's, or 0 without one.
public float TractorPower => GetBehavior<Pickup>()?.Power ?? 0f;
public bool Active
{
@ -1549,7 +1553,7 @@ public abstract class Entity
{
bay.DockedShip = ship;
ship.SetParent(this);
Zone.Entities.Remove(ship);
Zone.Remove(ship);
ship.Deactivate();
ship.Docked.OnNext(this);
}

View file

@ -42,7 +42,8 @@ public struct KinematicBody
}
// An item adrift in a zone: a death drop or jettisoned cargo. Zone.Release is its only adder; Zone's floating-item
// step (expiry) and TryPickUp are its only removers. Runtime state, never saved.
// step (expiry) and TryPickUp are its only removers. Zone.TryHold and Free are the only writers of Holder, the
// entity grabbing it with a tractor beam; a held item is placed by Zone, not drifted. Runtime state, never saved.
public sealed class FloatingItem
{
public FloatingBodyId Id;
@ -50,4 +51,9 @@ public sealed class FloatingItem
public KinematicBody Body;
public double ReleasedAt;
public float Lifetime;
// The entity holding it, or null when it floats free; HeldFrom is where it was claimed and HeldProgress how far
// along its pull to the holder (0..1).
public Entity Holder;
public float2 HeldFrom;
public float HeldProgress;
}

View file

@ -54,6 +54,7 @@ public class LoadoutGenerator
var entity = new Ship(ItemManager, null, hull, ItemManager.GameplaySettings.DefaultEntitySettings);
entity.Faction = Faction;
OutfitEntity(entity);
EquipPickup(entity);
StockConsumables(entity, ShipConsumableChance, ShipConsumableUnits);
return EntitySerializer.Pack(entity);
}
@ -287,6 +288,24 @@ public class LoadoutGenerator
FillInterior(entity);
}
// Every ship carries a tractor beam: picking up needs a pickup behaviour (scenarios ruling, "everyone gets tractor
// beams"). It goes in after the shared outfitting, which stations and turrets also run and which adds none, into
// whatever interior the hull has left; a hull with no cell for one cannot be generated.
private void EquipPickup(Entity entity)
{
var emptyShape = entity.UnoccupiedSpace;
var (product, design) = RandomProduct<GearData>(2, item =>
item.Behaviors.Any(b => b is PickupData) &&
item.Shape.FitsWithin(emptyShape, out _, out _), required: true);
if (design == null) throw new InvalidLoadoutException("No compatible tractor beam found for ship!");
design.Shape.FitsWithin(emptyShape, out var rotation, out var position);
var beam = ItemManager.CreateInstance(product) as EquippableItem;
beam.Rotation = rotation;
if (!entity.TryEquip(beam, position))
throw new InvalidLoadoutException("Failed to equip selected tractor beam!");
}
private void EquipHardpoints(Entity entity)
{
var hullData = ItemManager.GetData(entity.Hull) as HullData;

View file

@ -15,7 +15,29 @@ using float2 = CultMath.float2;
public class Zone
{
public Action<string> Log;
public ReactiveCollection<Entity> Entities = new ReactiveCollection<Entity>();
// Membership, in admission order. Admit and Remove are the only writers; everyone else reads this view, which has
// no mutating member, so the death watch below follows membership by construction.
private readonly ReactiveCollection<Entity> _entities = new ReactiveCollection<Entity>();
public MemberView Entities { get; }
public sealed class MemberView : IReadOnlyReactiveCollection<Entity>
{
private readonly ReactiveCollection<Entity> _members;
internal MemberView(ReactiveCollection<Entity> members) => _members = members;
public int Count => _members.Count;
public Entity this[int index] => _members[index];
public int IndexOf(Entity entity) => _members.IndexOf(entity);
public bool Contains(Entity entity) => _members.Contains(entity);
public IEnumerator<Entity> GetEnumerator() => _members.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public UniRx.IObservable<CollectionAddEvent<Entity>> ObserveAdd() => _members.ObserveAdd();
public UniRx.IObservable<int> ObserveCountChanged(bool notifyCurrentCount = false) => _members.ObserveCountChanged(notifyCurrentCount);
public UniRx.IObservable<CollectionMoveEvent<Entity>> ObserveMove() => _members.ObserveMove();
public UniRx.IObservable<CollectionRemoveEvent<Entity>> ObserveRemove() => _members.ObserveRemove();
public UniRx.IObservable<CollectionReplaceEvent<Entity>> ObserveReplace() => _members.ObserveReplace();
public UniRx.IObservable<Unit> ObserveReset() => _members.ObserveReset();
}
// Laid mines, in lay order. Zone owns their existence: Lay is the one adder, the mine step the one remover,
// and every removal is a blast. Runtime state, never saved.
@ -26,7 +48,8 @@ public class Zone
public ReactiveCollection<VapourCloud> Clouds = new ReactiveCollection<VapourCloud>();
// Floating items (death drops, jettisoned cargo), in release order. Zone owns their existence: Release is the one
// adder; StepFloatingItems (expiry) and TryPickUp are the only removers. Runtime state, never saved.
// adder; StepFloatingItems (expiry) and TryPickUp are the only removers. TryHold, Free and Remove-of-the-holder
// are the only writers of FloatingItem.Holder. Runtime state, never saved.
public ReactiveCollection<FloatingItem> FloatingItems = new ReactiveCollection<FloatingItem>();
public Dictionary<CultRecordKey, BodyData> Planets = new Dictionary<CultRecordKey, BodyData>();
public Dictionary<CultRecordKey, Planet> PlanetInstances = new Dictionary<CultRecordKey, Planet>();
@ -105,30 +128,13 @@ public class Zone
// The death ordinal of the loot dice: the nth death in this zone rolls on SimulationDice.For(CombatSeed, LootStream, n).
private uint _deaths;
// One death subscription per member entity, owned here: a membership record and its subscription are created
// together, and only when the entity is not already a member. Leaving Entities (Remove) or resetting it (Clear)
// disposes it; nothing else touches it.
// One death subscription per member entity: Admit creates it together with the membership, Remove disposes it
// together with the membership, and nothing else touches either.
private readonly Dictionary<Entity, IDisposable> _deathWatches = new Dictionary<Entity, IDisposable>();
private void WatchDeath(Entity entity)
{
if (_deathWatches.ContainsKey(entity)) return;
_deathWatches.Add(entity, entity.Death.Subscribe(_ => { DropLoot(entity); Entities.Remove(entity); entity.Deactivate(); }));
}
private void UnwatchDeath(Entity entity)
{
if (_deathWatches.Remove(entity, out var watch)) watch.Dispose();
}
private void UnwatchAll()
{
foreach (var watch in _deathWatches.Values) watch.Dispose();
_deathWatches.Clear();
}
public Zone(ItemManager itemManager, PlanetSettings settings, ZonePack pack, GalaxyZone galaxyZone, Galaxy galaxy)
{
Entities = new MemberView(_entities);
_time = pack.Time;
GalaxyZone = galaxyZone;
Galaxy = galaxy;
@ -144,11 +150,8 @@ public class Zone
// ObserveAdd fires for all of them. Forbidden writer: no presentation may remove an entity from Zone.
// The death watch lives exactly as long as the entity's membership in this zone: installed on admission,
// disposed on removal (death, dock, jump), so an entity admitted again never holds a second one.
Entities.ObserveAdd().Subscribe(add => WatchDeath(add.Value));
Entities.ObserveRemove().Subscribe(remove => UnwatchDeath(remove.Value));
Entities.ObserveReset().Subscribe(_ => UnwatchAll());
// The one record of a removal for observers (ruling reveal-ends-on-perception): read by the next Sense pass.
Entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz,
_entities.ObserveRemove().Subscribe(remove => _departures.Add(new Departure(remove.Value, remove.Value.Position.xz,
remove.Value.Visibility)));
Targets.Add(new EntityTargets());
@ -205,17 +208,32 @@ public class Zone
// TODO: Associate planets with stored entities for planetary colonies
}
// The one admission into this zone (docs/scenarios-cut.md, 2.2): the only writer of Entities.Add and the only
// The one admission into this zone (docs/scenarios-cut.md, 2.2): the only writer of membership and the only
// creator of agents. Construction, new-run staging (RunStart), warp arrival, undock and the console's spawned
// turret all join through here. A piloted entity gets the zone's Minion; an unpiloted one gets no agent at all.
// Only a ship can be piloted. The Death subscription above covers every join, whichever caller admits it.
// Only a ship can be piloted. The death watch is installed here, for every join, whichever caller admits it.
// An entity that is already a member is refused: it would hold a second watch and a second agent.
public void Admit(Entity entity, bool piloted)
{
Entities.Add(entity);
if (_entities.Contains(entity)) throw new InvalidOperationException("entity: already a member of this zone");
_entities.Add(entity);
_deathWatches.Add(entity, entity.Death.Subscribe(_ => { DropLoot(entity); Remove(entity); entity.Deactivate(); }));
entity.Activate();
if (piloted) Agents.Add(CreateAgent((Ship) entity));
}
// The one removal from this zone (death, dock, jump): membership and its death watch leave together. False, and
// nothing changes, when the entity is not a member. The caller deactivates; Remove does not.
public bool Remove(Entity entity)
{
if (!_entities.Remove(entity)) return false;
_deathWatches.Remove(entity, out var watch);
watch.Dispose();
foreach (var floating in FloatingItems)
if (ReferenceEquals(floating.Holder, entity)) Free(floating.Id);
return true;
}
private Agent CreateAgent(Ship ship)
{
var agent = new Minion(ship);
@ -231,7 +249,7 @@ public class Zone
{
Radius = Pack.Radius,
Mass = Pack.Mass,
Entities = Entities.Select(EntitySerializer.Pack).ToList(),
Entities = _entities.Select(EntitySerializer.Pack).ToList(),
Orbits = Orbits.Keys.Select(key => new CultRecordRef<OrbitData>(key)).ToList(),
Planets = Planets.Keys.Select(key => new CultRecordRef<BodyData>(key)).ToList(),
Time = _time,
@ -315,14 +333,44 @@ public class Zone
// The one commit of a pickup: the first of the taker's cargo bays that accepts the item stores it and the item
// stops floating. Otherwise nothing changes.
// An item held by another entity is refused: one holder per item holds at the commit as well.
public bool TryPickUp(Entity taker, FloatingBodyId id)
{
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
if (floating == null || !taker.CargoBays.Any(bay => bay.TryStore(floating.Item))) return false;
if (floating == null || (floating.Holder != null && !ReferenceEquals(floating.Holder, taker))) return false;
if (!taker.CargoBays.Any(bay => bay.TryStore(floating.Item))) return false;
FloatingItems.Remove(floating);
return true;
}
// The tractor beam's claim on a floating item: false if it is absent or already held. The item stays where it is
// until its pull begins; StepFloatingItems places it from then on.
public bool TryHold(Entity holder, FloatingBodyId id)
{
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
if (floating == null || floating.Holder != null) return false;
floating.Holder = holder;
floating.HeldFrom = floating.Body.Position;
floating.HeldProgress = 0;
return true;
}
// How far along its pull to the holder a held item is, 0..1.
public void SetHeldProgress(FloatingBodyId id, float progress)
{
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
if (floating?.Holder != null) floating.HeldProgress = saturate(progress);
}
// Lets a held item go: it floats on from where it is with the Velocity and Drift it had.
public void Free(FloatingBodyId id)
{
var floating = FloatingItems.FirstOrDefault(f => f.Id.Equals(id));
if (floating == null) return;
floating.Holder = null;
floating.HeldProgress = 0;
}
// Called from the death subscription, before the entity leaves the zone: one generator per death. Each equipped
// item but the hull drops on a LootDropProbability draw; every cargo stack drops whole. Each drop leaves from the
// dead entity's position at a random heading. Draw order: Equipment order (the probability draw, then the heading
@ -344,12 +392,18 @@ public class Zone
}
// After StepClouds and before FireControl.Step: each floating item drifts as a KinematicBody on the zone's force
// and is removed once its lifetime has run out.
// and is removed once its lifetime has run out. A held item is neither integrated nor expired: it is placed
// between where it was claimed and its holder, now that the entities have moved.
private void StepFloatingItems(float deltaTime)
{
var settings = _itemManager.GameplaySettings;
foreach (var floating in FloatingItems.ToArray())
{
if (floating.Holder != null)
{
floating.Body.Position = lerp(floating.HeldFrom, floating.Holder.Position.xz, smoothstep(0f, 1f, floating.HeldProgress));
continue;
}
floating.Body.Step(deltaTime, GetForce(floating.Body.Position), settings);
if (floating.ReleasedAt + floating.Lifetime <= _time) FloatingItems.Remove(floating);
}

View file

@ -60,9 +60,9 @@ public class FieldTester : MonoBehaviour
Debug.LogError("FieldTester expects a CapabilityPresentationBinder on TestField's GameObject; no capability events will reach the field.");
else
binder.Bind(_capabilityEvents,
grabTargetResolver: handle => handle >= 0 && handle < _pickups.Count ? _pickups[handle].transform : null,
grabInitialVelocityResolver: handle => Vector3.forward *
(PickupTravelDistance / lerp(PickupTravelTimeMin, PickupTravelTimeMax, _pickups[handle].lerp)));
grabTargetResolver: id => (int) id.Value >= 0 && (int) id.Value < _pickups.Count ? _pickups[(int) id.Value].transform : null,
grabInitialVelocityResolver: id => Vector3.forward *
(PickupTravelDistance / lerp(PickupTravelTimeMin, PickupTravelTimeMax, _pickups[(int) id.Value].lerp)));
// Impact absorption (fork L): what used to be FieldDriver's own click handler is now this
// scene's stand-in for whatever will eventually detect a hit and publish AbsorbEvent.
@ -102,7 +102,7 @@ public class FieldTester : MonoBehaviour
if (!TestField.CanGrab) return;
click.Clear();
var p = _pickups[i1];
_capabilityEvents.PublishGrab(new GrabEvent(i1, GrabPhase.Started, p.transform.position.ToCultMath(), 0));
_capabilityEvents.PublishGrab(new GrabEvent(new FloatingBodyId((uint) i1), GrabPhase.Started, p.transform.position.ToCultMath(), 0));
p.transform = null;
_pickups[i1] = p;
};

View file

@ -40,10 +40,10 @@ public class ZoneRenderer : MonoBehaviour
public LODHandler LODHandler;
public Slime SlimeRenderer;
public Camera SlimeGravityCamera;
public GridObject SimpleCommodityPickup;
public GridObject CompoundCommodityPickup;
public GridObject GearPickup;
public GridObject WeaponPickup;
public ItemPickup SimpleCommodityPickup;
public ItemPickup CompoundCommodityPickup;
public ItemPickup GearPickup;
public ItemPickup WeaponPickup;
public MineInstance MinePrefab;
public VapourCloudInstance CloudPrefab;
public Material[] MapGravityMaterials;
@ -94,7 +94,7 @@ public class ZoneRenderer : MonoBehaviour
private bool _showAsteroidUI;
public Dictionary<Wormhole, (GameObject gravity, CompassIcon icon)> WormholeInstances = new Dictionary<Wormhole, (GameObject, CompassIcon)>();
private List<ItemPickup> _loot = new List<ItemPickup>();
private Dictionary<FloatingItem, ItemPickup> _floatingItems = new Dictionary<FloatingItem, ItemPickup>();
private Dictionary<Mine, MineInstance> _mines = new Dictionary<Mine, MineInstance>();
private Dictionary<VapourCloud, VapourCloudInstance> _clouds = new Dictionary<VapourCloud, VapourCloudInstance>();
@ -236,6 +236,15 @@ public class ZoneRenderer : MonoBehaviour
instance.Blast();
}));
// Every floating item, whoever released it (a death drop, jettisoned cargo), is drawn through this one
// subscription. An item leaves Zone.FloatingItems by expiry or by pickup, and either way its pickup is gone.
foreach (var floating in zone.FloatingItems) LoadFloatingItem(zone, floating);
_zoneSubscriptions.Add(zone.FloatingItems.ObserveAdd().Subscribe(e => LoadFloatingItem(zone, e.Value)));
_zoneSubscriptions.Add(zone.FloatingItems.ObserveRemove().Subscribe(e =>
{
if (_floatingItems.Remove(e.Value, out var pickup) && pickup) Destroy(pickup.gameObject);
}));
// Every vapour cloud, whoever vented it, is drawn through this one subscription. A cloud leaves Zone.Clouds
// when its opacity has reached nothing, so a removal destroys its fog bank with nothing left to show.
foreach (var cloud in zone.Clouds) LoadCloud(zone, cloud);
@ -274,6 +283,29 @@ public class ZoneRenderer : MonoBehaviour
_mines.Add(mine, instance);
}
private void LoadFloatingItem(Zone zone, FloatingItem floating)
{
var prefab = floating.Item switch
{
SimpleCommodity _ => SimpleCommodityPickup,
CompoundCommodity _ => CompoundCommodityPickup,
EquippableItem equippableItem when ItemManager.GetData(equippableItem) is WeaponItemData => WeaponPickup,
EquippableItem _ => GearPickup,
_ => throw new NotImplementedException()
};
var pickup = Instantiate(prefab, ZoneRoot);
pickup.Bind(zone, floating);
pickup.ScanLabel.text = ItemManager.Title(floating.Item);
if (floating.Item is CraftedItemInstance craftedItemInstance)
{
var c = ItemManager.GetTier(craftedItemInstance).tier.Color.ToColor();
c.a = 0;
pickup.ScanLabel.color = c;
}
else pickup.ScanLabel.color = new Color(.75f, .75f, .75f, 0);
_floatingItems.Add(floating, pickup);
}
private void LoadCloud(Zone zone, VapourCloud cloud)
{
var instance = Instantiate(CloudPrefab);
@ -312,9 +344,9 @@ public class ZoneRenderer : MonoBehaviour
if (instance) Destroy(instance.gameObject);
_clouds.Clear();
foreach(var gridObject in _loot)
if(gridObject) Destroy(gridObject.gameObject);
_loot.Clear();
foreach (var pickup in _floatingItems.Values)
if (pickup) Destroy(pickup.gameObject);
_floatingItems.Clear();
foreach (var entity in Zone.Entities)
{
@ -487,7 +519,7 @@ public class ZoneRenderer : MonoBehaviour
void Update()
{
var maxDepth = 0f;
foreach (var loot in _loot)
foreach (var loot in _floatingItems.Values)
{
loot.ViewOrigin = PerspectiveEntity.Position;
loot.ViewDirection = PerspectiveEntity.Aim;
@ -603,41 +635,6 @@ public class ZoneRenderer : MonoBehaviour
// MinimapGravityQuad.transform.position = gravPos;
// MinimapTintQuad.transform.position = gravPos - Vector3.up*10;
}
public void DestroyLoot(ItemPickup loot)
{
_loot.Remove(loot);
}
public void DropItem(Vector3 position, Vector3 velocity, ItemInstance item)
{
var gridObject = item switch
{
SimpleCommodity _ => Instantiate(SimpleCommodityPickup),
CompoundCommodity _ => Instantiate(CompoundCommodityPickup),
EquippableItem equippableItem when ItemManager.GetData(equippableItem) is WeaponItemData => Instantiate(WeaponPickup),
EquippableItem _ => Instantiate(GearPickup),
_ => throw new NotImplementedException()
};
var t = gridObject.transform;
t.parent = ZoneRoot;
gridObject.Zone = Zone;
t.position = position;
gridObject.Velocity = velocity;
var itemPickup = gridObject.gameObject.GetComponent<ItemPickup>();
itemPickup.Item = item;
itemPickup.ZoneRenderer = this;
itemPickup.ScanLabel.text = ItemManager.Title(item);
if (item is CraftedItemInstance craftedItemInstance)
{
var c = ItemManager.GetTier(craftedItemInstance).tier.Color.ToColor();
c.a = 0;
itemPickup.ScanLabel.color = c;
}
else itemPickup.ScanLabel.color = new Color(.75f, .75f, .75f, 0);
gridObject.gameObject.AddComponent<TimedDestroy>().Duration = Settings.PickupLifetime;
_loot.Add(itemPickup);
}
}

BIN
GameData/Aetheria.cc (Stored with Git LFS)

Binary file not shown.

View file

@ -113,11 +113,10 @@ public sealed class BrownoutTests : IDisposable
// not flight, and a lone off-centre thruster could not fire without turning.
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Consumer")), new int2(2, 1)));
zone.Entities.Add(ship);
zone.Admit(ship, piloted: false);
// Ship.Turn stays 0, so Ship.Update commands no rotation: no incidental torque thrust from the
// ship trying to turn. Aim steers nothing, so this one is only a plausible aim.
ship.Aim = float3(0, 0, 1);
ship.Activate();
return ship;
}

View file

@ -26,7 +26,7 @@ public sealed class CapabilityEventsTests
events.Thrust.Subscribe(thrust.Add);
var absorbEvent = new AbsorbEvent(float3(1, 2, 3), float3(0, 0, 1), 5f, DamageType.Corrosive, AbsorbLayer.Hull);
var grabEvent = new GrabEvent(7, GrabPhase.Started, float3(1, 0, 0), 1.5f);
var grabEvent = new GrabEvent(new FloatingBodyId(7), GrabPhase.Started, float3(1, 0, 0), 1.5f);
var meleeEvent = new MeleeEvent(float3(0, 0, 1), 10f, 90f, 0.5f);
var thrustEvent = new ThrustEvent(float2(1, 0), 0.5f, float2(0.2f, -0.2f));

View file

@ -96,9 +96,8 @@ public sealed class ConditionRatioTests : IDisposable
// not flight, and a lone off-centre thruster could not fire without turning.
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Consumer")), new int2(2, 1)));
zone.Entities.Add(ship);
zone.Admit(ship, piloted: false);
ship.Aim = float3(0, 0, 1); // Turn stays 0, as in BrownoutTests
ship.Activate();
consumer = ship.Equipment.Single(e => e.Data.Name == "Consumer");
Assert.True(ship.GetBehavior<Thruster>().Torque == 0f, "the lone thruster turns nothing");

View file

@ -83,7 +83,8 @@ public sealed class ConsumableSupplyTests : IDisposable
var stranger = seed.Upsert(new Faction { Name = "Stranger", ShortName = "STR" });
maker = seed.Upsert(new Faction { Name = "Maker", ShortName = "MKR", Allegiance = { { ally, 1f } } });
var skiff = seed.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = Solid(4, 3), Price = 100 });
// Interior for the crate, the capacitor and the tractor beam every generated ship carries.
var skiff = seed.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = Solid(5, 3), Price = 100 });
var platform = seed.Upsert(new HullData { Name = "Platform", HullType = HullType.Station, Shape = Solid(5, 5), Price = 100 });
// A hold with no cell cannot take a consumable.
var crate = seed.Upsert(new CargoBayData
@ -93,6 +94,7 @@ public sealed class ConsumableSupplyTests : IDisposable
});
var dock = seed.Upsert(new DockingBayData { Name = "Dock", Shape = new Shape(), Price = 5, InteriorShape = holds == Holds.DockRefuses ? new Shape(1, 1) : Solid(1, 1) });
var capacitor = seed.Upsert(new GearData { Name = "Cap", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new CapacitorData() } });
var tractor = seed.Upsert(new GearData { Name = "Tractor", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new PickupData() } });
var heater = seed.Upsert(new GearData
{
Name = "Heater", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1,
@ -114,7 +116,7 @@ public sealed class ConsumableSupplyTests : IDisposable
foreach (var (name, design) in new[]
{
("Skiff", skiff.Key), ("Platform", platform.Key), ("Crate", crate.Key), ("Dock", dock.Key),
("Cap", capacitor.Key), ("Heater", heater.Key), ("Orphan", orphan.Key)
("Cap", capacitor.Key), ("Tractor", tractor.Key), ("Heater", heater.Key), ("Orphan", orphan.Key)
}.Concat(sold))
Sell(name + " by Maker", design, maker);
@ -333,11 +335,13 @@ public sealed class ConsumableSupplyTests : IDisposable
}
// What the plain catalog leaves in both generators' Random, measured on the tree before consumables existed
// (origin/master d4a27def, the same fixture and seeds). The gated and plain catalogs agreeing is not enough:
// a step that drew unconditionally would draw in both. This pins the value itself.
// (origin/master d4a27def, the same fixture and seeds), plus the one product draw each of the three generated
// ships makes for its tractor beam (loot-2): with that draw taken out the tree leaves the d4a27def values
// 0.85624266, 0.037778974, 0.7931649, 0.3596493, 0.17511952, 0.6642336, 0.31897342, 0.5977364. The gated and
// plain catalogs agreeing is not enough: a step that drew unconditionally would draw in both. This pins the value itself.
var beforeConsumables = new[]
{
0.85624266f, 0.037778974f, 0.7931649f, 0.3596493f, 0.17511952f, 0.6642336f, 0.31897342f, 0.5977364f
0.3596493f, 0.03616327f, 0.8790466f, 0.16682059f, 0.5977364f, 0.08633745f, 0.2869571f, 0.6890472f
};
var plainAfter = AfterGeneration(plain);
Assert.Equal(beforeConsumables, plainAfter);

View file

@ -83,8 +83,7 @@ public sealed class ConsumableTests : IDisposable
var ship = new Ship(items, zone, Gear(cache.Get(hullRef)), new EntitySettings());
Assert.True(ship.TryEquip(Gear(cache.Get(engineRef)), EngineCell));
Assert.True(ship.TryEquip(Gear(cache.Get(holdRef)), HoldCell));
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
var data = cache.Get(consumableRef);
return new Rig
{

View file

@ -230,8 +230,7 @@ public sealed partial class ContactRevealTests : IDisposable
Assert.True(ship.TryFindSpace(item, out var at));
Assert.True(ship.TryEquip(item, at));
}
_zone.Entities.Add(ship);
ship.Activate();
_zone.Admit(ship, piloted: false);
ship.Position = position;
ship.Aim = float3(0, 0, 1);
_zone.Update(0f); // brings the gear online
@ -603,8 +602,8 @@ public sealed partial class ContactRevealTests : IDisposable
{
switch (how)
{
case "remove": zone.Entities.Remove(e); break;
case "warp": e.Deactivate(); zone.Entities.Remove(e); break;
case "remove": zone.Remove(e); break;
case "warp": e.Deactivate(); zone.Remove(e); break;
case "death": e.Hull.Durability = 0f; e.HullDamage.OnNext(1f); break;
default: throw new Exception("fixture: " + how);
}
@ -643,7 +642,7 @@ public sealed partial class ContactRevealTests : IDisposable
Assert.True(e.Visibility * o.PassiveInfo(e.Position.xz, 1f) <= _items.GameplaySettings.TargetDetectionInfoThreshold, "fixture: its last light is below o's gate");
Assert.Equal(2, times.Count);
_zone.Entities.Remove(e);
_zone.Remove(e);
Assert.Equal(2, o.RevealsOf(e).Count());
var lapsed = false;
@ -729,7 +728,7 @@ public sealed partial class ContactRevealTests : IDisposable
var gate = _items.GameplaySettings.TargetDetectionInfoThreshold / o.PassiveInfo(e.Position.xz, 1f);
ShowOnly(e, gate * against, Guns(e)[0]);
_zone.Entities.Remove(e);
_zone.Remove(e);
Tick(.1f);
Assert.Equal(ended ? 0 : 1, o.RevealsOf(e).Count());
@ -799,7 +798,7 @@ public sealed partial class ContactRevealTests : IDisposable
ShowOnly(e, threshold * 8f, Guns(e)[0]);
Assert.True(e.Visibility * o.PassiveInfo(e.Position.xz, 1f) == threshold, "fixture: the ship's light at removal is exactly the gate");
_zone.Entities.Remove(e);
_zone.Remove(e);
Tick(.1f);
Assert.Single(o.RevealsOf(e));
@ -816,8 +815,9 @@ public sealed partial class ContactRevealTests : IDisposable
Tick(1f); // the gun cools down
ShowOnly(e, 1000f, gun); // bright enough for any gate
_zone.Entities.Remove(e);
_zone.Entities.Add(e);
_zone.Remove(e);
e.Deactivate();
_zone.Admit(e, piloted: false);
Tick(.1f);
Assert.Equal(at, Assert.Single(o.RevealsOf(e)).Time);
@ -932,7 +932,7 @@ public sealed partial class ContactRevealTests : IDisposable
var first = Assert.Single(o.RevealsOf(locker)); // fixture: the lock paints every tick, and the first one is recorded
Assert.True(locker.Visibility * o.PassiveInfo(locker.Position.xz, 1f) <= _items.GameplaySettings.TargetDetectionInfoThreshold, "fixture: its departure is below o's gate");
_zone.Entities.Remove(locker); // dies, warps or docks while its latest paint is still in o's inbox
_zone.Remove(locker); // dies, warps or docks while its latest paint is still in o's inbox
var inboxed = ((System.Collections.IList) typeof(Entity).GetField("_paintInbox", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(o)).Count;
Assert.True(inboxed > 0 || o.IncomingPaints.Count > 0, "fixture: a paint is in flight");
Tick(.1f);
@ -984,7 +984,7 @@ public sealed partial class ContactRevealTests : IDisposable
for (var tick = 0; tick < 100 && inbox.Count == 0; tick++) Tick(.1f);
Assert.NotEmpty(inbox); // fixture: the ping crossed o and its paint waits to be published
_zone.Entities.Remove(p);
_zone.Remove(p);
Tick(.1f);
Assert.Single(o.IncomingPaints);
@ -1158,7 +1158,7 @@ public sealed partial class ContactRevealTests : IDisposable
ShowOnly(e, 1f, gun);
var threshold = _items.GameplaySettings.TargetDetectionInfoThreshold;
Assert.True(e.Visibility * o.PassiveInfo(e.Position.xz, 1f) <= threshold, "fixture: from where o is, the departure is below the gate");
_zone.Entities.Remove(e);
_zone.Remove(e);
Tick(.1f);
Assert.Single(o.RevealsOf(e));
@ -1188,8 +1188,8 @@ public sealed partial class ContactRevealTests : IDisposable
var threshold = _items.GameplaySettings.TargetDetectionInfoThreshold;
Assert.True(a.Visibility * o.PassiveInfo(a.Position.xz, 1f) > threshold && b.Visibility * o.PassiveInfo(b.Position.xz, 1f) > threshold, "fixture: both departures pass the gate");
_zone.Entities.Remove(firstThenSecond ? a : b);
_zone.Entities.Remove(firstThenSecond ? b : a);
_zone.Remove(firstThenSecond ? a : b);
_zone.Remove(firstThenSecond ? b : a);
Tick(.1f);
Assert.Empty(o.RevealsOf(a));
@ -1215,7 +1215,7 @@ public sealed partial class ContactRevealTests : IDisposable
Tick(.01f);
Assert.Same(guns[1], Assert.Single(o.RevealsOf(e)).Item); // fixture: one gun ended as destroyed, one waiting
e.Deactivate(); // the warp path: the ship's own subscriptions to the observer go with it
_zone.Entities.Remove(e);
_zone.Remove(e);
for (var i = 0; i < 40; i++) Tick(.1f); // past BearingMemory (2 s)
Assert.Equal(0, RevealBookSubjects(o)); // fixture: pruned
@ -1307,7 +1307,7 @@ public sealed partial class ContactRevealTests : IDisposable
Assert.Contains(identified.VisibilitySources.Values, v => v.Item == gunItems[0] && v.Strength > 0f);
var departed = _zone.Entities.Single(x => x.Equipment.Contains(gunItems[18]));
foreach (var source in departed.VisibilitySources.Keys.ToArray()) departed.SetSignature(source, SignatureChannel.Reflection, 0f, gunItems[18]);
_zone.Entities.Remove(departed);
_zone.Remove(departed);
Assert.Single((System.Collections.IList) typeof(Zone).GetProperty("Departures", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(_zone));
sense();
Assert.Single(o.RevealsOf(departed)); // fixture: awaiting its prune

View file

@ -64,8 +64,7 @@ public sealed class ContactTests : IDisposable
{
var hullRef = Items.ItemData.RefOf<ItemData>(Items.ItemData.GetByName<HullData>(hull));
var ship = new Ship(Items, Zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = Lot++ }, new EntitySettings());
Zone.Entities.Add(ship);
ship.Activate();
Zone.Admit(ship, piloted: false);
ship.Position = at;
ship.Velocity = velocity;
return ship;

View file

@ -306,10 +306,11 @@ public sealed partial class RunStartTests
else
{
// The envelope sums the live columns, the drives' yaw among them (turn-authority-full), so its reach sits above
// the allocated one by the turn the drives add: 1.24 nose-on and 1.12 broadside at the last measurement. The
// ceiling is that, from the columns the allocator solves with, not an open bound: an envelope inflated by a
// factor (doubled: 2.5 and 2.2) breaks it.
Assert.InRange(reach.noseOn, measured.noseOn * .9f, allocated.noseOn * 1.3f);
// the allocated one by the turn the drives add: 1.37 nose-on and 1.17 broadside at the last measurement
// (1.24 and 1.12 before every generated ship drew a tractor beam: the extra draw per ship fits the Longinus
// with other rolls). The ceiling is that, from the columns
// the allocator solves with, not an open bound: an envelope inflated by a factor (doubled: 2.7 and 2.3) breaks it.
Assert.InRange(reach.noseOn, measured.noseOn * .9f, allocated.noseOn * 1.5f);
Assert.InRange(reach.broadside, measured.broadside * .9f, allocated.broadside * 1.2f);
Assert.InRange(measured.noseOn, allocated.noseOn * .9f, allocated.noseOn * 1.15f);
Assert.InRange(measured.broadside, allocated.broadside * .85f, allocated.broadside * 1.15f);

View file

@ -176,10 +176,8 @@ public sealed class FireAuthorityTests : IDisposable
// extra gear a test needs (a second aimable item, a shield, a reactor) must go on before Activate().
beforeActivate?.Invoke(items, shooter, target);
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, targetRange);
@ -327,7 +325,7 @@ public sealed class FireAuthorityTests : IDisposable
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
var before = e.Target.Hull.Durability;
e.Zone.Entities.Remove(e.Target);
e.Zone.Remove(e.Target);
e.Zone.Update(10f); // well past arrival
Assert.Equal(before, e.Target.Hull.Durability);

View file

@ -176,10 +176,8 @@ public sealed class FireControlCut11Tests : IDisposable
Assert.True(target.TryEquip(shield, spos));
}
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3(37, 0, -11);
target.Position = float3(37, 0, 89); // 100 units straight ahead of the shooter
@ -279,7 +277,7 @@ public sealed class FireControlCut11Tests : IDisposable
// What the HUD's arrival countdown reads: time left in flight at commit, 5 s - 4.7 s.
Assert.Equal(.3f, committed.ArrivalIn, 3);
e.Zone.Entities.Remove(e.Target); // the target is gone before the shot arrives
e.Zone.Remove(e.Target); // the target is gone before the shot arrives
e.Zone.Update(1f);
Assert.NotNull(resolved);
@ -306,7 +304,7 @@ public sealed class FireControlCut11Tests : IDisposable
e.Zone.Update(1f);
SafeAssert.Empty(committed);
e.Zone.Entities.Remove(e.Target);
e.Zone.Remove(e.Target);
e.Zone.Update(1f);
foreach (var outcome in new[] { SafeAssert.Only(committed), SafeAssert.Only(resolved) })

View file

@ -223,10 +223,8 @@ public sealed class FireControlCut123Tests : IDisposable
cockpit = target.Equipment.Single(x => x.EquippableItem == ci);
}
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
// Cut 11.3: the shooter sits off the origin.
shooter.Position = float3(37, 0, -11);

View file

@ -241,10 +241,8 @@ public sealed partial class FireControlCut124Tests : IDisposable
Assert.True(target.TryEquip(shield, spos));
}
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
// Off-origin (the fixture rule): the shooter sits away from the world origin, and the target sits
// `targetRange` ahead of it along the shooter's own +z, so a stationary target's TravelDirection is
@ -514,8 +512,7 @@ public sealed partial class FireControlCut124Tests : IDisposable
Data = e.Items.ItemData.RefOf<ItemData>(e.Items.ItemData.GetByName<GearData>("Marker")), Durability = 1000000, Lot = lot + 1
};
Assert.True(ship.TryEquip(marker, new int2(1, 1)));
e.Zone.Entities.Add(ship);
ship.Activate();
e.Zone.Admit(ship, piloted: false);
ship.Position = position;
return ship;
}
@ -1464,7 +1461,7 @@ public sealed partial class FireControlCut124Tests : IDisposable
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
Assert.True(SafeAssert.OnlyShot(e.Zone).Target == e.Target, "fixture: the pushed-out round keeps its target");
e.Zone.Update(.01f);
e.Zone.Entities.Remove(e.Target);
e.Zone.Remove(e.Target);
e.Zone.Update(3f);
SafeAssert.NoShots(e.Zone);
@ -1509,7 +1506,7 @@ public sealed partial class FireControlCut124Tests : IDisposable
var e = Build(TestSettings(), SolidShape(5, 4), velocity: 20f, fuse: WeaponFuse.Proximity, blastRadius: 4f, damage: 100f);
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
using var c = e.Zone.ShotCommitted.Subscribe(_ => throw new InvalidOperationException("observer"));
e.Zone.Entities.Remove(e.Target);
e.Zone.Remove(e.Target);
Assert.ThrowsAny<Exception>(() => e.Zone.Update(.01f));
var second = Record.Exception(() => e.Zone.Update(.01f));
@ -2003,7 +2000,7 @@ public sealed partial class FireControlCut124Tests : IDisposable
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
for (var i = 0; i < updatesBeforeLeaving; i++) e.Zone.Update(.01f);
e.Zone.Entities.Remove(e.Shooter);
e.Zone.Remove(e.Shooter);
e.Zone.Update(4f);
SafeAssert.NoShots(e.Zone);
@ -2021,7 +2018,7 @@ public sealed partial class FireControlCut124Tests : IDisposable
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
e.Zone.Update(.01f);
e.Zone.Entities.Remove(e.Shooter);
e.Zone.Remove(e.Shooter);
e.Zone.Update(6f);
SafeAssert.NoShots(e.Zone);
@ -2043,7 +2040,7 @@ public sealed partial class FireControlCut124Tests : IDisposable
for (var i = 0; i < 12; i++) FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
e.Zone.Update(.01f);
e.Zone.Entities.Remove(e.Shooter);
e.Zone.Remove(e.Shooter);
e.Zone.Update(6f);
SafeAssert.NoShots(e.Zone);
@ -2273,7 +2270,7 @@ public sealed partial class FireControlCut124Tests : IDisposable
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
e.Zone.Update(committedFirst ? 4.6f : .01f);
Assert.Equal(committedFirst, e.Zone.PendingShots[0].Committed);
e.Zone.Entities.Remove(e.Target);
e.Zone.Remove(e.Target);
e.Zone.Update(6f);
foreach (var s in subscriptions) s.Dispose();

View file

@ -268,10 +268,8 @@ public sealed class FireControlCut12Tests : IDisposable
Assert.True(target.TryEquip(shield, spos));
}
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
// Cut 11.3: the shooter sits off the origin -- a shooter at float3.zero makes `target - source` and
// `target + source` the same vector, hiding a whole class of sign mutants.

View file

@ -154,10 +154,8 @@ public sealed class FireControlCut4Tests : IDisposable
var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 9 };
Assert.True(target.TryEquip(targetGun, new int2(0, 0)));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, targetRange);
@ -233,8 +231,7 @@ public sealed class FireControlCut4Tests : IDisposable
Assert.True(shooter.TryEquip(gun, new int2(0, 0)));
var weapon = (InstantWeapon) shooter.Equipment.Single(e => e.EquippableItem == gun).Behaviors.Single(b => b is Weapon);
zone.Entities.Add(shooter);
shooter.Activate();
zone.Admit(shooter, piloted: false);
zone.Update(0f); // warm up resolved stats
// No target, no OnFire subscriber -- exactly a headless shooter with no target and no presentation.
@ -276,7 +273,7 @@ public sealed class FireControlCut4Tests : IDisposable
var ship = new Ship(items, zone, hull, new EntitySettings());
ship.Position = position;
ship.Direction = direction;
zone.Entities.Add(ship);
zone.Admit(ship, piloted: false);
return ship;
}

View file

@ -183,10 +183,8 @@ public sealed class FireControlCut5Tests : IDisposable
beforeActivate?.Invoke(items, shooter, target);
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, targetRange);
@ -338,10 +336,8 @@ public sealed class FireControlCut5Tests : IDisposable
var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 5 };
Assert.True(target.TryEquip(targetGun, new int2(0, 0)));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = targetPosition;
@ -426,10 +422,8 @@ public sealed class FireControlCut5Tests : IDisposable
var targetGun = new EquippableItem { Data = gunRef, Durability = 1, Lot = 4 };
Assert.True(target.TryEquip(targetGun, new int2(0, 0)));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
return (items, shooter, target, weaponItem);
}
@ -575,8 +569,7 @@ public sealed class FireControlCut5Tests : IDisposable
Assert.True(target.TryEquip(medium, mediumPos));
var mediumItem = target.Equipment.Single(e => e.EquippableItem == medium);
zone.Entities.Add(target);
target.Activate();
zone.Admit(target, piloted: false);
return (items, target, bigItem, mediumItem);
}
@ -591,8 +584,7 @@ public sealed class FireControlCut5Tests : IDisposable
var (items, target, big, medium) = BuildRevealTarget(settings);
var observerHullRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("Hull"));
var observer = new Ship(items, target.Zone, new EquippableItem { Data = observerHullRef, Durability = 1000, Lot = 1 }, new EntitySettings());
target.Zone.Entities.Add(observer);
observer.Activate();
target.Zone.Admit(observer, piloted: false);
observer.SetTarget(target);
observer.RecordReveal(target, big, SignatureChannel.LaunchFlash);

View file

@ -155,10 +155,8 @@ public sealed class FireControlCut6Tests : IDisposable
var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 5 };
Assert.True(target.TryEquip(targetGun, new int2(0, 0)));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, targetRange);

View file

@ -137,10 +137,8 @@ public sealed class FireControlCut6cTests : IDisposable
var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 9 };
Assert.True(target.TryEquip(targetGun, new int2(0, 0)));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, targetRange);
@ -211,6 +209,7 @@ public sealed class FireControlCut6cTests : IDisposable
var cargo = seedCache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = new Shape(), Price = 5 });
var capacitor = seedCache.Upsert(new GearData { Name = "Cap", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new CapacitorData() } });
var targeting = seedCache.Upsert(new GearData { Name = "Targeting", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new TargetingSystemData() } });
var tractor = seedCache.Upsert(new GearData { Name = "Tractor", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new PickupData() } });
// Everything but the targeting system is sold by Maker, who IS in the galaxy. Targeting is sold
// only by Targeting Maker, who is NOT -- the single point of failure 6c.2 fixes.
@ -218,6 +217,7 @@ public sealed class FireControlCut6cTests : IDisposable
{
("Armed by Maker", hull.Key, maker), ("Gun by Maker", gun.Key, maker),
("Crate by Maker", cargo.Key, maker), ("Cap by Maker", capacitor.Key, maker),
("Tractor by Maker", tractor.Key, maker),
("Targeting by Targeting Maker", targeting.Key, targetingMaker)
})
seedCache.Upsert(new FactionProductData { Name = name, Design = new CultRecordRef<CraftedItemData>(design), Manufacturer = factionMaker });
@ -266,8 +266,7 @@ public sealed class FireControlCut6cTests : IDisposable
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test", Owner = null }, null);
var entity = EntitySerializer.Unpack(items, zone, pack);
zone.Entities.Add(entity);
entity.Activate();
zone.Admit(entity, piloted: false);
zone.Update(0f);
Assert.Null(entity.GetBehavior<TargetingSystem>());

View file

@ -155,10 +155,8 @@ public sealed class FireControlCut6dTests : IDisposable
Assert.True(target.TryEquip(marker, markerCell.Value));
}
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, targetRange);

View file

@ -131,10 +131,8 @@ public sealed class FireControlCut7Tests : IDisposable
beforeActivate?.Invoke(items, shooter, target);
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, 100);

View file

@ -137,10 +137,8 @@ public sealed class FireControlCut8Tests : IDisposable
var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 4 };
Assert.True(target.TryEquip(targetGun, new int2(0, 0)));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, 100);
@ -216,10 +214,8 @@ public sealed class FireControlCut8Tests : IDisposable
var bystanderGun = new EquippableItem { Data = bystanderGunRef, Durability = 1, Lot = 4 };
Assert.True(bystander.TryEquip(bystanderGun, new int2(0, 0))); // _orderedEquipment must not be empty/null for Update to run
zone.Entities.Add(locker);
zone.Entities.Add(bystander);
locker.Activate();
bystander.Activate();
zone.Admit(locker, piloted: false);
zone.Admit(bystander, piloted: false);
locker.Position = float3.zero;
bystander.Position = float3(0, 0, 100);
@ -297,41 +293,4 @@ public sealed class FireControlCut8Tests : IDisposable
Assert.Empty(e.Target.EntityInfoGathered);
Assert.True(e.Target.VisibleEntities.Count == 0, "the target's visible set must be empty");
}
// ---- Ordering risk (8.1's own callout): EntityInstance's loot-drop subscription (EntityInstance.cs:292-325)
// reads Entity.Equipment on Entity.HullDamage, independent of Entity.Death/Deactivate. EntityInstance
// itself is a MonoBehaviour and cannot run headlessly (it lives in the Gameplay assembly, not
// Aetheria.Shared), so the actual drop call (ZoneRenderer.DropItem) is an operator check, not something
// this suite reaches. What this suite CAN confirm headlessly is the data precondition that call depends
// on: Deactivate() must not clear Equipment, regardless of subscription order relative to Zone's own
// Death-triggered removal. ----
// Mirrors EntityInstance's own subscription shape (HullDamage, re-checking Durability itself) established
// AFTER the target has already joined the zone -- so Zone's Death-derived Deactivate (subscribed at join
// time, per 8.1) fires first on the same HullDamage.OnNext call, and this handler observes Equipment
// strictly after that. If Deactivate ever starts clearing Equipment, this goes red.
[Fact]
public void LootStillDropsOnDeath_EquipmentSurvivesDeactivate()
{
var e = Build(TestSettings(), damage: 10000, hullDurability: 5); // one hit is lethal
List<EquippedItem> equipmentAtDeath = null;
e.Target.HullDamage.Subscribe(_ =>
{
if (e.Target.Hull.Durability < .01f) equipmentAtDeath = e.Target.Equipment.ToList();
});
FireControl.Fire(e.Weapon, e.WeaponItem, e.Shooter);
e.Zone.Update(.1f);
Assert.True(e.Target.Hull.Durability < .01f);
Assert.False(e.Target.Active); // Zone's Death subscription (subscribed before this test's own) already
// deactivated the target by the time this handler runs.
Assert.NotNull(equipmentAtDeath);
Assert.NotEmpty(equipmentAtDeath); // the target's own gun -- Deactivate must not have cleared Equipment
// Operator check, not reachable headlessly: confirm in play that ZoneRenderer.DropItem still fires for
// each surviving equipment/cargo entry (EntityInstance.cs:292-325) after this cut, i.e. that loot
// actually drops on a live kill, not only that the data behind it survived Deactivate().
}
}

View file

@ -128,10 +128,8 @@ public sealed class FireControlCut9Tests : IDisposable
Assert.True(target.TryFindSpace(targetMarker, out var targetMarkerPos));
Assert.True(target.TryEquip(targetMarker, targetMarkerPos));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, 100);
@ -339,10 +337,8 @@ public sealed class FireControlCut9Tests : IDisposable
var targetGun = new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 5 };
Assert.True(target.TryEquip(targetGun, new int2(0, 0)));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = float3(0, 0, 100);
@ -566,10 +562,8 @@ public sealed class FireControlCut9Tests : IDisposable
var bystanderGun = new EquippableItem { Data = bystanderGunRef, Durability = 1, Lot = 4 };
Assert.True(bystander.TryEquip(bystanderGun, new int2(0, 0))); // _orderedEquipment must not be empty/null for Update to run
zone.Entities.Add(locker);
zone.Entities.Add(bystander);
locker.Activate();
bystander.Activate();
zone.Admit(locker, piloted: false);
zone.Admit(bystander, piloted: false);
locker.Position = float3.zero;
bystander.Position = float3(0, 0, 100);
@ -602,7 +596,7 @@ public sealed class FireControlCut9Tests : IDisposable
// the target dies or leaves while the locker is inactive -- its own Zone.Entities.ObserveRemove
// subscription (the only thing that would have nulled Target) was disposed by Deactivate above, so
// nothing reacts to this removal yet.
s.Zone.Entities.Remove(s.Bystander);
s.Zone.Remove(s.Bystander);
s.Locker.Activate(); // undock
Assert.True(s.Locker.Active);

View file

@ -237,10 +237,8 @@ public sealed class FireControlPerWeaponTests : IDisposable
var target = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000000, Lot = lot++ }, new EntitySettings());
Assert.True(target.TryEquip(Make("Gun", lot++, 1), new int2(0, 0)));
zone.Entities.Add(shooter);
zone.Entities.Add(target);
shooter.Activate();
target.Activate();
zone.Admit(shooter, piloted: false);
zone.Admit(target, piloted: false);
shooter.Position = float3(-53, 0, -101);
target.Position = shooter.Position + float3(0, 0, targetRange);

View file

@ -95,8 +95,7 @@ public sealed class FireControlTests : IDisposable
weapons[i] = ship.Equipment.Single(e => e.EquippableItem == gun);
}
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
return (items, ship, weapons);
}
@ -195,8 +194,7 @@ public sealed class FireControlTests : IDisposable
// gun so the target is a real, ordinarily-constructed ship rather than a workaround-shaped one.
var targetGunRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<GearData>("Gun"));
Assert.True(target.TryEquip(new EquippableItem { Data = targetGunRef, Durability = 1, Lot = 1 }, new int2(0, 0)));
zone.Entities.Add(target);
target.Activate();
zone.Admit(target, piloted: false);
shooter.Position = float3.zero;
target.Position = targetPosition;

View file

@ -239,9 +239,8 @@ public sealed class FloatingBodyTests : IDisposable
Lot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(design), Origin = new Attributed(), Quality = .5f, Roles = new List<RoleFill>() })
};
var ship = new Ship(items, zone, hull, new EntitySettings());
zone.Entities.Add(ship);
zone.Admit(ship, piloted: false);
ship.Aim = float3(0, 0, 1);
ship.Activate();
ship.Position = float3(x, 0, 0);
return ship;
}

View file

@ -82,8 +82,7 @@ public sealed class IffAndCombatTests : IDisposable
Assert.True(ship.TryEquip(gun, HardpointCell));
}
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
return ship;
}
@ -132,8 +131,9 @@ public sealed class IffAndCombatTests : IDisposable
a.SetIff(b, true);
Assert.True(a.EntityHostility[b]);
zone.Entities.Remove(b); // b leaves the zone: a's override on b must be cleared
zone.Entities.Add(b); // b returns as a "new" contact from a's perspective
zone.Remove(b); // b leaves the zone: a's override on b must be cleared
b.Deactivate();
zone.Admit(b, piloted: false); // b returns as a "new" contact from a's perspective
Assert.False(a.EntityHostility[b]); // recomputed fresh from the derived (non-hostile) rule
Assert.False(a.IsHostileTo(b));

View file

@ -33,8 +33,7 @@ public sealed partial class FireControlCut124Tests
{
var hullRef = e.Items.ItemData.RefOf<ItemData>(e.Items.ItemData.GetByName<HullData>("Hull"));
var ship = new Ship(e.Items, e.Zone, new EquippableItem { Data = hullRef, Durability = 100000, Lot = lot }, new EntitySettings());
e.Zone.Entities.Add(ship);
ship.Activate();
e.Zone.Admit(ship, piloted: false);
ship.Position = position;
return ship;
}
@ -136,7 +135,7 @@ public sealed partial class FireControlCut124Tests
var gone = Build(TestSettings(), SolidShape(5, 4), velocity: 50f, damage: 100f);
var goneAbsorbs = new[] { gone.Target, gone.Shooter }.SelectMany(x => Collect(x, subs)).ToList();
FireControl.Fire(gone.Weapon, gone.WeaponItem, gone.Shooter);
gone.Zone.Entities.Remove(gone.Target);
gone.Zone.Remove(gone.Target);
for (var i = 0; i < 40; i++) gone.Zone.Update(.1f);
foreach (var s in subs) s.Dispose();

View file

@ -97,8 +97,7 @@ public sealed class InputCapacitorTests : IDisposable
Assert.True(ship.TryEquip(Mint(cache, items, reactorData)));
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
return (ship, items, ship.GetBehavior<Reactor>());
}

View file

@ -501,7 +501,8 @@ public sealed class LoadoutTests : IDisposable
}
});
var capacitor = cache.Upsert(new GearData { Name = "Cap", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new CapacitorData() } });
foreach (var (name, design) in new[] { ("Twin by Maker", hull.Key), ("Cap by Maker", capacitor.Key) })
var tractor = cache.Upsert(new GearData { Name = "Tractor", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new PickupData() } });
foreach (var (name, design) in new[] { ("Twin by Maker", hull.Key), ("Cap by Maker", capacitor.Key), ("Tractor by Maker", tractor.Key) })
cache.Upsert(new FactionProductData { Name = name, Design = new CultRecordRef<CraftedItemData>(design), Manufacturer = maker });
cache.FlushAsync().Wait();

View file

@ -104,8 +104,7 @@ public sealed class LockWarningTests : IDisposable
var gearRef = _items.ItemData.RefOf<ItemData>(_items.ItemData.GetByName<GearData>(launcher));
Assert.True(ship.TryEquip(new EquippableItem { Data = gearRef, Durability = 1, Lot = _nextLot++ }, HardpointCell));
}
_zone.Entities.Add(ship);
ship.Activate();
_zone.Admit(ship, piloted: false);
ship.Position = position;
_zone.Update(0f); // brings the gear online
return ship;

View file

@ -133,8 +133,7 @@ public sealed class LootTests : IDisposable
Assert.True(ship.CargoBays.Single().TryStore(lab.Ore(7)));
Assert.True(ship.CargoBays.Single().TryStore(lab.Make("Spare")));
}
lab.Zone.Entities.Add(ship);
ship.Activate();
lab.Zone.Admit(ship, piloted: false);
ship.Position = at;
return ship;
}
@ -153,8 +152,7 @@ public sealed class LootTests : IDisposable
Assert.True(station.TryEquip(spare, pos));
Assert.True(station.CargoBays.Single().TryStore(lab.Ore(3)));
}
lab.Zone.Entities.Add(station);
station.Activate();
lab.Zone.Admit(station, piloted: false);
station.Position = at;
return station;
}
@ -393,10 +391,10 @@ public sealed class LootTests : IDisposable
var lab = Build(probability: 1f);
var ship = Spawn(lab, float3(3, 0, 4));
// What Entity.TryDock then Entity.TryUndock do to the zone: Entities.Remove + Deactivate, then Zone.Admit.
lab.Zone.Entities.Remove(ship);
lab.Zone.Remove(ship);
ship.Deactivate();
lab.Zone.Admit(ship, piloted: false);
lab.Zone.Entities.Remove(ship);
lab.Zone.Remove(ship);
lab.Zone.Admit(ship, piloted: false);
Kill(ship);
var dropped = lab.Zone.FloatingItems.Select(f => f.Item).ToList();
@ -412,7 +410,7 @@ public sealed class LootTests : IDisposable
var ship = Spawn(lab, float3(3, 0, 4));
// What ActionGameManager does on a jump.
ship.Deactivate();
lab.Zone.Entities.Remove(ship);
lab.Zone.Remove(ship);
ship.Zone = other;
other.Admit(ship, piloted: false);
Kill(ship);
@ -509,9 +507,10 @@ public sealed class LootTests : IDisposable
Assert.True(lab.Zone.FloatingItems.Select(f => Math.Round(Math.Atan2(f.Body.Velocity.y, f.Body.Velocity.x), 3)).Distinct().Count() > 1);
}
// A member added twice holds one death subscription, so it drops each item once, as a single death would.
// Membership has one owner (aetheria-release zone-entities-encapsulation): a second admission of a member is
// refused, so it never holds a second death watch and the zone never lists it twice.
[Fact]
public void AMemberAddedTwiceStillDropsEachItemOnce()
public void AMemberAdmittedTwiceIsRefusedAndStillDropsEachItemOnce()
{
var once = Build("Once", probability: 1f);
Kill(Spawn(once, float3(3, 0, 4)));
@ -520,20 +519,45 @@ public sealed class LootTests : IDisposable
var lab = Build("Twice", probability: 1f);
var ship = Spawn(lab, float3(3, 0, 4));
lab.Zone.Entities.Add(ship);
Assert.Throws<InvalidOperationException>(() => lab.Zone.Admit(ship, piloted: false));
Assert.Equal(1, lab.Zone.Entities.Count(e => e == ship));
Kill(ship);
Assert.Equal(baseline, lab.Zone.FloatingItems.Count);
}
// Clearing Entities removes every member, so a death afterwards drops nothing in this zone.
// Remove is the one removal: a non-member is refused with nothing changed, and a member's removal takes its death
// watch with it, so a death signal afterwards drops nothing and a re-admission watches exactly once.
[Fact]
public void AClearedZoneDropsNothingWhenAFormerMemberDies()
public void RemovingANonMemberChangesNothingAndARemovedMembersWatchGoesWithIt()
{
var lab = Build("Cleared", probability: 1f);
var lab = Build(probability: 1f);
var ship = Spawn(lab, float3(3, 0, 4));
lab.Zone.Entities.Clear();
var stranger = Spawn(Build("Elsewhere", probability: 1f), float3(1, 0, 1));
var members = lab.Zone.Entities.Count;
Assert.False(lab.Zone.Remove(stranger));
Assert.Equal(members, lab.Zone.Entities.Count);
Assert.True(lab.Zone.Remove(ship));
Assert.False(lab.Zone.Remove(ship));
Kill(ship);
Assert.Empty(lab.Zone.FloatingItems);
ship.Deactivate();
lab.Zone.Admit(ship, piloted: false);
Kill(ship);
var dropped = lab.Zone.FloatingItems.Select(f => f.Item).ToList();
Assert.NotEmpty(dropped);
Assert.Equal(dropped.Count, dropped.Distinct().Count());
}
// The membership view has no mutating member: a caller can read it and subscribe to it, and nothing else.
[Fact]
public void TheMembershipViewOffersNoWayToMutateMembership()
{
var lab = Build();
Spawn(lab, float3(3, 0, 4));
Assert.IsNotAssignableFrom<System.Collections.Generic.ICollection<Entity>>(lab.Zone.Entities);
Assert.IsNotAssignableFrom<System.Collections.IList>(lab.Zone.Entities);
}
// The pickup is whole or nothing at the arithmetic boundary: two stacks of 90 hold 20 more in all and no free

View file

@ -130,8 +130,7 @@ public sealed class MineTests : IDisposable
var targeting = lab.Make("Targeting", 1);
Assert.True(layer.TryFindSpace(targeting, out var tpos));
Assert.True(layer.TryEquip(targeting, tpos));
zone.Entities.Add(layer);
layer.Activate();
zone.Admit(layer, piloted: false);
layer.Position = layerAt ?? float3(-53, 0, -101);
lab.Layer = layer;
zone.Update(1f);
@ -152,8 +151,7 @@ public sealed class MineTests : IDisposable
Assert.True(ship.TryFindSpace(gear, out var spos));
Assert.True(ship.TryEquip(gear, spos));
}
lab.Zone.Entities.Add(ship);
ship.Activate();
lab.Zone.Admit(ship, piloted: false);
ship.Position = at;
if (shield != null) ship.Shield.Item.Enabled.Value = true;
return ship;

View file

@ -220,10 +220,9 @@ public sealed partial class MiningCut3Tests : IDisposable
s.Cache.Upsert(gun);
Assert.True(ship.TryEquip(Mint(s, gun)));
}
s.Zone.Entities.Add(ship);
s.Zone.Admit(ship, piloted: false);
Hold(ship, at);
ship.Aim = float3(0, 0, 1);
ship.Activate();
return ship;
}
@ -865,10 +864,9 @@ public sealed partial class MiningCut3Tests : IDisposable
var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings());
Assert.True(ship.TryEquip(Mint(s, s.Eye)));
foreach (var weapon in weapons) Assert.True(ship.TryEquip(Mint(s, weapon)));
s.Zone.Entities.Add(ship);
s.Zone.Admit(ship, piloted: false);
Hold(ship, eyeAt);
ship.Aim = float3(0, 0, 1);
ship.Activate();
Tick((ship, eyeAt)); // weapons read their ranges when they run
return ship;
}

View file

@ -53,10 +53,9 @@ public sealed partial class MiningCut3Tests
var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings());
Assert.True(ship.TryEquip(Mint(s, eye)));
Assert.True(ship.TryEquip(Mint(s, gun)));
s.Zone.Entities.Add(ship);
s.Zone.Admit(ship, piloted: false);
Hold(ship, at);
ship.Aim = float3(0, 0, 1);
ship.Activate();
Tick((ship, at)); // equipment comes online and weapons read their ranges on the first tick
return ship;
}
@ -176,7 +175,7 @@ public sealed partial class MiningCut3Tests
Assert.True(expected.ToHashSet().SetEquals(offered), $"sensitivity {sensitivity}: {expected.Count} visible, {offered.Count} offered");
visibleSeen += expected.Count;
if (examined < inReach.Count) pruned++;
s.Zone.Entities.Remove(observer);
s.Zone.Remove(observer);
}
Assert.True(visibleSeen > 50 && pruned > 4, $"degenerate: {visibleSeen} visible, {pruned} queries pruned");
}
@ -421,7 +420,7 @@ public sealed partial class MiningCut3Tests
var brightest = all.Max(c => s.Zone.ChunkVisibility(c));
var unit = Observer(s, float2(5000, 0), 350f, 1f, Falloff());
var sensitivity = 120f / (brightest * unit.DetectionReachPerVisibility());
s.Zone.Entities.Remove(unit);
s.Zone.Remove(unit);
var offered = new List<ChunkId>();
long examined = 0, inReach = 0;
@ -436,7 +435,7 @@ public sealed partial class MiningCut3Tests
observer.VisibleChunksInReach(offered);
examined += s.Zone.Targets.Examined - before;
inReach += all.Count(c => length(At(s, c) - at) <= 350f);
s.Zone.Entities.Remove(observer);
s.Zone.Remove(observer);
}
Assert.True(examined < .265 * inReach, $"examined {examined} of {inReach} rocks in reach: the visibility bound has loosened");
}

View file

@ -33,7 +33,7 @@ public sealed partial class MiningCut3Tests
{
var observer = Observer(s, at, reach, sensitivity, Falloff());
if (inReach.Count(observer.ChunkVisible) >= share * inReach.Count) return (s, observer);
s.Zone.Entities.Remove(observer);
s.Zone.Remove(observer);
}
throw new InvalidOperationException("no sensitivity sees the rocks");
}
@ -42,7 +42,7 @@ public sealed partial class MiningCut3Tests
private static Ship Sight(Scene s, Ship observer, float2 at, bool enemy = false)
{
var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings());
s.Zone.Entities.Add(ship);
s.Zone.Admit(ship, piloted: false);
Hold(ship, at);
observer.VisibleEntities.Add(ship);
if (enemy) observer.VisibleEnemies.Add(ship);
@ -265,7 +265,7 @@ public sealed partial class MiningCut3Tests
observer.Aim = float3(cos(heading), 0, sin(heading));
Check($"the reticle at {heading}", Cost(observer.TargetUnderReticle, TargetRef.None), reticle);
}
s.Zone.Entities.Remove(observer);
s.Zone.Remove(observer);
}
}
Assert.True(failures.Count == 0, string.Join("; ", failures));
@ -336,10 +336,9 @@ public sealed partial class MiningCut3Tests
var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings());
Assert.True(ship.TryEquip(Mint(s, s.Eye)));
Assert.True(ship.TryEquip(Mint(s, launcher)));
s.Zone.Entities.Add(ship);
s.Zone.Admit(ship, piloted: false);
Hold(ship, at);
ship.Aim = float3(0, 0, 1);
ship.Activate();
Tick((ship, at));
Assert.Contains(ship.Weapons, w => w.Range == 400f);
Assert.True(AllChunks(s).Any(c => length(At(s, c) - at) <= 400f && ship.ChunkVisible(c)), "there are rocks within the launcher's range");
@ -375,7 +374,7 @@ public sealed partial class MiningCut3Tests
private static Ship Unseen(Scene s, float2 at)
{
var ship = new Ship(s.Items, s.Zone, Mint(s, s.Hull), new EntitySettings());
s.Zone.Entities.Add(ship);
s.Zone.Admit(ship, piloted: false);
Hold(ship, at);
return ship;
}

View file

@ -266,8 +266,7 @@ public sealed class MunitionPartsTests : IDisposable
Assert.True(ship.TryEquip(new EquippableItem { Data = cache.RefOf<ItemData>(gear), Durability = gear.Durability, Lot = 1 }, new int2(x, y)), name);
}
Assert.True(ship.TryEquip(new EquippableItem { Data = cache.RefOf<ItemData>(gt), Durability = gt.Durability, Lot = 2 }, hardpoint.Position));
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
// Settle at the item's optimal temperature, as ConditionRatioTests does, so heat and wear read their identity and only
// the role's quality moves a stat.
var item = ship.Equipment.Single(e => e.Data.Name == "GT 3K");

View file

@ -124,8 +124,7 @@ public sealed class PaintModeTests : IDisposable
var gearRef2 = _items.ItemData.RefOf<ItemData>(_items.ItemData.GetByName<GearData>(sensor2));
Assert.True(ship.TryEquip(new EquippableItem { Data = gearRef2, Durability = 1, Lot = _nextLot++ }, new int2(1, 0)));
}
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
ship.Position = position;
ship.SetSignature(new object(), SignatureChannel.Reflection, 5f, null);
zone.Update(0f); // brings the gear online
@ -331,7 +330,7 @@ public sealed class PaintModeTests : IDisposable
if (tick == 2 && illuminatorDies)
{
Assert.True(SensorOf(a).PingRadius < 100f, "fixture: A dies before its wavefront reaches T");
_zone.Entities.Remove(a);
_zone.Remove(a);
}
track.Add(Track(c, t));
}
@ -422,7 +421,7 @@ public sealed class PaintModeTests : IDisposable
for (var tick = 1; tick <= 24; tick++)
{
Tick(.1f);
if (tick == removePingerAfterTick) _zone.Entities.Remove(a);
if (tick == removePingerAfterTick) _zone.Remove(a);
Near(tick <= 3 ? 0f : .21213f, Track(c, t));
Assert.Equal(tick == 4 ? 1 : 0, t.IncomingPaints.Count);
}
@ -683,7 +682,7 @@ public sealed class PaintModeTests : IDisposable
SensorOf(a).Ping();
for (var tick = 1; tick <= 3; tick++) Tick(.1f);
Assert.True(SensorOf(a).PingRadius > 50f); // the front had passed 50 m by the move
a.Deactivate(); _zone.Entities.Remove(a); a.Zone = z2; z2.Admit(a, false);
a.Deactivate(); _zone.Remove(a); a.Zone = z2; z2.Admit(a, false);
var paints = 0;
for (var tick = 1; tick <= 24; tick++) { z2.Update(.1f); paints += near.IncomingPaints.Count + far.IncomingPaints.Count; }
Assert.Equal(0, paints);
@ -703,8 +702,8 @@ public sealed class PaintModeTests : IDisposable
var paints = 0;
for (var tick = 1; tick <= 30; tick++)
{
if (tick == 5) { _zone.Entities.Remove(t); t.Deactivate(); }
if (tick == 8) { _zone.Entities.Add(t); t.Activate(); }
if (tick == 5) { _zone.Remove(t); t.Deactivate(); }
if (tick == 8) { _zone.Admit(t, piloted: false); }
Tick(.1f);
paints += t.IncomingPaints.Count;
}

View file

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System.Linq;
using Xunit;
// Pickup is a capability (aetheria-release cut loot-2), so a ship that can pick things up carries the gear that grants
// it. Generation is the one place a ship's gear is chosen, so this runs the real catalog through it.
public sealed partial class RunStartTests
{
// The beams a pack carries fitted. A station may stock one in its hold to sell, which is the point of selling it.
private int PickupGear(EntityPack pack) => pack.Equipment.Select(fitted => fitted.item)
.Count(item => _items.GetData(item) is GearData gear && gear.Behaviors.Any(b => b is PickupData));
[Fact]
public void EveryGeneratedShipCarriesATractorBeam()
{
// Every ship hull a product sells.
var generator = PreludeGenerator();
foreach (var hull in LoadoutGenerator.SoldHulls(_cache).Where(hull => hull.HullType == HullType.Ship))
Assert.True(PickupGear(generator.GenerateShipLoadout(candidate => candidate == hull)) == 1, $"a {hull.Name} carries exactly one tractor beam");
// Ships generated for each faction of a main galaxy, and the starting ship the game stages.
var main = Launch(new MainGalaxy(), Inputs(() => GalaxySeed));
Assert.Empty(main.failures);
var factions = main.galaxy.Factions.ToArray();
for (var i = 0; i < 50; i++)
{
var faction = factions[i % factions.Length];
var maker = new LoadoutGenerator(ref _items.Random, _items, main.galaxy, main.galaxy.Entrance, faction, .5f);
Assert.True(PickupGear(maker.GenerateShipLoadout()) == 1, $"ship {i} of {faction.Name} carries exactly one tractor beam");
}
Assert.Equal(1, main.staged.Player.Equipment.Count(item => item.Behaviors.OfType<Pickup>().Any()));
Assert.All(main.arena.Pack.Entities.Where(pack => pack is ShipPack), pack => Assert.Equal(1, PickupGear(pack)));
// Stations and turrets share the interior outfitting and get none.
for (var i = 0; i < 4; i++)
{
Assert.Equal(0, PickupGear(generator.GenerateStationLoadout()));
Assert.Equal(0, PickupGear(generator.GenerateTurretLoadout()));
}
Assert.All(main.arena.Pack.Entities.Where(pack => !(pack is ShipPack)), pack => Assert.Equal(0, PickupGear(pack)));
}
}

View file

@ -0,0 +1,706 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CultMath;
using GameCult.Caching;
using UniRx;
using Xunit;
using static CultMath.math;
using float2 = CultMath.float2;
using float3 = CultMath.float3;
using int2 = CultMath.int2;
// The tractor beam in the sim (aetheria-release cut loot-2): a Pickup behaviour spins its beam up in sim time and runs
// one timed grab of a floating item, ending in Zone.TryPickUp. Rulings: jettison-shared-floating-items,
// unity-presents-only-r3, sim-speed-presentation; the question loot-grab-release-cancels, built hold-required. Every
// grab here is run by Zone.Update ticking a ship's own gear: nothing calls the behaviour directly.
public sealed class PickupTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-pickup-" + Guid.NewGuid().ToString("N"));
private readonly List<CultCache> _caches = new List<CultCache>();
public PickupTests() => Directory.CreateDirectory(_root);
public void Dispose()
{
foreach (var c in _caches) c.Dispose();
Directory.Delete(_root, true);
}
private static PerformanceStat Constant(float v) => new PerformanceStat { Min = v, Max = v };
private static Shape Solid(int w, int h)
{
var shape = new Shape(w, h);
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
return shape;
}
private static PickupData Beam(float extend = .3f, float envelop = .2f, float pull = .5f) => new PickupData
{
Range = Constant(75), BeamRadius = Constant(25), GrabPower = Constant(.5f), SpinRate = Constant(2),
ExtendTime = Constant(extend), EnvelopTime = Constant(envelop), PullTime = Constant(pull)
};
private sealed class Lab
{
public ItemManager Items;
public Zone Zone;
public int Lot = 1;
public EquippableItem Make(string name) => new EquippableItem
{
Data = Items.ItemData.RefOf<ItemData>(Items.ItemData.GetByName<EquippableItemData>(name)), Durability = 1, Lot = Lot++
};
public SimpleCommodity Ore(int quantity) => new SimpleCommodity
{
Data = Items.ItemData.RefOf<ItemData>(Items.ItemData.GetByName<SimpleCommodityData>("Ore")), Quantity = quantity
};
}
private Lab Build(PickupData beam, float lifetime = 30f)
{
var settings = new GameplaySettings
{
DefaultEntitySettings = new EntitySettings(),
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
TargetDetectionInfoThreshold = .1f, TargetArmorInfoThreshold = .2f, TargetGearInfoThreshold = .8f,
FiringArc = 170, CommitHorizon = .5f, SchematicCellSize = 1f,
UnaidedAccuracy = .05f, UnaidedTracking = 10f, UnaidedPrecision = 2f, AgentMinHitProbability = 0f,
LootDropProbability = 0, LootDropVelocity = 25, FloatingItemLifetime = lifetime
};
var cache = AetheriaStores.Open(Path.Combine(_root, Guid.NewGuid().ToString("N") + ".cc"), catalogWritable: true);
_caches.Add(cache);
cache.Upsert(new VerseGrammar { Revision = 1 });
cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = Solid(5, 5), Durability = 1000000, Mass = 1000 });
cache.Upsert(new GearData
{
Name = "Tractor", Hardpoint = HardpointType.Tool, Shape = Solid(1, 1), Durability = 1,
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000,
Behaviors = { beam }
});
cache.Upsert(new GearData
{
Name = "Spare", Hardpoint = HardpointType.Tool, Shape = Solid(1, 1), Durability = 1,
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000
});
cache.Upsert(new GearData
{
Name = "Block", Hardpoint = HardpointType.Tool, Shape = Solid(2, 2), Durability = 1,
MinimumTemperature = -1000, MaximumTemperature = 1000, OptimalTemperature = 0, PlateauWidth = 2000
});
cache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = Solid(4, 4), Durability = 4 });
cache.Upsert(new SimpleCommodityData { Name = "Ore", Shape = Solid(1, 1), MaxStack = 100 });
cache.FlushAsync().Wait();
var ledger = new ProvenanceLedger();
for (var i = 1; i <= 9000; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
var items = new ItemManager(cache, ledger, settings, _ => { });
var zone = new Zone(items, new PlanetSettings(), new ZonePack { Radius = 0 }, new GalaxyZone { Name = "Pickup", Owner = null }, null);
return new Lab { Items = items, Zone = zone };
}
// A ship with a cargo bay, aimed along +x and (unless told otherwise) carrying the tractor beam.
private static Ship Spawn(Lab lab, float3 at, bool beam = true)
{
var hull = lab.Items.ItemData.RefOf<ItemData>(lab.Items.ItemData.GetByName<HullData>("Skiff"));
var ship = new Ship(lab.Items, lab.Zone, new EquippableItem { Data = hull, Durability = 1000000, Lot = lab.Lot++ }, new EntitySettings());
var crate = lab.Make("Crate");
Assert.True(ship.TryFindSpace(crate, out var cratePos));
Assert.True(ship.TryEquip(crate, cratePos));
if (beam) Equip(ship, lab.Make("Tractor"));
lab.Zone.Admit(ship, piloted: false);
ship.Position = at;
ship.Aim = float3(1, 0, 0);
return ship;
}
private static void Equip(Ship ship, EquippableItem gear)
{
Assert.True(ship.TryFindSpace(gear, out var pos));
Assert.True(ship.TryEquip(gear, pos));
}
private static FloatingItem Drift(Lab lab, float x, float y, float2 velocity = default, int ore = 5) =>
lab.Zone.Release(lab.Ore(ore), float2(x, y), velocity);
private static List<GrabEvent> Watch(Entity ship, CompositeDisposable bag)
{
var events = new List<GrabEvent>();
ship.Capabilities.Grab.Subscribe(events.Add).AddTo(bag);
return events;
}
private static void Tick(Lab lab, int ticks, float dt)
{
for (var i = 0; i < ticks; i++) lab.Zone.Update(dt);
}
// Ticks until the condition holds, for at most twenty sim seconds; a grab that never comes fails here instead of hanging.
private static void Until(Lab lab, float dt, Func<bool> done)
{
for (var tick = 0; tick < 1200 && !done(); tick++) lab.Zone.Update(dt);
Assert.True(done(), "the expected moment never came");
}
private static void Kill(Entity e) => e.HeatstrokeDeath.OnNext(Unit.Default);
[Fact]
public void NoPickupBehaviourNoGrab()
{
using var bag = new CompositeDisposable();
// The same ship and item, with and without the gear: only the gear grabs.
var bare = Build(Beam());
var plain = Spawn(bare, float3(0, 0, 0), beam: false);
var left = Drift(bare, 40, 0);
var plainEvents = Watch(plain, bag);
plain.TractorCommand = 1;
Tick(bare, 240, 1 / 60f);
Assert.True(left.Holder == null, "left is held");
Assert.True(bare.Zone.FloatingItems.Contains(left), "left left the zone");
Assert.Equal(0f, plain.TractorPower);
Assert.Empty(plainEvents);
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var item = Drift(lab, 40, 0);
var events = Watch(ship, bag);
ship.TractorCommand = 1;
Tick(lab, 60, 1 / 60f);
Assert.True(ReferenceEquals(ship, item.Holder), "item is not held by ship");
Assert.Equal(GrabPhase.Started, events.First().Phase);
Assert.True(ship.TractorPower > 0f);
}
[Theory]
[InlineData(60)]
[InlineData(600)]
public void TheBeamSpinsUpInSimTime(int ticksPerSecond)
{
var dt = 1f / ticksPerSecond;
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var item = Drift(lab, 40, 0);
// Power rises two per sim second and not faster, at any tick rate; the claim comes on the tick it passes GrabPower.
ship.TractorCommand = 1;
int claim = -1, full = -1;
var before = 0f;
for (var tick = 1; tick <= ticksPerSecond; tick++)
{
lab.Zone.Update(dt);
if (claim < 0 && item.Holder != null)
{
claim = tick;
Assert.True(ship.TractorPower >= .5f);
Assert.True(before < .5f);
}
if (full < 0 && ship.TractorPower >= 1f) full = tick;
before = ship.TractorPower;
}
Assert.Equal(ticksPerSecond / 4, claim);
Assert.Equal(ticksPerSecond / 2, full);
// Command 0 spins it down at the same rate.
ship.TractorCommand = 0;
var empty = -1;
for (var tick = 1; tick <= ticksPerSecond; tick++)
{
lab.Zone.Update(dt);
if (empty < 0 && ship.TractorPower <= 0f) empty = tick;
}
Assert.Equal(ticksPerSecond / 2, empty);
}
[Theory]
[InlineData(.3f, .2f, .5f, 60)]
[InlineData(1f, 1f, 1f, 180)]
public void AGrabTakesExactlyItsPhasesThenCommits(float extend, float envelop, float pull, int ticksAfterClaim)
{
foreach (var rate in new[] { 60, 600 })
{
var dt = 1f / rate;
var lab = Build(Beam(extend, envelop, pull));
var ship = Spawn(lab, float3(0, 0, 0));
var item = Drift(lab, 40, 0);
ship.TractorCommand = 1;
int claim = -1, commit = -1;
for (var tick = 1; tick <= rate * 6 && commit < 0; tick++)
{
lab.Zone.Update(dt);
if (claim < 0 && item.Holder != null) claim = tick;
if (!lab.Zone.FloatingItems.Contains(item)) commit = tick;
}
Assert.True(claim > 0);
Assert.Equal(ticksAfterClaim * rate / 60, commit - claim);
Assert.Equal(5, ship.CargoBays.Single().Cargo.Keys.OfType<SimpleCommodity>().Single().Quantity);
}
}
[Fact]
public void GrabEventsNameTheFloatingItem()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var other = Drift(lab, 40, 20);
var item = Drift(lab, 40, 0);
var events = Watch(ship, bag);
ship.TractorCommand = 1;
for (var tick = 1; tick <= 150; tick++) lab.Zone.Update(1 / 60f);
Assert.Equal(new[] { GrabPhase.Started, GrabPhase.Extend, GrabPhase.Envelop, GrabPhase.Pull, GrabPhase.Completed },
events.Take(5).Select(e => e.Phase).ToArray());
Assert.All(events.Take(5), e => Assert.Equal(item.Id, e.Target));
Assert.NotEqual(other.Id, item.Id);
Assert.Equal(.3f, events[1].PhaseDuration, 4);
Assert.Equal(.2f, events[2].PhaseDuration, 4);
Assert.Equal(.5f, events[3].PhaseDuration, 4);
}
// Each phase is published on the first tick at or after its boundary (.3, .5 and 1 s after the claim), at any
// tick rate or an uneven one. The clock is a float sum, so a boundary an exact number of ticks away can land one
// tick late (Envelop and Pull do at 1/600): the rule is "not before, at most one tick after", not a tick count.
[Theory]
[InlineData(1, 60, 60, 60, 60)]
[InlineData(1, 600, 600, 600, 600)]
[InlineData(4, 60, 24, 90, 33)]
public void EachPhaseFiresOnTheFirstTickAtOrAfterItsBoundary(int cycle, int a, int b, int c, int d)
{
using var bag = new CompositeDisposable();
var rates = cycle == 1 ? new[] { a } : new[] { a, b, c, d };
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
Drift(lab, 40, 0);
var events = Watch(ship, bag);
ship.TractorCommand = 1;
var since = -1.0;
var times = new List<double>();
var steps = new List<double>();
for (var tick = 0; tick < rates.Length * 3000 && events.Count(e => e.Phase == GrabPhase.Completed) == 0; tick++)
{
var dt = 1f / rates[tick % rates.Length];
lab.Zone.Update(dt);
if (since >= 0) since += dt;
else if (events.Count > 0) since = 0;
while (times.Count < events.Count) { times.Add(since); steps.Add(dt); }
}
var phases = events.Select(e => e.Phase).ToList();
Assert.Equal(GrabPhase.Completed, phases.Last());
foreach (var (phase, boundary) in new[] { (GrabPhase.Envelop, .3), (GrabPhase.Pull, .5), (GrabPhase.Completed, 1.0) })
{
var i = phases.IndexOf(phase);
Assert.True(i >= 0, "the phase never came");
Assert.True(times[i] >= boundary - 1e-5, "the phase fired before its boundary");
// Where no tick lands exactly on a boundary (the uneven cycle) the tick before must be strictly short of it;
// on a regular grid the float sum may leave that tick a hair short, so it may be equal.
var slack = cycle == 1 ? 1e-5 : -1e-5;
Assert.True(times[i] - steps[i] < boundary + slack, "the phase fired after the first tick at or past its boundary");
}
}
[Fact]
public void ReleasingTheBeamCancelsAndTheItemFloatsFree()
{
using var bag = new CompositeDisposable();
const float dt = 1 / 60f;
var lab = Build(Beam(), lifetime: 2f);
var ship = Spawn(lab, float3(0, 0, 0));
var item = Drift(lab, 40, 0, float2(3, 1));
var events = Watch(ship, bag);
ship.TractorCommand = 1;
var tick = 0;
while (!events.Any(e => e.Phase == GrabPhase.Pull) && tick++ < 600) lab.Zone.Update(dt);
Assert.True(ReferenceEquals(ship, item.Holder), "item is not held by ship");
var velocity = item.Body.Velocity;
var drift = item.Body.Drift;
// The key comes up during the pull: Power falls below GrabPower and the grab is dropped.
ship.TractorCommand = 0;
while (!events.Any(e => e.Phase == GrabPhase.Cancelled) && tick++ < 600) lab.Zone.Update(dt);
Assert.True(tick < 110);
Assert.DoesNotContain(events, e => e.Phase == GrabPhase.Completed);
Assert.True(item.Holder == null, "item is held");
Assert.True(lab.Zone.FloatingItems.Contains(item), "item left the zone");
// (It has taken the one step of its freeing tick, so it is the held velocity less a tick's drag.)
Assert.Equal(velocity.x, item.Body.Velocity.x, velocity.x * .01f);
Assert.Equal(velocity.y, item.Body.Velocity.y, velocity.y * .01f);
Assert.True(velocity.x > 1f);
// It drifts on with that velocity and expires on the clock it was released on, not from the cancel.
var at = item.Body.Position;
lab.Zone.Update(dt);
Assert.True(item.Body.Position.x > at.x);
while (lab.Zone.FloatingItems.Contains(item) && tick++ < 600) lab.Zone.Update(dt);
Assert.InRange(tick, 118, 122);
}
[Fact]
public void AHoldThatFillsMidGrabCancelsAtTheEnd()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var bay = ship.CargoBays.Single();
var item = Drift(lab, 40, 0);
var events = Watch(ship, bag);
ship.TractorCommand = 1;
Until(lab, 1 / 60f, () => item.Holder != null);
// The hold fills while the grab runs: the item slides in and then floats free.
for (var i = 0; i < 16; i++) Assert.True(bay.TryStore(lab.Make("Spare")));
var tick = 0;
while (!events.Any(e => e.Phase == GrabPhase.Cancelled) && tick++ < 300) lab.Zone.Update(1 / 60f);
Assert.Equal(new[] { GrabPhase.Started, GrabPhase.Extend, GrabPhase.Envelop, GrabPhase.Pull, GrabPhase.Cancelled },
events.Select(e => e.Phase).ToArray());
Assert.True(item.Holder == null, "item is held");
Assert.True(lab.Zone.FloatingItems.Contains(item), "item left the zone");
Assert.Equal(16, bay.Cargo.Count);
}
[Fact]
public void AFullHoldNeverClaimsWhatItCannotStore()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var bay = ship.CargoBays.Single();
for (var i = 0; i < 16; i++) Assert.True(bay.TryStore(lab.Make("Spare")));
var item = Drift(lab, 40, 0, float2(3, 1));
var events = Watch(ship, bag);
ship.TractorCommand = 1;
Tick(lab, 300, 1 / 60f);
Assert.Equal(1f, ship.TractorPower);
Assert.Empty(events);
Assert.True(item.Holder == null, "item is held");
Assert.True(item.Body.Position.x > 40f, "item was pinned instead of drifting");
}
[Fact]
public void AnItemTheHoldCannotStoreDoesNotBlockOneItCan()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var bay = ship.CargoBays.Single();
for (var i = 0; i < 15; i++) Assert.True(bay.TryStore(lab.Make("Spare")));
// One free cell: the 2x2 block is nearest the axis and cannot be stored; the ore beside it can.
var block = lab.Zone.Release(lab.Make("Block"), float2(30, 0), default);
var ore = Drift(lab, 50, 10);
var events = Watch(ship, bag);
ship.TractorCommand = 1;
Tick(lab, 150, 1 / 60f);
Assert.Contains(events, e => e.Phase == GrabPhase.Completed);
Assert.All(events, e => Assert.Equal(ore.Id, e.Target));
Assert.False(lab.Zone.FloatingItems.Contains(ore), "ore is still in the zone");
Assert.True(lab.Zone.FloatingItems.Contains(block), "block left the zone");
Assert.True(block.Holder == null, "block is held");
}
[Fact]
public void AnItemInTheBeamsFarCornerIsCollected()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
// 74 along and 20 off the axis: inside the beam, though 76.7 from the ship, past Range as a straight distance.
var corner = Drift(lab, 74, 20);
var events = Watch(ship, bag);
ship.TractorCommand = 1;
Tick(lab, 150, 1 / 60f);
Assert.DoesNotContain(events, e => e.Phase == GrabPhase.Cancelled);
Assert.Contains(events, e => e.Phase == GrabPhase.Completed);
Assert.False(lab.Zone.FloatingItems.Contains(corner), "corner is still in the zone");
}
[Fact]
public void AnItemLeavingTheBeamBeforeThePullIsCancelled()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var item = Drift(lab, 40, 0);
var events = Watch(ship, bag);
ship.TractorCommand = 1;
Until(lab, 1 / 60f, () => item.Holder != null);
// The pilot swings the beam away while the grab extends: the item is off the axis and the grab is dropped.
ship.Aim = float3(0, 0, 1);
Tick(lab, 3, 1 / 60f);
Assert.Equal(GrabPhase.Cancelled, events.Last().Phase);
Assert.True(item.Holder == null, "item is held");
}
[Fact]
public void OneHolderPerItem()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var a = Spawn(lab, float3(0, 0, 0));
var b = Spawn(lab, float3(0, 0, 10));
var eventsA = Watch(a, bag);
var eventsB = Watch(b, bag);
// One item, two beams: exactly one claims it, the other never does, and the non-holder cannot take it.
var only = Drift(lab, 40, 5);
a.TractorCommand = 1;
b.TractorCommand = 1;
Tick(lab, 20, 1 / 60f);
Assert.Equal(1, new[] { eventsA, eventsB }.Count(e => e.Any(x => x.Phase == GrabPhase.Started)));
var holder = only.Holder;
var loser = ReferenceEquals(holder, a) ? b : a;
Assert.False(lab.Zone.TryPickUp(loser, only.Id));
Assert.False(lab.Zone.TryHold(loser, only.Id));
Assert.True(lab.Zone.FloatingItems.Contains(only), "only left the zone");
Assert.Empty(loser.CargoBays.Single().Cargo);
Tick(lab, 120, 1 / 60f);
Assert.False(lab.Zone.FloatingItems.Contains(only), "only is still in the zone");
Assert.Single(holder.CargoBays.Single().Cargo);
Assert.Empty(loser.CargoBays.Single().Cargo);
// Two items, each nearest a different beam's axis: each ship holds its own.
var first = Drift(lab, 60, 0);
var second = Drift(lab, 60, 10);
Tick(lab, 20, 1 / 60f);
Assert.True(first.Holder != null, "first is free");
Assert.True(second.Holder != null, "second is free");
Assert.False(ReferenceEquals(first.Holder, second.Holder), "one holder for two items");
// Both ships' nearest item is the same one: the one that loses it takes the other.
var shared = Build(Beam());
var p = Spawn(shared, float3(0, 0, 0));
var q = Spawn(shared, float3(0, 0, 10));
var near = Drift(shared, 40, 5);
var far = Drift(shared, 40, -14);
p.TractorCommand = 1;
q.TractorCommand = 1;
Tick(shared, 20, 1 / 60f);
Assert.True(near.Holder != null, "near is free");
Assert.True(far.Holder != null, "far is free");
Assert.False(ReferenceEquals(near.Holder, far.Holder), "one holder for two items");
}
[Fact]
public void AGrabIsDroppedWhenItsGearGoesInactiveOrTheItemFallsOutOfRangeBeforeThePull()
{
using var bag = new CompositeDisposable();
const float dt = 1 / 60f;
// Gear switched off mid-grab: cancelled, the item free.
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var item = Drift(lab, 40, 0);
var events = Watch(ship, bag);
ship.TractorCommand = 1;
Tick(lab, 30, dt);
Assert.True(ReferenceEquals(ship, item.Holder), "item is not held by ship");
ship.Equipment.Single(e => e.Behaviors.OfType<Pickup>().Any()).Enabled.Value = false;
Tick(lab, 2, dt);
Assert.Equal(GrabPhase.Cancelled, events.Last().Phase);
Assert.True(item.Holder == null, "item is held");
// The ship leaves while the beam is still extending: cancelled. The same departure during the pull is not.
foreach (var departAtPull in new[] { false, true })
{
var other = Build(Beam());
var pilot = Spawn(other, float3(0, 0, 0));
var target = Drift(other, 40, 0);
var seen = Watch(pilot, bag);
pilot.TractorCommand = 1;
Until(other, dt, () => seen.Any(e => departAtPull ? e.Phase == GrabPhase.Pull : e.Phase == GrabPhase.Extend));
pilot.Position = float3(-200, 0, 0);
Tick(other, 120, dt);
if (departAtPull)
{
Assert.Equal(GrabPhase.Completed, seen.First(e => e.Phase == GrabPhase.Completed || e.Phase == GrabPhase.Cancelled).Phase);
Assert.False(other.Zone.FloatingItems.Contains(target), "target is still in the zone");
}
else
{
Assert.Equal(GrabPhase.Cancelled, seen.First(e => e.Phase == GrabPhase.Completed || e.Phase == GrabPhase.Cancelled).Phase);
Assert.True(target.Holder == null, "target is held");
Assert.True(other.Zone.FloatingItems.Contains(target), "target left the zone");
}
}
}
[Fact]
public void SelectionIsTheItemNearestTheBeamAxis()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var events = Watch(ship, bag);
ship.TractorCommand = 1;
// The nearer to the axis wins, though the other is closer to the ship.
var near = Drift(lab, 20, 12);
var onAxis = Drift(lab, 60, 3);
Tick(lab, 30, 1 / 60f);
Assert.True(ReferenceEquals(ship, onAxis.Holder), "onAxis is not held by ship");
Assert.True(near.Holder == null, "near is held");
Assert.Equal(onAxis.Id, events.First().Target);
// Out of the beam, past its range or behind the ship, an item is never taken.
foreach (var (x, y) in new[] { (30f, 30f), (80f, 0f), (-10f, 0f) })
{
var other = Build(Beam());
var lone = Spawn(other, float3(0, 0, 0));
var stray = Drift(other, x, y);
var attempts = Watch(lone, bag);
lone.TractorCommand = 1;
Tick(other, 240, 1 / 60f);
Assert.Empty(attempts);
Assert.True(stray.Holder == null, "stray is held");
Assert.True(other.Zone.FloatingItems.Contains(stray), "stray left the zone");
}
// Equally far from the axis, the lower id.
var tie = Build(Beam());
var pilot = Spawn(tie, float3(0, 0, 0));
var lower = Drift(tie, 40, 5);
var higher = Drift(tie, 40, -5);
pilot.TractorCommand = 1;
Tick(tie, 30, 1 / 60f);
Assert.True(ReferenceEquals(pilot, lower.Holder), "lower is not held by pilot");
Assert.True(higher.Holder == null, "higher is held");
}
[Fact]
public void AHeldItemIsPlacedNotIntegratedAndDoesNotExpire()
{
using var bag = new CompositeDisposable();
const float dt = 1 / 60f;
var lab = Build(Beam(), lifetime: 1f);
var ship = Spawn(lab, float3(0, 0, 0));
var item = Drift(lab, 40, 0, float2(3, 0));
var events = Watch(ship, bag);
ship.TractorCommand = 1;
Until(lab, dt, () => item.Holder != null);
var claimed = item.Body.Position;
Assert.Equal(claimed, item.HeldFrom);
// Through Extend and Envelop the item stays where it was claimed, whatever velocity it had.
for (var guard = 0; guard < 1200 && !events.Any(e => e.Phase == GrabPhase.Pull); guard++)
{
lab.Zone.Update(dt);
if (events.Any(e => e.Phase == GrabPhase.Pull)) break;
Assert.Equal(claimed, item.Body.Position);
}
Assert.Contains(events, e => e.Phase == GrabPhase.Pull);
// During the pull it moves toward the holder, and a held item outlives its lifetime.
lab.Zone.Update(dt * 10);
Assert.True(item.Body.Position.x < claimed.x);
Assert.True(item.Body.Position.x > 0);
var held = Build(Beam(), lifetime: 1f);
var carrier = Spawn(held, float3(5, 0, 7));
var stuck = Drift(held, 40, 0);
Assert.True(held.Zone.TryHold(carrier, stuck.Id));
Tick(held, 180, dt);
Assert.True(held.Zone.FloatingItems.Contains(stuck), "stuck left the zone");
Assert.Equal(float2(40, 0), stuck.Body.Position);
// The pose eases (smoothstep): a quarter of the progress is 15.625 per cent of the way, not 25.
held.Zone.SetHeldProgress(stuck.Id, .25f);
held.Zone.Update(dt);
Assert.Equal(40f - 35f * .15625f, stuck.Body.Position.x, 3);
Assert.Equal(7f * .15625f, stuck.Body.Position.y, 3);
// Placed, not drifted: the same progress held for two steps puts it at the same halfway point.
held.Zone.SetHeldProgress(stuck.Id, .5f);
held.Zone.Update(dt);
held.Zone.Update(dt);
Assert.Equal(22.5f, stuck.Body.Position.x, 3);
Assert.Equal(3.5f, stuck.Body.Position.y, 3);
// At progress 1 it is at the holder; freed, an item past its lifetime goes on the next step.
held.Zone.SetHeldProgress(stuck.Id, 1);
held.Zone.Update(dt);
Assert.Equal(5f, stuck.Body.Position.x, 3);
Assert.Equal(7f, stuck.Body.Position.y, 3);
held.Zone.Free(stuck.Id);
held.Zone.Update(dt);
Assert.False(held.Zone.FloatingItems.Contains(stuck), "stuck is still in the zone");
}
[Fact]
public void AnInactiveBeamSpinsDownAndMustSpinUpAgain()
{
using var bag = new CompositeDisposable();
const float dt = 1 / 60f;
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
var gear = ship.Equipment.Single(e => e.Behaviors.OfType<Pickup>().Any());
ship.TractorCommand = 1;
Tick(lab, 60, dt);
Assert.Equal(1f, ship.TractorPower);
// Switched off with the key still down, the beam spins down at the spin rate...
gear.Enabled.Value = false;
Tick(lab, 15, dt);
Assert.InRange(ship.TractorPower, .45f, .55f);
Tick(lab, 30, dt);
Assert.Equal(0f, ship.TractorPower);
// ...and switched back on it spins up from nothing: an item in the beam is not claimed at once.
var item = Drift(lab, 40, 0);
var events = Watch(ship, bag);
gear.Enabled.Value = true;
Tick(lab, 10, dt);
Assert.Empty(events);
Assert.True(item.Holder == null, "item is held");
Tick(lab, 20, dt);
Assert.True(ReferenceEquals(ship, item.Holder), "item is not held by ship");
}
[Fact]
public void RemovingOneHolderFreesOnlyItsOwnItems()
{
var lab = Build(Beam());
var a = Spawn(lab, float3(0, 0, 0));
var b = Spawn(lab, float3(0, 0, 50));
var mine = Drift(lab, 40, 0);
var theirs = Drift(lab, 40, 50);
a.TractorCommand = 1;
b.TractorCommand = 1;
Tick(lab, 30, 1 / 60f);
Assert.True(ReferenceEquals(a, mine.Holder), "mine is not held by a");
Assert.True(ReferenceEquals(b, theirs.Holder), "theirs is not held by b");
Kill(a);
Assert.True(mine.Holder == null, "mine is held");
Assert.True(ReferenceEquals(b, theirs.Holder), "theirs was freed with a");
}
[Fact]
public void TheHoldersLeavingFreesItsItems()
{
using var bag = new CompositeDisposable();
var lab = Build(Beam());
var ship = Spawn(lab, float3(0, 0, 0));
Assert.True(ship.CargoBays.Single().TryStore(lab.Ore(7)));
var item = Drift(lab, 40, 0, ore: 3);
ship.TractorCommand = 1;
Tick(lab, 30, 1 / 60f);
Assert.True(ReferenceEquals(ship, item.Holder), "item is not held by ship");
var before = lab.Zone.FloatingItems.Count;
// Death goes through the zone's one removal: the held item is free, and the ship's own cargo drops as always.
Kill(ship);
Assert.True(item.Holder == null, "item is held");
Assert.True(lab.Zone.FloatingItems.Contains(item), "item left the zone");
Assert.Equal(before + 1, lab.Zone.FloatingItems.Count);
Assert.Equal(7, ((SimpleCommodity) lab.Zone.FloatingItems.Last().Item).Quantity);
Tick(lab, 60, 1 / 60f);
Assert.True(lab.Zone.FloatingItems.Contains(item), "item left the zone");
Assert.True(item.Holder == null, "item is held");
}
}

View file

@ -101,8 +101,7 @@ public sealed class PowerBusTests : IDisposable
if (reactorFirst) { EquipReactor(); EquipCapacitor(); } else { EquipCapacitor(); EquipReactor(); }
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
var reactor = ship.GetBehavior<Reactor>();
var capacitor = ship.GetBehavior<Capacitor>();

View file

@ -103,8 +103,7 @@ public sealed class PowerCurveTests : IDisposable
Assert.True(ship.TryEquip(Mint(cache, items, reactorData)));
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Engine"))));
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
return ship;
}
@ -408,9 +407,8 @@ public sealed class PowerCurveTests : IDisposable
var ship = new Ship(items, zone, hullItem, new EntitySettings());
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Drain")), HardpointCell));
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Booster")), SecondHardpointCell));
zone.Entities.Add(ship);
var error = Assert.Throws<InvalidOperationException>(() => ship.Activate());
var error = Assert.Throws<InvalidOperationException>(() => zone.Admit(ship, piloted: false));
Assert.Contains("Drain", error.Message); // the item whose request would be corrupted
Assert.Contains("Booster", error.Message); // the item whose modifier would corrupt it
}

View file

@ -72,8 +72,7 @@ public sealed class PowerTierTests : IDisposable
((ReactorData) reactorData.Behaviors[0]).Charge = Constant(reactorCharge);
Assert.True(ship.TryEquip(Mint(cache, items, reactorData)));
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
var reactor = ship.GetBehavior<Reactor>();
ship.Update(0f);

View file

@ -314,9 +314,8 @@ public sealed class RestoredHullsTests
Assert.True(ship.TryEquip(fillerItem, filled.Position));
}
zone.Entities.Add(ship);
zone.Admit(ship, piloted: false);
ship.Position = float3.zero;
ship.Activate();
zone.Update(0f); // warm-up: resolves equipment stats before the driven ticks read them
return ship;
}
@ -618,9 +617,8 @@ public sealed class RestoredHullsTests
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test", Owner = null }, null);
var ship = (Ship) EntitySerializer.Unpack(items, zone, pack);
zone.Entities.Add(ship);
zone.Admit(ship, piloted: false);
ship.Position = float3.zero;
ship.Activate();
zone.Update(0f);
ship.MovementDirection = float2(0, 1); // full forward

View file

@ -80,6 +80,52 @@ public sealed class RunSaveTests : IDisposable
Assert.Equal(8, RecordsIn(Run));
}
// Continue finds the player's entity by its place in the saved zone: the index Capture writes must be the entity's
// place in the live zone, and survive the round trip to the same entity's pack.
[Fact]
public void ThePlayersZoneIndexSurvivesSaveAndContinue()
{
using (var cache = AetheriaStores.Open(Catalog, catalogWritable: true))
{
var shape = new Shape(4, 4);
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
foreach (var name in new[] { "Skiff", "Barge" })
cache.Upsert(new HullData { Name = name, HullType = HullType.Ship, Shape = shape, Durability = 100, Mass = 100 });
cache.FlushAsync().Wait();
}
using (var cache = Open())
RunSave.Commit(cache, Game(cache), Zones(2, StageZoneContents(cache), 0), new ProvenanceLedger());
using (var cache = Open())
{
var galaxy = new Galaxy(cache, cache.GetGlobal<SavedGame>(), _ => { });
var ledger = new ProvenanceLedger();
for (var i = 1; i <= 2; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
var items = new ItemManager(cache, ledger, TestSettings(), _ => { });
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), galaxy.Zones[0], galaxy);
galaxy.Zones[0].Contents = zone;
Ship Make(string hull, int lot) => new Ship(items, zone, new EquippableItem
{
Data = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>(hull)), Durability = 100, Lot = lot
}, new EntitySettings());
var other = Make("Skiff", 1);
var player = Make("Barge", 2);
zone.Admit(other, piloted: false);
zone.Admit(player, piloted: false);
var (game, zones) = RunSave.Capture(cache, galaxy, zone, player, new SavedActionBarBinding[0], 0);
RunSave.Commit(cache, game, zones, items.Lots);
}
using (var cache = Open())
{
var saved = cache.GetGlobal<SavedGame>();
Assert.Equal(1, saved.CurrentZoneEntity);
var packs = cache.Get(saved.Zones[saved.CurrentZone]).Contents.Entities;
Assert.Equal("Barge", cache.Get(packs[saved.CurrentZoneEntity].Hull.Data).Name);
}
}
[Fact]
public void ResumeThenSaveKeepsUnloadedZoneContents()
{
@ -307,7 +353,8 @@ public sealed class RunSaveTests : IDisposable
var gear = cache.Upsert(new GearData { Name = "Scanner", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 5 });
var cargo = cache.Upsert(new CargoBayData { Name = "Hold", Shape = new Shape(), InteriorShape = new Shape(), Price = 3 });
var capacitor = cache.Upsert(new GearData { Name = "Cap", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new CapacitorData() } });
foreach (var (name, design) in new[] { ("Runabout by Generator Maker", hull.Key), ("Scanner by Generator Maker", gear.Key), ("Hold by Generator Maker", cargo.Key), ("Cap by Generator Maker", capacitor.Key) })
var tractor = cache.Upsert(new GearData { Name = "Tractor", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new PickupData() } });
foreach (var (name, design) in new[] { ("Runabout by Generator Maker", hull.Key), ("Scanner by Generator Maker", gear.Key), ("Hold by Generator Maker", cargo.Key), ("Cap by Generator Maker", capacitor.Key), ("Tractor by Generator Maker", tractor.Key) })
cache.Upsert(new FactionProductData { Name = name, Design = new CultRecordRef<CraftedItemData>(design), Manufacturer = maker });
cache.FlushAsync().Wait();
}

View file

@ -95,8 +95,7 @@ public sealed class ShieldReserveTests : IDisposable
var shieldData = cache.GetByName<GearData>("Shield");
Assert.True(ship.TryEquip(Mint(cache, items, shieldData)));
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
return (ship, ship.GetBehavior<Shield>());
}

View file

@ -158,8 +158,7 @@ public sealed class SignatureTests : IDisposable
Assert.True(ship.TryEquip(item, at));
}
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
ship.Aim = float3(0, 0, 1);
zone.Update(1f);
return rig;

View file

@ -189,8 +189,7 @@ public sealed partial class FireControlCut124Tests
var e = Build(TestSettings(), SolidShape(5, 4), hardpointArc: SolutionArc, weaponRange: 200f);
var hull = e.Items.ItemData.RefOf<ItemData>(e.HullData);
var decoy = new Ship(e.Items, e.Zone, new EquippableItem { Data = hull, Durability = 1000000, Lot = 400 }, new EntitySettings());
e.Zone.Entities.Add(decoy);
decoy.Activate();
e.Zone.Admit(decoy, piloted: false);
var at = radians(40f);
decoy.Position = e.Shooter.Position + float3(sin(at), 0, cos(at)) * 100f;
e.Shooter.VisibleEntities.Add(decoy);

View file

@ -71,8 +71,7 @@ public sealed class StatResolverTests : IDisposable
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Battery")), HardpointCell));
if (withBooster)
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Booster")), SecondHardpointCell));
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
return ship;
}
@ -287,9 +286,8 @@ public sealed class StatResolverTests : IDisposable
var hullItem = Mint(cache, items, cache.GetByName<HullData>("Skiff"));
var ship = new Ship(items, zone, hullItem, new EntitySettings());
Assert.True(ship.TryEquip(Mint(cache, items, cache.GetByName<GearData>("Loop")), HardpointCell));
zone.Entities.Add(ship);
var error = Assert.Throws<InvalidOperationException>(() => ship.Activate());
var error = Assert.Throws<InvalidOperationException>(() => zone.Admit(ship, piloted: false));
Assert.Contains(ship.Name, error.Message); // the entity, by name ("Skiff", HullData.Name)
Assert.Contains(nameof(CapacitorData), error.Message);
Assert.Contains(nameof(CapacitorData.Capacity), error.Message);
@ -513,8 +511,7 @@ public sealed class StatResolverTests : IDisposable
Assert.True(ship.TryEquip(boost, cells[i + 1]));
}
ship.OverrideShutdown = true;
zone.Entities.Add(ship);
ship.Activate();
zone.Admit(ship, piloted: false);
Func<ConsumableItem> mint = null;
if (consumable != null)

View file

@ -138,8 +138,7 @@ public sealed class TargetingSystemTests : IDisposable
targetingItem = target.Equipment.Single(e => e.EquippableItem == targeting);
}
zone.Entities.Add(target);
target.Activate();
zone.Admit(target, piloted: false);
return (items, target, weapon, wideItem, narrowItem, targetingItem);
}
@ -151,8 +150,7 @@ public sealed class TargetingSystemTests : IDisposable
var hullRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("TestHull"));
var hull = new EquippableItem { Data = hullRef, Durability = 1, Lot = 1 };
var observer = new Ship(items, zone, hull, new EntitySettings());
zone.Entities.Add(observer);
observer.Activate();
zone.Admit(observer, piloted: false);
return observer;
}
@ -388,9 +386,8 @@ public sealed class TargetingSystemTests : IDisposable
targeting.Lot = items.Lots.Add(new Lot { Design = cache.RefOf<ItemData>(targetingData), Origin = new Attributed(), Quality = .5f, Roles = new System.Collections.Generic.List<RoleFill>() });
Assert.True(ship.TryEquip(targeting));
zone.Entities.Add(ship);
zone.Admit(ship, piloted: false);
ship.Aim = float3(0, 0, 1);
ship.Activate();
return ship;
}
@ -429,11 +426,12 @@ public sealed class TargetingSystemTests : IDisposable
var cargo = cache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = new Shape(), Price = 5 });
var capacitor = cache.Upsert(new GearData { Name = "Cap", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new CapacitorData() } });
var targeting = cache.Upsert(new GearData { Name = "Targeting", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new TargetingSystemData() } });
var tractor = cache.Upsert(new GearData { Name = "Tractor", Hardpoint = HardpointType.Tool, Shape = new Shape(), Price = 1, Behaviors = { new PickupData() } });
foreach (var (name, design) in new[]
{
("Armed by Maker", hull.Key), ("Gun by Maker", gun.Key), ("Crate by Maker", cargo.Key),
("Cap by Maker", capacitor.Key), ("Targeting by Maker", targeting.Key)
("Cap by Maker", capacitor.Key), ("Targeting by Maker", targeting.Key), ("Tractor by Maker", tractor.Key)
})
cache.Upsert(new FactionProductData { Name = name, Design = new CultRecordRef<CraftedItemData>(design), Manufacturer = maker });
cache.FlushAsync().Wait();

View file

@ -105,9 +105,8 @@ public sealed class ThrottleLockTests : IDisposable
}
Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.CounterClockwise), new int2(1, 3)));
Assert.True(ship.TryEquip(Mint(cache, items, gear, ItemRotation.Clockwise), new int2(3, 3)));
zone.Entities.Add(ship);
zone.Admit(ship, piloted: false);
ship.Aim = float3(0, 0, 1);
ship.Activate();
// A thruster is live from its first performance update; the allocator reads that every tick.
foreach (var item in ship.Equipment) item.UpdatePerformance();

View file

@ -130,10 +130,8 @@ public sealed class VapourCloudTests : IDisposable
// Held off until a test pulls the trigger: the dump vents on its first run.
lab.Dump.Enabled.Value = false;
zone.Entities.Add(lab.Observer);
zone.Entities.Add(lab.Target);
lab.Observer.Activate();
lab.Target.Activate();
zone.Admit(lab.Observer, piloted: false);
zone.Admit(lab.Target, piloted: false);
lab.Observer.Position = float3(0, 0, 0);
lab.Observer.Aim = float3(0, 0, 1);
lab.Target.Position = float3(0, 0, distance);