Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Extensions/AI/BehaviorTree.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
80 changes: 80 additions & 0 deletions Extensions/AI/Pathfinding.js
Original file line number Diff line number Diff line change
@@ -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
}
}
25 changes: 25 additions & 0 deletions Extensions/AI/SteeringBehaviors.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
145 changes: 145 additions & 0 deletions Extensions/Physics3DBehavior/SoftBodyBodyUpdater.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
}
95 changes: 95 additions & 0 deletions Extensions/Physics3DBehavior/SoftBodyVisualizer.ts
Original file line number Diff line number Diff line change
@@ -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);
}
},
};
}
7 changes: 7 additions & 0 deletions Extensions/Physics3DBehavior/softbody-loader.js
Original file line number Diff line number Diff line change
@@ -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.
}
})();