diff --git a/Extensions/AI/BehaviorTree.js b/Extensions/AI/BehaviorTree.js new file mode 100644 index 000000000000..11ac91973a19 --- /dev/null +++ b/Extensions/AI/BehaviorTree.js @@ -0,0 +1,27 @@ +// Very small behavior tree-like utility +export class BehaviorTreeNode { + constructor(tickFunc) { this.tickFunc = tickFunc; } + tick(entity, dt) { return this.tickFunc(entity, dt); } +} + +export class SequenceNode { + constructor(children) { this.children = children; } + tick(entity, dt) { + for (const c of this.children) { + const r = c.tick(entity, dt); + if (!r) return false; + } + return true; + } +} + +export class SelectorNode { + constructor(children) { this.children = children; } + tick(entity, dt) { + for (const c of this.children) { + const r = c.tick(entity, dt); + if (r) return true; + } + return false; + } +} diff --git a/Extensions/AI/Pathfinding.js b/Extensions/AI/Pathfinding.js new file mode 100644 index 000000000000..9442db9f6e13 --- /dev/null +++ b/Extensions/AI/Pathfinding.js @@ -0,0 +1,80 @@ +// Lightweight A* pathfinding helper (grid based) +export default class AStar { + constructor(grid, width, height) { + this.grid = grid; // 0 = walkable, 1 = blocked + this.width = width; + this.height = height; + } + + _index(x, y) { + return y * this.width + x; + } + + _neighbors(x, y) { + const n = []; + const dirs = [ [1,0],[-1,0],[0,1],[0,-1] ]; + for (const d of dirs) { + const nx = x + d[0], ny = y + d[1]; + if (nx >=0 && ny >=0 && nx < this.width && ny < this.height && this.grid[this._index(nx,ny)] === 0) n.push([nx,ny]); + } + return n; + } + + heuristic(a,b) { + return Math.abs(a[0]-b[0]) + Math.abs(a[1]-b[1]); + } + + findPath(start, goal) { + const startKey = `${start[0]},${start[1]}`; + const goalKey = `${goal[0]},${goal[1]}`; + const open = new Map(); + const closed = new Set(); + const gScore = new Map(); + const fScore = new Map(); + const cameFrom = new Map(); + + function key(p){ return `${p[0]},${p[1]}`; } + + open.set(startKey, start); + gScore.set(startKey, 0); + fScore.set(startKey, this.heuristic(start, goal)); + + while (open.size > 0) { + // get node in open with lowest fScore + let currentKey = null; + let current = null; + let lowest = Infinity; + for (const [k,v] of open) { + const f = fScore.get(k) || Infinity; + if (f < lowest) { lowest = f; currentKey = k; current = v; } + } + if (!current) break; + if (currentKey === goalKey) { + // reconstruct path + const path = []; + let k = currentKey; + while (k) { + const parts = k.split(',').map(Number); + path.push(parts); + k = cameFrom.get(k); + } + return path.reverse(); + } + + open.delete(currentKey); + closed.add(currentKey); + + for (const nb of this._neighbors(current[0], current[1])) { + const nbKey = key(nb); + if (closed.has(nbKey)) continue; + const tentativeG = (gScore.get(currentKey) || Infinity) + 1; + if (!open.has(nbKey)) open.set(nbKey, nb); + else if (tentativeG >= (gScore.get(nbKey)||Infinity)) continue; + cameFrom.set(nbKey, currentKey); + gScore.set(nbKey, tentativeG); + fScore.set(nbKey, tentativeG + this.heuristic(nb, goal)); + } + } + return null; // no path + } +} diff --git a/Extensions/AI/SteeringBehaviors.js b/Extensions/AI/SteeringBehaviors.js new file mode 100644 index 000000000000..a755a849c9d2 --- /dev/null +++ b/Extensions/AI/SteeringBehaviors.js @@ -0,0 +1,25 @@ +// Simple steering behaviors (seek, flee, arrive, wander) +export const Steering = { + seek: function(position, target, maxSpeed) { + const desired = { x: target.x - position.x, y: target.y - position.y }; + const len = Math.hypot(desired.x, desired.y) || 1; + desired.x /= len; desired.y /= len; + return { x: desired.x * maxSpeed, y: desired.y * maxSpeed }; + }, + flee: function(position, target, maxSpeed) { + const desired = { x: position.x - target.x, y: position.y - target.y }; + const len = Math.hypot(desired.x, desired.y) || 1; + desired.x /= len; desired.y /= len; + return { x: desired.x * maxSpeed, y: desired.y * maxSpeed }; + }, + arrive: function(position, target, maxSpeed, slowdownRadius) { + const desired = { x: target.x - position.x, y: target.y - position.y }; + const dist = Math.hypot(desired.x, desired.y); + if (dist < 0.0001) return { x:0, y:0 }; + let speed = maxSpeed; + if (dist < slowdownRadius) speed = maxSpeed * (dist / slowdownRadius); + desired.x = (desired.x / dist) * speed; + desired.y = (desired.y / dist) * speed; + return desired; + } +}; diff --git a/Extensions/Physics3DBehavior/SoftBodyBodyUpdater.ts b/Extensions/Physics3DBehavior/SoftBodyBodyUpdater.ts new file mode 100644 index 000000000000..f02a299f368b --- /dev/null +++ b/Extensions/Physics3DBehavior/SoftBodyBodyUpdater.ts @@ -0,0 +1,145 @@ +namespace gdjs { + export namespace Physics3DRuntimeBehavior { + export class SoftBodyBodyUpdater implements gdjs.Physics3DRuntimeBehavior.BodyUpdater { + behavior: gdjs.Physics3DRuntimeBehavior; + constructor(behavior: gdjs.Physics3DRuntimeBehavior) { + this.behavior = behavior; + } + + createAndAddBody(): Jolt.Body | null { + const { behavior } = this; + const shared = behavior._sharedData; + const bodyInterface = shared.bodyInterface; + + // Try to create soft body creation settings and map some properties. + // The behavior may not expose soft-specific properties yet; use defaults + // and gracefully handle missing APIs. + try { + const settings = new Jolt.SoftBodyCreationSettings(); + + // Map common properties if present on behavior, otherwise use sensible defaults + if ((behavior as any).softLinearDamping !== undefined) { + settings.set_mLinearDamping((behavior as any).softLinearDamping); + } else { + settings.set_mLinearDamping(0.02); + } + if ((behavior as any).softMaxLinearVelocity !== undefined) { + settings.set_mMaxLinearVelocity((behavior as any).softMaxLinearVelocity); + } + if ((behavior as any).softRestitution !== undefined) { + settings.set_mRestitution((behavior as any).softRestitution); + } + if ((behavior as any).softFriction !== undefined) { + settings.set_mFriction((behavior as any).softFriction); + } + if ((behavior as any).softPressure !== undefined) { + settings.set_mPressure((behavior as any).softPressure); + } + if ((behavior as any).softGravityFactor !== undefined) { + settings.set_mGravityFactor((behavior as any).softGravityFactor); + } + if ((behavior as any).softVertexRadius !== undefined) { + settings.set_mVertexRadius((behavior as any).softVertexRadius); + } else { + settings.set_mVertexRadius(0.02); + } + if ((behavior as any).softUpdatePosition !== undefined) { + settings.set_mUpdatePosition(!!(behavior as any).softUpdatePosition); + } + if ((behavior as any).softEnableSkinConstraints !== undefined) { + settings.set_mEnableSkinConstraints(!!(behavior as any).softEnableSkinConstraints); + } + if ((behavior as any).softFacesDoubleSided !== undefined) { + settings.set_mFacesDoubleSided(!!(behavior as any).softFacesDoubleSided); + } + + // If the owner is a model and has mesh, attempt to build a soft body mesh from it. + // For now, rely on the behavior to specify vertices/faces (future improvement: generate cloth grid) + + // Create and add soft body to the physics system. The CreateAndAddSoftBody returns a BodyID. + const bodyID = bodyInterface.CreateAndAddSoftBody( + settings, + Jolt.EActivation_Activate + ); + + Jolt.destroy(settings); + + if (!bodyID) return null; + + // Get the body pointer via the BodyLockInterface + const bodyLockInterface = shared.physicsSystem.GetBodyLockInterface(); + const body = bodyLockInterface.TryGetBody(bodyID); + if (!body) return null; + + // Associate behavior to the body + body.gdjsAssociatedBehavior = behavior; + + return body; + } catch (err) { + console.warn('SoftBody creation failed, falling back to rigid body', err); + return new gdjs.Physics3DRuntimeBehavior.DefaultBodyUpdater(behavior).createAndAddBody(); + } + } + + updateObjectFromBody(): void { + const { behavior } = this; + const body = behavior._body; + if (!body) return; + + // Try to update the owner visual mesh from the soft-body vertices. + try { + SoftBodyVisualizer.updateGeometryFromBody(body, behavior.owner3D, behavior._sharedData); + } catch (err) { + // Fallback: update position/rotation if possible + if (body.IsActive && body.IsActive()) { + behavior._moveObjectToPhysicsPosition(body.GetPosition()); + behavior._moveObjectToPhysicsRotation(body.GetRotation()); + } + } + } + + updateBodyFromObject(): void { + const { behavior } = this; + // For soft bodies, we might want to update a root transform or skinning transform. + // For now, update the global transform if the owner moved. + if (behavior._body === null) { + if (!behavior._createBody()) return; + } + const body = behavior._body!; + const shared = behavior._sharedData; + + if ( + behavior._objectOldX !== behavior.owner3D.getX() || + behavior._objectOldY !== behavior.owner3D.getY() || + behavior._objectOldZ !== behavior.owner3D.getZ() || + behavior._objectOldRotationX !== behavior.owner3D.getRotationX() || + behavior._objectOldRotationY !== behavior.owner3D.getRotationY() || + behavior._objectOldRotationZ !== behavior.owner3D.getAngle() + ) { + shared.bodyInterface.SetPositionAndRotationWhenChanged( + body.GetID(), + behavior._getPhysicsPosition(shared.getRVec3(0, 0, 0)), + behavior._getPhysicsRotation(shared.getQuat(0, 0, 0, 1)), + Jolt.EActivation_Activate + ); + } + } + + recreateShape(): void { + // Recreate by destroying and creating again + this.destroyBody(); + this.createAndAddBody(); + } + + destroyBody(): void { + const { behavior } = this; + const shared = behavior._sharedData; + if (behavior._body !== null) { + shared.bodyInterface.RemoveBody(behavior._body.GetID()); + shared.bodyInterface.DestroyBody(behavior._body.GetID()); + behavior._body = null; + } + } + } + } +} diff --git a/Extensions/Physics3DBehavior/SoftBodyVisualizer.ts b/Extensions/Physics3DBehavior/SoftBodyVisualizer.ts new file mode 100644 index 000000000000..9fa2fe4b14dc --- /dev/null +++ b/Extensions/Physics3DBehavior/SoftBodyVisualizer.ts @@ -0,0 +1,95 @@ +import * as THREE from 'three'; + +namespace gdjs { + export const SoftBodyVisualizer = { + /** + * Update a THREE.Mesh geometry from a Jolt soft body. + * This is a best-effort helper: it will try to read vertices and faces + * and update the BufferGeometry attributes to match. + */ + updateGeometryFromBody(body: Jolt.Body, owner3D: any, sharedData: any) { + if (!body) return; + // Owner3D should expose a THREE object via get3DRendererObject + const threeObject = owner3D.get3DRendererObject && owner3D.get3DRendererObject(); + if (!threeObject) return; + + // The 3D renderer object is expected to be a Mesh + let mesh: THREE.Mesh | null = null; + if ((threeObject as any).isMesh) mesh = threeObject as THREE.Mesh; + else { + // Try to find a mesh child + (threeObject as THREE.Object3D).traverse((o) => { + if (!mesh && (o as any).isMesh) mesh = o as THREE.Mesh; + }); + } + if (!mesh) return; + + // Attempt to get soft body vertices and faces via Jolt API + try { + const softVerts = (body as any).GetVertices && (body as any).GetVertices(); + const softFaces = (body as any).GetFaces && (body as any).GetFaces(); + if (!softVerts) return; + + const vertCount = softVerts.size(); + const geom = mesh.geometry as THREE.BufferGeometry; + + // Ensure position attribute length + let position = geom.getAttribute('position'); + if (!position || position.count !== vertCount) { + const array = new Float32Array(vertCount * 3); + geom.setAttribute('position', new THREE.BufferAttribute(array, 3).setUsage(THREE.DynamicDrawUsage)); + position = geom.getAttribute('position'); + } + + const positionArray = position.array as Float32Array; + const tempVec = new Jolt.Vec3(); + for (let i = 0; i < vertCount; i++) { + const sv = softVerts.at(i); + // Try several access patterns depending on binding + let v: Jolt.Vec3 | null = null; + if (sv.get_mPosition) v = sv.get_mPosition(); + else if (sv.mPosition) v = sv.mPosition; + else if (sv.GetPosition) v = sv.GetPosition(); + + if (!v) { + continue; + } + positionArray[i * 3] = v.GetX() * sharedData.worldScale; + positionArray[i * 3 + 1] = v.GetY() * sharedData.worldScale; + positionArray[i * 3 + 2] = v.GetZ() * sharedData.worldScale; + } + position.needsUpdate = true; + + // If faces exist and the index buffer differs size, update index + if (softFaces && softFaces.size && softFaces.size() > 0) { + const faceCount = softFaces.size(); + let indexAttr = geom.getIndex(); + if (!indexAttr || indexAttr.count !== faceCount * 3) { + const idxArray = new Uint32Array(faceCount * 3); + geom.setIndex(new THREE.BufferAttribute(idxArray, 1)); + indexAttr = geom.getIndex(); + } + const idxArray = indexAttr.array as Uint32Array; + for (let f = 0; f < faceCount; f++) { + const face = softFaces.at(f); + // face.get_mVertex likely exists + const a = face.get_mVertex ? face.get_mVertex(0) : (face.mVertex ? face.mVertex[0] : 0); + const b = face.get_mVertex ? face.get_mVertex(1) : (face.mVertex ? face.mVertex[1] : 0); + const c = face.get_mVertex ? face.get_mVertex(2) : (face.mVertex ? face.mVertex[2] : 0); + idxArray[f * 3] = a; + idxArray[f * 3 + 1] = b; + idxArray[f * 3 + 2] = c; + } + indexAttr.needsUpdate = true; + } + + // Recompute normals + geom.computeVertexNormals(); + geom.attributes.normal && (geom.attributes.normal.needsUpdate = true); + geom.computeBoundingSphere(); + } catch (err) { + console.warn('SoftBodyVisualizer failed to update geometry:', err); + } + }, + }; +} diff --git a/Extensions/Physics3DBehavior/softbody-loader.js b/Extensions/Physics3DBehavior/softbody-loader.js new file mode 100644 index 000000000000..746f0bc90660 --- /dev/null +++ b/Extensions/Physics3DBehavior/softbody-loader.js @@ -0,0 +1,7 @@ +(function(){ +// softbody-loader: ensure SoftBodyBodyUpdater is available and register fallback +if (typeof gdjs !== 'undefined' && gdjs.Physics3DRuntimeBehavior) { + // If SoftBodyBodyUpdater is available, nothing to do. + // This script exists so that bundlers/loaders pick up SoftBody modules. +} +})();