From 78869264e2ca7cdc05b2dd94d930a1470ea6a749 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Mar 2026 16:35:35 +0200 Subject: [PATCH 1/5] feat(physics3d): isolate ragdoll and joints runtime/editor updates --- Extensions/Physics3DBehavior/JsExtension.js | 4146 ++++++++++++--- .../Physics3DRuntimeBehavior.ts | 4642 ++++++++++++++++- .../Physics3DBehavior/Physics3DTools.ts | 373 ++ .../Editors/Physics3DEditor/index.js | 376 ++ 4 files changed, 8857 insertions(+), 680 deletions(-) diff --git a/Extensions/Physics3DBehavior/JsExtension.js b/Extensions/Physics3DBehavior/JsExtension.js index ed6ceb2d1448..1fd25ceee789 100644 --- a/Extensions/Physics3DBehavior/JsExtension.js +++ b/Extensions/Physics3DBehavior/JsExtension.js @@ -270,6 +270,330 @@ module.exports = { return true; } + if (propertyName === 'ragdollRole') { + if (!behaviorContent.hasChild('ragdollRole')) { + behaviorContent.addChild('ragdollRole').setStringValue('None'); + } + behaviorContent.getChild('ragdollRole').setStringValue(newValue); + return true; + } + + if (propertyName === 'ragdollGroupTag') { + if (!behaviorContent.hasChild('ragdollGroupTag')) { + behaviorContent.addChild('ragdollGroupTag').setStringValue(''); + } + behaviorContent.getChild('ragdollGroupTag').setStringValue(newValue); + return true; + } + + if (propertyName === 'jointAutoWakeBodies') { + if (!behaviorContent.hasChild('jointAutoWakeBodies')) { + behaviorContent.addChild('jointAutoWakeBodies').setBoolValue(true); + } + behaviorContent + .getChild('jointAutoWakeBodies') + .setBoolValue(newValue === '1' || newValue === 'true'); + return true; + } + + if (propertyName === 'jointAutoStabilityPreset') { + const normalizedValue = newValue.toLowerCase(); + let presetValue = 'Balanced'; + if (normalizedValue === 'stable') presetValue = 'Stable'; + else if (normalizedValue === 'ultrastable') + presetValue = 'UltraStable'; + if (!behaviorContent.hasChild('jointAutoStabilityPreset')) { + behaviorContent + .addChild('jointAutoStabilityPreset') + .setStringValue('Stable'); + } + behaviorContent + .getChild('jointAutoStabilityPreset') + .setStringValue(presetValue); + return true; + } + + if (propertyName === 'jointAutoBreakForce') { + const newValueAsNumber = Math.max(0, parseFloat(newValue)); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointAutoBreakForce')) { + behaviorContent.addChild('jointAutoBreakForce').setDoubleValue(0); + } + behaviorContent + .getChild('jointAutoBreakForce') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointAutoBreakTorque') { + const newValueAsNumber = Math.max(0, parseFloat(newValue)); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointAutoBreakTorque')) { + behaviorContent.addChild('jointAutoBreakTorque').setDoubleValue(0); + } + behaviorContent + .getChild('jointAutoBreakTorque') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorEnabled') { + if (!behaviorContent.hasChild('jointEditorEnabled')) { + behaviorContent.addChild('jointEditorEnabled').setBoolValue(false); + } + behaviorContent + .getChild('jointEditorEnabled') + .setBoolValue(newValue === '1' || newValue === 'true'); + return true; + } + + if (propertyName === 'jointEditorTargetObjectName') { + if (!behaviorContent.hasChild('jointEditorTargetObjectName')) { + behaviorContent + .addChild('jointEditorTargetObjectName') + .setStringValue(''); + } + behaviorContent + .getChild('jointEditorTargetObjectName') + .setStringValue(newValue); + return true; + } + + if (propertyName === 'jointEditorType') { + const normalizedValue = newValue.toLowerCase(); + let jointType = 'None'; + if (normalizedValue === 'fixed') jointType = 'Fixed'; + else if (normalizedValue === 'point') jointType = 'Point'; + else if (normalizedValue === 'hinge') jointType = 'Hinge'; + else if (normalizedValue === 'slider') jointType = 'Slider'; + else if (normalizedValue === 'distance') jointType = 'Distance'; + else if (normalizedValue === 'cone') jointType = 'Cone'; + else if ( + normalizedValue === 'swingtwist' || + normalizedValue === 'swing twist' + ) + jointType = 'SwingTwist'; + if (!behaviorContent.hasChild('jointEditorType')) { + behaviorContent.addChild('jointEditorType').setStringValue('None'); + } + behaviorContent.getChild('jointEditorType').setStringValue(jointType); + return true; + } + + if (propertyName === 'jointEditorAnchorOffsetX') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorAnchorOffsetX')) { + behaviorContent + .addChild('jointEditorAnchorOffsetX') + .setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorAnchorOffsetX') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorAnchorOffsetY') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorAnchorOffsetY')) { + behaviorContent + .addChild('jointEditorAnchorOffsetY') + .setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorAnchorOffsetY') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorAnchorOffsetZ') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorAnchorOffsetZ')) { + behaviorContent + .addChild('jointEditorAnchorOffsetZ') + .setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorAnchorOffsetZ') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorTargetAnchorOffsetX') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorTargetAnchorOffsetX')) { + behaviorContent + .addChild('jointEditorTargetAnchorOffsetX') + .setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorTargetAnchorOffsetX') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorTargetAnchorOffsetY') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorTargetAnchorOffsetY')) { + behaviorContent + .addChild('jointEditorTargetAnchorOffsetY') + .setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorTargetAnchorOffsetY') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorTargetAnchorOffsetZ') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorTargetAnchorOffsetZ')) { + behaviorContent + .addChild('jointEditorTargetAnchorOffsetZ') + .setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorTargetAnchorOffsetZ') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorUseCustomAxis') { + if (!behaviorContent.hasChild('jointEditorUseCustomAxis')) { + behaviorContent + .addChild('jointEditorUseCustomAxis') + .setBoolValue(false); + } + behaviorContent + .getChild('jointEditorUseCustomAxis') + .setBoolValue(newValue === '1' || newValue === 'true'); + return true; + } + + if (propertyName === 'jointEditorAxisX') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorAxisX')) { + behaviorContent.addChild('jointEditorAxisX').setDoubleValue(1); + } + behaviorContent + .getChild('jointEditorAxisX') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorAxisY') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorAxisY')) { + behaviorContent.addChild('jointEditorAxisY').setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorAxisY') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorAxisZ') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorAxisZ')) { + behaviorContent.addChild('jointEditorAxisZ').setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorAxisZ') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorHingeMinAngle') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorHingeMinAngle')) { + behaviorContent + .addChild('jointEditorHingeMinAngle') + .setDoubleValue(-60); + } + behaviorContent + .getChild('jointEditorHingeMinAngle') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorHingeMaxAngle') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorHingeMaxAngle')) { + behaviorContent + .addChild('jointEditorHingeMaxAngle') + .setDoubleValue(60); + } + behaviorContent + .getChild('jointEditorHingeMaxAngle') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorDistanceMin') { + const newValueAsNumber = Math.max(0, parseFloat(newValue)); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorDistanceMin')) { + behaviorContent + .addChild('jointEditorDistanceMin') + .setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorDistanceMin') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorDistanceMax') { + const newValueAsNumber = Math.max(0, parseFloat(newValue)); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorDistanceMax')) { + behaviorContent + .addChild('jointEditorDistanceMax') + .setDoubleValue(0); + } + behaviorContent + .getChild('jointEditorDistanceMax') + .setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'jointEditorPreviewEnabled') { + if (!behaviorContent.hasChild('jointEditorPreviewEnabled')) { + behaviorContent + .addChild('jointEditorPreviewEnabled') + .setBoolValue(true); + } + behaviorContent + .getChild('jointEditorPreviewEnabled') + .setBoolValue(newValue === '1' || newValue === 'true'); + return true; + } + + if (propertyName === 'jointEditorPreviewSize') { + const newValueAsNumber = Math.max(1, parseFloat(newValue)); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!behaviorContent.hasChild('jointEditorPreviewSize')) { + behaviorContent + .addChild('jointEditorPreviewSize') + .setDoubleValue(8); + } + behaviorContent + .getChild('jointEditorPreviewSize') + .setDoubleValue(newValueAsNumber); + return true; + } + return false; }; behavior.getProperties = function (behaviorContent) { @@ -630,1063 +954,3531 @@ module.exports = { .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) .setHidden(true); // Hidden as required to be changed in the full editor. - return behaviorProperties; - }; - - behavior.initializeContent = function (behaviorContent) { - behaviorContent.addChild('object3D').setStringValue(''); - behaviorContent.addChild('bodyType').setStringValue('Dynamic'); - behaviorContent.addChild('bullet').setBoolValue(false); - behaviorContent.addChild('fixedRotation').setBoolValue(false); - behaviorContent.addChild('shape').setStringValue('Box'); - behaviorContent.addChild('meshShapeResourceName').setStringValue(''); - behaviorContent.addChild('shapeOrientation').setStringValue('Z'); - behaviorContent.addChild('shapeDimensionA').setDoubleValue(0); - behaviorContent.addChild('shapeDimensionB').setDoubleValue(0); - behaviorContent.addChild('shapeDimensionC').setDoubleValue(0); - behaviorContent.addChild('shapeOffsetX').setDoubleValue(0); - behaviorContent.addChild('shapeOffsetY').setDoubleValue(0); - behaviorContent.addChild('shapeOffsetZ').setDoubleValue(0); - behaviorContent.addChild('massCenterOffsetX').setDoubleValue(0); - behaviorContent.addChild('massCenterOffsetY').setDoubleValue(0); - behaviorContent.addChild('massCenterOffsetZ').setDoubleValue(0); - behaviorContent.addChild('massOverride').setDoubleValue(0); - behaviorContent.addChild('density').setDoubleValue(1.0); - behaviorContent.addChild('friction').setDoubleValue(0.3); - behaviorContent.addChild('restitution').setDoubleValue(0.1); - behaviorContent.addChild('linearDamping').setDoubleValue(0.1); - behaviorContent.addChild('angularDamping').setDoubleValue(0.1); - behaviorContent.addChild('gravityScale').setDoubleValue(1); - behaviorContent.addChild('layers').setIntValue((1 << 4) | (1 << 0)); - behaviorContent.addChild('masks').setIntValue((1 << 4) | (1 << 0)); - }; - - const sharedData = new gd.BehaviorSharedDataJsImplementation(); - sharedData.updateProperty = function ( - sharedContent, - propertyName, - newValue - ) { - if (propertyName === 'gravityX') { - const newValueAsNumber = parseFloat(newValue); - if (newValueAsNumber !== newValueAsNumber) return false; - sharedContent.getChild('gravityX').setDoubleValue(newValueAsNumber); - return true; + // Ragdoll properties + if (!behaviorContent.hasChild('ragdollRole')) { + behaviorContent.addChild('ragdollRole').setStringValue('None'); } - - if (propertyName === 'gravityY') { - const newValueAsNumber = parseFloat(newValue); - if (newValueAsNumber !== newValueAsNumber) return false; - sharedContent.getChild('gravityY').setDoubleValue(newValueAsNumber); - return true; + behaviorProperties + .getOrCreate('ragdollRole') + .setValue(behaviorContent.getChild('ragdollRole').getStringValue()) + .setType('Choice') + .setLabel(_('Ragdoll Body Part')) + .setDescription( + _( + 'Assign a ragdoll role to this object. Objects with the same Group Tag will auto-connect using appropriate joint types.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .addChoice('None', _('None')) + .addChoice('Head', _('Head')) + .addChoice('Chest', _('Chest')) + .addChoice('Hips', _('Hips')) + .addChoice('UpperArmL', _('Upper Arm Left')) + .addChoice('LowerArmL', _('Lower Arm Left')) + .addChoice('UpperArmR', _('Upper Arm Right')) + .addChoice('LowerArmR', _('Lower Arm Right')) + .addChoice('ThighL', _('Thigh Left')) + .addChoice('ShinL', _('Shin Left')) + .addChoice('ThighR', _('Thigh Right')) + .addChoice('ShinR', _('Shin Right')) + .setGroup(_('Ragdoll')); + + if (!behaviorContent.hasChild('ragdollGroupTag')) { + behaviorContent.addChild('ragdollGroupTag').setStringValue(''); } + behaviorProperties + .getOrCreate('ragdollGroupTag') + .setValue( + behaviorContent.getChild('ragdollGroupTag').getStringValue() + ) + .setType('String') + .setLabel(_('Ragdoll Group Tag')) + .setDescription( + _( + 'A shared tag that groups body parts together. Objects with the same tag will be auto-connected into a ragdoll.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Ragdoll')); - if (propertyName === 'gravityZ') { - const newValueAsNumber = parseFloat(newValue); - if (newValueAsNumber !== newValueAsNumber) return false; - sharedContent.getChild('gravityZ').setDoubleValue(newValueAsNumber); - return true; + if (!behaviorContent.hasChild('jointAutoWakeBodies')) { + behaviorContent.addChild('jointAutoWakeBodies').setBoolValue(true); } + behaviorProperties + .getOrCreate('jointAutoWakeBodies') + .setValue( + behaviorContent.getChild('jointAutoWakeBodies').getBoolValue() + ? 'true' + : 'false' + ) + .setType('Boolean') + .setLabel(_('Auto wake linked bodies')) + .setDescription( + _( + 'When enabled, linked bodies are automatically activated after joint creation/changes so the constraint effect is immediate.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Realism')); - if (propertyName === 'worldScale') { - const newValueAsNumber = parseInt(newValue, 10); - if (newValueAsNumber !== newValueAsNumber) return false; - if (!sharedContent.hasChild('worldScale')) { - sharedContent.addChild('worldScale'); - } - sharedContent.getChild('worldScale').setDoubleValue(newValueAsNumber); - return true; + if (!behaviorContent.hasChild('jointAutoStabilityPreset')) { + behaviorContent + .addChild('jointAutoStabilityPreset') + .setStringValue('Stable'); } - return false; - }; - sharedData.getProperties = function (sharedContent) { - const sharedProperties = new gd.MapStringPropertyDescriptor(); + behaviorProperties + .getOrCreate('jointAutoStabilityPreset') + .setValue( + behaviorContent + .getChild('jointAutoStabilityPreset') + .getStringValue() + ) + .setType('Choice') + .setLabel(_('Default joint stability')) + .addChoice('Balanced', _('Balanced')) + .addChoice('Stable', _('Stable')) + .addChoice('UltraStable', _('Ultra Stable')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Realism')); - sharedProperties - .getOrCreate('gravityX') + if (!behaviorContent.hasChild('jointAutoBreakForce')) { + behaviorContent.addChild('jointAutoBreakForce').setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointAutoBreakForce') .setValue( - sharedContent.getChild('gravityX').getDoubleValue().toString(10) + behaviorContent + .getChild('jointAutoBreakForce') + .getDoubleValue() + .toString(10) ) .setType('Number') - .setMeasurementUnit(gd.MeasurementUnit.getNewton()); - sharedProperties - .getOrCreate('gravityY') + .setLabel(_('Auto break force')) + .setDescription( + _( + 'If > 0, newly created joints break automatically when reaction force exceeds this threshold.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Realism')); + + if (!behaviorContent.hasChild('jointAutoBreakTorque')) { + behaviorContent.addChild('jointAutoBreakTorque').setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointAutoBreakTorque') .setValue( - sharedContent.getChild('gravityY').getDoubleValue().toString(10) + behaviorContent + .getChild('jointAutoBreakTorque') + .getDoubleValue() + .toString(10) ) .setType('Number') - .setMeasurementUnit(gd.MeasurementUnit.getNewton()); - sharedProperties - .getOrCreate('gravityZ') + .setLabel(_('Auto break torque')) + .setDescription( + _( + 'If > 0, newly created joints break automatically when reaction torque exceeds this threshold.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Realism')); + + if (!behaviorContent.hasChild('jointEditorEnabled')) { + behaviorContent.addChild('jointEditorEnabled').setBoolValue(false); + } + behaviorProperties + .getOrCreate('jointEditorEnabled') .setValue( - sharedContent.getChild('gravityZ').getDoubleValue().toString(10) + behaviorContent.getChild('jointEditorEnabled').getBoolValue() + ? 'true' + : 'false' + ) + .setType('Boolean') + .setLabel(_('Enable joint editor link')) + .setDescription( + _( + 'Automatically creates and maintains one physical joint from this object to a target object name.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorTargetObjectName')) { + behaviorContent + .addChild('jointEditorTargetObjectName') + .setStringValue(''); + } + behaviorProperties + .getOrCreate('jointEditorTargetObjectName') + .setValue( + behaviorContent + .getChild('jointEditorTargetObjectName') + .getStringValue() + ) + .setType('String') + .setLabel(_('Target object name')) + .setDescription( + _( + 'Object name to link to. The nearest valid instance is selected and kept linked.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorType')) { + behaviorContent.addChild('jointEditorType').setStringValue('None'); + } + behaviorProperties + .getOrCreate('jointEditorType') + .setValue( + behaviorContent.getChild('jointEditorType').getStringValue() + ) + .setType('Choice') + .setLabel(_('Joint type')) + .addChoice('None', _('None')) + .addChoice('Fixed', _('Fixed')) + .addChoice('Point', _('Point')) + .addChoice('Hinge', _('Hinge')) + .addChoice('Slider', _('Slider')) + .addChoice('Distance', _('Distance')) + .addChoice('Cone', _('Cone')) + .addChoice('SwingTwist', _('SwingTwist')) + .setDescription( + _( + 'Joint editor supports only Scene3D::Model3DObject and Scene3D::Cube3DObject for real physical links.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorAnchorOffsetX')) { + behaviorContent + .addChild('jointEditorAnchorOffsetX') + .setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorAnchorOffsetX') + .setValue( + behaviorContent + .getChild('jointEditorAnchorOffsetX') + .getDoubleValue() + .toString(10) ) .setType('Number') - .setMeasurementUnit(gd.MeasurementUnit.getNewton()); + .setLabel(_('Source anchor offset X')) + .setDescription( + _( + 'Local offset in pixels from this object center to place the joint anchor.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); - sharedProperties - .getOrCreate('worldScale') + if (!behaviorContent.hasChild('jointEditorAnchorOffsetY')) { + behaviorContent + .addChild('jointEditorAnchorOffsetY') + .setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorAnchorOffsetY') .setValue( - sharedContent.getChild('worldScale').getDoubleValue().toString(10) + behaviorContent + .getChild('jointEditorAnchorOffsetY') + .getDoubleValue() + .toString(10) ) - .setType('Number'); + .setType('Number') + .setLabel(_('Source anchor offset Y')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); - return sharedProperties; - }; - sharedData.initializeContent = function (behaviorContent) { - behaviorContent.addChild('gravityX').setDoubleValue(0); - behaviorContent.addChild('gravityY').setDoubleValue(0); - behaviorContent.addChild('gravityZ').setDoubleValue(-9.8); - behaviorContent.addChild('worldScale').setDoubleValue(100); - }; + if (!behaviorContent.hasChild('jointEditorAnchorOffsetZ')) { + behaviorContent + .addChild('jointEditorAnchorOffsetZ') + .setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorAnchorOffsetZ') + .setValue( + behaviorContent + .getChild('jointEditorAnchorOffsetZ') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Source anchor offset Z')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); - const aut = extension - .addBehavior( - 'Physics3DBehavior', - _('3D physics engine'), - 'Physics3D', - _( - 'Simulate realistic 3D physics for this object including gravity, forces, collisions, etc.' - ), - '', - 'JsPlatform/Extensions/physics3d.svg', - 'Physics3DBehavior', - //@ts-ignore The class hierarchy is incorrect leading to a type error, but this is valid. - behavior, - sharedData - ) - .markAsIrrelevantForChildObjects() - .addIncludeFile( - 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' - ) - .addRequiredFile('Extensions/Physics3DBehavior/jolt-physics.wasm.js') - .addRequiredFile('Extensions/Physics3DBehavior/jolt-physics.wasm.wasm') - .setOpenFullEditorLabel(_('Edit shape and advanced settings')); + if (!behaviorContent.hasChild('jointEditorTargetAnchorOffsetX')) { + behaviorContent + .addChild('jointEditorTargetAnchorOffsetX') + .setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorTargetAnchorOffsetX') + .setValue( + behaviorContent + .getChild('jointEditorTargetAnchorOffsetX') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Target anchor offset X')) + .setDescription( + _( + 'Local offset in pixels from target object center to place the joint anchor.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorTargetAnchorOffsetY')) { + behaviorContent + .addChild('jointEditorTargetAnchorOffsetY') + .setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorTargetAnchorOffsetY') + .setValue( + behaviorContent + .getChild('jointEditorTargetAnchorOffsetY') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Target anchor offset Y')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorTargetAnchorOffsetZ')) { + behaviorContent + .addChild('jointEditorTargetAnchorOffsetZ') + .setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorTargetAnchorOffsetZ') + .setValue( + behaviorContent + .getChild('jointEditorTargetAnchorOffsetZ') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Target anchor offset Z')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorUseCustomAxis')) { + behaviorContent + .addChild('jointEditorUseCustomAxis') + .setBoolValue(false); + } + behaviorProperties + .getOrCreate('jointEditorUseCustomAxis') + .setValue( + behaviorContent.getChild('jointEditorUseCustomAxis').getBoolValue() + ? 'true' + : 'false' + ) + .setType('Boolean') + .setLabel(_('Use custom axis')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorAxisX')) { + behaviorContent.addChild('jointEditorAxisX').setDoubleValue(1); + } + behaviorProperties + .getOrCreate('jointEditorAxisX') + .setValue( + behaviorContent + .getChild('jointEditorAxisX') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Custom axis X')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorAxisY')) { + behaviorContent.addChild('jointEditorAxisY').setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorAxisY') + .setValue( + behaviorContent + .getChild('jointEditorAxisY') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Custom axis Y')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorAxisZ')) { + behaviorContent.addChild('jointEditorAxisZ').setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorAxisZ') + .setValue( + behaviorContent + .getChild('jointEditorAxisZ') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Custom axis Z')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorHingeMinAngle')) { + behaviorContent + .addChild('jointEditorHingeMinAngle') + .setDoubleValue(-60); + } + behaviorProperties + .getOrCreate('jointEditorHingeMinAngle') + .setValue( + behaviorContent + .getChild('jointEditorHingeMinAngle') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Hinge/Twist min angle')) + .setDescription(_('Used by Hinge, Cone and SwingTwist presets.')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorHingeMaxAngle')) { + behaviorContent + .addChild('jointEditorHingeMaxAngle') + .setDoubleValue(60); + } + behaviorProperties + .getOrCreate('jointEditorHingeMaxAngle') + .setValue( + behaviorContent + .getChild('jointEditorHingeMaxAngle') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Hinge/Twist max angle')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorDistanceMin')) { + behaviorContent.addChild('jointEditorDistanceMin').setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorDistanceMin') + .setValue( + behaviorContent + .getChild('jointEditorDistanceMin') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Distance min')) + .setDescription( + _( + 'Used by Distance joint. Leave min/max at 0 for automatic distance based on current pose.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorDistanceMax')) { + behaviorContent.addChild('jointEditorDistanceMax').setDoubleValue(0); + } + behaviorProperties + .getOrCreate('jointEditorDistanceMax') + .setValue( + behaviorContent + .getChild('jointEditorDistanceMax') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Distance max')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorPreviewEnabled')) { + behaviorContent + .addChild('jointEditorPreviewEnabled') + .setBoolValue(true); + } + behaviorProperties + .getOrCreate('jointEditorPreviewEnabled') + .setValue( + behaviorContent.getChild('jointEditorPreviewEnabled').getBoolValue() + ? 'true' + : 'false' + ) + .setType('Boolean') + .setLabel(_('Show realtime preview')) + .setDescription( + _( + 'Display source/target/anchor points and axis line in real time while editing.' + ) + ) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + if (!behaviorContent.hasChild('jointEditorPreviewSize')) { + behaviorContent.addChild('jointEditorPreviewSize').setDoubleValue(8); + } + behaviorProperties + .getOrCreate('jointEditorPreviewSize') + .setValue( + behaviorContent + .getChild('jointEditorPreviewSize') + .getDoubleValue() + .toString(10) + ) + .setType('Number') + .setLabel(_('Preview size')) + .setQuickCustomizationVisibility(gd.QuickCustomization.Hidden) + .setGroup(_('Joint Editor (3D Object / 3D Box)')); + + return behaviorProperties; + }; + + behavior.initializeContent = function (behaviorContent) { + behaviorContent.addChild('object3D').setStringValue(''); + behaviorContent.addChild('bodyType').setStringValue('Dynamic'); + behaviorContent.addChild('bullet').setBoolValue(false); + behaviorContent.addChild('fixedRotation').setBoolValue(false); + behaviorContent.addChild('shape').setStringValue('Box'); + behaviorContent.addChild('meshShapeResourceName').setStringValue(''); + behaviorContent.addChild('shapeOrientation').setStringValue('Z'); + behaviorContent.addChild('shapeDimensionA').setDoubleValue(0); + behaviorContent.addChild('shapeDimensionB').setDoubleValue(0); + behaviorContent.addChild('shapeDimensionC').setDoubleValue(0); + behaviorContent.addChild('shapeOffsetX').setDoubleValue(0); + behaviorContent.addChild('shapeOffsetY').setDoubleValue(0); + behaviorContent.addChild('shapeOffsetZ').setDoubleValue(0); + behaviorContent.addChild('massCenterOffsetX').setDoubleValue(0); + behaviorContent.addChild('massCenterOffsetY').setDoubleValue(0); + behaviorContent.addChild('massCenterOffsetZ').setDoubleValue(0); + behaviorContent.addChild('massOverride').setDoubleValue(0); + behaviorContent.addChild('density').setDoubleValue(1.0); + behaviorContent.addChild('friction').setDoubleValue(0.3); + behaviorContent.addChild('restitution').setDoubleValue(0.1); + behaviorContent.addChild('linearDamping').setDoubleValue(0.1); + behaviorContent.addChild('angularDamping').setDoubleValue(0.1); + behaviorContent.addChild('gravityScale').setDoubleValue(1); + behaviorContent.addChild('layers').setIntValue((1 << 4) | (1 << 0)); + behaviorContent.addChild('masks').setIntValue((1 << 4) | (1 << 0)); + behaviorContent.addChild('ragdollRole').setStringValue('None'); + behaviorContent.addChild('ragdollGroupTag').setStringValue(''); + behaviorContent.addChild('jointAutoWakeBodies').setBoolValue(true); + behaviorContent + .addChild('jointAutoStabilityPreset') + .setStringValue('Stable'); + behaviorContent.addChild('jointAutoBreakForce').setDoubleValue(0); + behaviorContent.addChild('jointAutoBreakTorque').setDoubleValue(0); + behaviorContent.addChild('jointEditorEnabled').setBoolValue(false); + behaviorContent + .addChild('jointEditorTargetObjectName') + .setStringValue(''); + behaviorContent.addChild('jointEditorType').setStringValue('None'); + behaviorContent.addChild('jointEditorAnchorOffsetX').setDoubleValue(0); + behaviorContent.addChild('jointEditorAnchorOffsetY').setDoubleValue(0); + behaviorContent.addChild('jointEditorAnchorOffsetZ').setDoubleValue(0); + behaviorContent + .addChild('jointEditorTargetAnchorOffsetX') + .setDoubleValue(0); + behaviorContent + .addChild('jointEditorTargetAnchorOffsetY') + .setDoubleValue(0); + behaviorContent + .addChild('jointEditorTargetAnchorOffsetZ') + .setDoubleValue(0); + behaviorContent + .addChild('jointEditorUseCustomAxis') + .setBoolValue(false); + behaviorContent.addChild('jointEditorAxisX').setDoubleValue(1); + behaviorContent.addChild('jointEditorAxisY').setDoubleValue(0); + behaviorContent.addChild('jointEditorAxisZ').setDoubleValue(0); + behaviorContent + .addChild('jointEditorHingeMinAngle') + .setDoubleValue(-60); + behaviorContent.addChild('jointEditorHingeMaxAngle').setDoubleValue(60); + behaviorContent.addChild('jointEditorDistanceMin').setDoubleValue(0); + behaviorContent.addChild('jointEditorDistanceMax').setDoubleValue(0); + behaviorContent + .addChild('jointEditorPreviewEnabled') + .setBoolValue(true); + behaviorContent.addChild('jointEditorPreviewSize').setDoubleValue(8); + }; + + const sharedData = new gd.BehaviorSharedDataJsImplementation(); + sharedData.updateProperty = function ( + sharedContent, + propertyName, + newValue + ) { + if (propertyName === 'gravityX') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + sharedContent.getChild('gravityX').setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'gravityY') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + sharedContent.getChild('gravityY').setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'gravityZ') { + const newValueAsNumber = parseFloat(newValue); + if (newValueAsNumber !== newValueAsNumber) return false; + sharedContent.getChild('gravityZ').setDoubleValue(newValueAsNumber); + return true; + } + + if (propertyName === 'worldScale') { + const newValueAsNumber = parseInt(newValue, 10); + if (newValueAsNumber !== newValueAsNumber) return false; + if (!sharedContent.hasChild('worldScale')) { + sharedContent.addChild('worldScale'); + } + sharedContent.getChild('worldScale').setDoubleValue(newValueAsNumber); + return true; + } + return false; + }; + sharedData.getProperties = function (sharedContent) { + const sharedProperties = new gd.MapStringPropertyDescriptor(); + + sharedProperties + .getOrCreate('gravityX') + .setValue( + sharedContent.getChild('gravityX').getDoubleValue().toString(10) + ) + .setType('Number') + .setMeasurementUnit(gd.MeasurementUnit.getNewton()); + sharedProperties + .getOrCreate('gravityY') + .setValue( + sharedContent.getChild('gravityY').getDoubleValue().toString(10) + ) + .setType('Number') + .setMeasurementUnit(gd.MeasurementUnit.getNewton()); + sharedProperties + .getOrCreate('gravityZ') + .setValue( + sharedContent.getChild('gravityZ').getDoubleValue().toString(10) + ) + .setType('Number') + .setMeasurementUnit(gd.MeasurementUnit.getNewton()); + + sharedProperties + .getOrCreate('worldScale') + .setValue( + sharedContent.getChild('worldScale').getDoubleValue().toString(10) + ) + .setType('Number'); + + return sharedProperties; + }; + sharedData.initializeContent = function (behaviorContent) { + behaviorContent.addChild('gravityX').setDoubleValue(0); + behaviorContent.addChild('gravityY').setDoubleValue(0); + behaviorContent.addChild('gravityZ').setDoubleValue(-9.8); + behaviorContent.addChild('worldScale').setDoubleValue(100); + }; + + const aut = extension + .addBehavior( + 'Physics3DBehavior', + _('3D physics engine'), + 'Physics3D', + _( + 'Simulate realistic 3D physics for this object including gravity, forces, collisions, etc.' + ), + '', + 'JsPlatform/Extensions/physics3d.svg', + 'Physics3DBehavior', + //@ts-ignore The class hierarchy is incorrect leading to a type error, but this is valid. + behavior, + sharedData + ) + .markAsIrrelevantForChildObjects() + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .addRequiredFile('Extensions/Physics3DBehavior/jolt-physics.wasm.js') + .addRequiredFile('Extensions/Physics3DBehavior/jolt-physics.wasm.wasm') + .setOpenFullEditorLabel(_('Edit shape and advanced settings')); + + // Global + aut + .addExpression( + 'WorldScale', + _('World scale'), + _('Return the world scale.'), + _('Global'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('getWorldScale'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'GravityX', + _('World gravity on X axis'), + _('the world gravity on X axis') + + ' ' + + _( + 'While an object is needed, this will apply to all objects using the behavior.' + ), + _('the world gravity on X axis'), + _('Global'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Gravity (in Newton)') + ) + ) + .setFunctionName('setGravityX') + .setGetter('getGravityX'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'GravityY', + _('World gravity on Y axis'), + _('the world gravity on Y axis') + + ' ' + + _( + 'While an object is needed, this will apply to all objects using the behavior.' + ), + _('the world gravity on Y axis'), + _('Global'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Gravity (in Newton)') + ) + ) + .setFunctionName('setGravityY') + .setGetter('getGravityY'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'GravityZ', + _('World gravity on Z axis'), + _('the world gravity on Z axis') + + ' ' + + _( + 'While an object is needed, this will apply to all objects using the behavior.' + ), + _('the world gravity on Z axis'), + _('Global'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Gravity (in Newton)') + ) + ) + .setFunctionName('setGravityZ') + .setGetter('getGravityZ'); + + aut + .addScopedCondition( + 'IsDynamic', + _('Is dynamic'), + _('Check if an object is dynamic.'), + _('_PARAM0_ is dynamic'), + _('Dynamics'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('isDynamic'); + + aut + .addScopedCondition( + 'IsStatic', + _('Is static'), + _('Check if an object is static.'), + _('_PARAM0_ is static'), + _('Dynamics'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('isStatic'); + + aut + .addScopedCondition( + 'IsKinematic', + _('Is kinematic'), + _('Check if an object is kinematic.'), + _('_PARAM0_ is kinematic'), + _('Dynamics'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('isKinematic'); + + aut + .addScopedCondition( + 'IsBullet', + _('Is treated as a bullet'), + _('Check if the object is being treated as a bullet.'), + _('_PARAM0_ is treated as a bullet'), + _('Dynamics'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('isBullet'); + + aut + .addScopedAction( + 'SetBullet', + _('Treat as bullet'), + _( + 'Treat the object as a bullet. Better collision handling on high speeds at cost of some performance.' + ), + _('Treat _PARAM0_ as bullet: _PARAM2_'), + _('Dynamics'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('yesorno', _('Treat as bullet'), '', false) + .setDefaultValue('false') + .getCodeExtraInformation() + .setFunctionName('setBullet'); + + aut + .addScopedCondition( + 'HasFixedRotation', + _('Has fixed rotation'), + _('Check if an object has fixed rotation.'), + _('_PARAM0_ has fixed rotation'), + _('Dynamics'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('hasFixedRotation'); + + aut + .addScopedAction( + 'SetFixedRotation', + _('Fixed rotation'), + _( + "Enable or disable an object fixed rotation. If enabled the object won't be able to rotate. This action has no effect on characters." + ), + _('Set _PARAM0_ fixed rotation: _PARAM2_'), + _('Dynamics'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('yesorno', _('Fixed rotation'), '', false) + .setDefaultValue('false') + .getCodeExtraInformation() + .setFunctionName('setFixedRotation'); + + // Body settings + aut + .addScopedAction( + 'ShapeScale', + _('Shape scale'), + _( + 'Modify an object shape scale. It affects custom shape dimensions, if custom dimensions are not set the body will be scaled automatically to the object size.' + ), + _('the shape scale'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardOperatorParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Scale (1 by default)') + ) + ) + .getCodeExtraInformation() + .setFunctionName('setShapeScale') + .setGetter('getShapeScale'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'Density', + _('Density'), + _( + "the object density. The body's density and volume determine its mass." + ), + _('the density'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) + .setFunctionName('setDensity') + .setGetter('getDensity'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'ShapeOffsetX', + _('Shape offset X'), + _('the object shape offset on X.'), + _('the shape offset on X'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) + .setFunctionName('setShapeOffsetX') + .setGetter('getShapeOffsetX'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'ShapeOffsetY', + _('Shape offset Y'), + _('the object shape offset on Y.'), + _('the shape offset on Y'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) + .setFunctionName('setShapeOffsetY') + .setGetter('getShapeOffsetY'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'ShapeOffsetZ', + _('Shape offset Z'), + _('the object shape offset on Z.'), + _('the shape offset on Z'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) + .setFunctionName('setShapeOffsetZ') + .setGetter('getShapeOffsetZ'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'Friction', + _('Friction'), + _( + "the object friction. How much energy is lost from the movement of one object over another. The combined friction from two bodies is calculated as 'sqrt(bodyA.friction * bodyB.friction)'." + ), + _('the friction'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) + .setFunctionName('setFriction') + .setGetter('getFriction'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'Restitution', + _('Restitution'), + _( + "the object restitution. Energy conservation on collision. The combined restitution from two bodies is calculated as 'max(bodyA.restitution, bodyB.restitution)'." + ), + _('the restitution'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) + .setFunctionName('setRestitution') + .setGetter('getRestitution'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'LinearDamping', + _('Linear damping'), + _( + 'the object linear damping. How much movement speed is lost across the time.' + ), + _('the linear damping'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) + .setFunctionName('setLinearDamping') + .setGetter('getLinearDamping'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'AngularDamping', + _('Angular damping'), + _( + 'the object angular damping. How much angular speed is lost across the time.' + ), + _('the angular damping'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) + .setFunctionName('setAngularDamping') + .setGetter('getAngularDamping'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'GravityScale', + _('Gravity scale'), + _( + 'the object gravity scale. The gravity applied to an object is the world gravity multiplied by the object gravity scale.' + ), + _('the gravity scale'), + _('Body settings'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Scale (1 by default)') + ) + ) + .setFunctionName('setGravityScale') + .setGetter('getGravityScale'); + + // Filtering + aut + .addScopedCondition( + 'LayerEnabled', + _('Layer enabled'), + _('Check if an object has a specific layer enabled.'), + _('_PARAM0_ has layer _PARAM2_ enabled'), + _('Filtering'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Layer (1 - 8)')) + .getCodeExtraInformation() + .setFunctionName('layerEnabled'); + + aut + .addScopedAction( + 'EnableLayer', + _('Enable layer'), + _( + 'Enable or disable a layer for an object. Two objects collide if any layer of the first object matches any mask of the second one and vice versa.' + ), + _('Enable layer _PARAM2_ for _PARAM0_: _PARAM3_'), + _('Filtering'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Layer (1 - 8)')) + .addParameter('yesorno', _('Enable'), '', false) + .setDefaultValue('true') + .getCodeExtraInformation() + .setFunctionName('enableLayer'); + + aut + .addScopedCondition( + 'MaskEnabled', + _('Mask enabled'), + _('Check if an object has a specific mask enabled.'), + _('_PARAM0_ has mask _PARAM2_ enabled'), + _('Filtering'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Mask (1 - 8)')) + .getCodeExtraInformation() + .setFunctionName('maskEnabled'); + + aut + .addScopedAction( + 'EnableMask', + _('Enable mask'), + _( + 'Enable or disable a mask for an object. Two objects collide if any layer of the first object matches any mask of the second one and vice versa.' + ), + _('Enable mask _PARAM2_ for _PARAM0_: _PARAM3_'), + _('Filtering'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Mask (1 - 8)')) + .addParameter('yesorno', _('Enable'), '', false) + .setDefaultValue('true') + .getCodeExtraInformation() + .setFunctionName('enableMask'); + + // Velocity + aut + .addExpressionAndConditionAndAction( + 'number', + 'LinearVelocityX', + _('Linear velocity X'), + _('the object linear velocity on X'), + _('the linear velocity on X'), + _('Velocity'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Speed (in pixels per second)') + ) + ) + .setFunctionName('setLinearVelocityX') + .setGetter('getLinearVelocityX'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'LinearVelocityY', + _('Linear velocity Y'), + _('the object linear velocity on Y'), + _('the linear velocity on Y'), + _('Velocity'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Speed (in pixels per second)') + ) + ) + .setFunctionName('setLinearVelocityY') + .setGetter('getLinearVelocityY'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'LinearVelocityZ', + _('Linear velocity Z'), + _('the object linear velocity on Z'), + _('the linear velocity on Z'), + _('Velocity'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Speed (in pixels per second)') + ) + ) + .setFunctionName('setLinearVelocityZ') + .setGetter('getLinearVelocityZ'); + + aut + .addExpressionAndCondition( + 'number', + 'LinearVelocityLength', + _('Linear velocity'), + _('the object linear velocity length'), + _('the linear velocity length'), + _('Velocity'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Speed to compare to (in pixels per second)') + ) + ) + .setFunctionName('getLinearVelocityLength'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'AngularVelocityX', + _('Angular velocity X'), + _('the object angular velocity around X'), + _('the angular velocity around X'), + _('Velocity'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Angular speed (in degrees per second)') + ) + ) + .setFunctionName('setAngularVelocityX') + .setGetter('getAngularVelocityX'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'AngularVelocityY', + _('Angular velocity Y'), + _('the object angular velocity around Y'), + _('the angular velocity around Y'), + _('Velocity'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Angular speed (in degrees per second)') + ) + ) + .setFunctionName('setAngularVelocityY') + .setGetter('getAngularVelocityY'); + + aut + .addExpressionAndConditionAndAction( + 'number', + 'AngularVelocityZ', + _('Angular velocity Z'), + _('the object angular velocity around Z'), + _('the angular velocity around Z'), + _('Velocity'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .useStandardParameters( + 'number', + gd.ParameterOptions.makeNewOptions().setDescription( + _('Angular speed (in degrees per second)') + ) + ) + .setFunctionName('setAngularVelocityZ') + .setGetter('getAngularVelocityZ'); + + // Forces and impulses + aut + .addScopedAction( + 'ApplyForce', + _('Apply force (at a point)'), + _( + 'Apply a force to the object over time. It "accelerates" an object and must be used every frame during a time period.' + ), + _( + 'Apply a force of _PARAM2_ ; _PARAM3_ ; _PARAM4_ to _PARAM0_ at _PARAM5_ ; _PARAM6_ ; _PARAM7_' + ), + _('Forces & impulses'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('X component (N)')) + .addParameter('expression', _('Y component (N)')) + .addParameter('expression', _('Z component (N)')) + .setParameterLongDescription( + _('A force is like an acceleration but depends on the mass.') + ) + .addParameter('expression', _('Application point on X axis')) + .addParameter('expression', _('Application point on Y axis')) + .addParameter('expression', _('Application point on Z axis')) + .setParameterLongDescription( + _( + 'Use `MassCenterX`, `MassCenterY` and `MassCenterZ` expressions to avoid any rotation.' + ) + ) + .getCodeExtraInformation() + .setFunctionName('applyForce'); + + aut + .addScopedAction( + 'ApplyForceAtCenter', + _('Apply force (at center)'), + _( + 'Apply a force to the object over time. It "accelerates" an object and must be used every frame during a time period.' + ), + _( + 'Apply a force of _PARAM2_ ; _PARAM3_ ; _PARAM4_ at the center of _PARAM0_' + ), + _('Forces & impulses'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('X component (N)')) + .addParameter('expression', _('Y component (N)')) + .addParameter('expression', _('Z component (N)')) + .setParameterLongDescription( + _('A force is like an acceleration but depends on the mass.') + ) + .getCodeExtraInformation() + .setFunctionName('applyForceAtCenter'); + + aut + .addScopedAction( + 'ApplyForceTowardPosition', + _('Apply force toward position'), + _( + 'Apply a force to the object over time to move it toward a position. It "accelerates" an object and must be used every frame during a time period.' + ), + _( + 'Apply to _PARAM0_ a force of length _PARAM2_ towards _PARAM3_ ; _PARAM4_ ; _PARAM5_' + ), + _('Forces & impulses'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Length (N)')) + .setParameterLongDescription( + _('A force is like an acceleration but depends on the mass.') + ) + .addParameter('expression', _('X position')) + .addParameter('expression', _('Y position')) + .addParameter('expression', _('Z position')) + .getCodeExtraInformation() + .setFunctionName('applyForceTowardPosition'); + + aut + .addScopedAction( + 'ApplyImpulse', + _('Apply impulse (at a point)'), + _( + 'Apply an impulse to the object. It instantly changes the speed, to give an initial speed for instance.' + ), + _( + 'Apply an impulse of _PARAM2_ ; _PARAM3_ ; _PARAM4_ to _PARAM0_ at _PARAM5_ ; _PARAM6_ ; _PARAM7_' + ), + _('Forces & impulses'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('X component (N·s or kg·m·s⁻¹)')) + .addParameter('expression', _('Y component (N·s or kg·m·s⁻¹)')) + .addParameter('expression', _('Z component (N·s or kg·m·s⁻¹)')) + .setParameterLongDescription( + _('An impulse is like a speed addition but depends on the mass.') + ) + .addParameter('expression', _('Application point on X axis')) + .addParameter('expression', _('Application point on Y axis')) + .addParameter('expression', _('Application point on Z axis')) + .setParameterLongDescription( + _( + 'Use `MassCenterX`, `MassCenterY` and `MassCenterZ` expressions to avoid any rotation.' + ) + ) + .getCodeExtraInformation() + .setFunctionName('applyImpulse'); + + aut + .addScopedAction( + 'ApplyImpulseAtCenter', + _('Apply impulse (at center)'), + _( + 'Apply an impulse to the object. It instantly changes the speed, to give an initial speed for instance.' + ), + _( + 'Apply an impulse of _PARAM2_ ; _PARAM3_ ; _PARAM4_ at the center of _PARAM0_' + ), + _('Forces & impulses'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('X component (N·s or kg·m·s⁻¹)')) + .addParameter('expression', _('Y component (N·s or kg·m·s⁻¹)')) + .addParameter('expression', _('Z component (N·s or kg·m·s⁻¹)')) + .setParameterLongDescription( + _('An impulse is like a speed addition but depends on the mass.') + ) + .getCodeExtraInformation() + .setFunctionName('applyImpulseAtCenter'); + + aut + .addScopedAction( + 'ApplyImpulseTowardPosition', + _('Apply impulse toward position'), + _( + 'Apply an impulse to the object to move it toward a position. It instantly changes the speed, to give an initial speed for instance.' + ), + _( + 'Apply to _PARAM0_ an impulse of length _PARAM2_ towards _PARAM3_ ; _PARAM4_ ; _PARAM5_' + ), + _('Forces & impulses'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Length (N·s or kg·m·s⁻¹)')) + .setParameterLongDescription( + _('An impulse is like a speed addition but depends on the mass.') + ) + .addParameter('expression', _('X position')) + .addParameter('expression', _('Y position')) + .addParameter('expression', _('Z position')) + .getCodeExtraInformation() + .setFunctionName('applyImpulseTowardPosition'); + + aut + .addScopedAction( + 'ApplyTorque', + _('Apply torque (rotational force)'), + _( + 'Apply a torque (also called "rotational force") to the object. It "accelerates" an object rotation and must be used every frame during a time period.' + ), + _('Apply a torque of _PARAM2_ ; _PARAM3_ ; _PARAM4_ to _PARAM0_ an'), + _('Forces & impulses'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Torque around X (N·m)')) + .addParameter('expression', _('Torque around Y (N·m)')) + .addParameter('expression', _('Torque around Z (N·m)')) + .setParameterLongDescription( + _('A torque is like a rotation acceleration but depends on the mass.') + ) + .getCodeExtraInformation() + .setFunctionName('applyTorque'); + + aut + .addScopedAction( + 'ApplyAngularImpulse', + _('Apply angular impulse (rotational impulse)'), + _( + 'Apply an angular impulse (also called a "rotational impulse") to the object. It instantly changes the rotation speed, to give an initial speed for instance.' + ), + _( + 'Apply angular impulse of _PARAM2_ ; _PARAM3_ ; _PARAM4_ to _PARAM0_ an' + ), + _('Forces & impulses'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Angular impulse around X (N·m·s)')) + .addParameter('expression', _('Angular impulse around Y (N·m·s)')) + .addParameter('expression', _('Angular impulse around Z (N·m·s)')) + .setParameterLongDescription( + _( + 'An impulse is like a rotation speed addition but depends on the mass.' + ) + ) + .getCodeExtraInformation() + .setFunctionName('applyAngularImpulse'); + + aut + .addExpression( + 'Mass', + _('Mass'), + _('Return the mass of the object (in kilograms)'), + '', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('getMass'); + + aut + .addExpression( + 'InertiaAroundX', + _('Inertia around X'), + _( + 'Return the inertia around X axis of the object (in kilograms · meters²) when for its default rotation is (0°; 0°; 0°)' + ), + '', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('getInertiaAroundX'); + + aut + .addExpression( + 'InertiaAroundY', + _('Inertia around Y'), + _( + 'Return the inertia around Y axis of the object (in kilograms · meters²) when for its default rotation is (0°; 0°; 0°)' + ), + '', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('getInertiaAroundY'); + + aut + .addExpression( + 'InertiaAroundZ', + _('Inertia around Z'), + _( + 'Return the inertia around Z axis of the object (in kilograms · meters²) when for its default rotation is (0°; 0°; 0°)' + ), + '', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('getInertiaAroundZ'); + + aut + .addExpression( + 'MassCenterX', + _('Mass center X'), + _('Mass center X'), + '', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('getMassCenterX'); + + aut + .addExpression( + 'MassCenterZ', + _('Mass center Z'), + _('Mass center Z'), + '', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('getMassCenterZ'); + + aut + .addScopedAction( + 'RaycastClosest', + _('Cast ray (closest hit)'), + _( + 'Cast a 3D ray in the Jolt physics world and store the closest hit data (position, normal, reflection direction) in this behavior.' + ), + _( + 'Cast ray for _PARAM0_ from _PARAM2_; _PARAM3_; _PARAM4_ to _PARAM5_; _PARAM6_; _PARAM7_ (ignore self: _PARAM8_)' + ), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Start X')) + .addParameter('expression', _('Start Y')) + .addParameter('expression', _('Start Z')) + .addParameter('expression', _('End X')) + .addParameter('expression', _('End Y')) + .addParameter('expression', _('End Z')) + .addParameter('yesorno', _('Ignore this object while raycasting')) + .setFunctionName('raycastClosest'); + + aut + .addScopedCondition( + 'DidLastRaycastHit', + _('Last raycast hit'), + _('Check if the last raycast from this behavior hit a physics body.'), + _('Last raycast from _PARAM0_ hit a body'), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('didLastRaycastHit'); + + aut + .addScopedCondition( + 'DidLastRaycastHitObject', + _('Last raycast hit object'), + _( + 'Check if the last raycast from this behavior hit the specified object.' + ), + _('Last raycast from _PARAM0_ hit _PARAM2_'), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectPtr', _('Object to test')) + .setFunctionName('didLastRaycastHitObject'); + + aut + .addExpression( + 'LastRaycastHitX', + _('Last raycast hit X'), + _('Return X position of the last raycast hit point (in pixels).'), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastHitX'); + + aut + .addExpression( + 'LastRaycastHitY', + _('Last raycast hit Y'), + _('Return Y position of the last raycast hit point (in pixels).'), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastHitY'); + + aut + .addExpression( + 'LastRaycastHitZ', + _('Last raycast hit Z'), + _('Return Z position of the last raycast hit point (in pixels).'), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastHitZ'); + + aut + .addExpression( + 'LastRaycastNormalX', + _('Last raycast normal X'), + _('Return X component of the last raycast surface normal.'), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastNormalX'); + + aut + .addExpression( + 'LastRaycastNormalY', + _('Last raycast normal Y'), + _('Return Y component of the last raycast surface normal.'), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastNormalY'); + + aut + .addExpression( + 'LastRaycastNormalZ', + _('Last raycast normal Z'), + _('Return Z component of the last raycast surface normal.'), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastNormalZ'); + + aut + .addExpression( + 'LastRaycastReflectionDirectionX', + _('Last raycast reflection direction X'), + _( + 'Return X component of the reflected direction computed from the last raycast hit.' + ), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastReflectionDirectionX'); + + aut + .addExpression( + 'LastRaycastReflectionDirectionY', + _('Last raycast reflection direction Y'), + _( + 'Return Y component of the reflected direction computed from the last raycast hit.' + ), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastReflectionDirectionY'); + + aut + .addExpression( + 'LastRaycastReflectionDirectionZ', + _('Last raycast reflection direction Z'), + _( + 'Return Z component of the reflected direction computed from the last raycast hit.' + ), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastReflectionDirectionZ'); + + aut + .addExpression( + 'LastRaycastDistance', + _('Last raycast distance'), + _( + 'Return distance from ray start to hit point for the last raycast (in pixels).' + ), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastDistance'); + + aut + .addExpression( + 'LastRaycastFraction', + _('Last raycast fraction'), + _( + 'Return fraction (0..1) of the last raycast where the hit occurred.' + ), + _('Raycast'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .setFunctionName('getLastRaycastFraction'); + + // Joints + aut + .addScopedAction( + 'AddFixedJoint', + _('Add a fixed joint'), + _( + 'Add a fixed joint between two objects. They will be linked together and move as a single object.' + ), + _( + 'Add a fixed joint between _PARAM0_ and _PARAM2_, save the joint ID in _PARAM3_' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectPtr', _('Other object'), '', false) + .addParameter('scenevar', _('Variable where to store the joint ID')) + .setFunctionName('addFixedJoint'); + + aut + .addScopedAction( + 'AddPointJoint', + _('Add a point (ball and socket) joint'), + _( + 'Add a point joint between two objects. They will be linked together at a given world position, but will be able to rotate freely.' + ), + _( + 'Add a point joint between _PARAM0_ and _PARAM2_ at _PARAM3_;_PARAM4_;_PARAM5_, save ID in _PARAM6_' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectPtr', _('Other object'), '', false) + .addParameter('expression', _('Joint position X')) + .addParameter('expression', _('Joint position Y')) + .addParameter('expression', _('Joint position Z')) + .addParameter('scenevar', _('Variable where to store the joint ID')) + .setFunctionName('addPointJoint'); + + aut + .addScopedAction( + 'AddHingeJoint', + _('Add a hinge joint'), + _( + 'Add a hinge joint. Both objects will be linked together at a given position and allowed to rotate around the given axis.' + ), + _( + 'Add a hinge joint between _PARAM0_ and _PARAM2_ at position _PARAM3_;_PARAM4_;_PARAM5_ around axis _PARAM6_;_PARAM7_;_PARAM8_, save ID in _PARAM9_' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectPtr', _('Other object'), '', false) + .addParameter('expression', _('Joint position X')) + .addParameter('expression', _('Joint position Y')) + .addParameter('expression', _('Joint position Z')) + .addParameter('expression', _('Axis X')) + .addParameter('expression', _('Axis Y')) + .addParameter('expression', _('Axis Z')) + .addParameter('scenevar', _('Variable where to store the joint ID')) + .setFunctionName('addHingeJoint'); + + aut + .addScopedAction( + 'AddSliderJoint', + _('Add a slider joint'), + _( + 'Add a slider joint. Both objects will be linked but allowed to slide along an axis.' + ), + _( + 'Add a slider joint between _PARAM0_ and _PARAM2_ on axis _PARAM3_;_PARAM4_;_PARAM5_, save ID in _PARAM6_' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectPtr', _('Other object'), '', false) + .addParameter('expression', _('Axis X')) + .addParameter('expression', _('Axis Y')) + .addParameter('expression', _('Axis Z')) + .addParameter('scenevar', _('Variable where to store the joint ID')) + .setFunctionName('addSliderJoint'); + + aut + .addScopedAction( + 'AddDistanceJoint', + _('Add a distance joint'), + _( + 'Add a distance joint. Keeps a minimum and maximum distance between the center of mass of both objects, optionally using a spring.' + ), + _( + 'Add a distance joint between _PARAM0_ and _PARAM2_ (min: _PARAM3_, max: _PARAM4_, spring freq: _PARAM5_, damping: _PARAM6_), save ID to _PARAM7_' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectPtr', _('Other object'), '', false) + .addParameter('expression', _('Minimum distance (pixels)')) + .addParameter('expression', _('Maximum distance (pixels)')) + .addParameter('expression', _('Spring frequency (0 to disable)')) + .addParameter('expression', _('Spring damping ratio (e.g. 0.5)')) + .addParameter('scenevar', _('Variable where to store the joint ID')) + .setFunctionName('addDistanceJoint'); + + aut + .addScopedAction( + 'AddPulleyJoint', + _('Add a pulley joint'), + _( + 'Add a pulley joint between two objects using two fixed world pulley anchors and two local attachment points. The total rope length is constrained.' + ), + _( + 'Add a pulley joint between _PARAM0_ and _PARAM2_ (total length: _PARAM15_, ratio: _PARAM16_, enabled: _PARAM17_), save ID in _PARAM18_' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectPtr', _('Other object'), '', false) + .addParameter('expression', _('Pulley anchor A X (world, pixels)')) + .addParameter('expression', _('Pulley anchor A Y (world, pixels)')) + .addParameter('expression', _('Pulley anchor A Z (world, pixels)')) + .addParameter('expression', _('Pulley anchor B X (world, pixels)')) + .addParameter('expression', _('Pulley anchor B Y (world, pixels)')) + .addParameter('expression', _('Pulley anchor B Z (world, pixels)')) + .addParameter('expression', _('Local anchor A X (pixels)')) + .addParameter('expression', _('Local anchor A Y (pixels)')) + .addParameter('expression', _('Local anchor A Z (pixels)')) + .addParameter('expression', _('Local anchor B X (pixels)')) + .addParameter('expression', _('Local anchor B Y (pixels)')) + .addParameter('expression', _('Local anchor B Z (pixels)')) + .addParameter('expression', _('Total rope length (pixels)')) + .addParameter('expression', _('Pulley ratio (default 1.0)')) + .addParameter('yesorno', _('Enable joint'), '', false) + .addParameter('scenevar', _('Variable where to store the joint ID')) + .setFunctionName('addPulleyJoint'); + + aut + .addScopedAction( + 'AddConeJoint', + _('Add a cone joint'), + _( + 'Add a cone joint. Constraints the movement to a cone shape around a twist axis.' + ), + _( + 'Add a cone joint between _PARAM0_ and _PARAM2_ at _PARAM3_;_PARAM4_;_PARAM5_ (twist axis _PARAM6_;_PARAM7_;_PARAM8_, half angle _PARAM9_°), save ID to _PARAM10_' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectPtr', _('Other object'), '', false) + .addParameter('expression', _('Joint position X')) + .addParameter('expression', _('Joint position Y')) + .addParameter('expression', _('Joint position Z')) + .addParameter('expression', _('Twist axis X')) + .addParameter('expression', _('Twist axis Y')) + .addParameter('expression', _('Twist axis Z')) + .addParameter('expression', _('Half cone angle (degrees)')) + .addParameter('scenevar', _('Variable where to store the joint ID')) + .setFunctionName('addConeJoint'); + + aut + .addScopedAction( + 'RemoveJoint', + _('Remove a joint'), + _('Remove a joint by its ID.'), + _('Remove the joint _PARAM2_'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('removeJoint'); + + aut + .addScopedCondition( + 'IsJointFirstObject', + _('Is first object in a joint'), + _('Check if an object is the first object in a specific joint.'), + _('_PARAM0_ is the first object of joint _PARAM2_'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('isJointFirstObject'); + + aut + .addScopedCondition( + 'IsJointSecondObject', + _('Is second object in a joint'), + _('Check if an object is the second object in a specific joint.'), + _('_PARAM0_ is the second object of joint _PARAM2_'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('isJointSecondObject'); + + aut + .addScopedAction( + 'SetHingeJointLimits', + _('Set hinge joint limits'), + _('Set the min and max angles for a hinge joint.'), + _('Set hinge joint _PARAM2_ limits (min: _PARAM3_°, max: _PARAM4_°)'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Min angle (degrees)')) + .addParameter('expression', _('Max angle (degrees)')) + .setFunctionName('setHingeJointLimits'); + + aut + .addScopedAction( + 'SetHingeJointMotor', + _('Set hinge joint motor'), + _('Set the motor state and target for a hinge joint.'), + _( + 'Set hinge joint _PARAM2_ motor state to _PARAM3_ (target: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter( + 'stringWithSelector', + _('Motor State'), + '["Off", "Velocity", "Position"]', + false + ) + .addParameter('expression', _('Target velocity (deg/s) or angle (deg)')) + .setFunctionName('setHingeJointMotor'); + + aut + .addExpression( + 'HingeJointAngle', + _('Hinge joint angle'), + _('Return the current angle of a hinge joint (in degrees).'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointAngle'); + + aut + .addScopedAction( + 'SetSliderJointLimits', + _('Set slider joint limits'), + _('Set the min and max distance for a slider joint.'), + _( + 'Set slider joint _PARAM2_ limits (min: _PARAM3_ px, max: _PARAM4_ px)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Min limit (pixels)')) + .addParameter('expression', _('Max limit (pixels)')) + .setFunctionName('setSliderJointLimits'); + + aut + .addScopedAction( + 'SetSliderJointMotor', + _('Set slider joint motor'), + _('Set the motor state and target for a slider joint.'), + _( + 'Set slider joint _PARAM2_ motor state to _PARAM3_ (target: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter( + 'stringWithSelector', + _('Motor State'), + '["Off", "Velocity", "Position"]', + false + ) + .addParameter('expression', _('Target velocity (px/s) or pos. (px)')) + .setFunctionName('setSliderJointMotor'); + + aut + .addScopedAction( + 'SetHingeJointMotorLimits', + _('Set hinge motor limits'), + _('Set torque limits used by the hinge motor solver.'), + _( + 'Set hinge joint _PARAM2_ motor torque limits (min: _PARAM3_, max: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Min torque limit')) + .addParameter('expression', _('Max torque limit')) + .setFunctionName('setHingeJointMotorLimits'); + + aut + .addScopedAction( + 'SetHingeJointMotorSpring', + _('Set hinge motor spring'), + _('Set spring frequency and damping for the hinge motor response.'), + _( + 'Set hinge joint _PARAM2_ motor spring (frequency: _PARAM3_, damping: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Motor spring frequency (Hz)')) + .addParameter('expression', _('Motor spring damping')) + .setFunctionName('setHingeJointMotorSpring'); + + aut + .addScopedAction( + 'SetSliderJointMotorLimits', + _('Set slider motor limits'), + _('Set force limits used by the slider motor solver.'), + _( + 'Set slider joint _PARAM2_ motor force limits (min: _PARAM3_, max: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Min force limit')) + .addParameter('expression', _('Max force limit')) + .setFunctionName('setSliderJointMotorLimits'); + + aut + .addScopedAction( + 'SetSliderJointMotorSpring', + _('Set slider motor spring'), + _('Set spring frequency and damping for the slider motor response.'), + _( + 'Set slider joint _PARAM2_ motor spring (frequency: _PARAM3_, damping: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Motor spring frequency (Hz)')) + .addParameter('expression', _('Motor spring damping')) + .setFunctionName('setSliderJointMotorSpring'); + + aut + .addExpression( + 'SliderJointPosition', + _('Slider joint position'), + _('Return the current position of a slider joint (in pixels).'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointPosition'); + + aut + .addScopedAction( + 'SetDistanceJointDistance', + _('Set distance joint limits'), + _('Set the min and max distance for a distance joint.'), + _( + 'Set distance joint _PARAM2_ distance (min: _PARAM3_ px, max: _PARAM4_ px)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Min distance (pixels)')) + .addParameter('expression', _('Max distance (pixels)')) + .setFunctionName('setDistanceJointDistance'); + + aut + .addScopedAction( + 'SetPulleyJointLength', + _('Set pulley joint total length'), + _( + 'Set the total rope length of a pulley joint. Internally this sets min and max rope lengths to the same value.' + ), + _('Set pulley joint _PARAM2_ total rope length to _PARAM3_ px'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Total rope length (pixels)')) + .setFunctionName('setPulleyJointLength'); + + aut + .addExpression( + 'PulleyJointCurrentLength', + _('Pulley joint current length'), + _( + 'Return the current rope length of a pulley joint (in pixels), updated as bodies move.' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('getPulleyJointCurrentLength'); + + aut + .addExpression( + 'PulleyJointTotalLength', + _('Pulley joint total length'), + _( + 'Return the configured total rope length of a pulley joint (in pixels).' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('getPulleyJointTotalLength'); + + // ==================== Advanced Joint Customization ==================== + + // Hinge Joint Spring + aut + .addScopedAction( + 'SetHingeJointSpring', + _('Set hinge limits spring'), + _( + 'Set spring settings for hinge angle limits (frequency and damping).' + ), + _( + 'Set hinge joint _PARAM2_ limits spring (frequency: _PARAM3_, damping: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Spring frequency (Hz, 0 = disable)')) + .addParameter('expression', _('Damping ratio (0..1)')) + .setFunctionName('setHingeJointSpring'); + + // Hinge Joint Max Friction + aut + .addScopedAction( + 'SetHingeJointMaxFriction', + _('Set hinge joint friction'), + _('Set the maximum friction torque of a hinge joint.'), + _('Set hinge joint _PARAM2_ max friction torque to _PARAM3_'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Max friction torque')) + .setFunctionName('setHingeJointMaxFriction'); + + // Hinge Joint Has Limits + aut + .addScopedCondition( + 'HasHingeJointLimits', + _('Hinge joint has limits'), + _('Check if a hinge joint has angle limits enabled.'), + _('Hinge joint _PARAM2_ has limits'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('hasHingeJointLimits'); + + // Hinge Joint Min/Max Limit Expressions + aut + .addExpression( + 'HingeJointMinLimit', + _('Hinge joint min limit'), + _('Return the minimum angle limit of a hinge joint (in degrees).'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMinLimit'); + + aut + .addExpression( + 'HingeJointMaxLimit', + _('Hinge joint max limit'), + _('Return the maximum angle limit of a hinge joint (in degrees).'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMaxLimit'); + + // Slider Joint Spring + aut + .addScopedAction( + 'SetSliderJointSpring', + _('Set slider limits spring'), + _('Set spring settings for slider limits (frequency and damping).'), + _( + 'Set slider joint _PARAM2_ limits spring (frequency: _PARAM3_, damping: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Spring frequency (Hz, 0 = disable)')) + .addParameter('expression', _('Damping ratio (0..1)')) + .setFunctionName('setSliderJointSpring'); + + // Slider Joint Max Friction + aut + .addScopedAction( + 'SetSliderJointMaxFriction', + _('Set slider joint friction'), + _('Set the maximum friction force of a slider joint.'), + _('Set slider joint _PARAM2_ max friction force to _PARAM3_'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Max friction force')) + .setFunctionName('setSliderJointMaxFriction'); + + // Slider Joint Has Limits + aut + .addScopedCondition( + 'HasSliderJointLimits', + _('Slider joint has limits'), + _('Check if a slider joint has position limits enabled.'), + _('Slider joint _PARAM2_ has limits'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('hasSliderJointLimits'); - // Global + // Slider Joint Min/Max Limit Expressions aut .addExpression( - 'WorldScale', - _('World scale'), - _('Return the world scale.'), - _('Global'), + 'SliderJointMinLimit', + _('Slider joint min limit'), + _('Return the minimum position limit of a slider joint (in pixels).'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('getWorldScale'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMinLimit'); aut - .addExpressionAndConditionAndAction( - 'number', - 'GravityX', - _('World gravity on X axis'), - _('the world gravity on X axis') + - ' ' + - _( - 'While an object is needed, this will apply to all objects using the behavior.' - ), - _('the world gravity on X axis'), - _('Global'), + .addExpression( + 'SliderJointMaxLimit', + _('Slider joint max limit'), + _('Return the maximum position limit of a slider joint (in pixels).'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Gravity (in Newton)') - ) - ) - .setFunctionName('setGravityX') - .setGetter('getGravityX'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMaxLimit'); + // Distance Joint Spring aut - .addExpressionAndConditionAndAction( - 'number', - 'GravityY', - _('World gravity on Y axis'), - _('the world gravity on Y axis') + - ' ' + - _( - 'While an object is needed, this will apply to all objects using the behavior.' - ), - _('the world gravity on Y axis'), - _('Global'), + .addScopedAction( + 'SetDistanceJointSpring', + _('Set distance joint limits spring'), + _('Set spring settings for distance limits (frequency and damping).'), + _( + 'Set distance joint _PARAM2_ limits spring (frequency: _PARAM3_, damping: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Gravity (in Newton)') - ) - ) - .setFunctionName('setGravityY') - .setGetter('getGravityY'); + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Spring frequency (Hz, 0 = disable)')) + .addParameter('expression', _('Damping ratio (0..1)')) + .setFunctionName('setDistanceJointSpring'); + // Distance Joint Min/Max Distance Expressions aut - .addExpressionAndConditionAndAction( - 'number', - 'GravityZ', - _('World gravity on Z axis'), - _('the world gravity on Z axis') + - ' ' + - _( - 'While an object is needed, this will apply to all objects using the behavior.' - ), - _('the world gravity on Z axis'), - _('Global'), + .addExpression( + 'DistanceJointMinDistance', + _('Distance joint min distance'), + _( + 'Return the current minimum distance of a distance joint (in pixels).' + ), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Gravity (in Newton)') - ) + .addParameter('expression', _('Joint ID')) + .setFunctionName('getDistanceJointMinDistance'); + + aut + .addExpression( + 'DistanceJointMaxDistance', + _('Distance joint max distance'), + _( + 'Return the current maximum distance of a distance joint (in pixels).' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg' ) - .setFunctionName('setGravityZ') - .setGetter('getGravityZ'); + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('getDistanceJointMaxDistance'); + // Cone Joint Half Angle aut - .addScopedCondition( - 'IsDynamic', - _('Is dynamic'), - _('Check if an object is dynamic.'), - _('_PARAM0_ is dynamic'), - _('Dynamics'), + .addScopedAction( + 'SetConeJointHalfAngle', + _('Set cone joint angle'), + _('Update the half cone angle of a cone joint at runtime.'), + _('Set cone joint _PARAM2_ half angle to _PARAM3_°'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('isDynamic'); + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Half cone angle (degrees)')) + .setFunctionName('setConeJointHalfAngle'); + + // ==================== SwingTwist Joint ==================== aut - .addScopedCondition( - 'IsStatic', - _('Is static'), - _('Check if an object is static.'), - _('_PARAM0_ is static'), - _('Dynamics'), + .addScopedAction( + 'AddSwingTwistJoint', + _('Add a SwingTwist joint'), + _( + 'Add a SwingTwist joint (professional constraint for shoulders, hips, ragdolls). Allows independent control of swing cone and twist range.' + ), + _( + 'Add SwingTwist joint between _PARAM0_ and _PARAM2_ at (_PARAM3_, _PARAM4_, _PARAM5_), store ID in _PARAM12_' + ), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('isStatic'); + .addParameter('objectPtr', _('Other object')) + .addParameter('expression', _('Anchor X (pixels)')) + .addParameter('expression', _('Anchor Y (pixels)')) + .addParameter('expression', _('Anchor Z (pixels)')) + .addParameter('expression', _('Twist axis X')) + .addParameter('expression', _('Twist axis Y')) + .addParameter('expression', _('Twist axis Z')) + .addParameter('expression', _('Normal half cone angle (degrees)')) + .addParameter('expression', _('Plane half cone angle (degrees)')) + .addParameter('expression', _('Twist min angle (degrees)')) + .addParameter('expression', _('Twist max angle (degrees)')) + .addParameter('scenevar', _('Variable to store joint ID')) + .setFunctionName('addSwingTwistJoint'); + + // ==================== Joint Enable/Disable & Count ==================== aut - .addScopedCondition( - 'IsKinematic', - _('Is kinematic'), - _('Check if an object is kinematic.'), - _('_PARAM0_ is kinematic'), - _('Dynamics'), + .addScopedAction( + 'EnableJoint', + _('Enable joint'), + _('Enable a previously disabled joint.'), + _('Enable joint _PARAM2_'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('isKinematic'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('enableJoint'); aut - .addScopedCondition( - 'IsBullet', - _('Is treated as a bullet'), - _('Check if the object is being treated as a bullet.'), - _('_PARAM0_ is treated as a bullet'), - _('Dynamics'), + .addScopedAction( + 'DisableJoint', + _('Disable joint'), + _('Temporarily disable a joint without removing it.'), + _('Disable joint _PARAM2_'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('isBullet'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('disableJoint'); aut .addScopedAction( - 'SetBullet', - _('Treat as bullet'), + 'SetJointSolverOverrides', + _('Set joint solver overrides'), _( - 'Treat the object as a bullet. Better collision handling on high speeds at cost of some performance.' + 'Override velocity/position solver steps for this joint (0 = engine default).' ), - _('Treat _PARAM0_ as bullet: _PARAM2_'), - _('Dynamics'), + _( + 'Set joint _PARAM2_ solver overrides (velocity steps: _PARAM3_, position steps: _PARAM4_)' + ), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('yesorno', _('Treat as bullet'), '', false) - .setDefaultValue('false') - .getCodeExtraInformation() - .setFunctionName('setBullet'); + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Velocity steps override')) + .addParameter('expression', _('Position steps override')) + .setFunctionName('setJointSolverOverrides'); aut - .addScopedCondition( - 'HasFixedRotation', - _('Has fixed rotation'), - _('Check if an object has fixed rotation.'), - _('_PARAM0_ has fixed rotation'), - _('Dynamics'), + .addScopedAction( + 'SetJointPriority', + _('Set joint solver priority'), + _( + 'Set solver priority for a joint. Higher values are solved earlier.' + ), + _('Set joint _PARAM2_ priority to _PARAM3_'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('hasFixedRotation'); + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Priority')) + .setFunctionName('setJointPriority'); aut .addScopedAction( - 'SetFixedRotation', - _('Fixed rotation'), + 'SetJointStabilityPreset', + _('Set joint stability preset'), _( - "Enable or disable an object fixed rotation. If enabled the object won't be able to rotate. This action has no effect on characters." + 'Apply a pre-tuned stability preset on a joint (easy professional setup).' ), - _('Set _PARAM0_ fixed rotation: _PARAM2_'), - _('Dynamics'), + _('Set joint _PARAM2_ stability preset to _PARAM3_'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('yesorno', _('Fixed rotation'), '', false) - .setDefaultValue('false') - .getCodeExtraInformation() - .setFunctionName('setFixedRotation'); + .addParameter('expression', _('Joint ID')) + .addParameter( + 'stringWithSelector', + _('Preset'), + '["Balanced", "Stable", "UltraStable"]', + false + ) + .setFunctionName('setJointStabilityPreset'); - // Body settings aut .addScopedAction( - 'ShapeScale', - _('Shape scale'), + 'SetJointBreakThresholds', + _('Set joint break thresholds'), _( - 'Modify an object shape scale. It affects custom shape dimensions, if custom dimensions are not set the body will be scaled automatically to the object size.' + 'Set max reaction force/torque at which this joint automatically breaks.' ), - _('the shape scale'), - _('Body settings'), + _( + 'Set joint _PARAM2_ break thresholds (force: _PARAM3_, torque: _PARAM4_)' + ), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardOperatorParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Scale (1 by default)') - ) + .addParameter('expression', _('Joint ID')) + .addParameter('expression', _('Max break force (<=0 disables)')) + .addParameter('expression', _('Max break torque (<=0 disables)')) + .setFunctionName('setJointBreakThresholds'); + + aut + .addScopedAction( + 'ClearJointBreakThresholds', + _('Clear joint break thresholds'), + _('Disable automatic break thresholds on a joint.'), + _('Clear break thresholds for joint _PARAM2_'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' ) - .getCodeExtraInformation() - .setFunctionName('setShapeScale') - .setGetter('getShapeScale'); + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('clearJointBreakThresholds'); aut - .addExpressionAndConditionAndAction( - 'number', - 'Density', - _('Density'), - _( - "the object density. The body's density and volume determine its mass." - ), - _('the density'), - _('Body settings'), + .addScopedCondition( + 'IsJointEnabled', + _('Joint is enabled'), + _('Check if a joint is currently enabled.'), + _('Joint _PARAM2_ is enabled'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) - .setFunctionName('setDensity') - .setGetter('getDensity'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('isJointEnabled'); aut - .addExpressionAndConditionAndAction( - 'number', - 'ShapeOffsetX', - _('Shape offset X'), - _('the object shape offset on X.'), - _('the shape offset on X'), - _('Body settings'), + .addScopedCondition( + 'IsJointBroken', + _('Joint is broken'), + _( + 'Check if a joint has been automatically broken by its thresholds.' + ), + _('Joint _PARAM2_ is broken'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) - .setFunctionName('setShapeOffsetX') - .setGetter('getShapeOffsetX'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('isJointBroken'); aut - .addExpressionAndConditionAndAction( - 'number', - 'ShapeOffsetY', - _('Shape offset Y'), - _('the object shape offset on Y.'), - _('the shape offset on Y'), - _('Body settings'), + .addExpression( + 'JointCount', + _('Joint count'), + _('Return the total number of active joints.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) - .setFunctionName('setShapeOffsetY') - .setGetter('getShapeOffsetY'); + .setFunctionName('getJointCount'); aut - .addExpressionAndConditionAndAction( - 'number', - 'ShapeOffsetZ', - _('Shape offset Z'), - _('the object shape offset on Z.'), - _('the shape offset on Z'), - _('Body settings'), + .addExpression( + 'JointEditorJointId', + _('Joint editor joint ID'), + _( + 'Return the active joint ID managed by the Joint Editor GUI on this object (0 if none).' + ), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) - .setFunctionName('setShapeOffsetZ') - .setGetter('getShapeOffsetZ'); + .setFunctionName('getJointEditorJointId'); aut - .addExpressionAndConditionAndAction( - 'number', - 'Friction', - _('Friction'), + .addExpression( + 'JointReactionForce', + _('Joint reaction force'), _( - "the object friction. How much energy is lost from the movement of one object over another. The combined friction from two bodies is calculated as 'sqrt(bodyA.friction * bodyB.friction)'." + 'Return last measured reaction force of a joint. Useful for breakable constraints and debugging stability.' ), - _('the friction'), - _('Body settings'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) - .setFunctionName('setFriction') - .setGetter('getFriction'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getJointReactionForce'); aut - .addExpressionAndConditionAndAction( - 'number', - 'Restitution', - _('Restitution'), + .addExpression( + 'JointReactionTorque', + _('Joint reaction torque'), _( - "the object restitution. Energy conservation on collision. The combined restitution from two bodies is calculated as 'max(bodyA.restitution, bodyB.restitution)'." + 'Return last measured reaction torque of a joint. Useful for breakable constraints and debugging stability.' ), - _('the restitution'), - _('Body settings'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) - .setFunctionName('setRestitution') - .setGetter('getRestitution'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getJointReactionTorque'); + + // ==================== Hinge Motor Query Expressions ==================== aut - .addExpressionAndConditionAndAction( - 'number', - 'LinearDamping', - _('Linear damping'), + .addExpression( + 'HingeJointMotorSpeed', + _('Hinge joint motor speed'), _( - 'the object linear damping. How much movement speed is lost across the time.' + 'Return the target angular velocity of a hinge joint motor (degrees/second).' ), - _('the linear damping'), - _('Body settings'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) - .setFunctionName('setLinearDamping') - .setGetter('getLinearDamping'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMotorSpeed'); aut - .addExpressionAndConditionAndAction( - 'number', - 'AngularDamping', - _('Angular damping'), - _( - 'the object angular damping. How much angular speed is lost across the time.' - ), - _('the angular damping'), - _('Body settings'), + .addExpression( + 'HingeJointMotorTarget', + _('Hinge joint motor target'), + _('Return the target angle of a hinge joint motor (degrees).'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters('number', gd.ParameterOptions.makeNewOptions()) - .setFunctionName('setAngularDamping') - .setGetter('getAngularDamping'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMotorTarget'); aut - .addExpressionAndConditionAndAction( - 'number', - 'GravityScale', - _('Gravity scale'), - _( - 'the object gravity scale. The gravity applied to an object is the world gravity multiplied by the object gravity scale.' - ), - _('the gravity scale'), - _('Body settings'), + .addExpression( + 'HingeJointMaxFriction', + _('Hinge joint max friction'), + _('Return the maximum friction torque of a hinge joint.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Scale (1 by default)') - ) + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMaxFriction'); + + aut + .addExpression( + 'HingeJointMotorMinTorque', + _('Hinge motor min torque'), + _('Return hinge motor minimum torque limit.'), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg' ) - .setFunctionName('setGravityScale') - .setGetter('getGravityScale'); + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMotorMinTorque'); - // Filtering aut - .addScopedCondition( - 'LayerEnabled', - _('Layer enabled'), - _('Check if an object has a specific layer enabled.'), - _('_PARAM0_ has layer _PARAM2_ enabled'), - _('Filtering'), - 'JsPlatform/Extensions/physics3d.svg', + .addExpression( + 'HingeJointMotorMaxTorque', + _('Hinge motor max torque'), + _('Return hinge motor maximum torque limit.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('Layer (1 - 8)')) - .getCodeExtraInformation() - .setFunctionName('layerEnabled'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMotorMaxTorque'); aut - .addScopedAction( - 'EnableLayer', - _('Enable layer'), - _( - 'Enable or disable a layer for an object. Two objects collide if any layer of the first object matches any mask of the second one and vice versa.' - ), - _('Enable layer _PARAM2_ for _PARAM0_: _PARAM3_'), - _('Filtering'), - 'JsPlatform/Extensions/physics3d.svg', + .addExpression( + 'HingeJointMotorSpringFrequency', + _('Hinge motor spring frequency'), + _('Return hinge motor spring frequency.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('Layer (1 - 8)')) - .addParameter('yesorno', _('Enable'), '', false) - .setDefaultValue('true') - .getCodeExtraInformation() - .setFunctionName('enableLayer'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMotorSpringFrequency'); aut - .addScopedCondition( - 'MaskEnabled', - _('Mask enabled'), - _('Check if an object has a specific mask enabled.'), - _('_PARAM0_ has mask _PARAM2_ enabled'), - _('Filtering'), - 'JsPlatform/Extensions/physics3d.svg', + .addExpression( + 'HingeJointMotorSpringDamping', + _('Hinge motor spring damping'), + _('Return hinge motor spring damping.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('Mask (1 - 8)')) - .getCodeExtraInformation() - .setFunctionName('maskEnabled'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getHingeJointMotorSpringDamping'); + + // ==================== Slider Motor Query Expressions ==================== aut - .addScopedAction( - 'EnableMask', - _('Enable mask'), + .addExpression( + 'SliderJointMotorSpeed', + _('Slider joint motor speed'), _( - 'Enable or disable a mask for an object. Two objects collide if any layer of the first object matches any mask of the second one and vice versa.' + 'Return the target velocity of a slider joint motor (pixels/second).' ), - _('Enable mask _PARAM2_ for _PARAM0_: _PARAM3_'), - _('Filtering'), - 'JsPlatform/Extensions/physics3d.svg', + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('Mask (1 - 8)')) - .addParameter('yesorno', _('Enable'), '', false) - .setDefaultValue('true') - .getCodeExtraInformation() - .setFunctionName('enableMask'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMotorSpeed'); - // Velocity aut - .addExpressionAndConditionAndAction( - 'number', - 'LinearVelocityX', - _('Linear velocity X'), - _('the object linear velocity on X'), - _('the linear velocity on X'), - _('Velocity'), + .addExpression( + 'SliderJointMotorTarget', + _('Slider joint motor target'), + _('Return the target position of a slider joint motor (pixels).'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Speed (in pixels per second)') - ) - ) - .setFunctionName('setLinearVelocityX') - .setGetter('getLinearVelocityX'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMotorTarget'); aut - .addExpressionAndConditionAndAction( - 'number', - 'LinearVelocityY', - _('Linear velocity Y'), - _('the object linear velocity on Y'), - _('the linear velocity on Y'), - _('Velocity'), + .addExpression( + 'SliderJointMaxFriction', + _('Slider joint max friction'), + _('Return the maximum friction force of a slider joint.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Speed (in pixels per second)') - ) - ) - .setFunctionName('setLinearVelocityY') - .setGetter('getLinearVelocityY'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMaxFriction'); aut - .addExpressionAndConditionAndAction( - 'number', - 'LinearVelocityZ', - _('Linear velocity Z'), - _('the object linear velocity on Z'), - _('the linear velocity on Z'), - _('Velocity'), + .addExpression( + 'SliderJointMotorMinForce', + _('Slider motor min force'), + _('Return slider motor minimum force limit.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Speed (in pixels per second)') - ) - ) - .setFunctionName('setLinearVelocityZ') - .setGetter('getLinearVelocityZ'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMotorMinForce'); aut - .addExpressionAndCondition( - 'number', - 'LinearVelocityLength', - _('Linear velocity'), - _('the object linear velocity length'), - _('the linear velocity length'), - _('Velocity'), + .addExpression( + 'SliderJointMotorMaxForce', + _('Slider motor max force'), + _('Return slider motor maximum force limit.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Speed to compare to (in pixels per second)') - ) - ) - .setFunctionName('getLinearVelocityLength'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMotorMaxForce'); aut - .addExpressionAndConditionAndAction( - 'number', - 'AngularVelocityX', - _('Angular velocity X'), - _('the object angular velocity around X'), - _('the angular velocity around X'), - _('Velocity'), + .addExpression( + 'SliderJointMotorSpringFrequency', + _('Slider motor spring frequency'), + _('Return slider motor spring frequency.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Angular speed (in degrees per second)') - ) - ) - .setFunctionName('setAngularVelocityX') - .setGetter('getAngularVelocityX'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMotorSpringFrequency'); aut - .addExpressionAndConditionAndAction( - 'number', - 'AngularVelocityY', - _('Angular velocity Y'), - _('the object angular velocity around Y'), - _('the angular velocity around Y'), - _('Velocity'), + .addExpression( + 'SliderJointMotorSpringDamping', + _('Slider motor spring damping'), + _('Return slider motor spring damping.'), + _('Joints'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Angular speed (in degrees per second)') - ) - ) - .setFunctionName('setAngularVelocityY') - .setGetter('getAngularVelocityY'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getSliderJointMotorSpringDamping'); + + // ================================================================ + // ==================== RAGDOLL AUTOMATION SYSTEM ================== + // ================================================================ + + // ==================== Group Management ==================== aut - .addExpressionAndConditionAndAction( - 'number', - 'AngularVelocityZ', - _('Angular velocity Z'), - _('the object angular velocity around Z'), - _('the angular velocity around Z'), - _('Velocity'), + .addScopedAction( + 'CreateRagdollGroup', + _('Create ragdoll group'), + _( + 'Create a new ragdoll group for batch control of connected bodies and joints.' + ), + _('Create ragdoll group and store ID in _PARAM2_'), + _('Ragdoll'), + 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .useStandardParameters( - 'number', - gd.ParameterOptions.makeNewOptions().setDescription( - _('Angular speed (in degrees per second)') - ) - ) - .setFunctionName('setAngularVelocityZ') - .setGetter('getAngularVelocityZ'); + .addParameter('scenevar', _('Variable to store ragdoll ID')) + .setFunctionName('createRagdollGroup'); - // Forces and impulses aut .addScopedAction( - 'ApplyForce', - _('Apply force (at a point)'), - _( - 'Apply a force to the object over time. It "accelerates" an object and must be used every frame during a time period.' - ), + 'AddBodyToRagdollGroup', + _('Add body to ragdoll group'), _( - 'Apply a force of _PARAM2_ ; _PARAM3_ ; _PARAM4_ to _PARAM0_ at _PARAM5_ ; _PARAM6_ ; _PARAM7_' + "Add this object's physics body to a ragdoll group for batch control." ), - _('Forces & impulses'), + _('Add _PARAM0_ to ragdoll group _PARAM2_'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('X component (N)')) - .addParameter('expression', _('Y component (N)')) - .addParameter('expression', _('Z component (N)')) - .setParameterLongDescription( - _('A force is like an acceleration but depends on the mass.') - ) - .addParameter('expression', _('Application point on X axis')) - .addParameter('expression', _('Application point on Y axis')) - .addParameter('expression', _('Application point on Z axis')) - .setParameterLongDescription( - _( - 'Use `MassCenterX`, `MassCenterY` and `MassCenterZ` expressions to avoid any rotation.' - ) - ) - .getCodeExtraInformation() - .setFunctionName('applyForce'); + .addParameter('expression', _('Ragdoll group ID')) + .setFunctionName('addBodyToRagdollGroup'); aut .addScopedAction( - 'ApplyForceAtCenter', - _('Apply force (at center)'), - _( - 'Apply a force to the object over time. It "accelerates" an object and must be used every frame during a time period.' - ), + 'AddJointToRagdollGroup', + _('Add joint to ragdoll group'), _( - 'Apply a force of _PARAM2_ ; _PARAM3_ ; _PARAM4_ at the center of _PARAM0_' + 'Register an existing joint with a ragdoll group for batch control.' ), - _('Forces & impulses'), + _('Add joint _PARAM3_ to ragdoll group _PARAM2_'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('X component (N)')) - .addParameter('expression', _('Y component (N)')) - .addParameter('expression', _('Z component (N)')) - .setParameterLongDescription( - _('A force is like an acceleration but depends on the mass.') + .addParameter('expression', _('Ragdoll group ID')) + .addParameter('expression', _('Joint ID')) + .setFunctionName('addJointToRagdollGroup'); + + aut + .addScopedAction( + 'RemoveRagdollGroup', + _('Remove ragdoll group'), + _('Remove a ragdoll group and all its joints.'), + _('Remove ragdoll group _PARAM2_ and all its joints'), + _('Ragdoll'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' ) - .getCodeExtraInformation() - .setFunctionName('applyForceAtCenter'); + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Ragdoll group ID')) + .setFunctionName('removeRagdollGroup'); + + // ==================== Ragdoll State Control ==================== aut .addScopedAction( - 'ApplyForceTowardPosition', - _('Apply force toward position'), + 'SetRagdollMode', + _('Set ragdoll mode'), _( - 'Apply a force to the object over time to move it toward a position. It "accelerates" an object and must be used every frame during a time period.' - ), - _( - 'Apply to _PARAM0_ a force of length _PARAM2_ towards _PARAM3_ ; _PARAM4_ ; _PARAM5_' + 'Switch all bodies in a ragdoll between Dynamic (physics) and Kinematic (animation) mode.' ), - _('Forces & impulses'), + _('Set ragdoll _PARAM2_ mode to _PARAM3_'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('Length (N)')) - .setParameterLongDescription( - _('A force is like an acceleration but depends on the mass.') + .addParameter('expression', _('Ragdoll group ID')) + .addParameter( + 'stringWithSelector', + _('Mode'), + '["Dynamic", "Kinematic"]', + false ) - .addParameter('expression', _('X position')) - .addParameter('expression', _('Y position')) - .addParameter('expression', _('Z position')) - .getCodeExtraInformation() - .setFunctionName('applyForceTowardPosition'); + .setFunctionName('setRagdollMode'); aut .addScopedAction( - 'ApplyImpulse', - _('Apply impulse (at a point)'), - _( - 'Apply an impulse to the object. It instantly changes the speed, to give an initial speed for instance.' - ), + 'SetRagdollState', + _('Set ragdoll state'), _( - 'Apply an impulse of _PARAM2_ ; _PARAM3_ ; _PARAM4_ to _PARAM0_ at _PARAM5_ ; _PARAM6_ ; _PARAM7_' + 'Set a preset ragdoll state: Active (normal physics), Limp (floppy ragdoll), Stiff (muscle tension), Frozen (kinematic).' ), - _('Forces & impulses'), + _('Set ragdoll _PARAM2_ state to _PARAM3_'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('X component (N·s or kg·m·s⁻¹)')) - .addParameter('expression', _('Y component (N·s or kg·m·s⁻¹)')) - .addParameter('expression', _('Z component (N·s or kg·m·s⁻¹)')) - .setParameterLongDescription( - _('An impulse is like a speed addition but depends on the mass.') - ) - .addParameter('expression', _('Application point on X axis')) - .addParameter('expression', _('Application point on Y axis')) - .addParameter('expression', _('Application point on Z axis')) - .setParameterLongDescription( - _( - 'Use `MassCenterX`, `MassCenterY` and `MassCenterZ` expressions to avoid any rotation.' - ) + .addParameter('expression', _('Ragdoll group ID')) + .addParameter( + 'stringWithSelector', + _('State'), + '["Active", "Limp", "Stiff", "Frozen"]', + false ) - .getCodeExtraInformation() - .setFunctionName('applyImpulse'); + .setFunctionName('setRagdollState'); + + // ==================== Ragdoll Batch Controls ==================== aut .addScopedAction( - 'ApplyImpulseAtCenter', - _('Apply impulse (at center)'), - _( - 'Apply an impulse to the object. It instantly changes the speed, to give an initial speed for instance.' - ), + 'SetRagdollDamping', + _('Set ragdoll damping'), + _('Set linear and angular damping on ALL bodies in a ragdoll group.'), _( - 'Apply an impulse of _PARAM2_ ; _PARAM3_ ; _PARAM4_ at the center of _PARAM0_' + 'Set ragdoll _PARAM2_ damping (linear: _PARAM3_, angular: _PARAM4_)' ), - _('Forces & impulses'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('X component (N·s or kg·m·s⁻¹)')) - .addParameter('expression', _('Y component (N·s or kg·m·s⁻¹)')) - .addParameter('expression', _('Z component (N·s or kg·m·s⁻¹)')) - .setParameterLongDescription( - _('An impulse is like a speed addition but depends on the mass.') - ) - .getCodeExtraInformation() - .setFunctionName('applyImpulseAtCenter'); + .addParameter('expression', _('Ragdoll group ID')) + .addParameter('expression', _('Linear damping')) + .addParameter('expression', _('Angular damping')) + .setFunctionName('setRagdollDamping'); aut .addScopedAction( - 'ApplyImpulseTowardPosition', - _('Apply impulse toward position'), + 'SetRagdollStiffness', + _('Set ragdoll stiffness'), _( - 'Apply an impulse to the object to move it toward a position. It instantly changes the speed, to give an initial speed for instance.' + 'Set spring stiffness on ALL joints in a ragdoll group (simulates muscle tension).' ), _( - 'Apply to _PARAM0_ an impulse of length _PARAM2_ towards _PARAM3_ ; _PARAM4_ ; _PARAM5_' + 'Set ragdoll _PARAM2_ stiffness (frequency: _PARAM3_, damping: _PARAM4_)' ), - _('Forces & impulses'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('Length (N·s or kg·m·s⁻¹)')) - .setParameterLongDescription( - _('An impulse is like a speed addition but depends on the mass.') + .addParameter('expression', _('Ragdoll group ID')) + .addParameter('expression', _('Spring frequency (Hz)')) + .addParameter('expression', _('Damping ratio (0..1)')) + .setFunctionName('setRagdollStiffness'); + + aut + .addScopedAction( + 'SetRagdollFriction', + _('Set ragdoll friction'), + _('Set friction on ALL joints in a ragdoll group.'), + _('Set ragdoll _PARAM2_ friction to _PARAM3_'), + _('Ragdoll'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' ) - .addParameter('expression', _('X position')) - .addParameter('expression', _('Y position')) - .addParameter('expression', _('Z position')) - .getCodeExtraInformation() - .setFunctionName('applyImpulseTowardPosition'); + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('expression', _('Ragdoll group ID')) + .addParameter('expression', _('Friction')) + .setFunctionName('setRagdollFriction'); aut .addScopedAction( - 'ApplyTorque', - _('Apply torque (rotational force)'), + 'ApplyRagdollImpulse', + _('Apply ragdoll impulse'), _( - 'Apply a torque (also called "rotational force") to the object. It "accelerates" an object rotation and must be used every frame during a time period.' + 'Apply an impulse to ALL bodies in a ragdoll group (explosions, hits, knockbacks).' ), - _('Apply a torque of _PARAM2_ ; _PARAM3_ ; _PARAM4_ to _PARAM0_ an'), - _('Forces & impulses'), + _('Apply impulse (_PARAM3_, _PARAM4_, _PARAM5_) to ragdoll _PARAM2_'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('Torque around X (N·m)')) - .addParameter('expression', _('Torque around Y (N·m)')) - .addParameter('expression', _('Torque around Z (N·m)')) - .setParameterLongDescription( - _('A torque is like a rotation acceleration but depends on the mass.') - ) - .getCodeExtraInformation() - .setFunctionName('applyTorque'); + .addParameter('expression', _('Ragdoll group ID')) + .addParameter('expression', _('Impulse X')) + .addParameter('expression', _('Impulse Y')) + .addParameter('expression', _('Impulse Z')) + .setFunctionName('applyRagdollImpulse'); aut .addScopedAction( - 'ApplyAngularImpulse', - _('Apply angular impulse (rotational impulse)'), - _( - 'Apply an angular impulse (also called a "rotational impulse") to the object. It instantly changes the rotation speed, to give an initial speed for instance.' - ), + 'SetRagdollGravityScale', + _('Set ragdoll gravity scale'), _( - 'Apply angular impulse of _PARAM2_ ; _PARAM3_ ; _PARAM4_ to _PARAM0_ an' + 'Set gravity scale on ALL bodies in a ragdoll (0 = zero gravity, 1 = normal, 2 = double gravity).' ), - _('Forces & impulses'), + _('Set ragdoll _PARAM2_ gravity scale to _PARAM3_'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .addParameter('expression', _('Angular impulse around X (N·m·s)')) - .addParameter('expression', _('Angular impulse around Y (N·m·s)')) - .addParameter('expression', _('Angular impulse around Z (N·m·s)')) - .setParameterLongDescription( - _( - 'An impulse is like a rotation speed addition but depends on the mass.' - ) - ) - .getCodeExtraInformation() - .setFunctionName('applyAngularImpulse'); + .addParameter('expression', _('Ragdoll group ID')) + .addParameter('expression', _('Gravity scale (0-2)')) + .setFunctionName('setRagdollGravityScale'); + + // ==================== Ragdoll Queries ==================== aut .addExpression( - 'Mass', - _('Mass'), - _('Return the mass of the object (in kilograms)'), - '', + 'RagdollBodyCount', + _('Ragdoll body count'), + _('Return the number of bodies in a ragdoll group.'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('getMass'); + .addParameter('expression', _('Ragdoll group ID')) + .setFunctionName('getRagdollBodyCount'); aut .addExpression( - 'InertiaAroundX', - _('Inertia around X'), - _( - 'Return the inertia around X axis of the object (in kilograms · meters²) when for its default rotation is (0°; 0°; 0°)' - ), - '', + 'RagdollJointCount', + _('Ragdoll joint count'), + _('Return the number of joints in a ragdoll group.'), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('getInertiaAroundX'); + .addParameter('expression', _('Ragdoll group ID')) + .setFunctionName('getRagdollJointCount'); + + // ==================== Joint World Position ==================== aut .addExpression( - 'InertiaAroundY', - _('Inertia around Y'), + 'JointWorldX', + _('Joint world X position'), _( - 'Return the inertia around Y axis of the object (in kilograms · meters²) when for its default rotation is (0°; 0°; 0°)' + 'Return the world X position of a joint (midpoint of connected bodies, in pixels).' ), - '', + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('getInertiaAroundY'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getJointWorldX'); aut .addExpression( - 'InertiaAroundZ', - _('Inertia around Z'), + 'JointWorldY', + _('Joint world Y position'), _( - 'Return the inertia around Z axis of the object (in kilograms · meters²) when for its default rotation is (0°; 0°; 0°)' + 'Return the world Y position of a joint (midpoint of connected bodies, in pixels).' ), - '', + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('getInertiaAroundZ'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getJointWorldY'); aut .addExpression( - 'MassCenterX', - _('Mass center X'), - _('Mass center X'), - '', + 'JointWorldZ', + _('Joint world Z position'), + _( + 'Return the world Z position of a joint (midpoint of connected bodies, in pixels).' + ), + _('Ragdoll'), 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('getMassCenterX'); + .addParameter('expression', _('Joint ID')) + .setFunctionName('getJointWorldZ'); + + // ==================== Humanoid Ragdoll Template ==================== aut - .addExpression( - 'MassCenterY', - _('Mass center Y'), - _('Mass center Y'), - '', + .addScopedAction( + 'BuildHumanoidRagdoll', + _('Build humanoid ragdoll'), + _( + 'Automatically build a complete humanoid ragdoll from 11 body-part objects with proper joint types and weight distribution.' + ), + _( + 'Build humanoid ragdoll from _PARAM2_ body parts, store ID in _PARAM13_' + ), + _('Ragdoll'), + 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('getMassCenterY'); + .addParameter('objectPtr', _('Head')) + .addParameter('objectPtr', _('Chest')) + .addParameter('objectPtr', _('Hips')) + .addParameter('objectPtr', _('Upper Arm Left')) + .addParameter('objectPtr', _('Lower Arm Left')) + .addParameter('objectPtr', _('Upper Arm Right')) + .addParameter('objectPtr', _('Lower Arm Right')) + .addParameter('objectPtr', _('Thigh Left')) + .addParameter('objectPtr', _('Shin Left')) + .addParameter('objectPtr', _('Thigh Right')) + .addParameter('objectPtr', _('Shin Right')) + .addParameter('scenevar', _('Variable to store ragdoll ID')) + .setFunctionName('buildHumanoidRagdoll'); aut - .addExpression( - 'MassCenterZ', - _('Mass center Z'), - _('Mass center Z'), - '', + .addScopedAction( + 'BuildHumanoidRagdollFromTag', + _('Build humanoid ragdoll from tag'), + _( + 'Automatically find body parts by their ragdoll role and shared group tag, then build one humanoid ragdoll.' + ), + _( + 'Build humanoid ragdoll for group tag _PARAM2_ and store ID in _PARAM3_' + ), + _('Ragdoll'), + 'JsPlatform/Extensions/physics3d.svg', 'JsPlatform/Extensions/physics3d.svg' ) .addParameter('object', _('Object'), '', false) .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') - .getCodeExtraInformation() - .setFunctionName('getMassCenterZ'); + .addParameter('string', _('Ragdoll group tag')) + .addParameter('scenevar', _('Variable to store ragdoll ID')) + .setFunctionName('buildHumanoidRagdollFromTag'); } + // Bulk joints + extension + .addAction( + 'AddFixedJointsBetweenObjects', + _('Link fixed joints to all picked objects'), + _( + 'Create fixed joints between each picked source object and each picked target object.' + ), + _( + 'Link fixed joints between _PARAM0_ and _PARAM2_ (linked pairs: _PARAM3_, last joint ID: _PARAM4_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('objectList', _('Source objects'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectList', _('Target objects'), '', false) + .addParameter('scenevar', _('Variable to store linked pairs count')) + .addParameter('scenevar', _('Variable to store last joint ID')) + .getCodeExtraInformation() + .addIncludeFile('Extensions/Physics3DBehavior/Physics3DTools.js') + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .setFunctionName('gdjs.physics3d.addFixedJointsBetweenObjects'); + + extension + .addAction( + 'AddPointJointsBetweenObjects', + _('Link point joints to all picked objects'), + _( + 'Create point joints between each picked source object and each picked target object.' + ), + _( + 'Link point joints between _PARAM0_ and _PARAM2_ at _PARAM3_;_PARAM4_;_PARAM5_ (linked pairs: _PARAM6_, last joint ID: _PARAM7_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('objectList', _('Source objects'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectList', _('Target objects'), '', false) + .addParameter('expression', _('Joint position X')) + .addParameter('expression', _('Joint position Y')) + .addParameter('expression', _('Joint position Z')) + .addParameter('scenevar', _('Variable to store linked pairs count')) + .addParameter('scenevar', _('Variable to store last joint ID')) + .getCodeExtraInformation() + .addIncludeFile('Extensions/Physics3DBehavior/Physics3DTools.js') + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .setFunctionName('gdjs.physics3d.addPointJointsBetweenObjects'); + + extension + .addAction( + 'AddHingeJointsBetweenObjects', + _('Link hinge joints to all picked objects'), + _( + 'Create hinge joints between each picked source object and each picked target object.' + ), + _( + 'Link hinge joints between _PARAM0_ and _PARAM2_ at _PARAM3_;_PARAM4_;_PARAM5_ on axis _PARAM6_;_PARAM7_;_PARAM8_ (linked pairs: _PARAM9_, last joint ID: _PARAM10_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('objectList', _('Source objects'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectList', _('Target objects'), '', false) + .addParameter('expression', _('Joint position X')) + .addParameter('expression', _('Joint position Y')) + .addParameter('expression', _('Joint position Z')) + .addParameter('expression', _('Axis X')) + .addParameter('expression', _('Axis Y')) + .addParameter('expression', _('Axis Z')) + .addParameter('scenevar', _('Variable to store linked pairs count')) + .addParameter('scenevar', _('Variable to store last joint ID')) + .getCodeExtraInformation() + .addIncludeFile('Extensions/Physics3DBehavior/Physics3DTools.js') + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .setFunctionName('gdjs.physics3d.addHingeJointsBetweenObjects'); + + extension + .addAction( + 'AddSliderJointsBetweenObjects', + _('Link slider joints to all picked objects'), + _( + 'Create slider joints between each picked source object and each picked target object.' + ), + _( + 'Link slider joints between _PARAM0_ and _PARAM2_ on axis _PARAM3_;_PARAM4_;_PARAM5_ (linked pairs: _PARAM6_, last joint ID: _PARAM7_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('objectList', _('Source objects'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectList', _('Target objects'), '', false) + .addParameter('expression', _('Axis X')) + .addParameter('expression', _('Axis Y')) + .addParameter('expression', _('Axis Z')) + .addParameter('scenevar', _('Variable to store linked pairs count')) + .addParameter('scenevar', _('Variable to store last joint ID')) + .getCodeExtraInformation() + .addIncludeFile('Extensions/Physics3DBehavior/Physics3DTools.js') + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .setFunctionName('gdjs.physics3d.addSliderJointsBetweenObjects'); + + extension + .addAction( + 'AddDistanceJointsBetweenObjects', + _('Link distance joints to all picked objects'), + _( + 'Create distance joints between each picked source object and each picked target object.' + ), + _( + 'Link distance joints between _PARAM0_ and _PARAM2_ (min: _PARAM3_, max: _PARAM4_, spring: _PARAM5_, damping: _PARAM6_) (linked pairs: _PARAM7_, last joint ID: _PARAM8_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('objectList', _('Source objects'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectList', _('Target objects'), '', false) + .addParameter('expression', _('Minimum distance (pixels)')) + .addParameter('expression', _('Maximum distance (pixels)')) + .addParameter('expression', _('Spring frequency (0 to disable)')) + .addParameter('expression', _('Spring damping ratio')) + .addParameter('scenevar', _('Variable to store linked pairs count')) + .addParameter('scenevar', _('Variable to store last joint ID')) + .getCodeExtraInformation() + .addIncludeFile('Extensions/Physics3DBehavior/Physics3DTools.js') + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .setFunctionName('gdjs.physics3d.addDistanceJointsBetweenObjects'); + + extension + .addAction( + 'AddPulleyJointsBetweenObjects', + _('Link pulley joints to all picked objects'), + _( + 'Create pulley joints between each picked source object and each picked target object.' + ), + _( + 'Link pulley joints between _PARAM0_ and _PARAM2_ (total length: _PARAM15_, ratio: _PARAM16_, enabled: _PARAM17_) (linked pairs: _PARAM18_, last joint ID: _PARAM19_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('objectList', _('Source objects'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectList', _('Target objects'), '', false) + .addParameter('expression', _('Pulley anchor A X (world, pixels)')) + .addParameter('expression', _('Pulley anchor A Y (world, pixels)')) + .addParameter('expression', _('Pulley anchor A Z (world, pixels)')) + .addParameter('expression', _('Pulley anchor B X (world, pixels)')) + .addParameter('expression', _('Pulley anchor B Y (world, pixels)')) + .addParameter('expression', _('Pulley anchor B Z (world, pixels)')) + .addParameter('expression', _('Local anchor A X (pixels)')) + .addParameter('expression', _('Local anchor A Y (pixels)')) + .addParameter('expression', _('Local anchor A Z (pixels)')) + .addParameter('expression', _('Local anchor B X (pixels)')) + .addParameter('expression', _('Local anchor B Y (pixels)')) + .addParameter('expression', _('Local anchor B Z (pixels)')) + .addParameter('expression', _('Total rope length (pixels)')) + .addParameter('expression', _('Pulley ratio (default 1.0)')) + .addParameter('yesorno', _('Enable joint'), '', false) + .addParameter('scenevar', _('Variable to store linked pairs count')) + .addParameter('scenevar', _('Variable to store last joint ID')) + .getCodeExtraInformation() + .addIncludeFile('Extensions/Physics3DBehavior/Physics3DTools.js') + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .setFunctionName('gdjs.physics3d.addPulleyJointsBetweenObjects'); + + extension + .addAction( + 'AddConeJointsBetweenObjects', + _('Link cone joints to all picked objects'), + _( + 'Create cone joints between each picked source object and each picked target object.' + ), + _( + 'Link cone joints between _PARAM0_ and _PARAM2_ at _PARAM3_;_PARAM4_;_PARAM5_ (axis _PARAM6_;_PARAM7_;_PARAM8_, angle _PARAM9_°) (linked pairs: _PARAM10_, last joint ID: _PARAM11_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('objectList', _('Source objects'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectList', _('Target objects'), '', false) + .addParameter('expression', _('Joint position X')) + .addParameter('expression', _('Joint position Y')) + .addParameter('expression', _('Joint position Z')) + .addParameter('expression', _('Twist axis X')) + .addParameter('expression', _('Twist axis Y')) + .addParameter('expression', _('Twist axis Z')) + .addParameter('expression', _('Half cone angle (degrees)')) + .addParameter('scenevar', _('Variable to store linked pairs count')) + .addParameter('scenevar', _('Variable to store last joint ID')) + .getCodeExtraInformation() + .addIncludeFile('Extensions/Physics3DBehavior/Physics3DTools.js') + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .setFunctionName('gdjs.physics3d.addConeJointsBetweenObjects'); + + extension + .addAction( + 'AddSwingTwistJointsBetweenObjects', + _('Link SwingTwist joints to all picked objects'), + _( + 'Create SwingTwist joints between each picked source object and each picked target object.' + ), + _( + 'Link SwingTwist joints between _PARAM0_ and _PARAM2_ at _PARAM3_;_PARAM4_;_PARAM5_ (linked pairs: _PARAM13_, last joint ID: _PARAM14_)' + ), + _('Joints'), + 'JsPlatform/Extensions/physics3d.svg', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('objectList', _('Source objects'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .addParameter('objectList', _('Target objects'), '', false) + .addParameter('expression', _('Anchor X (pixels)')) + .addParameter('expression', _('Anchor Y (pixels)')) + .addParameter('expression', _('Anchor Z (pixels)')) + .addParameter('expression', _('Twist axis X')) + .addParameter('expression', _('Twist axis Y')) + .addParameter('expression', _('Twist axis Z')) + .addParameter('expression', _('Normal half cone angle (degrees)')) + .addParameter('expression', _('Plane half cone angle (degrees)')) + .addParameter('expression', _('Twist min angle (degrees)')) + .addParameter('expression', _('Twist max angle (degrees)')) + .addParameter('scenevar', _('Variable to store linked pairs count')) + .addParameter('scenevar', _('Variable to store last joint ID')) + .getCodeExtraInformation() + .addIncludeFile('Extensions/Physics3DBehavior/Physics3DTools.js') + .addIncludeFile( + 'Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js' + ) + .setFunctionName('gdjs.physics3d.addSwingTwistJointsBetweenObjects'); + // Collision extension .addCondition( diff --git a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts index aaf60479e030..e6b5056c3533 100644 --- a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts +++ b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts @@ -54,6 +54,64 @@ namespace gdjs { props: Physics3DNetworkSyncDataType; } + /** @category Behaviors > Physics 3D */ + export interface Physics3DRaycastResult { + hasHit: boolean; + hitX: float; + hitY: float; + hitZ: float; + normalX: float; + normalY: float; + normalZ: float; + reflectionDirectionX: float; + reflectionDirectionY: float; + reflectionDirectionZ: float; + distance: float; + fraction: float; + hitBehavior: gdjs.Physics3DRuntimeBehavior | null; + } + + const makeNewPhysics3DRaycastResult = (): Physics3DRaycastResult => ({ + hasHit: false, + hitX: 0, + hitY: 0, + hitZ: 0, + normalX: 0, + normalY: 0, + normalZ: 0, + reflectionDirectionX: 0, + reflectionDirectionY: 0, + reflectionDirectionZ: 0, + distance: 0, + fraction: 0, + hitBehavior: null, + }); + + const normalize3 = ( + x: float, + y: float, + z: float + ): [float, float, float, float] => { + const length = Math.sqrt(x * x + y * y + z * z); + if (length <= epsilon) { + return [0, 0, 0, 0]; + } + return [x / length, y / length, z / length, length]; + }; + + const vec3Length = (v: Jolt.Vec3): float => { + const x = v.GetX(); + const y = v.GetY(); + const z = v.GetZ(); + return Math.sqrt(x * x + y * y + z * z); + }; + + const vector2Length = (v: Jolt.Vector2): float => { + const x = v.GetComponent(0); + const y = v.GetComponent(1); + return Math.sqrt(x * x + y * y); + }; + const isModel3D = ( object: gdjs.RuntimeObject ): object is gdjs.Model3DRuntimeObject => { @@ -61,6 +119,23 @@ namespace gdjs { return object._modelResourceName; }; + interface JointRuntimeState { + breakForce: float; + breakTorque: float; + lastReactionForce: float; + lastReactionTorque: float; + isBroken: boolean; + } + + interface RagdollGroupData { + jointIds: number[]; + bodyBehaviors: Physics3DRuntimeBehavior[]; + bodyRoles: { [bodyUniqueId: string]: string }; + collisionFilter: Jolt.GroupFilterTable | null; + mode: 'Dynamic' | 'Kinematic'; + state: 'Active' | 'Limp' | 'Stiff' | 'Frozen'; + } + /** @category Behaviors > Physics 3D */ export class Physics3DSharedData { gravityX: float; @@ -90,6 +165,190 @@ namespace gdjs { private _physics3DHooks: Array = []; + /** Next joint ID counter */ + _nextJointId: number = 1; + /** Map of (string)jointId -> (Jolt.Constraint)constraint */ + joints: { [key: string]: Jolt.Constraint } = {}; + /** Extra runtime data for each joint (break thresholds, reaction force/torque, broken state). */ + _jointStates: { [key: string]: JointRuntimeState } = {}; + + // ==================== Ragdoll Group System ==================== + + /** Next ragdoll group ID counter */ + _nextRagdollId: number = 1; + /** Map of ragdoll group IDs to group data */ + _ragdollGroups: { [key: string]: RagdollGroupData } = {}; + + /** + * Create a new ragdoll group and return its ID. + */ + createRagdollGroup(): number { + const id = this._nextRagdollId++; + this._ragdollGroups[id.toString(10)] = { + jointIds: [], + bodyBehaviors: [], + bodyRoles: {}, + collisionFilter: null, + mode: 'Kinematic', + state: 'Frozen', + }; + return id; + } + + /** + * Get a ragdoll group by ID. + */ + getRagdollGroup(ragdollId: number | string): RagdollGroupData | null { + const key = ragdollId.toString(10); + if (!this._ragdollGroups.hasOwnProperty(key)) { + return null; + } + + const group = this._ragdollGroups[key]; + // Keep lists clean from stale entries. + const uniqueBehaviors = new Set(); + group.bodyBehaviors = group.bodyBehaviors.filter((behavior) => { + if (!behavior || uniqueBehaviors.has(behavior)) { + return false; + } + uniqueBehaviors.add(behavior); + return true; + }); + group.jointIds = group.jointIds.filter((jointId) => + this.joints.hasOwnProperty(jointId.toString(10)) + ); + return group; + } + + /** + * Add a body behavior to a ragdoll group. + */ + addBodyToRagdollGroup( + ragdollId: number | string, + behavior: Physics3DRuntimeBehavior + ): void { + const group = this.getRagdollGroup(ragdollId); + if (!group) return; + if (group.bodyBehaviors.indexOf(behavior) === -1) { + group.bodyBehaviors.push(behavior); + } + const bodyRole = behavior.getRagdollRole(); + if (bodyRole && bodyRole !== 'None') { + group.bodyRoles[behavior.owner.getUniqueId().toString(10)] = bodyRole; + } + } + + /** + * Remove a body behavior from any ragdoll group that references it. + */ + removeBodyFromAllRagdollGroups(behavior: Physics3DRuntimeBehavior): void { + const behaviorId = behavior.owner.getUniqueId().toString(10); + for (const key in this._ragdollGroups) { + if (!this._ragdollGroups.hasOwnProperty(key)) { + continue; + } + const group = this._ragdollGroups[key]; + group.bodyBehaviors = group.bodyBehaviors.filter((b) => b !== behavior); + delete group.bodyRoles[behaviorId]; + } + } + + /** + * Set/override the role of a body inside a ragdoll group. + */ + setRagdollBodyRole( + ragdollId: number | string, + behavior: Physics3DRuntimeBehavior, + role: string + ): void { + const group = this.getRagdollGroup(ragdollId); + if (!group) { + return; + } + this.addBodyToRagdollGroup(ragdollId, behavior); + group.bodyRoles[behavior.owner.getUniqueId().toString(10)] = role; + } + + /** + * Get a body role inside a ragdoll group. + */ + getRagdollBodyRole( + ragdollId: number | string, + behavior: Physics3DRuntimeBehavior + ): string { + const group = this.getRagdollGroup(ragdollId); + if (!group) { + return ''; + } + return group.bodyRoles[behavior.owner.getUniqueId().toString(10)] || ''; + } + + /** + * Add a joint to a ragdoll group. + */ + addJointToRagdollGroup(ragdollId: number | string, jointId: number): void { + const group = this.getRagdollGroup(ragdollId); + if (!group) return; + if (group.jointIds.indexOf(jointId) === -1) { + group.jointIds.push(jointId); + } + } + + /** + * Track a collision filter used by a ragdoll group so it can be cleaned up. + */ + setRagdollCollisionFilter( + ragdollId: number | string, + filter: Jolt.GroupFilterTable | null + ): void { + const group = this.getRagdollGroup(ragdollId); + if (!group) { + if (filter) { + Jolt.destroy(filter); + } + return; + } + if (group.collisionFilter && group.collisionFilter !== filter) { + Jolt.destroy(group.collisionFilter); + } + group.collisionFilter = filter; + } + + /** + * Remove a ragdoll group and all its joints. + */ + removeRagdollGroup(ragdollId: number | string): void { + const key = ragdollId.toString(10); + const group = this._ragdollGroups[key]; + if (!group) return; + + // Remove all joints in the group + const jointIds = group.jointIds.slice(); + for (const jointId of jointIds) { + this.removeJoint(jointId); + } + + // Restore default collision groups for all tracked bodies. + for (const behavior of group.bodyBehaviors) { + const body = behavior._body; + if (!body) { + continue; + } + const defaultCollisionGroup = new Jolt.CollisionGroup(); + this.bodyInterface.SetCollisionGroup( + body.GetID(), + defaultCollisionGroup + ); + Jolt.destroy(defaultCollisionGroup); + } + + if (group.collisionFilter) { + Jolt.destroy(group.collisionFilter); + group.collisionFilter = null; + } + delete this._ragdollGroups[key]; + } + constructor(instanceContainer: gdjs.RuntimeInstanceContainer, sharedData) { this._registeredBehaviors = new Set(); this.gravityX = sharedData.gravityX; @@ -257,6 +516,339 @@ namespace gdjs { this._registeredBehaviors.delete(physicsBehavior); } + /** + * Add a constraint to the tracked joints and return its ID. + */ + addJoint(constraint: Jolt.Constraint): integer { + // @ts-ignore - AddConstraint exists in the WASM module but not in type defs + this.physicsSystem.AddConstraint(constraint); + const jointId = this._nextJointId++; + const key = jointId.toString(10); + this.joints[key] = constraint; + this._jointStates[key] = { + breakForce: 0, + breakTorque: 0, + lastReactionForce: 0, + lastReactionTorque: 0, + isBroken: false, + }; + return jointId; + } + + /** + * Get a constraint by joint ID. + */ + getJoint(jointId: integer | string): Jolt.Constraint | null { + jointId = jointId.toString(10); + if (this.joints.hasOwnProperty(jointId)) { + return this.joints[jointId]; + } + return null; + } + + /** + * Find the joint ID of a given constraint. + */ + getJointId(constraint: Jolt.Constraint): integer { + for (const jointId in this.joints) { + if (this.joints.hasOwnProperty(jointId)) { + if (this.joints[jointId] === constraint) { + return parseInt(jointId, 10); + } + } + } + return 0; + } + + /** + * Find an existing joint between 2 bodies. + * Optionally restrict search to a specific constraint subtype. + */ + findJointIdBetweenBodies( + firstBody: Jolt.Body, + secondBody: Jolt.Body, + constraintSubType?: number + ): integer { + for (const jointId in this.joints) { + if (!this.joints.hasOwnProperty(jointId)) { + continue; + } + const constraint = this.joints[jointId]; + if ( + constraintSubType !== undefined && + constraint.GetSubType() !== constraintSubType + ) { + continue; + } + try { + const twoBodyConstraint = Jolt.castObject( + constraint, + Jolt.TwoBodyConstraint + ); + const body1 = twoBodyConstraint.GetBody1(); + const body2 = twoBodyConstraint.GetBody2(); + if ( + (body1 === firstBody && body2 === secondBody) || + (body1 === secondBody && body2 === firstBody) + ) { + return parseInt(jointId, 10); + } + } catch (_e) { + // Ignore non-two-body constraints. + } + } + return 0; + } + + /** + * Remove a constraint by joint ID. + */ + removeJoint( + jointId: integer | string, + markAsBroken: boolean = false + ): void { + const key = jointId.toString(10); + const numericJointId = parseInt(key, 10); + if (this.joints.hasOwnProperty(key)) { + const constraint = this.joints[key]; + // @ts-ignore - RemoveConstraint exists in the WASM module but not in type defs + this.physicsSystem.RemoveConstraint(constraint); + Jolt.destroy(constraint); + delete this.joints[key]; + } + + if (this._jointStates.hasOwnProperty(key)) { + if (markAsBroken) { + this._jointStates[key].isBroken = true; + this._jointStates[key].breakForce = 0; + this._jointStates[key].breakTorque = 0; + } else { + delete this._jointStates[key]; + } + } + + for (const ragdollKey in this._ragdollGroups) { + if (!this._ragdollGroups.hasOwnProperty(ragdollKey)) { + continue; + } + const ragdollGroup = this._ragdollGroups[ragdollKey]; + const idx = ragdollGroup.jointIds.indexOf(numericJointId); + if (idx !== -1) { + ragdollGroup.jointIds.splice(idx, 1); + } + } + } + + /** + * Configure automatic break thresholds for a joint. + * A value <= 0 disables the respective threshold. + */ + setJointBreakThresholds( + jointId: integer | string, + maxForce: float, + maxTorque: float + ): void { + const key = jointId.toString(10); + if (!this.joints.hasOwnProperty(key) || !this._jointStates[key]) { + return; + } + this._jointStates[key].breakForce = Math.max(0, maxForce); + this._jointStates[key].breakTorque = Math.max(0, maxTorque); + this._jointStates[key].isBroken = false; + } + + /** + * Clear automatic break thresholds for a joint. + */ + clearJointBreakThresholds(jointId: integer | string): void { + this.setJointBreakThresholds(jointId, 0, 0); + } + + /** + * Check if a joint has been broken by thresholds. + */ + isJointBroken(jointId: integer | string): boolean { + const key = jointId.toString(10); + return this._jointStates.hasOwnProperty(key) + ? this._jointStates[key].isBroken + : false; + } + + /** + * Last measured reaction force for a joint (N-like unit). + */ + getJointLastReactionForce(jointId: integer | string): float { + const key = jointId.toString(10); + return this._jointStates.hasOwnProperty(key) + ? this._jointStates[key].lastReactionForce + : 0; + } + + /** + * Last measured reaction torque for a joint. + */ + getJointLastReactionTorque(jointId: integer | string): float { + const key = jointId.toString(10); + return this._jointStates.hasOwnProperty(key) + ? this._jointStates[key].lastReactionTorque + : 0; + } + + private _computeJointFeedback( + constraint: Jolt.Constraint, + deltaTime: float + ): { force: float; torque: float } { + if (deltaTime <= epsilon) { + return { force: 0, torque: 0 }; + } + const invDeltaTime = 1 / deltaTime; + const subType = constraint.GetSubType(); + + try { + if (subType === Jolt.EConstraintSubType_Distance) { + const c = Jolt.castObject(constraint, Jolt.DistanceConstraint); + return { + force: Math.abs(c.GetTotalLambdaPosition()) * invDeltaTime, + torque: 0, + }; + } + + if (subType === Jolt.EConstraintSubType_Point) { + const c = Jolt.castObject(constraint, Jolt.PointConstraint); + return { + force: vec3Length(c.GetTotalLambdaPosition()) * invDeltaTime, + torque: 0, + }; + } + + if (subType === Jolt.EConstraintSubType_Hinge) { + const c = Jolt.castObject(constraint, Jolt.HingeConstraint); + const forceImpulse = vec3Length(c.GetTotalLambdaPosition()); + const rotationImpulse = vector2Length(c.GetTotalLambdaRotation()); + const limitsImpulse = Math.abs(c.GetTotalLambdaRotationLimits()); + const motorImpulse = Math.abs(c.GetTotalLambdaMotor()); + const torqueImpulse = Math.sqrt( + rotationImpulse * rotationImpulse + + limitsImpulse * limitsImpulse + + motorImpulse * motorImpulse + ); + return { + force: forceImpulse * invDeltaTime, + torque: torqueImpulse * invDeltaTime, + }; + } + + if (subType === Jolt.EConstraintSubType_Slider) { + const c = Jolt.castObject(constraint, Jolt.SliderConstraint); + const forcePositionImpulse = vector2Length( + c.GetTotalLambdaPosition() + ); + const forceLimitsImpulse = Math.abs(c.GetTotalLambdaPositionLimits()); + const forceMotorImpulse = Math.abs(c.GetTotalLambdaMotor()); + const forceImpulse = Math.sqrt( + forcePositionImpulse * forcePositionImpulse + + forceLimitsImpulse * forceLimitsImpulse + + forceMotorImpulse * forceMotorImpulse + ); + const torqueImpulse = vec3Length(c.GetTotalLambdaRotation()); + return { + force: forceImpulse * invDeltaTime, + torque: torqueImpulse * invDeltaTime, + }; + } + + if (subType === Jolt.EConstraintSubType_Cone) { + const c = Jolt.castObject(constraint, Jolt.ConeConstraint); + return { + force: vec3Length(c.GetTotalLambdaPosition()) * invDeltaTime, + torque: Math.abs(c.GetTotalLambdaRotation()) * invDeltaTime, + }; + } + + if (subType === Jolt.EConstraintSubType_SwingTwist) { + const c = Jolt.castObject(constraint, Jolt.SwingTwistConstraint); + const forceImpulse = vec3Length(c.GetTotalLambdaPosition()); + const twistImpulse = Math.abs(c.GetTotalLambdaTwist()); + const swingYImpulse = Math.abs(c.GetTotalLambdaSwingY()); + const swingZImpulse = Math.abs(c.GetTotalLambdaSwingZ()); + const motorImpulse = vec3Length(c.GetTotalLambdaMotor()); + const torqueImpulse = Math.sqrt( + twistImpulse * twistImpulse + + swingYImpulse * swingYImpulse + + swingZImpulse * swingZImpulse + + motorImpulse * motorImpulse + ); + return { + force: forceImpulse * invDeltaTime, + torque: torqueImpulse * invDeltaTime, + }; + } + } catch (_e) { + // If querying feedback fails for this joint type, keep defaults. + } + + return { force: 0, torque: 0 }; + } + + private _updateJointFeedbackAndBreaks(deltaTime: float): void { + const jointsToBreak: string[] = []; + for (const jointId in this.joints) { + if (!this.joints.hasOwnProperty(jointId)) { + continue; + } + const state = this._jointStates[jointId]; + if (!state) { + continue; + } + + const constraint = this.joints[jointId]; + const feedback = this._computeJointFeedback(constraint, deltaTime); + state.lastReactionForce = feedback.force; + state.lastReactionTorque = feedback.torque; + + const shouldBreakByForce = + state.breakForce > 0 && feedback.force >= state.breakForce; + const shouldBreakByTorque = + state.breakTorque > 0 && feedback.torque >= state.breakTorque; + if (shouldBreakByForce || shouldBreakByTorque) { + jointsToBreak.push(jointId); + } + } + + for (const jointId of jointsToBreak) { + this.removeJoint(jointId, true); + } + } + + /** + * Remove all joints associated with a body (called when a body is destroyed). + */ + removeJointsWithBody(body: Jolt.Body): void { + const jointIdsToRemove: string[] = []; + for (const jointId in this.joints) { + if (this.joints.hasOwnProperty(jointId)) { + const constraint = this.joints[jointId]; + try { + const twoBodyConstraint = Jolt.castObject( + constraint, + Jolt.TwoBodyConstraint + ); + if ( + twoBodyConstraint.GetBody1() === body || + twoBodyConstraint.GetBody2() === body + ) { + jointIdsToRemove.push(jointId); + } + } catch (_e) { + // Ignore non-two-body constraints. + } + } + } + for (const jointId of jointIdsToRemove) { + this.removeJoint(jointId); + } + } + step(deltaTime: float): void { for (const physicsBehavior of this._registeredBehaviors) { physicsBehavior._contactsStartedThisFrame.length = 0; @@ -271,6 +863,7 @@ namespace gdjs { const numSteps = deltaTime > 1.0 / 55.0 ? 2 : 1; this.jolt.Step(deltaTime, numSteps); + this._updateJointFeedbackAndBreaks(deltaTime); this.stepped = true; // It's important that updateBodyFromObject and updateObjectFromBody are @@ -295,6 +888,27 @@ namespace gdjs { gdjs.registerRuntimeSceneUnloadedCallback(function (runtimeScene) { const physics3DSharedData = runtimeScene.physics3DSharedData; if (physics3DSharedData) { + // Destroy ragdoll group filters before tearing down the world. + for (const ragdollId in physics3DSharedData._ragdollGroups) { + if (!physics3DSharedData._ragdollGroups.hasOwnProperty(ragdollId)) { + continue; + } + const group = physics3DSharedData._ragdollGroups[ragdollId]; + if (group.collisionFilter) { + Jolt.destroy(group.collisionFilter); + group.collisionFilter = null; + } + } + physics3DSharedData._ragdollGroups = {}; + + // Destroy all joints before destroying the physics world + for (const jointId in physics3DSharedData.joints) { + if (physics3DSharedData.joints.hasOwnProperty(jointId)) { + Jolt.destroy(physics3DSharedData.joints[jointId]); + } + } + physics3DSharedData.joints = {}; + physics3DSharedData._jointStates = {}; Jolt.destroy(physics3DSharedData.contactListener); Jolt.destroy(physics3DSharedData._tempVec3); Jolt.destroy(physics3DSharedData._tempRVec3); @@ -336,6 +950,41 @@ namespace gdjs { gravityScale: float; private layers: integer; private masks: integer; + private ragdollRole: string; + private ragdollGroupTag: string; + private jointAutoWakeBodies: boolean; + private jointAutoStabilityPreset: string; + private jointAutoBreakForce: float; + private jointAutoBreakTorque: float; + private jointEditorEnabled: boolean; + private jointEditorTargetObjectName: string; + private jointEditorType: string; + private jointEditorAnchorOffsetX: float; + private jointEditorAnchorOffsetY: float; + private jointEditorAnchorOffsetZ: float; + private jointEditorTargetAnchorOffsetX: float; + private jointEditorTargetAnchorOffsetY: float; + private jointEditorTargetAnchorOffsetZ: float; + private jointEditorUseCustomAxis: boolean; + private jointEditorAxisX: float; + private jointEditorAxisY: float; + private jointEditorAxisZ: float; + private jointEditorHingeMinAngle: float; + private jointEditorHingeMaxAngle: float; + private jointEditorDistanceMin: float; + private jointEditorDistanceMax: float; + private jointEditorPreviewEnabled: boolean; + private jointEditorPreviewSize: float; + private _jointEditorOwnedJointId: integer; + private _jointEditorOwnedTargetUniqueId: integer; + private _jointEditorOwnsJoint: boolean; + private _jointEditorLoggedUnsupportedType: boolean; + private _jointEditorPreviewGroup: THREE.Group | null; + private _jointEditorPreviewLinkLine: THREE.Line | null; + private _jointEditorPreviewAxisLine: THREE.Line | null; + private _jointEditorPreviewAnchorMesh: THREE.Mesh | null; + private _jointEditorPreviewSourceMesh: THREE.Mesh | null; + private _jointEditorPreviewTargetMesh: THREE.Mesh | null; shapeScale: number = 1; /** @@ -396,6 +1045,15 @@ namespace gdjs { _objectOldWidth: float = 0; _objectOldHeight: float = 0; _objectOldDepth: float = 0; + private _lastRaycastResult: Physics3DRaycastResult = + makeNewPhysics3DRaycastResult(); + /** + * Keeps stable target selection when a joint action references an object type + * that has multiple instances in the scene. + */ + private _preferredJointTargetsByObjectName: { + [objectName: string]: integer; + } = {}; constructor( instanceContainer: gdjs.RuntimeInstanceContainer, @@ -434,6 +1092,79 @@ namespace gdjs { this.gravityScale = behaviorData.gravityScale; this.layers = behaviorData.layers; this.masks = behaviorData.masks; + this.ragdollRole = behaviorData.ragdollRole || 'None'; + this.ragdollGroupTag = behaviorData.ragdollGroupTag || ''; + this.jointAutoWakeBodies = + behaviorData.jointAutoWakeBodies === undefined + ? true + : !!behaviorData.jointAutoWakeBodies; + this.jointAutoStabilityPreset = + behaviorData.jointAutoStabilityPreset || 'Stable'; + this.jointAutoBreakForce = Math.max( + 0, + behaviorData.jointAutoBreakForce || 0 + ); + this.jointAutoBreakTorque = Math.max( + 0, + behaviorData.jointAutoBreakTorque || 0 + ); + this.jointEditorEnabled = !!behaviorData.jointEditorEnabled; + this.jointEditorTargetObjectName = + behaviorData.jointEditorTargetObjectName || ''; + this.jointEditorType = behaviorData.jointEditorType || 'None'; + this.jointEditorAnchorOffsetX = + behaviorData.jointEditorAnchorOffsetX || 0; + this.jointEditorAnchorOffsetY = + behaviorData.jointEditorAnchorOffsetY || 0; + this.jointEditorAnchorOffsetZ = + behaviorData.jointEditorAnchorOffsetZ || 0; + this.jointEditorTargetAnchorOffsetX = + behaviorData.jointEditorTargetAnchorOffsetX || 0; + this.jointEditorTargetAnchorOffsetY = + behaviorData.jointEditorTargetAnchorOffsetY || 0; + this.jointEditorTargetAnchorOffsetZ = + behaviorData.jointEditorTargetAnchorOffsetZ || 0; + this.jointEditorUseCustomAxis = !!behaviorData.jointEditorUseCustomAxis; + this.jointEditorAxisX = + behaviorData.jointEditorAxisX !== undefined + ? behaviorData.jointEditorAxisX + : 1; + this.jointEditorAxisY = behaviorData.jointEditorAxisY || 0; + this.jointEditorAxisZ = behaviorData.jointEditorAxisZ || 0; + this.jointEditorHingeMinAngle = + behaviorData.jointEditorHingeMinAngle !== undefined + ? behaviorData.jointEditorHingeMinAngle + : -60; + this.jointEditorHingeMaxAngle = + behaviorData.jointEditorHingeMaxAngle !== undefined + ? behaviorData.jointEditorHingeMaxAngle + : 60; + this.jointEditorDistanceMin = Math.max( + 0, + behaviorData.jointEditorDistanceMin || 0 + ); + this.jointEditorDistanceMax = Math.max( + 0, + behaviorData.jointEditorDistanceMax || 0 + ); + this.jointEditorPreviewEnabled = + behaviorData.jointEditorPreviewEnabled === undefined + ? true + : !!behaviorData.jointEditorPreviewEnabled; + this.jointEditorPreviewSize = Math.max( + 1, + behaviorData.jointEditorPreviewSize || 8 + ); + this._jointEditorOwnedJointId = 0; + this._jointEditorOwnedTargetUniqueId = 0; + this._jointEditorOwnsJoint = false; + this._jointEditorLoggedUnsupportedType = false; + this._jointEditorPreviewGroup = null; + this._jointEditorPreviewLinkLine = null; + this._jointEditorPreviewAxisLine = null; + this._jointEditorPreviewAnchorMesh = null; + this._jointEditorPreviewSourceMesh = null; + this._jointEditorPreviewTargetMesh = null; this._sharedData = Physics3DSharedData.getSharedData( instanceContainer.getScene(), behaviorData.name @@ -492,6 +1223,110 @@ namespace gdjs { if (behaviorData.gravityScale !== undefined) { this.setGravityScale(behaviorData.gravityScale); } + if (behaviorData.ragdollRole !== undefined) { + this.setRagdollRole(behaviorData.ragdollRole); + } + if (behaviorData.ragdollGroupTag !== undefined) { + this.setRagdollGroupTag(behaviorData.ragdollGroupTag); + } + if (behaviorData.jointAutoWakeBodies !== undefined) { + this.setJointAutoWakeBodies(behaviorData.jointAutoWakeBodies); + } + if (behaviorData.jointAutoStabilityPreset !== undefined) { + this.setJointAutoStabilityPreset(behaviorData.jointAutoStabilityPreset); + } + if (behaviorData.jointAutoBreakForce !== undefined) { + this.setJointAutoBreakForce(behaviorData.jointAutoBreakForce); + } + if (behaviorData.jointAutoBreakTorque !== undefined) { + this.setJointAutoBreakTorque(behaviorData.jointAutoBreakTorque); + } + if (behaviorData.jointEditorEnabled !== undefined) { + this.setJointEditorEnabled(behaviorData.jointEditorEnabled); + } + if (behaviorData.jointEditorTargetObjectName !== undefined) { + this.setJointEditorTargetObjectName( + behaviorData.jointEditorTargetObjectName + ); + } + if (behaviorData.jointEditorType !== undefined) { + this.setJointEditorType(behaviorData.jointEditorType); + } + if (behaviorData.jointEditorAnchorOffsetX !== undefined) { + this.jointEditorAnchorOffsetX = behaviorData.jointEditorAnchorOffsetX; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorAnchorOffsetY !== undefined) { + this.jointEditorAnchorOffsetY = behaviorData.jointEditorAnchorOffsetY; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorAnchorOffsetZ !== undefined) { + this.jointEditorAnchorOffsetZ = behaviorData.jointEditorAnchorOffsetZ; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorTargetAnchorOffsetX !== undefined) { + this.jointEditorTargetAnchorOffsetX = + behaviorData.jointEditorTargetAnchorOffsetX; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorTargetAnchorOffsetY !== undefined) { + this.jointEditorTargetAnchorOffsetY = + behaviorData.jointEditorTargetAnchorOffsetY; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorTargetAnchorOffsetZ !== undefined) { + this.jointEditorTargetAnchorOffsetZ = + behaviorData.jointEditorTargetAnchorOffsetZ; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorUseCustomAxis !== undefined) { + this.jointEditorUseCustomAxis = !!behaviorData.jointEditorUseCustomAxis; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorAxisX !== undefined) { + this.jointEditorAxisX = behaviorData.jointEditorAxisX; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorAxisY !== undefined) { + this.jointEditorAxisY = behaviorData.jointEditorAxisY; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorAxisZ !== undefined) { + this.jointEditorAxisZ = behaviorData.jointEditorAxisZ; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorHingeMinAngle !== undefined) { + this.jointEditorHingeMinAngle = behaviorData.jointEditorHingeMinAngle; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorHingeMaxAngle !== undefined) { + this.jointEditorHingeMaxAngle = behaviorData.jointEditorHingeMaxAngle; + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorDistanceMin !== undefined) { + this.jointEditorDistanceMin = Math.max( + 0, + behaviorData.jointEditorDistanceMin + ); + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorDistanceMax !== undefined) { + this.jointEditorDistanceMax = Math.max( + 0, + behaviorData.jointEditorDistanceMax + ); + this._clearJointEditorOwnedJoint(); + } + if (behaviorData.jointEditorPreviewEnabled !== undefined) { + this.jointEditorPreviewEnabled = + !!behaviorData.jointEditorPreviewEnabled; + } + if (behaviorData.jointEditorPreviewSize !== undefined) { + this.jointEditorPreviewSize = Math.max( + 1, + behaviorData.jointEditorPreviewSize + ); + } // TODO: make these properties updatable. if (behaviorData.layers !== undefined) { @@ -659,10 +1494,20 @@ namespace gdjs { override onDestroy() { this._destroyedDuringFrameLogic = true; + this._disposeJointEditorPreview(); + this._clearJointEditorOwnedJoint(); + this._sharedData.removeBodyFromAllRagdollGroups(this); this.onDeActivate(); } _destroyBody() { + this._preferredJointTargetsByObjectName = {}; + this._disposeJointEditorPreview(); + this._clearJointEditorOwnedJoint(); + // Remove all joints associated with this body before destroying it + if (this._body !== null) { + this._sharedData.removeJointsWithBody(this._body); + } this.bodyUpdater.destroyBody(); this._contactsEndedThisFrame.length = 0; this._contactsStartedThisFrame.length = 0; @@ -1129,6 +1974,7 @@ namespace gdjs { ) { // Reset world step to update next frame this._sharedData.stepped = false; + this._syncJointEditorBinding(); } onObjectHotReloaded() { @@ -1176,6 +2022,9 @@ namespace gdjs { : 0; if (this._body) { + // Joints cannot survive body recreation because they reference body IDs. + // Remove them first to avoid dangling constraints affecting other bodies. + this._sharedData.removeJointsWithBody(this._body); this.bodyUpdater.destroyBody(); this._contactsEndedThisFrame.length = 0; this._contactsStartedThisFrame.length = 0; @@ -1323,11 +2172,11 @@ namespace gdjs { } setGravityY(gravityY: float): void { - if (this._sharedData.gravityX === gravityY) { + if (this._sharedData.gravityY === gravityY) { return; } - this._sharedData.gravityX = gravityY; + this._sharedData.gravityY = gravityY; this._sharedData.physicsSystem.SetGravity( this.getVec3( this._sharedData.gravityX, @@ -1338,7 +2187,7 @@ namespace gdjs { } setGravityZ(gravityZ: float): void { - if (this._sharedData.gravityX === gravityZ) { + if (this._sharedData.gravityZ === gravityZ) { return; } @@ -2084,6 +2933,3793 @@ namespace gdjs { if (!behavior1) return false; return behavior1.collisionChecker.hasCollisionStoppedWith(object2); } + + static raycastClosestInScene( + runtimeScene: gdjs.RuntimeScene, + startX: float, + startY: float, + startZ: float, + endX: float, + endY: float, + endZ: float, + ignoreBehavior: gdjs.Physics3DRuntimeBehavior | null = null, + outResult: Physics3DRaycastResult = makeNewPhysics3DRaycastResult() + ): Physics3DRaycastResult { + outResult.hasHit = false; + outResult.hitX = 0; + outResult.hitY = 0; + outResult.hitZ = 0; + outResult.normalX = 0; + outResult.normalY = 0; + outResult.normalZ = 0; + outResult.reflectionDirectionX = 0; + outResult.reflectionDirectionY = 0; + outResult.reflectionDirectionZ = 0; + outResult.distance = 0; + outResult.fraction = 0; + outResult.hitBehavior = null; + + const physics3DSharedData = runtimeScene.physics3DSharedData; + if (!physics3DSharedData) { + return outResult; + } + + const [ + incomingDirectionX, + incomingDirectionY, + incomingDirectionZ, + rayLength, + ] = normalize3(endX - startX, endY - startY, endZ - startZ); + if (rayLength <= epsilon) { + return outResult; + } + + const worldInvScale = physics3DSharedData.worldInvScale; + const worldScale = physics3DSharedData.worldScale; + + let rayCast: Jolt.RRayCast | null = null; + let rayCastSettings: Jolt.RayCastSettings | null = null; + let collector: Jolt.CastRayClosestHitCollisionCollector | null = null; + let broadPhaseLayerFilter: Jolt.DefaultBroadPhaseLayerFilter | null = + null; + let objectLayerFilter: Jolt.DefaultObjectLayerFilter | null = null; + let bodyFilter: Jolt.BodyFilterJS | Jolt.IgnoreSingleBodyFilter | null = + null; + let shapeFilter: Jolt.ShapeFilterJS2 | null = null; + + try { + rayCast = new Jolt.RRayCast( + physics3DSharedData.getRVec3( + startX * worldInvScale, + startY * worldInvScale, + startZ * worldInvScale + ), + physics3DSharedData.getVec3( + (endX - startX) * worldInvScale, + (endY - startY) * worldInvScale, + (endZ - startZ) * worldInvScale + ) + ); + + rayCastSettings = new Jolt.RayCastSettings(); + collector = new Jolt.CastRayClosestHitCollisionCollector(); + broadPhaseLayerFilter = new Jolt.DefaultBroadPhaseLayerFilter( + physics3DSharedData.jolt.GetObjectVsBroadPhaseLayerFilter(), + gdjs.Physics3DSharedData.allLayersMask + ); + objectLayerFilter = new Jolt.DefaultObjectLayerFilter( + physics3DSharedData.jolt.GetObjectLayerPairFilter(), + gdjs.Physics3DSharedData.allLayersMask + ); + if (ignoreBehavior && ignoreBehavior.getBody()) { + bodyFilter = new Jolt.IgnoreSingleBodyFilter( + ignoreBehavior.getBody()!.GetID() + ); + } else { + const defaultBodyFilter = new Jolt.BodyFilterJS(); + defaultBodyFilter.ShouldCollide = (_inBodyID: number) => true; + defaultBodyFilter.ShouldCollideLocked = (_inBody: number) => true; + bodyFilter = defaultBodyFilter; + } + shapeFilter = new Jolt.ShapeFilterJS2(); + shapeFilter.ShouldCollide = ( + _inShape1: number, + _inSubShapeIDOfShape1: number, + _inShape2: number, + _inSubShapeIDOfShape2: number + ) => true; + + const narrowPhaseQuery = + physics3DSharedData.physicsSystem.GetNarrowPhaseQueryNoLock(); + narrowPhaseQuery.CastRay( + rayCast, + rayCastSettings, + collector, + broadPhaseLayerFilter, + objectLayerFilter, + bodyFilter, + shapeFilter + ); + + if (!collector.HadHit()) { + return outResult; + } + + const hit = collector.mHit; + const hitPoint = rayCast.GetPointOnRay(hit.mFraction); + outResult.hasHit = true; + outResult.fraction = hit.mFraction; + outResult.distance = rayLength * hit.mFraction; + outResult.hitX = hitPoint.GetX() * worldScale; + outResult.hitY = hitPoint.GetY() * worldScale; + outResult.hitZ = hitPoint.GetZ() * worldScale; + + let normalX = -incomingDirectionX; + let normalY = -incomingDirectionY; + let normalZ = -incomingDirectionZ; + + const bodyLockInterface = + physics3DSharedData.physicsSystem.GetBodyLockInterfaceNoLock(); + const body = bodyLockInterface.TryGetBody(hit.mBodyID); + if (body) { + outResult.hitBehavior = body.gdjsAssociatedBehavior || null; + const normal = body.GetWorldSpaceSurfaceNormal( + hit.mSubShapeID2, + hitPoint + ); + const [normalizedX, normalizedY, normalizedZ, normalLength] = + normalize3(normal.GetX(), normal.GetY(), normal.GetZ()); + if (normalLength > epsilon) { + normalX = normalizedX; + normalY = normalizedY; + normalZ = normalizedZ; + } + } + + outResult.normalX = normalX; + outResult.normalY = normalY; + outResult.normalZ = normalZ; + + const dot = + incomingDirectionX * normalX + + incomingDirectionY * normalY + + incomingDirectionZ * normalZ; + const reflectedDirectionX = incomingDirectionX - 2 * dot * normalX; + const reflectedDirectionY = incomingDirectionY - 2 * dot * normalY; + const reflectedDirectionZ = incomingDirectionZ - 2 * dot * normalZ; + const [ + normalizedReflectionDirectionX, + normalizedReflectionDirectionY, + normalizedReflectionDirectionZ, + ] = normalize3( + reflectedDirectionX, + reflectedDirectionY, + reflectedDirectionZ + ); + outResult.reflectionDirectionX = normalizedReflectionDirectionX; + outResult.reflectionDirectionY = normalizedReflectionDirectionY; + outResult.reflectionDirectionZ = normalizedReflectionDirectionZ; + } catch { + // Ignore errors and keep a "no hit" result. + } finally { + if (shapeFilter) Jolt.destroy(shapeFilter); + if (bodyFilter) Jolt.destroy(bodyFilter); + if (objectLayerFilter) Jolt.destroy(objectLayerFilter); + if (broadPhaseLayerFilter) Jolt.destroy(broadPhaseLayerFilter); + if (collector) Jolt.destroy(collector); + if (rayCastSettings) Jolt.destroy(rayCastSettings); + if (rayCast) Jolt.destroy(rayCast); + } + + return outResult; + } + + raycastClosest( + startX: float, + startY: float, + startZ: float, + endX: float, + endY: float, + endZ: float, + ignoreSelf: boolean + ): void { + gdjs.Physics3DRuntimeBehavior.raycastClosestInScene( + this.owner.getRuntimeScene(), + startX, + startY, + startZ, + endX, + endY, + endZ, + ignoreSelf ? this : null, + this._lastRaycastResult + ); + } + + didLastRaycastHit(): boolean { + return this._lastRaycastResult.hasHit; + } + + didLastRaycastHitObject(object: gdjs.RuntimeObject): boolean { + if (!this._lastRaycastResult.hasHit) { + return false; + } + return this._lastRaycastResult.hitBehavior?.owner === object; + } + + getLastRaycastHitX(): float { + return this._lastRaycastResult.hitX; + } + + getLastRaycastHitY(): float { + return this._lastRaycastResult.hitY; + } + + getLastRaycastHitZ(): float { + return this._lastRaycastResult.hitZ; + } + + getLastRaycastNormalX(): float { + return this._lastRaycastResult.normalX; + } + + getLastRaycastNormalY(): float { + return this._lastRaycastResult.normalY; + } + + getLastRaycastNormalZ(): float { + return this._lastRaycastResult.normalZ; + } + + getLastRaycastReflectionDirectionX(): float { + return this._lastRaycastResult.reflectionDirectionX; + } + + getLastRaycastReflectionDirectionY(): float { + return this._lastRaycastResult.reflectionDirectionY; + } + + getLastRaycastReflectionDirectionZ(): float { + return this._lastRaycastResult.reflectionDirectionZ; + } + + getLastRaycastDistance(): float { + return this._lastRaycastResult.distance; + } + + getLastRaycastFraction(): float { + return this._lastRaycastResult.fraction; + } + + // ==================== Joint Methods ==================== + + /** + * Get the other object's physics body, creating it if needed. + * @returns The Jolt.Body of the other object, or null if unavailable. + */ + private _findPhysics3DBehaviorOnObject( + otherObject: gdjs.RuntimeObject + ): Physics3DRuntimeBehavior | null { + const sameNameBehavior = otherObject.getBehavior( + this.name + ) as Physics3DRuntimeBehavior | null; + if (sameNameBehavior) { + return sameNameBehavior; + } + + // Fallback: support joints between objects where Physics3D behavior names differ. + const rawBehaviors = (otherObject as any)._behaviors as + | gdjs.RuntimeBehavior[] + | undefined; + if (rawBehaviors) { + let firstPhysicsBehavior: Physics3DRuntimeBehavior | null = null; + for (const behavior of rawBehaviors) { + if (behavior instanceof Physics3DRuntimeBehavior) { + if (behavior.activated()) { + return behavior; + } + if (!firstPhysicsBehavior) { + firstPhysicsBehavior = behavior; + } + } + } + return firstPhysicsBehavior; + } + return null; + } + + private _resolveMotorState(motorState: string): number { + const normalized = (motorState || '').toLowerCase(); + if (normalized === 'velocity') { + return Jolt.EMotorState_Velocity; + } + if (normalized === 'position') { + return Jolt.EMotorState_Position; + } + return Jolt.EMotorState_Off; + } + + private _isJointSupportedRuntimeObject( + object: gdjs.RuntimeObject | null + ): boolean { + if (!object) { + return false; + } + const objectType = + typeof (object as any).getType === 'function' + ? (object as any).getType() + : object.type; + return ( + objectType === 'Scene3D::Model3DObject' || + objectType === 'Scene3D::Cube3DObject' + ); + } + + private _normalizeJointEditorType(jointType: string): string { + const normalized = (jointType || '').trim().toLowerCase(); + if (normalized === 'fixed') { + return 'Fixed'; + } + if (normalized === 'point') { + return 'Point'; + } + if (normalized === 'hinge') { + return 'Hinge'; + } + if (normalized === 'slider') { + return 'Slider'; + } + if (normalized === 'distance') { + return 'Distance'; + } + if (normalized === 'cone') { + return 'Cone'; + } + if ( + normalized === 'swingtwist' || + normalized === 'swing_twist' || + normalized === 'swing twist' + ) { + return 'SwingTwist'; + } + return 'None'; + } + + private _getConstraintSubTypeForJointEditorType(jointType: string): number { + if (jointType === 'Fixed') { + return Jolt.EConstraintSubType_Fixed; + } + if (jointType === 'Point') { + return Jolt.EConstraintSubType_Point; + } + if (jointType === 'Hinge') { + return Jolt.EConstraintSubType_Hinge; + } + if (jointType === 'Slider') { + return Jolt.EConstraintSubType_Slider; + } + if (jointType === 'Distance') { + return Jolt.EConstraintSubType_Distance; + } + if (jointType === 'Cone') { + return Jolt.EConstraintSubType_Cone; + } + if (jointType === 'SwingTwist') { + return Jolt.EConstraintSubType_SwingTwist; + } + return 0; + } + + private _activateBody(body: Jolt.Body | null): void { + if (!body || !this.jointAutoWakeBodies) { + return; + } + this._sharedData.bodyInterface.ActivateBody(body.GetID()); + } + + private _activateBodiesForConstraint( + constraint: Jolt.Constraint | null + ): void { + if (!constraint || !this.jointAutoWakeBodies) { + return; + } + const twoBodyConstraint = Jolt.castObject( + constraint, + Jolt.TwoBodyConstraint + ); + this._activateBody(twoBodyConstraint.GetBody1()); + this._activateBody(twoBodyConstraint.GetBody2()); + } + + private _activateBodiesForJoint(jointId: integer | string): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) { + return; + } + this._activateBodiesForConstraint(constraint); + } + + private _applyAutomaticJointTuning( + jointId: integer | string, + fallbackPreset: string = 'Stable' + ): void { + const numericJointId = + typeof jointId === 'string' ? parseInt(jointId, 10) : jointId; + if (numericJointId <= 0) { + return; + } + const constraint = this._sharedData.getJoint(numericJointId); + if (!constraint) { + return; + } + + const preferredPreset = ( + this.jointAutoStabilityPreset || + fallbackPreset || + 'Balanced' + ).trim(); + this.setJointStabilityPreset(numericJointId, preferredPreset); + + if (this.jointAutoBreakForce > 0 || this.jointAutoBreakTorque > 0) { + this._sharedData.setJointBreakThresholds( + numericJointId, + this.jointAutoBreakForce, + this.jointAutoBreakTorque + ); + } else { + this._sharedData.clearJointBreakThresholds(numericJointId); + } + + this._activateBodiesForConstraint(constraint); + } + + private _getBodyCenterInPixels(body: Jolt.Body): [float, float, float] { + const center = body.GetCenterOfMassPosition(); + const worldScale = this._sharedData.worldScale; + return [ + center.GetX() * worldScale, + center.GetY() * worldScale, + center.GetZ() * worldScale, + ]; + } + + private _applyJointEditorLocalOffset( + behavior: Physics3DRuntimeBehavior, + centerX: float, + centerY: float, + centerZ: float, + offsetX: float, + offsetY: float, + offsetZ: float + ): [float, float, float] { + const object3D = behavior.owner3D.get3DRendererObject(); + if (!object3D) { + return [centerX + offsetX, centerY + offsetY, centerZ + offsetZ]; + } + + const worldQuaternion = new THREE.Quaternion(); + object3D.getWorldQuaternion(worldQuaternion); + const offset = new THREE.Vector3( + offsetX, + offsetY, + offsetZ + ).applyQuaternion(worldQuaternion); + return [centerX + offset.x, centerY + offset.y, centerZ + offset.z]; + } + + private _computeJointEditorAnchorAndAxis( + targetBehavior: Physics3DRuntimeBehavior + ): { + sourceX: float; + sourceY: float; + sourceZ: float; + targetX: float; + targetY: float; + targetZ: float; + anchorX: float; + anchorY: float; + anchorZ: float; + axisX: float; + axisY: float; + axisZ: float; + distancePx: float; + } { + const sourceBody = this._body!; + const targetBody = targetBehavior._body!; + + const [sourceCenterX, sourceCenterY, sourceCenterZ] = + this._getBodyCenterInPixels(sourceBody); + const [targetCenterX, targetCenterY, targetCenterZ] = + this._getBodyCenterInPixels(targetBody); + + const [sourceX, sourceY, sourceZ] = this._applyJointEditorLocalOffset( + this, + sourceCenterX, + sourceCenterY, + sourceCenterZ, + this.jointEditorAnchorOffsetX, + this.jointEditorAnchorOffsetY, + this.jointEditorAnchorOffsetZ + ); + const [targetX, targetY, targetZ] = this._applyJointEditorLocalOffset( + targetBehavior, + targetCenterX, + targetCenterY, + targetCenterZ, + this.jointEditorTargetAnchorOffsetX, + this.jointEditorTargetAnchorOffsetY, + this.jointEditorTargetAnchorOffsetZ + ); + + const anchorX = (sourceX + targetX) * 0.5; + const anchorY = (sourceY + targetY) * 0.5; + const anchorZ = (sourceZ + targetZ) * 0.5; + + const [autoAxisX, autoAxisY, autoAxisZ, autoAxisLength] = normalize3( + targetX - sourceX, + targetY - sourceY, + targetZ - sourceZ + ); + let axisX = autoAxisX; + let axisY = autoAxisY; + let axisZ = autoAxisZ; + if (this.jointEditorUseCustomAxis) { + const [customAxisX, customAxisY, customAxisZ, customAxisLength] = + normalize3( + this.jointEditorAxisX, + this.jointEditorAxisY, + this.jointEditorAxisZ + ); + if (customAxisLength > epsilon) { + axisX = customAxisX; + axisY = customAxisY; + axisZ = customAxisZ; + } + } else if (autoAxisLength <= epsilon) { + [axisX, axisY, axisZ] = this._computeLimbAxis( + sourceBody, + targetBody, + 1, + 0, + 0 + ); + } + + const dx = targetX - sourceX; + const dy = targetY - sourceY; + const dz = targetZ - sourceZ; + const distancePx = Math.sqrt(dx * dx + dy * dy + dz * dz); + + return { + sourceX, + sourceY, + sourceZ, + targetX, + targetY, + targetZ, + anchorX, + anchorY, + anchorZ, + axisX, + axisY, + axisZ, + distancePx, + }; + } + + private _ensureJointEditorPreview(scene: THREE.Scene): void { + if (this._jointEditorPreviewGroup) { + if (this._jointEditorPreviewGroup.parent !== scene) { + this._jointEditorPreviewGroup.parent?.remove( + this._jointEditorPreviewGroup + ); + scene.add(this._jointEditorPreviewGroup); + } + return; + } + + const group = new THREE.Group(); + group.name = 'Physics3DJointEditorPreview'; + + const linkGeometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(), + new THREE.Vector3(), + ]); + const axisGeometry = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(), + new THREE.Vector3(), + ]); + const pointGeometry = new THREE.SphereGeometry(1, 12, 12); + + const linkMaterial = new THREE.LineBasicMaterial({ + color: 0x3aa6ff, + transparent: true, + opacity: 0.8, + depthTest: false, + }); + const axisMaterial = new THREE.LineBasicMaterial({ + color: 0xffa93a, + transparent: true, + opacity: 0.9, + depthTest: false, + }); + const sourceMaterial = new THREE.MeshBasicMaterial({ + color: 0x2ecc71, + transparent: true, + opacity: 0.9, + depthTest: false, + }); + const targetMaterial = new THREE.MeshBasicMaterial({ + color: 0xe74c3c, + transparent: true, + opacity: 0.9, + depthTest: false, + }); + const anchorMaterial = new THREE.MeshBasicMaterial({ + color: 0xf1c40f, + transparent: true, + opacity: 0.95, + depthTest: false, + }); + + const linkLine = new THREE.Line(linkGeometry, linkMaterial); + const axisLine = new THREE.Line(axisGeometry, axisMaterial); + const sourceMesh = new THREE.Mesh(pointGeometry, sourceMaterial); + const targetMesh = new THREE.Mesh(pointGeometry.clone(), targetMaterial); + const anchorMesh = new THREE.Mesh(pointGeometry.clone(), anchorMaterial); + linkLine.renderOrder = 10000; + axisLine.renderOrder = 10001; + sourceMesh.renderOrder = 10002; + targetMesh.renderOrder = 10002; + anchorMesh.renderOrder = 10003; + + group.add(linkLine); + group.add(axisLine); + group.add(sourceMesh); + group.add(targetMesh); + group.add(anchorMesh); + scene.add(group); + + this._jointEditorPreviewGroup = group; + this._jointEditorPreviewLinkLine = linkLine; + this._jointEditorPreviewAxisLine = axisLine; + this._jointEditorPreviewSourceMesh = sourceMesh; + this._jointEditorPreviewTargetMesh = targetMesh; + this._jointEditorPreviewAnchorMesh = anchorMesh; + } + + private _disposeJointEditorPreview(): void { + const group = this._jointEditorPreviewGroup; + if (!group) { + return; + } + group.parent?.remove(group); + group.traverse((object3D) => { + const anyObject = object3D as any; + if ( + anyObject.geometry && + typeof anyObject.geometry.dispose === 'function' + ) { + anyObject.geometry.dispose(); + } + if (anyObject.material) { + if (Array.isArray(anyObject.material)) { + for (const material of anyObject.material) { + if (material && typeof material.dispose === 'function') { + material.dispose(); + } + } + } else if (typeof anyObject.material.dispose === 'function') { + anyObject.material.dispose(); + } + } + }); + this._jointEditorPreviewGroup = null; + this._jointEditorPreviewLinkLine = null; + this._jointEditorPreviewAxisLine = null; + this._jointEditorPreviewAnchorMesh = null; + this._jointEditorPreviewSourceMesh = null; + this._jointEditorPreviewTargetMesh = null; + } + + private _updateJointEditorPreview( + targetBehavior: Physics3DRuntimeBehavior | null + ): void { + if ( + !this.jointEditorEnabled || + !this.jointEditorPreviewEnabled || + !this._isJointSupportedRuntimeObject(this.owner) + ) { + this._disposeJointEditorPreview(); + return; + } + + const runtimeScene = this.owner.getRuntimeScene(); + const layer = runtimeScene.getLayer(this.owner.getLayer()); + const scene = layer.get3DRendererObject() as THREE.Scene | null; + if (!scene) { + this._disposeJointEditorPreview(); + return; + } + + this._ensureJointEditorPreview(scene); + const group = this._jointEditorPreviewGroup; + if ( + !group || + !targetBehavior || + !this._body || + !targetBehavior._body || + !this._isJointSupportedRuntimeObject(targetBehavior.owner) + ) { + if (group) { + group.visible = false; + } + return; + } + + const previewData = this._computeJointEditorAnchorAndAxis(targetBehavior); + const previewSize = Math.max(1, this.jointEditorPreviewSize); + const axisLength = Math.max(20, previewSize * 3); + const pointScale = previewSize * 0.6; + + const source = new THREE.Vector3( + previewData.sourceX, + previewData.sourceY, + previewData.sourceZ + ); + const target = new THREE.Vector3( + previewData.targetX, + previewData.targetY, + previewData.targetZ + ); + const anchor = new THREE.Vector3( + previewData.anchorX, + previewData.anchorY, + previewData.anchorZ + ); + const axisEnd = new THREE.Vector3( + previewData.anchorX + previewData.axisX * axisLength, + previewData.anchorY + previewData.axisY * axisLength, + previewData.anchorZ + previewData.axisZ * axisLength + ); + + if (this._jointEditorPreviewLinkLine) { + const geometry = this._jointEditorPreviewLinkLine + .geometry as THREE.BufferGeometry; + geometry.setFromPoints([source, target]); + geometry.computeBoundingSphere(); + } + if (this._jointEditorPreviewAxisLine) { + const geometry = this._jointEditorPreviewAxisLine + .geometry as THREE.BufferGeometry; + geometry.setFromPoints([anchor, axisEnd]); + geometry.computeBoundingSphere(); + } + if (this._jointEditorPreviewSourceMesh) { + this._jointEditorPreviewSourceMesh.position.copy(source); + this._jointEditorPreviewSourceMesh.scale.set( + pointScale, + pointScale, + pointScale + ); + } + if (this._jointEditorPreviewTargetMesh) { + this._jointEditorPreviewTargetMesh.position.copy(target); + this._jointEditorPreviewTargetMesh.scale.set( + pointScale, + pointScale, + pointScale + ); + } + if (this._jointEditorPreviewAnchorMesh) { + const anchorScale = pointScale * 1.25; + this._jointEditorPreviewAnchorMesh.position.copy(anchor); + this._jointEditorPreviewAnchorMesh.scale.set( + anchorScale, + anchorScale, + anchorScale + ); + } + + group.visible = true; + } + + private _clearJointEditorOwnedJoint(): void { + if (this._jointEditorOwnsJoint && this._jointEditorOwnedJointId > 0) { + this._sharedData.removeJoint(this._jointEditorOwnedJointId); + } + this._jointEditorOwnedJointId = 0; + this._jointEditorOwnedTargetUniqueId = 0; + this._jointEditorOwnsJoint = false; + } + + private _createJointFromEditorType( + targetBehavior: Physics3DRuntimeBehavior + ): integer { + const bodyA = this._body; + const bodyB = targetBehavior._body; + if (!bodyA || !bodyB) { + return 0; + } + + const previewData = this._computeJointEditorAnchorAndAxis(targetBehavior); + const anchorX = previewData.anchorX; + const anchorY = previewData.anchorY; + const anchorZ = previewData.anchorZ; + const axisX = previewData.axisX; + const axisY = previewData.axisY; + const axisZ = previewData.axisZ; + const distancePx = previewData.distancePx; + + const hingeMinAngle = Math.min( + this.jointEditorHingeMinAngle, + this.jointEditorHingeMaxAngle + ); + const hingeMaxAngle = Math.max( + this.jointEditorHingeMinAngle, + this.jointEditorHingeMaxAngle + ); + + const variable = new gdjs.Variable(); + const normalizedType = this._normalizeJointEditorType( + this.jointEditorType + ); + if (normalizedType === 'Fixed') { + this.addFixedJoint(targetBehavior.owner, variable); + } else if (normalizedType === 'Point') { + this.addPointJoint( + targetBehavior.owner, + anchorX, + anchorY, + anchorZ, + variable + ); + } else if (normalizedType === 'Hinge') { + this.addHingeJoint( + targetBehavior.owner, + anchorX, + anchorY, + anchorZ, + axisX, + axisY, + axisZ, + variable + ); + } else if (normalizedType === 'Slider') { + this.addSliderJoint( + targetBehavior.owner, + axisX, + axisY, + axisZ, + variable + ); + } else if (normalizedType === 'Distance') { + let minDistance = Math.max(0, this.jointEditorDistanceMin); + let maxDistance = Math.max(0, this.jointEditorDistanceMax); + if (minDistance <= epsilon && maxDistance <= epsilon) { + minDistance = Math.max(0, distancePx * 0.9); + maxDistance = Math.max(minDistance + 0.01, distancePx * 1.1); + } else { + const orderedMin = Math.max(0, Math.min(minDistance, maxDistance)); + const orderedMax = Math.max( + orderedMin + 0.01, + Math.max(minDistance, maxDistance) + ); + minDistance = orderedMin; + maxDistance = orderedMax; + } + this.addDistanceJoint( + targetBehavior.owner, + minDistance, + maxDistance, + 2, + 0.35, + variable + ); + } else if (normalizedType === 'Cone') { + const coneHalfAngle = Math.max( + 5, + Math.min( + 170, + Math.max(Math.abs(hingeMinAngle), Math.abs(hingeMaxAngle)) + ) + ); + this.addConeJoint( + targetBehavior.owner, + anchorX, + anchorY, + anchorZ, + axisX, + axisY, + axisZ, + coneHalfAngle, + variable + ); + } else if (normalizedType === 'SwingTwist') { + this.addSwingTwistJoint( + targetBehavior.owner, + anchorX, + anchorY, + anchorZ, + axisX, + axisY, + axisZ, + 50, + 40, + hingeMinAngle, + hingeMaxAngle, + variable + ); + } else { + return 0; + } + + const jointId = Math.round(variable.getAsNumber()); + if (jointId <= 0) { + return 0; + } + + if (normalizedType === 'Fixed') { + this.setJointPriority(jointId, 150); + } else if (normalizedType === 'Hinge') { + this.setHingeJointLimits(jointId, hingeMinAngle, hingeMaxAngle); + this.setHingeJointMaxFriction(jointId, 8); + } else if (normalizedType === 'Slider') { + const sliderRange = Math.max( + 10, + this.jointEditorDistanceMax > epsilon + ? this.jointEditorDistanceMax + : distancePx * 0.5 + ); + this.setSliderJointLimits(jointId, -sliderRange, sliderRange); + this.setSliderJointMaxFriction(jointId, 6); + } else if (normalizedType === 'Distance') { + this.setDistanceJointSpring(jointId, 2, 0.35); + } + + this._applyAutomaticJointTuning( + jointId, + normalizedType === 'Fixed' ? 'UltraStable' : 'Stable' + ); + return jointId; + } + + private _syncJointEditorBinding(): void { + if ( + !this.jointEditorEnabled || + !this.activated() || + this._destroyedDuringFrameLogic + ) { + this._updateJointEditorPreview(null); + this._clearJointEditorOwnedJoint(); + return; + } + + if (!this._isJointSupportedRuntimeObject(this.owner)) { + this._updateJointEditorPreview(null); + this._clearJointEditorOwnedJoint(); + if (!this._jointEditorLoggedUnsupportedType) { + console.warn( + `[Physics3D] Joint Editor supports only Scene3D::Model3DObject and Scene3D::Cube3DObject. Object "${this.owner.getName()}" is ignored.` + ); + this._jointEditorLoggedUnsupportedType = true; + } + return; + } + this._jointEditorLoggedUnsupportedType = false; + + const targetObjectName = (this.jointEditorTargetObjectName || '').trim(); + const normalizedType = this._normalizeJointEditorType( + this.jointEditorType + ); + if (!targetObjectName || normalizedType === 'None') { + this._updateJointEditorPreview(null); + this._clearJointEditorOwnedJoint(); + return; + } + + if (this._body === null && !this._createBody()) { + this._updateJointEditorPreview(null); + return; + } + + const targetBehavior = + this._findBestAlternativePhysics3DBehavior(targetObjectName); + if (!this._isValidOtherBehaviorForJoint(targetBehavior)) { + this._updateJointEditorPreview(null); + this._clearJointEditorOwnedJoint(); + return; + } + if (!this._isJointSupportedRuntimeObject(targetBehavior.owner)) { + this._updateJointEditorPreview(null); + this._clearJointEditorOwnedJoint(); + return; + } + if (targetBehavior._body === null && !targetBehavior._createBody()) { + this._updateJointEditorPreview(null); + return; + } + + const bodyA = this._body; + const bodyB = targetBehavior._body; + if (!bodyA || !bodyB || bodyA === bodyB) { + this._updateJointEditorPreview(null); + this._clearJointEditorOwnedJoint(); + return; + } + this._updateJointEditorPreview(targetBehavior); + + const desiredSubType = + this._getConstraintSubTypeForJointEditorType(normalizedType); + if (desiredSubType === 0) { + this._updateJointEditorPreview(null); + this._clearJointEditorOwnedJoint(); + return; + } + + if (this._jointEditorOwnedJointId > 0) { + const trackedConstraint = this._sharedData.getJoint( + this._jointEditorOwnedJointId + ); + if (!trackedConstraint) { + this._jointEditorOwnedJointId = 0; + this._jointEditorOwnedTargetUniqueId = 0; + this._jointEditorOwnsJoint = false; + } else { + const trackedTwoBody = Jolt.castObject( + trackedConstraint, + Jolt.TwoBodyConstraint + ); + const trackedBody1 = trackedTwoBody.GetBody1(); + const trackedBody2 = trackedTwoBody.GetBody2(); + const matchesBodies = + (trackedBody1 === bodyA && trackedBody2 === bodyB) || + (trackedBody1 === bodyB && trackedBody2 === bodyA); + if ( + !matchesBodies || + trackedConstraint.GetSubType() !== desiredSubType || + this._jointEditorOwnedTargetUniqueId !== + targetBehavior.owner.getUniqueId() + ) { + if (this._jointEditorOwnsJoint) { + this._sharedData.removeJoint(this._jointEditorOwnedJointId); + } + this._jointEditorOwnedJointId = 0; + this._jointEditorOwnedTargetUniqueId = 0; + this._jointEditorOwnsJoint = false; + } + } + } + + if (this._jointEditorOwnedJointId > 0) { + this._applyAutomaticJointTuning( + this._jointEditorOwnedJointId, + normalizedType === 'Fixed' ? 'UltraStable' : 'Stable' + ); + return; + } + + const existingJointId = this._sharedData.findJointIdBetweenBodies( + bodyA, + bodyB, + desiredSubType + ); + if (existingJointId !== 0) { + this._jointEditorOwnedJointId = existingJointId; + this._jointEditorOwnedTargetUniqueId = + targetBehavior.owner.getUniqueId(); + this._jointEditorOwnsJoint = false; + this._applyAutomaticJointTuning( + existingJointId, + normalizedType === 'Fixed' ? 'UltraStable' : 'Stable' + ); + return; + } + + const createdJointId = this._createJointFromEditorType(targetBehavior); + if (createdJointId <= 0) { + this._clearJointEditorOwnedJoint(); + return; + } + + this._jointEditorOwnedJointId = createdJointId; + this._jointEditorOwnedTargetUniqueId = targetBehavior.owner.getUniqueId(); + this._jointEditorOwnsJoint = true; + } + + private _isValidOtherBehaviorForJoint( + behavior: Physics3DRuntimeBehavior | null, + excludedBehavior: Physics3DRuntimeBehavior | null = null + ): behavior is Physics3DRuntimeBehavior { + return ( + !!behavior && + behavior !== this && + behavior !== excludedBehavior && + behavior.activated() && + behavior._sharedData === this._sharedData + ); + } + + private _getDistanceSquaredToObject( + otherObject: gdjs.RuntimeObject + ): float { + const thisX = this.owner3D.getX(); + const thisY = this.owner3D.getY(); + const thisZ = this.owner3D.getZ(); + const otherObject3D = otherObject as unknown as gdjs.RuntimeObject3D; + const otherX = otherObject3D.getX(); + const otherY = otherObject3D.getY(); + const otherZ = + typeof (otherObject3D as any).getZ === 'function' + ? otherObject3D.getZ() + : 0; + const dx = otherX - thisX; + const dy = otherY - thisY; + const dz = otherZ - thisZ; + return dx * dx + dy * dy + dz * dz; + } + + private _findBestAlternativePhysics3DBehavior( + objectName: string, + excludedBehavior: Physics3DRuntimeBehavior | null = null + ): Physics3DRuntimeBehavior | null { + const candidates = this.owner + .getInstanceContainer() + .getObjects(objectName); + if (!candidates || candidates.length === 0) { + return null; + } + + // Reuse previous target for stable links across frames if still valid. + const cachedTargetId = + this._preferredJointTargetsByObjectName[objectName]; + if (cachedTargetId !== undefined) { + for (const candidate of candidates) { + if (candidate.getUniqueId() !== cachedTargetId) { + continue; + } + const candidateBehavior = + this._findPhysics3DBehaviorOnObject(candidate); + if ( + this._isValidOtherBehaviorForJoint( + candidateBehavior, + excludedBehavior + ) + ) { + return candidateBehavior; + } + break; + } + } + + let bestBehavior: Physics3DRuntimeBehavior | null = null; + let bestDistanceSquared = Number.POSITIVE_INFINITY; + for (const candidate of candidates) { + if (candidate === this.owner) { + continue; + } + const candidateBehavior = + this._findPhysics3DBehaviorOnObject(candidate); + if ( + !this._isValidOtherBehaviorForJoint( + candidateBehavior, + excludedBehavior + ) + ) { + continue; + } + const distanceSquared = this._getDistanceSquaredToObject(candidate); + if (distanceSquared < bestDistanceSquared) { + bestDistanceSquared = distanceSquared; + bestBehavior = candidateBehavior; + } + } + return bestBehavior; + } + + private _getOtherBody(otherObject: gdjs.RuntimeObject): Jolt.Body | null { + if (!otherObject) { + console.warn('[Physics3D] Joint creation failed: other object is null'); + return null; + } + if (!this._isJointSupportedRuntimeObject(this.owner)) { + console.warn( + `[Physics3D] Joint creation failed: source object "${this.owner.getName()}" must be Scene3D::Model3DObject or Scene3D::Cube3DObject.` + ); + return null; + } + if (!this._isJointSupportedRuntimeObject(otherObject)) { + console.warn( + `[Physics3D] Joint creation failed: target object "${otherObject.getName()}" must be Scene3D::Model3DObject or Scene3D::Cube3DObject.` + ); + return null; + } + + const targetObjectName = otherObject.getName(); + let otherBehavior = this._findPhysics3DBehaviorOnObject(otherObject); + if (!this._isValidOtherBehaviorForJoint(otherBehavior)) { + otherBehavior = this._findBestAlternativePhysics3DBehavior( + targetObjectName, + otherBehavior + ); + } + if (!this._isValidOtherBehaviorForJoint(otherBehavior)) { + console.warn( + `[Physics3D] Joint creation failed: no valid Physics3D target found for object "${targetObjectName}" (check if another instance exists and has active Physics3D behavior)` + ); + return null; + } + + // Force-create the body if it doesn't exist yet + // (physics body is lazy-initialized on first physics step) + if (otherBehavior._body === null) { + if (!otherBehavior._createBody()) { + console.warn( + `[Physics3D] Joint creation failed: unable to create body for object "${otherObject.getName()}"` + ); + return null; + } + } + + if (this._body !== null && otherBehavior._body === this._body) { + const alternativeBehavior = this._findBestAlternativePhysics3DBehavior( + targetObjectName, + otherBehavior + ); + if (!this._isValidOtherBehaviorForJoint(alternativeBehavior)) { + console.warn( + '[Physics3D] Joint creation failed: resolved both ends to the same body and no alternative target is available' + ); + return null; + } + otherBehavior = alternativeBehavior; + if (otherBehavior._body === null) { + if (!otherBehavior._createBody()) { + console.warn( + `[Physics3D] Joint creation failed: unable to create body for object "${otherBehavior.owner.getName()}"` + ); + return null; + } + } + } + + this._preferredJointTargetsByObjectName[targetObjectName] = + otherBehavior.owner.getUniqueId(); + return otherBehavior._body; + } + + /** + * Add a Fixed joint between this object and another. + * Both objects are locked together with no relative movement. + */ + addFixedJoint( + otherObject: gdjs.RuntimeObject, + variable: gdjs.Variable + ): void { + variable.setNumber(0); + if (this._body === null) { + if (!this._createBody()) return; + } + const body = this._body!; + const otherBody = this._getOtherBody(otherObject); + if (!otherBody) return; + const existingJointId = this._sharedData.findJointIdBetweenBodies( + body, + otherBody, + Jolt.EConstraintSubType_Fixed + ); + if (existingJointId !== 0) { + variable.setNumber(existingJointId); + this._applyAutomaticJointTuning(existingJointId, 'UltraStable'); + return; + } + + const settings = new Jolt.FixedConstraintSettings(); + settings.mAutoDetectPoint = true; + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(body, otherBody); + Jolt.destroy(settings); + + const jointId = this._sharedData.addJoint(constraint); + variable.setNumber(jointId); + this._applyAutomaticJointTuning(jointId, 'UltraStable'); + } + + /** + * Add a Point (Ball & Socket) joint between this object and another. + * Both objects are connected at a point but can rotate freely. + */ + addPointJoint( + otherObject: gdjs.RuntimeObject, + anchorX: float, + anchorY: float, + anchorZ: float, + variable: gdjs.Variable + ): void { + variable.setNumber(0); + if (this._body === null) { + if (!this._createBody()) return; + } + const body = this._body!; + const otherBody = this._getOtherBody(otherObject); + if (!otherBody) return; + const existingJointId = this._sharedData.findJointIdBetweenBodies( + body, + otherBody, + Jolt.EConstraintSubType_Point + ); + if (existingJointId !== 0) { + variable.setNumber(existingJointId); + this._applyAutomaticJointTuning(existingJointId, 'Stable'); + return; + } + + const worldInvScale = this._sharedData.worldInvScale; + const settings = new Jolt.PointConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + settings.mPoint1 = this._sharedData.getRVec3( + anchorX * worldInvScale, + anchorY * worldInvScale, + anchorZ * worldInvScale + ); + settings.mPoint2 = this._sharedData.getRVec3( + anchorX * worldInvScale, + anchorY * worldInvScale, + anchorZ * worldInvScale + ); + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(body, otherBody); + Jolt.destroy(settings); + + const jointId = this._sharedData.addJoint(constraint); + variable.setNumber(jointId); + this._applyAutomaticJointTuning(jointId, 'Stable'); + } + + /** + * Add a Hinge joint between this object and another. + * Allows rotation around a single axis, with optional limits and motor. + */ + addHingeJoint( + otherObject: gdjs.RuntimeObject, + anchorX: float, + anchorY: float, + anchorZ: float, + axisX: float, + axisY: float, + axisZ: float, + variable: gdjs.Variable + ): void { + variable.setNumber(0); + if (this._body === null) { + if (!this._createBody()) return; + } + const body = this._body!; + const otherBody = this._getOtherBody(otherObject); + if (!otherBody) return; + const existingJointId = this._sharedData.findJointIdBetweenBodies( + body, + otherBody, + Jolt.EConstraintSubType_Hinge + ); + if (existingJointId !== 0) { + variable.setNumber(existingJointId); + this._applyAutomaticJointTuning(existingJointId, 'Stable'); + return; + } + + const worldInvScale = this._sharedData.worldInvScale; + const settings = new Jolt.HingeConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + settings.mPoint1 = this._sharedData.getRVec3( + anchorX * worldInvScale, + anchorY * worldInvScale, + anchorZ * worldInvScale + ); + settings.mPoint2 = this._sharedData.getRVec3( + anchorX * worldInvScale, + anchorY * worldInvScale, + anchorZ * worldInvScale + ); + // Normalize axis + const axisLen = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + if (axisLen > 0) { + axisX /= axisLen; + axisY /= axisLen; + axisZ /= axisLen; + } else { + axisY = 1; // Default up axis + } + settings.mHingeAxis1 = this._sharedData.getVec3(axisX, axisY, axisZ); + settings.mHingeAxis2 = this._sharedData.getVec3(axisX, axisY, axisZ); + // Compute a perpendicular normal axis + let normalX: float, normalY: float, normalZ: float; + if (Math.abs(axisX) < 0.9) { + // Cross with X axis + normalX = 0; + normalY = -axisZ; + normalZ = axisY; + } else { + // Cross with Y axis + normalX = axisZ; + normalY = 0; + normalZ = -axisX; + } + const normalLen = Math.sqrt( + normalX * normalX + normalY * normalY + normalZ * normalZ + ); + if (normalLen > 0) { + normalX /= normalLen; + normalY /= normalLen; + normalZ /= normalLen; + } + settings.mNormalAxis1 = this._sharedData.getVec3( + normalX, + normalY, + normalZ + ); + settings.mNormalAxis2 = this._sharedData.getVec3( + normalX, + normalY, + normalZ + ); + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(body, otherBody); + Jolt.destroy(settings); + + const jointId = this._sharedData.addJoint(constraint); + variable.setNumber(jointId); + this._applyAutomaticJointTuning(jointId, 'Stable'); + } + + /** + * Add a Slider (Prismatic) joint between this object and another. + * Allows translation along a single axis. + */ + addSliderJoint( + otherObject: gdjs.RuntimeObject, + axisX: float, + axisY: float, + axisZ: float, + variable: gdjs.Variable + ): void { + variable.setNumber(0); + if (this._body === null) { + if (!this._createBody()) return; + } + const body = this._body!; + const otherBody = this._getOtherBody(otherObject); + if (!otherBody) return; + const existingJointId = this._sharedData.findJointIdBetweenBodies( + body, + otherBody, + Jolt.EConstraintSubType_Slider + ); + if (existingJointId !== 0) { + variable.setNumber(existingJointId); + this._applyAutomaticJointTuning(existingJointId, 'Stable'); + return; + } + + const settings = new Jolt.SliderConstraintSettings(); + settings.mAutoDetectPoint = true; + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + // Normalize axis + const [normalizedAxisX, normalizedAxisY, normalizedAxisZ, axisLen] = + normalize3(axisX, axisY, axisZ); + if (axisLen > epsilon) { + axisX = normalizedAxisX; + axisY = normalizedAxisY; + axisZ = normalizedAxisZ; + } else { + axisX = 1; + axisY = 0; + axisZ = 0; + } + const [normalX, normalY, normalZ] = this._computePerpendicularAxis( + axisX, + axisY, + axisZ + ); + settings.mSliderAxis1 = this._sharedData.getVec3(axisX, axisY, axisZ); + settings.mSliderAxis2 = this._sharedData.getVec3(axisX, axisY, axisZ); + settings.mNormalAxis1 = this._sharedData.getVec3( + normalX, + normalY, + normalZ + ); + settings.mNormalAxis2 = this._sharedData.getVec3( + normalX, + normalY, + normalZ + ); + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(body, otherBody); + Jolt.destroy(settings); + + const jointId = this._sharedData.addJoint(constraint); + variable.setNumber(jointId); + this._applyAutomaticJointTuning(jointId, 'Stable'); + } + + /** + * Add a Distance joint between this object and another. + * Keeps a min/max distance between two objects, optionally with a spring. + */ + addDistanceJoint( + otherObject: gdjs.RuntimeObject, + minDistance: float, + maxDistance: float, + springFrequency: float, + springDamping: float, + variable: gdjs.Variable + ): void { + variable.setNumber(0); + if (this._body === null) { + if (!this._createBody()) return; + } + const body = this._body!; + const otherBody = this._getOtherBody(otherObject); + if (!otherBody) return; + const existingJointId = this._sharedData.findJointIdBetweenBodies( + body, + otherBody, + Jolt.EConstraintSubType_Distance + ); + if (existingJointId !== 0) { + variable.setNumber(existingJointId); + this._applyAutomaticJointTuning(existingJointId, 'Stable'); + return; + } + + const worldInvScale = this._sharedData.worldInvScale; + const settings = new Jolt.DistanceConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + // Use centers of mass for points + const pos1 = body.GetCenterOfMassPosition(); + const pos2 = otherBody.GetCenterOfMassPosition(); + settings.mPoint1 = this._sharedData.getRVec3( + pos1.GetX(), + pos1.GetY(), + pos1.GetZ() + ); + settings.mPoint2 = this._sharedData.getRVec3( + pos2.GetX(), + pos2.GetY(), + pos2.GetZ() + ); + const minLimit = Math.max(0, Math.min(minDistance, maxDistance)); + const maxLimit = Math.max( + minLimit + epsilon, + Math.max(minDistance, maxDistance) + ); + settings.mMinDistance = minLimit * worldInvScale; + settings.mMaxDistance = maxLimit * worldInvScale; + if (springFrequency > 0) { + settings.mLimitsSpringSettings.mFrequency = Math.max( + 0, + springFrequency + ); + settings.mLimitsSpringSettings.mDamping = Math.max(0, springDamping); + } + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(body, otherBody); + Jolt.destroy(settings); + + const jointId = this._sharedData.addJoint(constraint); + variable.setNumber(jointId); + this._applyAutomaticJointTuning(jointId, 'Stable'); + } + + /** + * Add a Pulley joint between this object and another. + * The rope length is fixed (min = max = totalLength), and ratio controls mechanical advantage. + */ + addPulleyJoint( + otherObject: gdjs.RuntimeObject, + pulleyAnchorAX: float, + pulleyAnchorAY: float, + pulleyAnchorAZ: float, + pulleyAnchorBX: float, + pulleyAnchorBY: float, + pulleyAnchorBZ: float, + localAnchorAX: float, + localAnchorAY: float, + localAnchorAZ: float, + localAnchorBX: float, + localAnchorBY: float, + localAnchorBZ: float, + totalLength: float, + ratio: float, + enabled: boolean, + variable: gdjs.Variable + ): void { + variable.setNumber(0); + if (this._body === null) { + if (!this._createBody()) return; + } + const body = this._body!; + const otherBody = this._getOtherBody(otherObject); + if (!otherBody) return; + + const existingJointId = this._sharedData.findJointIdBetweenBodies( + body, + otherBody, + Jolt.EConstraintSubType_Pulley + ); + if (existingJointId !== 0) { + variable.setNumber(existingJointId); + const existingConstraint = this._sharedData.getJoint(existingJointId); + if (existingConstraint) { + existingConstraint.SetEnabled(!!enabled); + this._activateBodiesForConstraint(existingConstraint); + } + this._applyAutomaticJointTuning(existingJointId, 'Stable'); + return; + } + + const worldInvScale = this._sharedData.worldInvScale; + const clampedTotalLength = Math.max(epsilon, totalLength) * worldInvScale; + const clampedRatio = Math.max(epsilon, ratio); + const settings = new Jolt.PulleyConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_LocalToBodyCOM; + settings.mBodyPoint1 = this._sharedData.getRVec3( + localAnchorAX * worldInvScale, + localAnchorAY * worldInvScale, + localAnchorAZ * worldInvScale + ); + settings.mBodyPoint2 = this._sharedData.getRVec3( + localAnchorBX * worldInvScale, + localAnchorBY * worldInvScale, + localAnchorBZ * worldInvScale + ); + settings.mFixedPoint1 = this._sharedData.getRVec3( + pulleyAnchorAX * worldInvScale, + pulleyAnchorAY * worldInvScale, + pulleyAnchorAZ * worldInvScale + ); + settings.mFixedPoint2 = this._sharedData.getRVec3( + pulleyAnchorBX * worldInvScale, + pulleyAnchorBY * worldInvScale, + pulleyAnchorBZ * worldInvScale + ); + settings.mRatio = clampedRatio; + settings.mMinLength = clampedTotalLength; + settings.mMaxLength = clampedTotalLength; + settings.mEnabled = !!enabled; + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(body, otherBody); + Jolt.destroy(settings); + + const jointId = this._sharedData.addJoint(constraint); + variable.setNumber(jointId); + this._applyAutomaticJointTuning(jointId, 'Stable'); + + // Re-apply explicit enabled state because automatic tuning may wake the bodies. + if (!enabled) { + const createdConstraint = this._sharedData.getJoint(jointId); + if (createdConstraint) { + createdConstraint.SetEnabled(false); + } + } + } + + /** + * Add a Cone joint between this object and another. + * Restricts the rotation within a cone shape. + */ + addConeJoint( + otherObject: gdjs.RuntimeObject, + anchorX: float, + anchorY: float, + anchorZ: float, + twistAxisX: float, + twistAxisY: float, + twistAxisZ: float, + halfConeAngle: float, + variable: gdjs.Variable + ): void { + variable.setNumber(0); + if (this._body === null) { + if (!this._createBody()) return; + } + const body = this._body!; + const otherBody = this._getOtherBody(otherObject); + if (!otherBody) return; + const existingJointId = this._sharedData.findJointIdBetweenBodies( + body, + otherBody, + Jolt.EConstraintSubType_Cone + ); + if (existingJointId !== 0) { + variable.setNumber(existingJointId); + this._applyAutomaticJointTuning(existingJointId, 'Stable'); + return; + } + + const worldInvScale = this._sharedData.worldInvScale; + const settings = new Jolt.ConeConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + settings.mPoint1 = this._sharedData.getRVec3( + anchorX * worldInvScale, + anchorY * worldInvScale, + anchorZ * worldInvScale + ); + settings.mPoint2 = this._sharedData.getRVec3( + anchorX * worldInvScale, + anchorY * worldInvScale, + anchorZ * worldInvScale + ); + // Normalize twist axis + const axisLen = Math.sqrt( + twistAxisX * twistAxisX + + twistAxisY * twistAxisY + + twistAxisZ * twistAxisZ + ); + if (axisLen > 0) { + twistAxisX /= axisLen; + twistAxisY /= axisLen; + twistAxisZ /= axisLen; + } else { + twistAxisY = 1; + } + settings.mTwistAxis1 = this._sharedData.getVec3( + twistAxisX, + twistAxisY, + twistAxisZ + ); + settings.mTwistAxis2 = this._sharedData.getVec3( + twistAxisX, + twistAxisY, + twistAxisZ + ); + settings.mHalfConeAngle = gdjs.toRad( + Math.max(0, Math.min(179.0, halfConeAngle)) + ); + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(body, otherBody); + Jolt.destroy(settings); + + const jointId = this._sharedData.addJoint(constraint); + variable.setNumber(jointId); + this._applyAutomaticJointTuning(jointId, 'Stable'); + } + + /** + * Add a SwingTwist joint between this object and another. + * Best for shoulders, hips, and ragdoll joints. Allows independent + * control of swing (cone) and twist ranges. + */ + addSwingTwistJoint( + otherObject: gdjs.RuntimeObject, + anchorX: float, + anchorY: float, + anchorZ: float, + twistAxisX: float, + twistAxisY: float, + twistAxisZ: float, + normalHalfConeAngle: float, + planeHalfConeAngle: float, + twistMinAngle: float, + twistMaxAngle: float, + variable: gdjs.Variable + ): void { + variable.setNumber(0); + if (this._body === null) { + if (!this._createBody()) return; + } + const body = this._body!; + const otherBody = this._getOtherBody(otherObject); + if (!otherBody) return; + const existingJointId = this._sharedData.findJointIdBetweenBodies( + body, + otherBody, + Jolt.EConstraintSubType_SwingTwist + ); + if (existingJointId !== 0) { + variable.setNumber(existingJointId); + this._applyAutomaticJointTuning(existingJointId, 'Stable'); + return; + } + + const worldInvScale = this._sharedData.worldInvScale; + const settings = new Jolt.SwingTwistConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + settings.mPosition1 = this._sharedData.getRVec3( + anchorX * worldInvScale, + anchorY * worldInvScale, + anchorZ * worldInvScale + ); + settings.mPosition2 = this._sharedData.getRVec3( + anchorX * worldInvScale, + anchorY * worldInvScale, + anchorZ * worldInvScale + ); + // Normalize twist axis + const axisLen = Math.sqrt( + twistAxisX * twistAxisX + + twistAxisY * twistAxisY + + twistAxisZ * twistAxisZ + ); + if (axisLen > 0) { + twistAxisX /= axisLen; + twistAxisY /= axisLen; + twistAxisZ /= axisLen; + } else { + twistAxisX = 1; + } + settings.mTwistAxis1 = this._sharedData.getVec3( + twistAxisX, + twistAxisY, + twistAxisZ + ); + settings.mTwistAxis2 = this._sharedData.getVec3( + twistAxisX, + twistAxisY, + twistAxisZ + ); + // Compute plane axis perpendicular to twist axis + let planeX: float, planeY: float, planeZ: float; + if (Math.abs(twistAxisX) < 0.9) { + planeX = 0; + planeY = -twistAxisZ; + planeZ = twistAxisY; + } else { + planeX = twistAxisZ; + planeY = 0; + planeZ = -twistAxisX; + } + const planeLen = Math.sqrt( + planeX * planeX + planeY * planeY + planeZ * planeZ + ); + if (planeLen > 0) { + planeX /= planeLen; + planeY /= planeLen; + planeZ /= planeLen; + } + settings.mPlaneAxis1 = this._sharedData.getVec3(planeX, planeY, planeZ); + settings.mPlaneAxis2 = this._sharedData.getVec3(planeX, planeY, planeZ); + const clampedNormalHalfConeAngle = Math.max( + 0, + Math.min(179, normalHalfConeAngle) + ); + const clampedPlaneHalfConeAngle = Math.max( + 0, + Math.min(179, planeHalfConeAngle) + ); + settings.mNormalHalfConeAngle = gdjs.toRad(clampedNormalHalfConeAngle); + settings.mPlaneHalfConeAngle = gdjs.toRad(clampedPlaneHalfConeAngle); + const orderedMinTwistAngle = Math.min(twistMinAngle, twistMaxAngle); + const orderedMaxTwistAngle = Math.max(twistMinAngle, twistMaxAngle); + const minTwistAngle = Math.max( + -179, + Math.min(179, orderedMinTwistAngle) + ); + const maxTwistAngle = Math.max( + minTwistAngle, + Math.max(-179, Math.min(179, orderedMaxTwistAngle)) + ); + settings.mTwistMinAngle = gdjs.toRad(minTwistAngle); + settings.mTwistMaxAngle = gdjs.toRad(maxTwistAngle); + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(body, otherBody); + Jolt.destroy(settings); + + const jointId = this._sharedData.addJoint(constraint); + variable.setNumber(jointId); + this._applyAutomaticJointTuning(jointId, 'Stable'); + } + + /** + * Remove a joint by its ID. + */ + removeJoint(jointId: integer | string): void { + const numericJointId = + typeof jointId === 'string' ? parseInt(jointId, 10) : jointId; + if (numericJointId === this._jointEditorOwnedJointId) { + this._jointEditorOwnedJointId = 0; + this._jointEditorOwnedTargetUniqueId = 0; + this._jointEditorOwnsJoint = false; + } + this._sharedData.removeJoint(jointId); + } + + /** + * Check if this object is the first body in a joint. + */ + isJointFirstObject(jointId: integer | string): boolean { + if (this._body === null) return false; + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return false; + const twoBodyConstraint = Jolt.castObject( + constraint, + Jolt.TwoBodyConstraint + ); + return twoBodyConstraint.GetBody1() === this._body; + } + + /** + * Check if this object is the second body in a joint. + */ + isJointSecondObject(jointId: integer | string): boolean { + if (this._body === null) return false; + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return false; + const twoBodyConstraint = Jolt.castObject( + constraint, + Jolt.TwoBodyConstraint + ); + return twoBodyConstraint.GetBody2() === this._body; + } + + /** + * Override solver iterations for a specific joint. + * Use 0 to return to engine defaults. + */ + setJointSolverOverrides( + jointId: integer | string, + velocitySteps: float, + positionSteps: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const clampedVelocitySteps = Math.max( + 0, + Math.min(255, Math.round(velocitySteps)) + ); + const clampedPositionSteps = Math.max( + 0, + Math.min(255, Math.round(positionSteps)) + ); + constraint.SetNumVelocityStepsOverride(clampedVelocitySteps); + constraint.SetNumPositionStepsOverride(clampedPositionSteps); + this._activateBodiesForConstraint(constraint); + } + + /** + * Set solver priority for a specific joint. + */ + setJointPriority(jointId: integer | string, priority: float): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const clampedPriority = Math.max(0, Math.min(255, Math.round(priority))); + constraint.SetConstraintPriority(clampedPriority); + this._activateBodiesForConstraint(constraint); + } + + /** + * Apply a ready-to-use stability preset on a joint. + * - "Balanced": engine defaults + * - "Stable": stronger solver for most gameplay constraints + * - "UltraStable": highest stability, more CPU cost + */ + setJointStabilityPreset(jointId: integer | string, preset: string): void { + const normalizedPreset = (preset || '') + .toLowerCase() + .replace(/[\s_-]/g, ''); + if (normalizedPreset === 'stable') { + this.setJointSolverOverrides(jointId, 8, 4); + this.setJointPriority(jointId, 100); + } else if (normalizedPreset === 'ultrastable') { + this.setJointSolverOverrides(jointId, 12, 6); + this.setJointPriority(jointId, 150); + } else { + this.setJointSolverOverrides(jointId, 0, 0); + this.setJointPriority(jointId, 0); + } + } + + /** + * Set automatic break thresholds for a joint. + * Pass <= 0 to disable a threshold. + */ + setJointBreakThresholds( + jointId: integer | string, + maxForce: float, + maxTorque: float + ): void { + this._sharedData.setJointBreakThresholds(jointId, maxForce, maxTorque); + this._activateBodiesForJoint(jointId); + } + + /** + * Disable automatic break thresholds for a joint. + */ + clearJointBreakThresholds(jointId: integer | string): void { + this._sharedData.clearJointBreakThresholds(jointId); + this._activateBodiesForJoint(jointId); + } + + /** + * Check if a joint has been broken by thresholds. + */ + isJointBroken(jointId: integer | string): boolean { + return this._sharedData.isJointBroken(jointId); + } + + /** + * Get last reaction force measured for a joint. + */ + getJointReactionForce(jointId: integer | string): float { + return this._sharedData.getJointLastReactionForce(jointId); + } + + /** + * Get last reaction torque measured for a joint. + */ + getJointReactionTorque(jointId: integer | string): float { + return this._sharedData.getJointLastReactionTorque(jointId); + } + + /** + * Set motor torque limits on a hinge joint. + */ + setHingeJointMotorLimits( + jointId: integer | string, + minTorque: float, + maxTorque: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + const motorSettings = hingeConstraint.GetMotorSettings(); + const low = Math.min(minTorque, maxTorque); + const high = Math.max(minTorque, maxTorque); + motorSettings.mMinTorqueLimit = low; + motorSettings.mMaxTorqueLimit = high; + this._activateBodiesForConstraint(constraint); + } + + /** + * Set motor spring settings on a hinge joint. + */ + setHingeJointMotorSpring( + jointId: integer | string, + frequency: float, + damping: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + const springSettings = hingeConstraint.GetMotorSettings().mSpringSettings; + springSettings.mFrequency = Math.max(0, frequency); + springSettings.mDamping = Math.max(0, damping); + this._activateBodiesForConstraint(constraint); + } + + /** + * Set motor force limits on a slider joint. + */ + setSliderJointMotorLimits( + jointId: integer | string, + minForce: float, + maxForce: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + const motorSettings = sliderConstraint.GetMotorSettings(); + const low = Math.min(minForce, maxForce); + const high = Math.max(minForce, maxForce); + motorSettings.mMinForceLimit = low; + motorSettings.mMaxForceLimit = high; + this._activateBodiesForConstraint(constraint); + } + + /** + * Set motor spring settings on a slider joint. + */ + setSliderJointMotorSpring( + jointId: integer | string, + frequency: float, + damping: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + const springSettings = + sliderConstraint.GetMotorSettings().mSpringSettings; + springSettings.mFrequency = Math.max(0, frequency); + springSettings.mDamping = Math.max(0, damping); + this._activateBodiesForConstraint(constraint); + } + + /** + * Set the limits on a hinge joint (in degrees). + */ + setHingeJointLimits( + jointId: integer | string, + limitsMin: float, + limitsMax: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + const minLimit = Math.min(limitsMin, limitsMax); + const maxLimit = Math.max(limitsMin, limitsMax); + hingeConstraint.SetLimits(gdjs.toRad(minLimit), gdjs.toRad(maxLimit)); + this._activateBodiesForConstraint(constraint); + } + + /** + * Set the motor on a hinge joint. + * @param motorState "Off", "Velocity", or "Position" + * @param target Target velocity (deg/s) or angle (degrees) + */ + setHingeJointMotor( + jointId: integer | string, + motorState: string, + target: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + const state = this._resolveMotorState(motorState); + hingeConstraint.SetMotorState(state); + if (state === Jolt.EMotorState_Velocity) { + hingeConstraint.SetTargetAngularVelocity(gdjs.toRad(target)); + } else if (state === Jolt.EMotorState_Position) { + hingeConstraint.SetTargetAngle(gdjs.toRad(target)); + } + this._activateBodiesForConstraint(constraint); + } + + /** + * Get the current angle of a hinge joint (in degrees). + */ + getHingeJointAngle(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return gdjs.toDegrees(hingeConstraint.GetCurrentAngle()); + } + + /** + * Set the limits on a slider joint (in pixels). + */ + setSliderJointLimits( + jointId: integer | string, + limitsMin: float, + limitsMax: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + const minLimit = Math.min(limitsMin, limitsMax); + const maxLimit = Math.max(limitsMin, limitsMax); + sliderConstraint.SetLimits( + minLimit * this._sharedData.worldInvScale, + maxLimit * this._sharedData.worldInvScale + ); + this._activateBodiesForConstraint(constraint); + } + + /** + * Set the motor on a slider joint. + * @param motorState "Off", "Velocity", or "Position" + * @param target Target velocity (pixels/s) or position (pixels) + */ + setSliderJointMotor( + jointId: integer | string, + motorState: string, + target: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + const state = this._resolveMotorState(motorState); + sliderConstraint.SetMotorState(state); + if (state === Jolt.EMotorState_Velocity) { + sliderConstraint.SetTargetVelocity( + target * this._sharedData.worldInvScale + ); + } else if (state === Jolt.EMotorState_Position) { + sliderConstraint.SetTargetPosition( + target * this._sharedData.worldInvScale + ); + } + this._activateBodiesForConstraint(constraint); + } + + getRagdollRole(): string { + return this.ragdollRole || 'None'; + } + + setRagdollRole(role: string): void { + this.ragdollRole = role || 'None'; + } + + getRagdollGroupTag(): string { + return this.ragdollGroupTag || ''; + } + + setRagdollGroupTag(groupTag: string): void { + this.ragdollGroupTag = groupTag || ''; + } + + setJointAutoWakeBodies(enable: boolean): void { + this.jointAutoWakeBodies = !!enable; + } + + setJointAutoStabilityPreset(preset: string): void { + const normalizedPreset = (preset || '') + .toLowerCase() + .replace(/[\s_-]/g, ''); + if (normalizedPreset === 'stable') { + this.jointAutoStabilityPreset = 'Stable'; + } else if (normalizedPreset === 'ultrastable') { + this.jointAutoStabilityPreset = 'UltraStable'; + } else { + this.jointAutoStabilityPreset = 'Balanced'; + } + } + + setJointAutoBreakForce(force: float): void { + this.jointAutoBreakForce = Math.max(0, force); + } + + setJointAutoBreakTorque(torque: float): void { + this.jointAutoBreakTorque = Math.max(0, torque); + } + + setJointEditorEnabled(enable: boolean): void { + this.jointEditorEnabled = !!enable; + if (!this.jointEditorEnabled) { + this._updateJointEditorPreview(null); + this._clearJointEditorOwnedJoint(); + } + } + + setJointEditorTargetObjectName(objectName: string): void { + const normalizedName = (objectName || '').trim(); + if (this.jointEditorTargetObjectName === normalizedName) { + return; + } + this.jointEditorTargetObjectName = normalizedName; + this._clearJointEditorOwnedJoint(); + } + + setJointEditorType(jointType: string): void { + const normalizedType = this._normalizeJointEditorType(jointType); + if (this.jointEditorType === normalizedType) { + return; + } + this.jointEditorType = normalizedType; + this._clearJointEditorOwnedJoint(); + } + + getJointEditorJointId(): integer { + return this._jointEditorOwnedJointId; + } + + /** + * Get the current position of a slider joint (in pixels). + */ + getSliderJointPosition(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return ( + sliderConstraint.GetCurrentPosition() * this._sharedData.worldScale + ); + } + + /** + * Set the distance on a distance joint (in pixels). + */ + setDistanceJointDistance( + jointId: integer | string, + minDistance: float, + maxDistance: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const distanceConstraint = Jolt.castObject( + constraint, + Jolt.DistanceConstraint + ); + const minLimit = Math.max(0, Math.min(minDistance, maxDistance)); + const maxLimit = Math.max( + minLimit + epsilon, + Math.max(minDistance, maxDistance) + ); + distanceConstraint.SetDistance( + minLimit * this._sharedData.worldInvScale, + maxLimit * this._sharedData.worldInvScale + ); + this._activateBodiesForConstraint(constraint); + } + + /** + * Set the total rope length on a pulley joint (in pixels). + * Internally this enforces a fixed length by setting min = max. + */ + setPulleyJointLength(jointId: integer | string, totalLength: float): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const pulleyConstraint = Jolt.castObject(constraint, Jolt.PulleyConstraint); + const clampedLength = Math.max(epsilon, totalLength); + const lengthInMeters = clampedLength * this._sharedData.worldInvScale; + pulleyConstraint.SetLength(lengthInMeters, lengthInMeters); + this._activateBodiesForConstraint(constraint); + } + + /** + * Get current rope length of a pulley joint (in pixels). + */ + getPulleyJointCurrentLength(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const pulleyConstraint = Jolt.castObject(constraint, Jolt.PulleyConstraint); + return pulleyConstraint.GetCurrentLength() * this._sharedData.worldScale; + } + + /** + * Get configured total rope length of a pulley joint (in pixels). + * This returns the midpoint between min and max lengths. + */ + getPulleyJointTotalLength(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const pulleyConstraint = Jolt.castObject(constraint, Jolt.PulleyConstraint); + return ( + ((pulleyConstraint.GetMinLength() + pulleyConstraint.GetMaxLength()) * + 0.5) * + this._sharedData.worldScale + ); + } + + // ==================== Advanced Joint Customization ==================== + + /** + * Set spring settings on hinge joint limits. + */ + setHingeJointSpring( + jointId: integer | string, + frequency: float, + damping: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + const springSettings = new Jolt.SpringSettings(); + springSettings.mFrequency = Math.max(0, frequency); + springSettings.mDamping = Math.max(0, damping); + hingeConstraint.SetLimitsSpringSettings(springSettings); + Jolt.destroy(springSettings); + this._activateBodiesForConstraint(constraint); + } + + /** + * Set max friction torque on a hinge joint. + */ + setHingeJointMaxFriction( + jointId: integer | string, + maxFriction: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + hingeConstraint.SetMaxFrictionTorque(maxFriction); + this._activateBodiesForConstraint(constraint); + } + + /** + * Check if a hinge joint has limits enabled. + */ + hasHingeJointLimits(jointId: integer | string): boolean { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return false; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return hingeConstraint.HasLimits(); + } + + /** + * Get the minimum limit angle of a hinge joint (in degrees). + */ + getHingeJointMinLimit(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return gdjs.toDegrees(hingeConstraint.GetLimitsMin()); + } + + /** + * Get the maximum limit angle of a hinge joint (in degrees). + */ + getHingeJointMaxLimit(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return gdjs.toDegrees(hingeConstraint.GetLimitsMax()); + } + + /** + * Set spring settings on slider joint limits. + */ + setSliderJointSpring( + jointId: integer | string, + frequency: float, + damping: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + const springSettings = new Jolt.SpringSettings(); + springSettings.mFrequency = Math.max(0, frequency); + springSettings.mDamping = Math.max(0, damping); + sliderConstraint.SetLimitsSpringSettings(springSettings); + Jolt.destroy(springSettings); + this._activateBodiesForConstraint(constraint); + } + + /** + * Set max friction force on a slider joint. + */ + setSliderJointMaxFriction( + jointId: integer | string, + maxFriction: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + sliderConstraint.SetMaxFrictionForce(maxFriction); + this._activateBodiesForConstraint(constraint); + } + + /** + * Check if a slider joint has limits enabled. + */ + hasSliderJointLimits(jointId: integer | string): boolean { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return false; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.HasLimits(); + } + + /** + * Get the minimum limit of a slider joint (in pixels). + */ + getSliderJointMinLimit(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetLimitsMin() * this._sharedData.worldScale; + } + + /** + * Get the maximum limit of a slider joint (in pixels). + */ + getSliderJointMaxLimit(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetLimitsMax() * this._sharedData.worldScale; + } + + /** + * Set spring settings on a distance joint. + */ + setDistanceJointSpring( + jointId: integer | string, + frequency: float, + damping: float + ): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const distanceConstraint = Jolt.castObject( + constraint, + Jolt.DistanceConstraint + ); + const springSettings = new Jolt.SpringSettings(); + springSettings.mFrequency = Math.max(0, frequency); + springSettings.mDamping = Math.max(0, damping); + distanceConstraint.SetLimitsSpringSettings(springSettings); + Jolt.destroy(springSettings); + this._activateBodiesForConstraint(constraint); + } + + /** + * Get the current minimum distance of a distance joint (in pixels). + */ + getDistanceJointMinDistance(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const distanceConstraint = Jolt.castObject( + constraint, + Jolt.DistanceConstraint + ); + return distanceConstraint.GetMinDistance() * this._sharedData.worldScale; + } + + /** + * Get the current maximum distance of a distance joint (in pixels). + */ + getDistanceJointMaxDistance(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const distanceConstraint = Jolt.castObject( + constraint, + Jolt.DistanceConstraint + ); + return distanceConstraint.GetMaxDistance() * this._sharedData.worldScale; + } + + /** + * Update the half cone angle of a cone joint at runtime (in degrees). + */ + setConeJointHalfAngle(jointId: integer | string, halfAngle: float): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + const coneConstraint = Jolt.castObject(constraint, Jolt.ConeConstraint); + coneConstraint.SetHalfConeAngle( + gdjs.toRad(Math.max(0, Math.min(179, halfAngle))) + ); + this._activateBodiesForConstraint(constraint); + } + + // ==================== Joint Enable/Disable ==================== + + /** + * Enable a joint. + */ + enableJoint(jointId: integer | string): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + constraint.SetEnabled(true); + this._activateBodiesForConstraint(constraint); + } + + /** + * Disable a joint without removing it. + */ + disableJoint(jointId: integer | string): void { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return; + constraint.SetEnabled(false); + } + + /** + * Check if a joint is enabled. + */ + isJointEnabled(jointId: integer | string): boolean { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return false; + return constraint.GetEnabled(); + } + + /** + * Get the total number of active joints. + */ + getJointCount(): integer { + return Object.keys(this._sharedData.joints).length; + } + + // ==================== Hinge Motor Queries ==================== + + /** + * Get the current target angular velocity of a hinge joint motor (deg/s). + */ + getHingeJointMotorSpeed(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return gdjs.toDegrees(hingeConstraint.GetTargetAngularVelocity()); + } + + /** + * Get the current target angle of a hinge joint motor (degrees). + */ + getHingeJointMotorTarget(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return gdjs.toDegrees(hingeConstraint.GetTargetAngle()); + } + + /** + * Get the max friction torque of a hinge joint. + */ + getHingeJointMaxFriction(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return hingeConstraint.GetMaxFrictionTorque(); + } + + /** + * Get hinge motor minimum torque limit. + */ + getHingeJointMotorMinTorque(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return hingeConstraint.GetMotorSettings().mMinTorqueLimit; + } + + /** + * Get hinge motor maximum torque limit. + */ + getHingeJointMotorMaxTorque(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return hingeConstraint.GetMotorSettings().mMaxTorqueLimit; + } + + /** + * Get hinge motor spring frequency. + */ + getHingeJointMotorSpringFrequency(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return hingeConstraint.GetMotorSettings().mSpringSettings.mFrequency; + } + + /** + * Get hinge motor spring damping. + */ + getHingeJointMotorSpringDamping(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const hingeConstraint = Jolt.castObject(constraint, Jolt.HingeConstraint); + return hingeConstraint.GetMotorSettings().mSpringSettings.mDamping; + } + + // ==================== Slider Motor Queries ==================== + + /** + * Get the current target velocity of a slider joint motor (px/s). + */ + getSliderJointMotorSpeed(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetTargetVelocity() * this._sharedData.worldScale; + } + + /** + * Get the current target position of a slider joint motor (px). + */ + getSliderJointMotorTarget(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetTargetPosition() * this._sharedData.worldScale; + } + + /** + * Get the max friction force of a slider joint. + */ + getSliderJointMaxFriction(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetMaxFrictionForce(); + } + + /** + * Get slider motor minimum force limit. + */ + getSliderJointMotorMinForce(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetMotorSettings().mMinForceLimit; + } + + /** + * Get slider motor maximum force limit. + */ + getSliderJointMotorMaxForce(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetMotorSettings().mMaxForceLimit; + } + + /** + * Get slider motor spring frequency. + */ + getSliderJointMotorSpringFrequency(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetMotorSettings().mSpringSettings.mFrequency; + } + + /** + * Get slider motor spring damping. + */ + getSliderJointMotorSpringDamping(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const sliderConstraint = Jolt.castObject( + constraint, + Jolt.SliderConstraint + ); + return sliderConstraint.GetMotorSettings().mSpringSettings.mDamping; + } + + // ================================================================ + // ==================== RAGDOLL AUTOMATION SYSTEM ================== + // ================================================================ + + // ==================== Group Management ==================== + + /** + * Create a new ragdoll group and store the ID in a variable. + */ + createRagdollGroup(variable: gdjs.Variable): void { + const id = this._sharedData.createRagdollGroup(); + variable.setNumber(id); + } + + /** + * Add this object's body to a ragdoll group. + */ + addBodyToRagdollGroup(ragdollId: integer | string): void { + this._sharedData.addBodyToRagdollGroup(ragdollId, this); + } + + /** + * Add a joint to a ragdoll group. + */ + addJointToRagdollGroup( + ragdollId: integer | string, + jointId: integer | string + ): void { + this._sharedData.addJointToRagdollGroup( + ragdollId, + typeof jointId === 'string' ? parseInt(jointId, 10) : jointId + ); + } + + /** + * Remove a ragdoll group and all its joints. + */ + removeRagdollGroup(ragdollId: integer | string): void { + this._sharedData.removeRagdollGroup(ragdollId); + } + + // ==================== Ragdoll Mode Switching ==================== + + /** + * Switch all bodies in a ragdoll group between Dynamic and Kinematic. + * @param mode "Dynamic" or "Kinematic" + */ + setRagdollMode(ragdollId: integer | string, mode: string): void { + const group = this._sharedData.getRagdollGroup(ragdollId); + if (!group) return; + const normalizedMode = (mode || '').toLowerCase(); + const isKinematic = normalizedMode === 'kinematic'; + const motionType = isKinematic + ? Jolt.EMotionType_Kinematic + : Jolt.EMotionType_Dynamic; + for (const behavior of group.bodyBehaviors) { + const body = behavior._body; + if (!body) continue; + this._sharedData.bodyInterface.SetMotionType( + body.GetID(), + motionType, + Jolt.EActivation_Activate + ); + if (isKinematic) { + // Freeze any residual energy when switching to animation-driven mode. + this._sharedData.bodyInterface.SetLinearVelocity( + body.GetID(), + this._sharedData.getVec3(0, 0, 0) + ); + this._sharedData.bodyInterface.SetAngularVelocity( + body.GetID(), + this._sharedData.getVec3(0, 0, 0) + ); + } else { + this._sharedData.bodyInterface.ActivateBody(body.GetID()); + } + behavior.bodyType = isKinematic ? 'Kinematic' : 'Dynamic'; + } + group.mode = isKinematic ? 'Kinematic' : 'Dynamic'; + } + + /** + * Set preset ragdoll state for realistic simulation. + * - "Active": Normal dynamic physics + * - "Limp": High damping, no friction (unconscious/dead) + * - "Stiff": High spring stiffness, high friction (muscle tension) + * - "Frozen": Kinematic mode (animation-driven) + */ + setRagdollState(ragdollId: integer | string, state: string): void { + const group = this._sharedData.getRagdollGroup(ragdollId); + if (!group) return; + const normalizedState = (state || '').toLowerCase(); + + if (normalizedState === 'frozen') { + this.setRagdollMode(ragdollId, 'Kinematic'); + group.state = 'Frozen'; + return; + } + + // Ensure dynamic mode for physics states + this.setRagdollMode(ragdollId, 'Dynamic'); + + if (normalizedState === 'limp') { + // High damping, no friction => floppy ragdoll + for (const behavior of group.bodyBehaviors) { + behavior.setLinearDamping(2.0); + behavior.setAngularDamping(2.0); + } + this._setRagdollJointsFriction(group, 0); + this._setRagdollJointsSpring(group, 0, 0); + group.state = 'Limp'; + } else if (normalizedState === 'stiff') { + // Low damping, high spring, high friction => tense muscles + for (const behavior of group.bodyBehaviors) { + behavior.setLinearDamping(0.3); + behavior.setAngularDamping(0.5); + } + this._setRagdollJointsFriction(group, 100); + this._setRagdollJointsSpring(group, 10, 0.5); + group.state = 'Stiff'; + } else { + // "Active" - moderate values + for (const behavior of group.bodyBehaviors) { + behavior.setLinearDamping(0.5); + behavior.setAngularDamping(0.5); + } + this._setRagdollJointsFriction(group, 5); + this._setRagdollJointsSpring(group, 2, 0.3); + group.state = 'Active'; + } + } + + // ==================== Ragdoll Batch Controls ==================== + + /** + * Set linear and angular damping on ALL bodies in a ragdoll group. + */ + setRagdollDamping( + ragdollId: integer | string, + linearDamping: float, + angularDamping: float + ): void { + const group = this._sharedData.getRagdollGroup(ragdollId); + if (!group) return; + for (const behavior of group.bodyBehaviors) { + behavior.setLinearDamping(Math.max(0, linearDamping)); + behavior.setAngularDamping(Math.max(0, angularDamping)); + } + } + + /** + * Set spring stiffness on ALL joints in a ragdoll group. + */ + setRagdollStiffness( + ragdollId: integer | string, + frequency: float, + damping: float + ): void { + const group = this._sharedData.getRagdollGroup(ragdollId); + if (!group) return; + this._setRagdollJointsSpring( + group, + Math.max(0, frequency), + Math.max(0, damping) + ); + } + + /** + * Set friction on ALL joints in a ragdoll group. + */ + setRagdollFriction(ragdollId: integer | string, friction: float): void { + const group = this._sharedData.getRagdollGroup(ragdollId); + if (!group) return; + this._setRagdollJointsFriction(group, Math.max(0, friction)); + } + + /** Internal: set spring on all joints in a group */ + private _setRagdollJointsSpring( + group: RagdollGroupData, + frequency: float, + damping: float + ): void { + const clampedFrequency = Math.max(0, frequency); + const clampedDamping = Math.max(0, damping); + for (const jointId of group.jointIds) { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) continue; + const subType = constraint.GetSubType(); + try { + const springSettings = new Jolt.SpringSettings(); + springSettings.mFrequency = clampedFrequency; + springSettings.mDamping = clampedDamping; + if (subType === Jolt.EConstraintSubType_Hinge) { + Jolt.castObject( + constraint, + Jolt.HingeConstraint + ).SetLimitsSpringSettings(springSettings); + } else if (subType === Jolt.EConstraintSubType_Slider) { + Jolt.castObject( + constraint, + Jolt.SliderConstraint + ).SetLimitsSpringSettings(springSettings); + } else if (subType === Jolt.EConstraintSubType_Distance) { + Jolt.castObject( + constraint, + Jolt.DistanceConstraint + ).SetLimitsSpringSettings(springSettings); + } + Jolt.destroy(springSettings); + } catch (_e) { + // Constraint type doesn't support springs, skip + } + } + } + + /** Internal: set friction on all joints in a group */ + private _setRagdollJointsFriction( + group: RagdollGroupData, + friction: float + ): void { + for (const jointId of group.jointIds) { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) continue; + try { + const subType = constraint.GetSubType(); + if (subType === Jolt.EConstraintSubType_Hinge) { + Jolt.castObject( + constraint, + Jolt.HingeConstraint + ).SetMaxFrictionTorque(friction); + } else if (subType === Jolt.EConstraintSubType_Slider) { + Jolt.castObject( + constraint, + Jolt.SliderConstraint + ).SetMaxFrictionForce(friction); + } else if (subType === Jolt.EConstraintSubType_SwingTwist) { + Jolt.castObject( + constraint, + Jolt.SwingTwistConstraint + ).SetMaxFrictionTorque(friction); + } + } catch (_e) { + // Constraint type doesn't support friction, skip + } + } + } + + /** + * Apply an impulse to ALL bodies in a ragdoll group (e.g. explosion or hit). + */ + applyRagdollImpulse( + ragdollId: integer | string, + impulseX: float, + impulseY: float, + impulseZ: float + ): void { + const group = this._sharedData.getRagdollGroup(ragdollId); + if (!group) return; + const worldInvScale = this._sharedData.worldInvScale; + for (const behavior of group.bodyBehaviors) { + const body = behavior.getBody(); + if (!body) continue; + this._sharedData.bodyInterface.AddImpulse( + body.GetID(), + this._sharedData.getVec3( + impulseX * worldInvScale, + impulseY * worldInvScale, + impulseZ * worldInvScale + ) + ); + } + } + + /** + * Set gravity scale on ALL bodies in a ragdoll group. + */ + setRagdollGravityScale( + ragdollId: integer | string, + gravityScale: float + ): void { + const group = this._sharedData.getRagdollGroup(ragdollId); + if (!group) return; + for (const behavior of group.bodyBehaviors) { + const body = behavior.getBody(); + if (!body) continue; + this._sharedData.bodyInterface.SetGravityFactor( + body.GetID(), + gravityScale + ); + } + } + + // ==================== Ragdoll Queries ==================== + + /** + * Get the number of bodies in a ragdoll group. + */ + getRagdollBodyCount(ragdollId: integer | string): integer { + const group = this._sharedData.getRagdollGroup(ragdollId); + return group ? group.bodyBehaviors.length : 0; + } + + /** + * Get the number of joints in a ragdoll group. + */ + getRagdollJointCount(ragdollId: integer | string): integer { + const group = this._sharedData.getRagdollGroup(ragdollId); + return group ? group.jointIds.length : 0; + } + + // ==================== Joint World Position Queries ==================== + + /** + * Get the world X position of a joint (midpoint of the two connected bodies). + */ + getJointWorldX(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const twoBody = Jolt.castObject(constraint, Jolt.TwoBodyConstraint); + const pos1 = twoBody.GetBody1().GetCenterOfMassPosition(); + const pos2 = twoBody.GetBody2().GetCenterOfMassPosition(); + return ((pos1.GetX() + pos2.GetX()) / 2) * this._sharedData.worldScale; + } + + /** + * Get the world Y position of a joint (midpoint of the two connected bodies). + */ + getJointWorldY(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const twoBody = Jolt.castObject(constraint, Jolt.TwoBodyConstraint); + const pos1 = twoBody.GetBody1().GetCenterOfMassPosition(); + const pos2 = twoBody.GetBody2().GetCenterOfMassPosition(); + return ((pos1.GetY() + pos2.GetY()) / 2) * this._sharedData.worldScale; + } + + /** + * Get the world Z position of a joint (midpoint of the two connected bodies). + */ + getJointWorldZ(jointId: integer | string): float { + const constraint = this._sharedData.getJoint(jointId); + if (!constraint) return 0; + const twoBody = Jolt.castObject(constraint, Jolt.TwoBodyConstraint); + const pos1 = twoBody.GetBody1().GetCenterOfMassPosition(); + const pos2 = twoBody.GetBody2().GetCenterOfMassPosition(); + return ((pos1.GetZ() + pos2.GetZ()) / 2) * this._sharedData.worldScale; + } + + // ==================== Humanoid Ragdoll Template ==================== + + private _resolveRagdollPartBehavior( + object: gdjs.RuntimeObject | null + ): Physics3DRuntimeBehavior | null { + if (!object) { + return null; + } + const behavior = this._findPhysics3DBehaviorOnObject(object); + if ( + !behavior || + !behavior.activated() || + behavior._sharedData !== this._sharedData + ) { + return null; + } + if (behavior._body === null && !behavior._createBody()) { + return null; + } + return behavior; + } + + private _computeLimbAxis( + bodyA: Jolt.Body, + bodyB: Jolt.Body, + fallbackX: float, + fallbackY: float, + fallbackZ: float + ): [float, float, float] { + const posA = bodyA.GetCenterOfMassPosition(); + const posB = bodyB.GetCenterOfMassPosition(); + const [axisX, axisY, axisZ, length] = normalize3( + posB.GetX() - posA.GetX(), + posB.GetY() - posA.GetY(), + posB.GetZ() - posA.GetZ() + ); + if (length <= epsilon) { + return [fallbackX, fallbackY, fallbackZ]; + } + return [axisX, axisY, axisZ]; + } + + private _computePerpendicularAxis( + axisX: float, + axisY: float, + axisZ: float + ): [float, float, float] { + // First try a cross product with world up. + let normalX = axisZ; + let normalY = 0; + let normalZ = -axisX; + let [nx, ny, nz, nLen] = normalize3(normalX, normalY, normalZ); + if (nLen <= epsilon) { + // Fallback if axis is parallel to world up. + normalX = 0; + normalY = -axisZ; + normalZ = axisY; + [nx, ny, nz, nLen] = normalize3(normalX, normalY, normalZ); + } + if (nLen <= epsilon) { + return [1, 0, 0]; + } + return [nx, ny, nz]; + } + + private _applyHumanoidMassDistribution( + parts: Array<{ + role: string; + behavior: Physics3DRuntimeBehavior; + massRatio: float; + }> + ): void { + if (parts.length === 0) { + return; + } + + let measuredTotalMass = 0; + let sumRatios = 0; + for (const part of parts) { + sumRatios += part.massRatio; + const body = part.behavior._body; + if (!body || !body.IsDynamic()) { + continue; + } + const invMass = body.GetMotionProperties().GetInverseMass(); + if (invMass > epsilon) { + measuredTotalMass += 1 / invMass; + } + } + if (sumRatios <= epsilon) { + return; + } + const totalMass = measuredTotalMass > epsilon ? measuredTotalMass : 75; + + for (const part of parts) { + const body = part.behavior._body; + if (!body || !body.IsDynamic()) { + continue; + } + const targetMass = Math.max( + 0.01, + (totalMass * part.massRatio) / sumRatios + ); + body.GetMotionProperties().ScaleToMass(targetMass); + this._sharedData.bodyInterface.ActivateBody(body.GetID()); + } + } + + private _configureHumanoidRagdollCollisionFiltering( + ragdollId: number, + parts: Array<{ + role: string; + behavior: Physics3DRuntimeBehavior; + }> + ): void { + if (parts.length === 0) { + return; + } + + const groupFilter = new Jolt.GroupFilterTable(parts.length); + const roleToSubGroupId: { [role: string]: number } = {}; + for (let i = 0; i < parts.length; i++) { + roleToSubGroupId[parts[i].role] = i; + } + + const adjacentPairs: Array<[string, string]> = [ + ['Head', 'Chest'], + ['Chest', 'Hips'], + ['Chest', 'UpperArmL'], + ['UpperArmL', 'LowerArmL'], + ['Chest', 'UpperArmR'], + ['UpperArmR', 'LowerArmR'], + ['Hips', 'ThighL'], + ['ThighL', 'ShinL'], + ['Hips', 'ThighR'], + ['ThighR', 'ShinR'], + ]; + for (const [roleA, roleB] of adjacentPairs) { + const subA = roleToSubGroupId[roleA]; + const subB = roleToSubGroupId[roleB]; + if (subA === undefined || subB === undefined) { + continue; + } + groupFilter.DisableCollision(subA, subB); + } + + this._sharedData.setRagdollCollisionFilter(ragdollId, groupFilter); + for (let i = 0; i < parts.length; i++) { + const body = parts[i].behavior._body; + if (!body) { + continue; + } + const collisionGroup = new Jolt.CollisionGroup( + groupFilter, + ragdollId, + i + ); + this._sharedData.bodyInterface.SetCollisionGroup( + body.GetID(), + collisionGroup + ); + Jolt.destroy(collisionGroup); + } + } + + buildHumanoidRagdollFromTag( + groupTag: string, + variable: gdjs.Variable + ): void { + const normalizedTag = (groupTag || '').trim(); + if (!normalizedTag) { + variable.setNumber(0); + return; + } + + const roleToObject: { [role: string]: gdjs.RuntimeObject | null } = { + Head: null, + Chest: null, + Hips: null, + UpperArmL: null, + LowerArmL: null, + UpperArmR: null, + LowerArmR: null, + ThighL: null, + ShinL: null, + ThighR: null, + ShinR: null, + }; + const allInstances = this.owner + .getInstanceContainer() + .getAdhocListOfAllInstances(); + for (const object of allInstances) { + const behavior = this._findPhysics3DBehaviorOnObject(object); + if (!behavior || behavior._sharedData !== this._sharedData) { + continue; + } + if ((behavior.getRagdollGroupTag() || '').trim() !== normalizedTag) { + continue; + } + const role = behavior.getRagdollRole(); + if ( + !role || + role === 'None' || + !roleToObject.hasOwnProperty(role) || + roleToObject[role] !== null + ) { + continue; + } + roleToObject[role] = object; + } + + if (!roleToObject.Chest || !roleToObject.Hips) { + // Chest and hips are the minimal core for a usable humanoid ragdoll. + variable.setNumber(0); + return; + } + + this.buildHumanoidRagdoll( + roleToObject.Head as unknown as gdjs.RuntimeObject, + roleToObject.Chest as unknown as gdjs.RuntimeObject, + roleToObject.Hips as unknown as gdjs.RuntimeObject, + roleToObject.UpperArmL as unknown as gdjs.RuntimeObject, + roleToObject.LowerArmL as unknown as gdjs.RuntimeObject, + roleToObject.UpperArmR as unknown as gdjs.RuntimeObject, + roleToObject.LowerArmR as unknown as gdjs.RuntimeObject, + roleToObject.ThighL as unknown as gdjs.RuntimeObject, + roleToObject.ShinL as unknown as gdjs.RuntimeObject, + roleToObject.ThighR as unknown as gdjs.RuntimeObject, + roleToObject.ShinR as unknown as gdjs.RuntimeObject, + variable + ); + } + + /** + * Build a complete humanoid ragdoll from 11 body-part objects. + * Automatically creates joints with appropriate types and weight distribution: + * - Head -> Chest: Cone joint (neck) + * - Chest -> Hips: Fixed joint (torso) + * - Chest -> UpperArmL/R: SwingTwist (shoulders) + * - UpperArmL/R -> LowerArmL/R: Hinge (elbows) + * - Hips -> ThighL/R: SwingTwist (hips) + * - ThighL/R -> ShinL/R: Hinge (knees) + */ + buildHumanoidRagdoll( + head: gdjs.RuntimeObject, + chest: gdjs.RuntimeObject, + hips: gdjs.RuntimeObject, + upperArmL: gdjs.RuntimeObject, + lowerArmL: gdjs.RuntimeObject, + upperArmR: gdjs.RuntimeObject, + lowerArmR: gdjs.RuntimeObject, + thighL: gdjs.RuntimeObject, + shinL: gdjs.RuntimeObject, + thighR: gdjs.RuntimeObject, + shinR: gdjs.RuntimeObject, + variable: gdjs.Variable + ): void { + const partDefinitions: Array<{ + role: string; + object: gdjs.RuntimeObject | null; + massRatio: float; + }> = [ + { role: 'Head', object: head, massRatio: 0.08 }, + { role: 'Chest', object: chest, massRatio: 0.35 }, + { role: 'Hips', object: hips, massRatio: 0.15 }, + { role: 'UpperArmL', object: upperArmL, massRatio: 0.04 }, + { role: 'LowerArmL', object: lowerArmL, massRatio: 0.03 }, + { role: 'UpperArmR', object: upperArmR, massRatio: 0.04 }, + { role: 'LowerArmR', object: lowerArmR, massRatio: 0.03 }, + { role: 'ThighL', object: thighL, massRatio: 0.08 }, + { role: 'ShinL', object: shinL, massRatio: 0.05 }, + { role: 'ThighR', object: thighR, massRatio: 0.08 }, + { role: 'ShinR', object: shinR, massRatio: 0.05 }, + ]; + + const uniquePartBehaviors = new Set(); + const parts: Array<{ + role: string; + behavior: Physics3DRuntimeBehavior; + massRatio: float; + }> = []; + for (const partDefinition of partDefinitions) { + const behavior = this._resolveRagdollPartBehavior( + partDefinition.object + ); + if (!behavior) { + continue; + } + const uniqueId = behavior.owner.getUniqueId(); + if (uniquePartBehaviors.has(uniqueId)) { + continue; + } + uniquePartBehaviors.add(uniqueId); + parts.push({ + role: partDefinition.role, + behavior, + massRatio: partDefinition.massRatio, + }); + } + if (parts.length < 2) { + variable.setNumber(0); + return; + } + + // Create the ragdoll group + const ragdollId = this._sharedData.createRagdollGroup(); + variable.setNumber(ragdollId); + + const roleToBehavior: { [role: string]: Physics3DRuntimeBehavior } = {}; + for (const part of parts) { + roleToBehavior[part.role] = part.behavior; + this._sharedData.addBodyToRagdollGroup(ragdollId, part.behavior); + this._sharedData.setRagdollBodyRole( + ragdollId, + part.behavior, + part.role + ); + } + + // Better physical distribution: keep total mass but redistribute per body-part. + this._applyHumanoidMassDistribution(parts); + + const registerJoint = ( + jointId: number, + stabilityPreset: 'Balanced' | 'Stable' | 'UltraStable' = 'Stable' + ): number => { + if (jointId <= 0) { + return 0; + } + this._sharedData.addJointToRagdollGroup(ragdollId, jointId); + this.setJointStabilityPreset(jointId, stabilityPreset); + this.setJointPriority(jointId, 120); + return jointId; + }; + + // Helper to create a joint between two body parts. + const createHingeJoint = ( + behaviorA: Physics3DRuntimeBehavior, + behaviorB: Physics3DRuntimeBehavior, + minAngle: float, + maxAngle: float + ): number => { + const bodyA = behaviorA.getBody()!; + const bodyB = behaviorB.getBody()!; + const existing = this._sharedData.findJointIdBetweenBodies( + bodyA, + bodyB, + Jolt.EConstraintSubType_Hinge + ); + if (existing !== 0) { + return existing; + } + + const settings = new Jolt.HingeConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + + // Use midpoint between the two bodies + const posA = bodyA.GetCenterOfMassPosition(); + const posB = bodyB.GetCenterOfMassPosition(); + const midX = (posA.GetX() + posB.GetX()) / 2; + const midY = (posA.GetY() + posB.GetY()) / 2; + const midZ = (posA.GetZ() + posB.GetZ()) / 2; + settings.mPoint1 = this._sharedData.getRVec3(midX, midY, midZ); + settings.mPoint2 = this._sharedData.getRVec3(midX, midY, midZ); + + const [axisX, axisY, axisZ] = this._computeLimbAxis( + bodyA, + bodyB, + 0, + -1, + 0 + ); + const [normalX, normalY, normalZ] = this._computePerpendicularAxis( + axisX, + axisY, + axisZ + ); + settings.mHingeAxis1 = this._sharedData.getVec3(axisX, axisY, axisZ); + settings.mHingeAxis2 = this._sharedData.getVec3(axisX, axisY, axisZ); + settings.mNormalAxis1 = this._sharedData.getVec3( + normalX, + normalY, + normalZ + ); + settings.mNormalAxis2 = this._sharedData.getVec3( + normalX, + normalY, + normalZ + ); + const minLimit = Math.min(minAngle, maxAngle); + const maxLimit = Math.max(minAngle, maxAngle); + settings.mLimitsMin = gdjs.toRad(minLimit); + settings.mLimitsMax = gdjs.toRad(maxLimit); + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(bodyA, bodyB); + Jolt.destroy(settings); + return this._sharedData.addJoint(constraint); + }; + + const createSwingTwistJoint = ( + behaviorA: Physics3DRuntimeBehavior, + behaviorB: Physics3DRuntimeBehavior, + normalHalfConeAngle: float, + planeHalfConeAngle: float, + twistMin: float, + twistMax: float + ): number => { + const bodyA = behaviorA.getBody()!; + const bodyB = behaviorB.getBody()!; + const existing = this._sharedData.findJointIdBetweenBodies( + bodyA, + bodyB, + Jolt.EConstraintSubType_SwingTwist + ); + if (existing !== 0) { + return existing; + } + + const settings = new Jolt.SwingTwistConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + + const posA = bodyA.GetCenterOfMassPosition(); + const posB = bodyB.GetCenterOfMassPosition(); + const midX = (posA.GetX() + posB.GetX()) / 2; + const midY = (posA.GetY() + posB.GetY()) / 2; + const midZ = (posA.GetZ() + posB.GetZ()) / 2; + settings.mPosition1 = this._sharedData.getRVec3(midX, midY, midZ); + settings.mPosition2 = this._sharedData.getRVec3(midX, midY, midZ); + + const [twistAxisX, twistAxisY, twistAxisZ] = this._computeLimbAxis( + bodyA, + bodyB, + 0, + -1, + 0 + ); + const [planeAxisX, planeAxisY, planeAxisZ] = + this._computePerpendicularAxis(twistAxisX, twistAxisY, twistAxisZ); + settings.mTwistAxis1 = this._sharedData.getVec3( + twistAxisX, + twistAxisY, + twistAxisZ + ); + settings.mTwistAxis2 = this._sharedData.getVec3( + twistAxisX, + twistAxisY, + twistAxisZ + ); + settings.mPlaneAxis1 = this._sharedData.getVec3( + planeAxisX, + planeAxisY, + planeAxisZ + ); + settings.mPlaneAxis2 = this._sharedData.getVec3( + planeAxisX, + planeAxisY, + planeAxisZ + ); + settings.mNormalHalfConeAngle = gdjs.toRad(normalHalfConeAngle); + settings.mPlaneHalfConeAngle = gdjs.toRad(planeHalfConeAngle); + const minTwist = Math.min(twistMin, twistMax); + const maxTwist = Math.max(twistMin, twistMax); + settings.mTwistMinAngle = gdjs.toRad(minTwist); + settings.mTwistMaxAngle = gdjs.toRad(maxTwist); + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(bodyA, bodyB); + Jolt.destroy(settings); + return this._sharedData.addJoint(constraint); + }; + + const createConeJoint = ( + behaviorA: Physics3DRuntimeBehavior, + behaviorB: Physics3DRuntimeBehavior, + halfAngle: float + ): number => { + const bodyA = behaviorA.getBody()!; + const bodyB = behaviorB.getBody()!; + const existing = this._sharedData.findJointIdBetweenBodies( + bodyA, + bodyB, + Jolt.EConstraintSubType_Cone + ); + if (existing !== 0) { + return existing; + } + + const settings = new Jolt.ConeConstraintSettings(); + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + + const posA = bodyA.GetCenterOfMassPosition(); + const posB = bodyB.GetCenterOfMassPosition(); + const midX = (posA.GetX() + posB.GetX()) / 2; + const midY = (posA.GetY() + posB.GetY()) / 2; + const midZ = (posA.GetZ() + posB.GetZ()) / 2; + settings.mPoint1 = this._sharedData.getRVec3(midX, midY, midZ); + settings.mPoint2 = this._sharedData.getRVec3(midX, midY, midZ); + + const [twistAxisX, twistAxisY, twistAxisZ] = this._computeLimbAxis( + bodyA, + bodyB, + 0, + 1, + 0 + ); + settings.mTwistAxis1 = this._sharedData.getVec3( + twistAxisX, + twistAxisY, + twistAxisZ + ); + settings.mTwistAxis2 = this._sharedData.getVec3( + twistAxisX, + twistAxisY, + twistAxisZ + ); + settings.mHalfConeAngle = gdjs.toRad( + Math.max(0, Math.min(179.0, halfAngle)) + ); + + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(bodyA, bodyB); + Jolt.destroy(settings); + return this._sharedData.addJoint(constraint); + }; + + const createFixedJoint = ( + behaviorA: Physics3DRuntimeBehavior, + behaviorB: Physics3DRuntimeBehavior + ): number => { + const bodyA = behaviorA.getBody()!; + const bodyB = behaviorB.getBody()!; + const existing = this._sharedData.findJointIdBetweenBodies( + bodyA, + bodyB, + Jolt.EConstraintSubType_Fixed + ); + if (existing !== 0) { + return existing; + } + + const settings = new Jolt.FixedConstraintSettings(); + settings.mAutoDetectPoint = true; + settings.mSpace = Jolt.EConstraintSpace_WorldSpace; + // @ts-ignore - Create exists on TwoBodyConstraintSettings WASM + const constraint = settings.Create(bodyA, bodyB); + Jolt.destroy(settings); + return this._sharedData.addJoint(constraint); + }; + + const headB = roleToBehavior.Head || null; + const chestB = roleToBehavior.Chest || null; + const hipsB = roleToBehavior.Hips || null; + const upperArmLB = roleToBehavior.UpperArmL || null; + const lowerArmLB = roleToBehavior.LowerArmL || null; + const upperArmRB = roleToBehavior.UpperArmR || null; + const lowerArmRB = roleToBehavior.LowerArmR || null; + const thighLB = roleToBehavior.ThighL || null; + const shinLB = roleToBehavior.ShinL || null; + const thighRB = roleToBehavior.ThighR || null; + const shinRB = roleToBehavior.ShinR || null; + + // Create all joints and register them to the ragdoll group. + if (headB && chestB && headB.getBody() && chestB.getBody()) { + // Neck: Cone joint with controlled movement. + const jId = registerJoint(createConeJoint(headB, chestB, 35), 'Stable'); + if (jId) { + this.setJointPriority(jId, 140); + } + } + if (chestB && hipsB && chestB.getBody() && hipsB.getBody()) { + // Torso: Fixed core, very stable. + registerJoint(createFixedJoint(chestB, hipsB), 'UltraStable'); + } + if (chestB && upperArmLB && chestB.getBody() && upperArmLB.getBody()) { + // Left shoulder: SwingTwist. + const jId = registerJoint( + createSwingTwistJoint(chestB, upperArmLB, 65, 50, -70, 70), + 'Stable' + ); + if (jId) { + this.setJointSolverOverrides(jId, 10, 5); + } + } + if ( + upperArmLB && + lowerArmLB && + upperArmLB.getBody() && + lowerArmLB.getBody() + ) { + // Left elbow: Hinge (one-way bend). + const jId = registerJoint( + createHingeJoint(upperArmLB, lowerArmLB, 0, 145), + 'Stable' + ); + if (jId) { + this.setHingeJointMaxFriction(jId, 20); + } + } + if (chestB && upperArmRB && chestB.getBody() && upperArmRB.getBody()) { + // Right shoulder: SwingTwist. + const jId = registerJoint( + createSwingTwistJoint(chestB, upperArmRB, 65, 50, -70, 70), + 'Stable' + ); + if (jId) { + this.setJointSolverOverrides(jId, 10, 5); + } + } + if ( + upperArmRB && + lowerArmRB && + upperArmRB.getBody() && + lowerArmRB.getBody() + ) { + // Right elbow: Hinge (one-way bend). + const jId = registerJoint( + createHingeJoint(upperArmRB, lowerArmRB, 0, 145), + 'Stable' + ); + if (jId) { + this.setHingeJointMaxFriction(jId, 20); + } + } + if (hipsB && thighLB && hipsB.getBody() && thighLB.getBody()) { + // Left hip: SwingTwist. + const jId = registerJoint( + createSwingTwistJoint(hipsB, thighLB, 55, 40, -35, 35), + 'Stable' + ); + if (jId) { + this.setJointSolverOverrides(jId, 10, 5); + } + } + if (thighLB && shinLB && thighLB.getBody() && shinLB.getBody()) { + // Left knee: Hinge. + const jId = registerJoint( + createHingeJoint(thighLB, shinLB, 0, 145), + 'Stable' + ); + if (jId) { + this.setHingeJointMaxFriction(jId, 25); + } + } + if (hipsB && thighRB && hipsB.getBody() && thighRB.getBody()) { + // Right hip: SwingTwist. + const jId = registerJoint( + createSwingTwistJoint(hipsB, thighRB, 55, 40, -35, 35), + 'Stable' + ); + if (jId) { + this.setJointSolverOverrides(jId, 10, 5); + } + } + if (thighRB && shinRB && thighRB.getBody() && shinRB.getBody()) { + // Right knee: Hinge. + const jId = registerJoint( + createHingeJoint(thighRB, shinRB, 0, 145), + 'Stable' + ); + if (jId) { + this.setHingeJointMaxFriction(jId, 25); + } + } + + this._configureHumanoidRagdollCollisionFiltering( + ragdollId, + parts.map((part) => ({ role: part.role, behavior: part.behavior })) + ); + + // ===== Start in "Frozen" state (kinematic) ===== + this.setRagdollState(ragdollId, 'Frozen'); + } } gdjs.registerBehavior( diff --git a/Extensions/Physics3DBehavior/Physics3DTools.ts b/Extensions/Physics3DBehavior/Physics3DTools.ts index 27b60ea2f820..37169636394e 100644 --- a/Extensions/Physics3DBehavior/Physics3DTools.ts +++ b/Extensions/Physics3DBehavior/Physics3DTools.ts @@ -3,6 +3,110 @@ namespace gdjs { * @category Behaviors > Physics 3D */ export namespace physics3d { + const _findActivePhysics3DBehavior = ( + object: gdjs.RuntimeObject, + preferredBehaviorName: string + ): gdjs.Physics3DRuntimeBehavior | null => { + const preferredBehavior = object.getBehavior( + preferredBehaviorName + ) as gdjs.Physics3DRuntimeBehavior | null; + if ( + preferredBehavior && + preferredBehavior instanceof gdjs.Physics3DRuntimeBehavior && + preferredBehavior.activated() + ) { + return preferredBehavior; + } + + const rawBehaviors = (object as any)._behaviors as + | gdjs.RuntimeBehavior[] + | undefined; + if (!rawBehaviors) { + return null; + } + for (const behavior of rawBehaviors) { + if ( + behavior instanceof gdjs.Physics3DRuntimeBehavior && + behavior.activated() + ) { + return behavior; + } + } + return null; + }; + + type BulkJointCreator = ( + sourceBehavior: gdjs.Physics3DRuntimeBehavior, + targetObject: gdjs.RuntimeObject, + jointIdVariable: gdjs.Variable + ) => void; + + const _linkPickedObjectsWithJoints = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable, + createJoint: BulkJointCreator + ): void => { + const sourceObjects = gdjs.objectsListsToArray( + sourceObjectsLists as unknown as Hashtable + ); + const targetObjects = gdjs.objectsListsToArray( + targetObjectsLists as unknown as Hashtable + ); + + const processedPairKeys = new Set(); + const tempJointIdVariable = new gdjs.Variable(); + + let linkedPairsCount = 0; + let lastJointId = 0; + for (const sourceObject of sourceObjects) { + const sourceBehavior = _findActivePhysics3DBehavior( + sourceObject, + sourceBehaviorName + ); + if (!sourceBehavior) { + continue; + } + + for (const targetObject of targetObjects) { + if (targetObject === sourceObject) { + continue; + } + const targetBehavior = _findActivePhysics3DBehavior( + targetObject, + sourceBehaviorName + ); + if ( + !targetBehavior || + targetBehavior._sharedData !== sourceBehavior._sharedData + ) { + continue; + } + + const idA = sourceObject.getUniqueId(); + const idB = targetObject.getUniqueId(); + const pairKey = idA < idB ? `${idA}:${idB}` : `${idB}:${idA}`; + if (processedPairKeys.has(pairKey)) { + continue; + } + processedPairKeys.add(pairKey); + + tempJointIdVariable.setNumber(0); + createJoint(sourceBehavior, targetObject, tempJointIdVariable); + const jointId = Math.round(tempJointIdVariable.getAsNumber()); + if (jointId > 0) { + linkedPairsCount++; + lastJointId = jointId; + } + } + } + + linkedPairsCountVariable.setNumber(linkedPairsCount); + lastJointIdVariable.setNumber(lastJointId); + }; + export const areObjectsColliding = function ( objectsLists1: Hashtable>, behaviorName: string, @@ -51,6 +155,275 @@ namespace gdjs { ); }; + export const addFixedJointsBetweenObjects = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable + ): void => { + _linkPickedObjectsWithJoints( + sourceObjectsLists, + sourceBehaviorName, + targetObjectsLists, + linkedPairsCountVariable, + lastJointIdVariable, + (sourceBehavior, targetObject, jointIdVariable) => { + sourceBehavior.addFixedJoint(targetObject, jointIdVariable); + } + ); + }; + + export const addPointJointsBetweenObjects = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + anchorX: float, + anchorY: float, + anchorZ: float, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable + ): void => { + _linkPickedObjectsWithJoints( + sourceObjectsLists, + sourceBehaviorName, + targetObjectsLists, + linkedPairsCountVariable, + lastJointIdVariable, + (sourceBehavior, targetObject, jointIdVariable) => { + sourceBehavior.addPointJoint( + targetObject, + anchorX, + anchorY, + anchorZ, + jointIdVariable + ); + } + ); + }; + + export const addHingeJointsBetweenObjects = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + anchorX: float, + anchorY: float, + anchorZ: float, + axisX: float, + axisY: float, + axisZ: float, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable + ): void => { + _linkPickedObjectsWithJoints( + sourceObjectsLists, + sourceBehaviorName, + targetObjectsLists, + linkedPairsCountVariable, + lastJointIdVariable, + (sourceBehavior, targetObject, jointIdVariable) => { + sourceBehavior.addHingeJoint( + targetObject, + anchorX, + anchorY, + anchorZ, + axisX, + axisY, + axisZ, + jointIdVariable + ); + } + ); + }; + + export const addSliderJointsBetweenObjects = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + axisX: float, + axisY: float, + axisZ: float, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable + ): void => { + _linkPickedObjectsWithJoints( + sourceObjectsLists, + sourceBehaviorName, + targetObjectsLists, + linkedPairsCountVariable, + lastJointIdVariable, + (sourceBehavior, targetObject, jointIdVariable) => { + sourceBehavior.addSliderJoint( + targetObject, + axisX, + axisY, + axisZ, + jointIdVariable + ); + } + ); + }; + + export const addDistanceJointsBetweenObjects = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + minDistance: float, + maxDistance: float, + springFrequency: float, + springDamping: float, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable + ): void => { + _linkPickedObjectsWithJoints( + sourceObjectsLists, + sourceBehaviorName, + targetObjectsLists, + linkedPairsCountVariable, + lastJointIdVariable, + (sourceBehavior, targetObject, jointIdVariable) => { + sourceBehavior.addDistanceJoint( + targetObject, + minDistance, + maxDistance, + springFrequency, + springDamping, + jointIdVariable + ); + } + ); + }; + + export const addPulleyJointsBetweenObjects = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + pulleyAnchorAX: float, + pulleyAnchorAY: float, + pulleyAnchorAZ: float, + pulleyAnchorBX: float, + pulleyAnchorBY: float, + pulleyAnchorBZ: float, + localAnchorAX: float, + localAnchorAY: float, + localAnchorAZ: float, + localAnchorBX: float, + localAnchorBY: float, + localAnchorBZ: float, + totalLength: float, + ratio: float, + enabled: boolean, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable + ): void => { + _linkPickedObjectsWithJoints( + sourceObjectsLists, + sourceBehaviorName, + targetObjectsLists, + linkedPairsCountVariable, + lastJointIdVariable, + (sourceBehavior, targetObject, jointIdVariable) => { + sourceBehavior.addPulleyJoint( + targetObject, + pulleyAnchorAX, + pulleyAnchorAY, + pulleyAnchorAZ, + pulleyAnchorBX, + pulleyAnchorBY, + pulleyAnchorBZ, + localAnchorAX, + localAnchorAY, + localAnchorAZ, + localAnchorBX, + localAnchorBY, + localAnchorBZ, + totalLength, + ratio, + enabled, + jointIdVariable + ); + } + ); + }; + + export const addConeJointsBetweenObjects = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + anchorX: float, + anchorY: float, + anchorZ: float, + twistAxisX: float, + twistAxisY: float, + twistAxisZ: float, + halfConeAngle: float, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable + ): void => { + _linkPickedObjectsWithJoints( + sourceObjectsLists, + sourceBehaviorName, + targetObjectsLists, + linkedPairsCountVariable, + lastJointIdVariable, + (sourceBehavior, targetObject, jointIdVariable) => { + sourceBehavior.addConeJoint( + targetObject, + anchorX, + anchorY, + anchorZ, + twistAxisX, + twistAxisY, + twistAxisZ, + halfConeAngle, + jointIdVariable + ); + } + ); + }; + + export const addSwingTwistJointsBetweenObjects = ( + sourceObjectsLists: Hashtable>, + sourceBehaviorName: string, + targetObjectsLists: Hashtable>, + anchorX: float, + anchorY: float, + anchorZ: float, + twistAxisX: float, + twistAxisY: float, + twistAxisZ: float, + normalHalfConeAngle: float, + planeHalfConeAngle: float, + twistMinAngle: float, + twistMaxAngle: float, + linkedPairsCountVariable: gdjs.Variable, + lastJointIdVariable: gdjs.Variable + ): void => { + _linkPickedObjectsWithJoints( + sourceObjectsLists, + sourceBehaviorName, + targetObjectsLists, + linkedPairsCountVariable, + lastJointIdVariable, + (sourceBehavior, targetObject, jointIdVariable) => { + sourceBehavior.addSwingTwistJoint( + targetObject, + anchorX, + anchorY, + anchorZ, + twistAxisX, + twistAxisY, + twistAxisZ, + normalHalfConeAngle, + planeHalfConeAngle, + twistMinAngle, + twistMaxAngle, + jointIdVariable + ); + } + ); + }; + type BehaviorNamePair = { character: string; physics: string }; const isOnPlatformAdapter = ( diff --git a/newIDE/app/src/BehaviorsEditor/Editors/Physics3DEditor/index.js b/newIDE/app/src/BehaviorsEditor/Editors/Physics3DEditor/index.js index e42efcb21e3b..be131bb36aca 100644 --- a/newIDE/app/src/BehaviorsEditor/Editors/Physics3DEditor/index.js +++ b/newIDE/app/src/BehaviorsEditor/Editors/Physics3DEditor/index.js @@ -139,6 +139,49 @@ const Physics3DEditor = (props: Props): React.Node => { const masksValues = parseInt(properties.get('masks').getValue(), 10); const isStatic = properties.get('bodyType').getValue() === 'Static'; + const isJointEditorSupportedObjectType = + object.getType() === 'Scene3D::Model3DObject' || + object.getType() === 'Scene3D::Cube3DObject'; + const hasJointEditorProperties = properties.has('jointEditorEnabled'); + const hasRagdollRoleProperty = properties.has('ragdollRole'); + const hasRagdollGroupTagProperty = properties.has('ragdollGroupTag'); + const hasJointAutoWakeBodiesProperty = properties.has('jointAutoWakeBodies'); + const hasJointAutoStabilityPresetProperty = properties.has( + 'jointAutoStabilityPreset' + ); + const hasJointAutoBreakForceProperty = properties.has('jointAutoBreakForce'); + const hasJointAutoBreakTorqueProperty = properties.has( + 'jointAutoBreakTorque' + ); + const hasRagdollAndJointRealismProperties = + hasRagdollRoleProperty || + hasRagdollGroupTagProperty || + hasJointAutoWakeBodiesProperty || + hasJointAutoStabilityPresetProperty || + hasJointAutoBreakForceProperty || + hasJointAutoBreakTorqueProperty; + const isRagdollSectionExpandedByDefault = + (hasRagdollRoleProperty && + properties.get('ragdollRole').getValue() !== 'None') || + (hasRagdollGroupTagProperty && + !!properties + .get('ragdollGroupTag') + .getValue() + .trim()); + const isJointAutoWakeBodiesEnabled = + hasJointAutoWakeBodiesProperty && + properties.get('jointAutoWakeBodies').getValue() === 'true'; + const isJointEditorEnabled = + hasJointEditorProperties && + properties.get('jointEditorEnabled').getValue() === 'true'; + const isJointEditorPreviewEnabled = + hasJointEditorProperties && + properties.has('jointEditorPreviewEnabled') && + properties.get('jointEditorPreviewEnabled').getValue() === 'true'; + const isJointEditorUsingCustomAxis = + hasJointEditorProperties && + properties.has('jointEditorUseCustomAxis') && + properties.get('jointEditorUseCustomAxis').getValue() === 'true'; const canShapeBeOriented = properties.get('shape').getValue() !== 'Sphere' && @@ -493,6 +536,339 @@ const Physics3DEditor = (props: Props): React.Node => { disabled={isStatic} /> + {hasRagdollAndJointRealismProperties && ( + + + + Ragdoll & Joint Realism + + + + + {hasRagdollRoleProperty && ( + + + updateBehaviorProperty('ragdollRole', newValue) + } + /> + + )} + {hasRagdollGroupTagProperty && ( + + + updateBehaviorProperty('ragdollGroupTag', newValue) + } + /> + + )} + {hasJointAutoWakeBodiesProperty && ( + + + updateBehaviorProperty( + 'jointAutoWakeBodies', + checked ? '1' : '0' + ) + } + /> + + )} + {hasJointAutoStabilityPresetProperty && ( + + + updateBehaviorProperty( + 'jointAutoStabilityPreset', + newValue + ) + } + /> + + )} + {(hasJointAutoBreakForceProperty || + hasJointAutoBreakTorqueProperty) && ( + + {hasJointAutoBreakForceProperty && ( + + updateBehaviorProperty('jointAutoBreakForce', newValue) + } + /> + )} + {hasJointAutoBreakTorqueProperty && ( + + updateBehaviorProperty('jointAutoBreakTorque', newValue) + } + /> + )} + + )} + + + + )} + {hasJointEditorProperties && !isJointEditorSupportedObjectType && ( + + + + Joint editor is available only for 3D Model and 3D Box objects. + + + + )} + {hasJointEditorProperties && isJointEditorSupportedObjectType && ( + + + + Joint Editor + + + + + + + updateBehaviorProperty( + 'jointEditorEnabled', + checked ? '1' : '0' + ) + } + /> + {properties.has('jointEditorPreviewEnabled') && ( + + updateBehaviorProperty( + 'jointEditorPreviewEnabled', + checked ? '1' : '0' + ) + } + /> + )} + + + + updateBehaviorProperty('jointEditorType', newValue) + } + /> + + updateBehaviorProperty( + 'jointEditorTargetObjectName', + newValue + ) + } + /> + + {properties.has('jointEditorPreviewSize') && ( + + + updateBehaviorProperty('jointEditorPreviewSize', newValue) + } + /> + + )} + + + updateBehaviorProperty('jointEditorAnchorOffsetX', newValue) + } + /> + + updateBehaviorProperty('jointEditorAnchorOffsetY', newValue) + } + /> + + updateBehaviorProperty('jointEditorAnchorOffsetZ', newValue) + } + /> + + + + updateBehaviorProperty( + 'jointEditorTargetAnchorOffsetX', + newValue + ) + } + /> + + updateBehaviorProperty( + 'jointEditorTargetAnchorOffsetY', + newValue + ) + } + /> + + updateBehaviorProperty( + 'jointEditorTargetAnchorOffsetZ', + newValue + ) + } + /> + + + + updateBehaviorProperty( + 'jointEditorUseCustomAxis', + checked ? '1' : '0' + ) + } + /> + + {isJointEditorUsingCustomAxis && ( + + + updateBehaviorProperty('jointEditorAxisX', newValue) + } + /> + + updateBehaviorProperty('jointEditorAxisY', newValue) + } + /> + + updateBehaviorProperty('jointEditorAxisZ', newValue) + } + /> + + )} + + + updateBehaviorProperty('jointEditorHingeMinAngle', newValue) + } + /> + + updateBehaviorProperty('jointEditorHingeMaxAngle', newValue) + } + /> + + + + updateBehaviorProperty('jointEditorDistanceMin', newValue) + } + /> + + updateBehaviorProperty('jointEditorDistanceMax', newValue) + } + /> + + + + + )} Date: Mon, 2 Mar 2026 19:11:25 +0200 Subject: [PATCH 2/5] style: run prettier on changed files for CI --- .../Physics3DRuntimeBehavior.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts index e6b5056c3533..999503fb31ee 100644 --- a/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts +++ b/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.ts @@ -4799,10 +4799,7 @@ namespace gdjs { settings.mPlaneHalfConeAngle = gdjs.toRad(clampedPlaneHalfConeAngle); const orderedMinTwistAngle = Math.min(twistMinAngle, twistMaxAngle); const orderedMaxTwistAngle = Math.max(twistMinAngle, twistMaxAngle); - const minTwistAngle = Math.max( - -179, - Math.min(179, orderedMinTwistAngle) - ); + const minTwistAngle = Math.max(-179, Math.min(179, orderedMinTwistAngle)); const maxTwistAngle = Math.max( minTwistAngle, Math.max(-179, Math.min(179, orderedMaxTwistAngle)) @@ -5261,7 +5258,10 @@ namespace gdjs { setPulleyJointLength(jointId: integer | string, totalLength: float): void { const constraint = this._sharedData.getJoint(jointId); if (!constraint) return; - const pulleyConstraint = Jolt.castObject(constraint, Jolt.PulleyConstraint); + const pulleyConstraint = Jolt.castObject( + constraint, + Jolt.PulleyConstraint + ); const clampedLength = Math.max(epsilon, totalLength); const lengthInMeters = clampedLength * this._sharedData.worldInvScale; pulleyConstraint.SetLength(lengthInMeters, lengthInMeters); @@ -5274,7 +5274,10 @@ namespace gdjs { getPulleyJointCurrentLength(jointId: integer | string): float { const constraint = this._sharedData.getJoint(jointId); if (!constraint) return 0; - const pulleyConstraint = Jolt.castObject(constraint, Jolt.PulleyConstraint); + const pulleyConstraint = Jolt.castObject( + constraint, + Jolt.PulleyConstraint + ); return pulleyConstraint.GetCurrentLength() * this._sharedData.worldScale; } @@ -5285,10 +5288,13 @@ namespace gdjs { getPulleyJointTotalLength(jointId: integer | string): float { const constraint = this._sharedData.getJoint(jointId); if (!constraint) return 0; - const pulleyConstraint = Jolt.castObject(constraint, Jolt.PulleyConstraint); + const pulleyConstraint = Jolt.castObject( + constraint, + Jolt.PulleyConstraint + ); return ( - ((pulleyConstraint.GetMinLength() + pulleyConstraint.GetMaxLength()) * - 0.5) * + (pulleyConstraint.GetMinLength() + pulleyConstraint.GetMaxLength()) * + 0.5 * this._sharedData.worldScale ); } From b84a7becf33ec6f611252018eaceffa9f0374286 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Mar 2026 19:33:54 +0200 Subject: [PATCH 3/5] fix(ci): validate downloaded libGD assets before type checks --- newIDE/app/scripts/import-libGD.js | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/newIDE/app/scripts/import-libGD.js b/newIDE/app/scripts/import-libGD.js index 2accaaeac2f9..f131bdbbc508 100644 --- a/newIDE/app/scripts/import-libGD.js +++ b/newIDE/app/scripts/import-libGD.js @@ -102,12 +102,37 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { ); }; + const validateDownloadedLibGdJs = baseUrl => { + const libGdJsPath = path.join(__dirname, '..', 'public', 'libGD.js'); + const libGdWasmPath = path.join(__dirname, '..', 'public', 'libGD.wasm'); + + if (!shell.test('-f', libGdJsPath) || !shell.test('-f', libGdWasmPath)) { + shell.echo( + `⚠️ Downloaded libGD.js is incomplete (baseUrl=${baseUrl}), trying another source.` + ); + throw new Error('Incomplete libGD.js download'); + } + + const syntaxCheckResult = shell.exec(`node --check "${libGdJsPath}"`, { + silent: true, + }); + if (syntaxCheckResult.code !== 0) { + shell.echo( + `⚠️ Downloaded libGD.js is not valid JavaScript (baseUrl=${baseUrl}), trying another source.` + ); + throw new Error('Invalid libGD.js JavaScript syntax'); + } + }; + const downloadLibGdJs = baseUrl => Promise.all([ downloadLocalFile(baseUrl + '/libGD.js', '../public/libGD.js'), downloadLocalFile(baseUrl + '/libGD.wasm', '../public/libGD.wasm'), ]).then( - responses => {}, + responses => { + validateDownloadedLibGdJs(baseUrl); + return responses; + }, error => { if (error.statusCode === 403) { shell.echo( From d1f7f7abd3aa71a76c654509e898fca5cc0e6f4f Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Mar 2026 20:21:40 +0200 Subject: [PATCH 4/5] fix(ci): retry and validate libGD downloads in detached head --- newIDE/app/scripts/import-libGD.js | 53 +++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/newIDE/app/scripts/import-libGD.js b/newIDE/app/scripts/import-libGD.js index f131bdbbc508..c1b595122923 100644 --- a/newIDE/app/scripts/import-libGD.js +++ b/newIDE/app/scripts/import-libGD.js @@ -1,6 +1,7 @@ const shell = require('shelljs'); const { downloadLocalFile } = require('./lib/DownloadLocalFile'); const path = require('path'); +const fs = require('fs'); const sourceDirectory = '../../../Binaries/embuild/GDevelop.js'; const destinationTestDirectory = '../node_modules/libGD.js-for-tests-only'; @@ -49,13 +50,19 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { let branch = (branchShellString.stdout || '').trim(); if (branch === 'HEAD') { // We're in detached HEAD. Try to read the branch from the CI environment variables. - if (process.env.APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH) { + if (process.env.SEMAPHORE_GIT_BRANCH) { + branch = process.env.SEMAPHORE_GIT_BRANCH; + } else if (process.env.APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH) { branch = process.env.APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH; } else if (process.env.APPVEYOR_REPO_BRANCH) { branch = process.env.APPVEYOR_REPO_BRANCH; } } + if (branch === 'HEAD') { + branch = ''; + } + if (!branch) { shell.echo( `⚠️ Can't find the branch of the associated commit - if you're in detached HEAD, you need to be on a branch instead.` @@ -85,7 +92,7 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { } resolve( - downloadLibGdJs( + downloadLibGdJsWithRetries( `https://s3.amazonaws.com/gdevelop-gdevelop.js/${branch}/commit/${hash}` ) ); @@ -97,28 +104,43 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { `ℹ️ Trying to download libGD.js from ${branchName}, latest build.` ); - return downloadLibGdJs( + return downloadLibGdJsWithRetries( `https://s3.amazonaws.com/gdevelop-gdevelop.js/${branchName}/latest` ); }; + const MIN_LIBGD_JS_SIZE_BYTES = 1024 * 1024; + const MIN_LIBGD_WASM_SIZE_BYTES = 1024 * 1024; + const validateDownloadedLibGdJs = baseUrl => { const libGdJsPath = path.join(__dirname, '..', 'public', 'libGD.js'); const libGdWasmPath = path.join(__dirname, '..', 'public', 'libGD.wasm'); if (!shell.test('-f', libGdJsPath) || !shell.test('-f', libGdWasmPath)) { shell.echo( - `⚠️ Downloaded libGD.js is incomplete (baseUrl=${baseUrl}), trying another source.` + `Warning: Downloaded libGD.js is incomplete (baseUrl=${baseUrl}), trying another source.` ); throw new Error('Incomplete libGD.js download'); } + const libGdJsSize = fs.statSync(libGdJsPath).size; + const libGdWasmSize = fs.statSync(libGdWasmPath).size; + if ( + libGdJsSize < MIN_LIBGD_JS_SIZE_BYTES || + libGdWasmSize < MIN_LIBGD_WASM_SIZE_BYTES + ) { + shell.echo( + `Warning: Downloaded libGD.js assets are unexpectedly small (baseUrl=${baseUrl}), trying another source.` + ); + throw new Error('Incomplete libGD.js download (unexpected file size)'); + } + const syntaxCheckResult = shell.exec(`node --check "${libGdJsPath}"`, { silent: true, }); if (syntaxCheckResult.code !== 0) { shell.echo( - `⚠️ Downloaded libGD.js is not valid JavaScript (baseUrl=${baseUrl}), trying another source.` + `Warning: Downloaded libGD.js is not valid JavaScript (baseUrl=${baseUrl}), trying another source.` ); throw new Error('Invalid libGD.js JavaScript syntax'); } @@ -158,6 +180,27 @@ if (shell.test('-f', path.join(sourceDirectory, 'libGD.js'))) { } ); + const wait = milliseconds => + new Promise(resolve => { + setTimeout(resolve, milliseconds); + }); + + const downloadLibGdJsWithRetries = (baseUrl, maxAttempts = 3) => { + let attempt = 1; + const download = () => + downloadLibGdJs(baseUrl).catch(error => { + if (attempt >= maxAttempts) { + throw error; + } + attempt += 1; + shell.echo( + `Warning: Retrying libGD.js download from ${baseUrl} (attempt ${attempt}/${maxAttempts}).` + ); + return wait(attempt * 1000).then(download); + }); + return download(); + }; + const onLibGdJsDownloaded = response => { shell.echo('✅ libGD.js downloaded and stored in public/libGD.js'); From e1f6efd1bfd38b78b5440b9866fc1b3916229f3a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 3 Mar 2026 21:37:47 +0200 Subject: [PATCH 5/5] fix(physics3d): restore MassCenterY expression registration --- Extensions/Physics3DBehavior/JsExtension.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Extensions/Physics3DBehavior/JsExtension.js b/Extensions/Physics3DBehavior/JsExtension.js index 1fd25ceee789..93a0c437a031 100644 --- a/Extensions/Physics3DBehavior/JsExtension.js +++ b/Extensions/Physics3DBehavior/JsExtension.js @@ -2518,6 +2518,19 @@ module.exports = { .getCodeExtraInformation() .setFunctionName('getMassCenterX'); + aut + .addExpression( + 'MassCenterY', + _('Mass center Y'), + _('Mass center Y'), + '', + 'JsPlatform/Extensions/physics3d.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter('behavior', _('Behavior'), 'Physics3DBehavior') + .getCodeExtraInformation() + .setFunctionName('getMassCenterY'); + aut .addExpression( 'MassCenterZ',