Files
bo3-js/js/instances/BasePart.js

153 lines
4.2 KiB
JavaScript
Raw Normal View History

2025-10-12 18:03:20 +02:00
import * as THREE from "three";
import * as CANNON from "cannon-es";
import { Instance } from "./Instance.js";
export class BasePart extends Instance {
constructor() {
super("BasePart");
// === Visual properties ===
this.Color = 0x00ff00;
this.Size = new THREE.Vector3(1, 1, 1);
this.Position = new THREE.Vector3(0, 0, 0);
this.Orientation = new THREE.Euler(0, 0, 0);
this.Anchored = false; // Roblox-style toggle
// === Three.js setup ===
const geometry = new THREE.BoxGeometry(this.Size.x, this.Size.y, this.Size.z);
const material = new THREE.MeshStandardMaterial({ color: this.Color });
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.castShadow = true;
this.mesh.receiveShadow = true;
this.mesh.userData.basePart = this;
// === Cannon.js setup ===
const shape = new CANNON.Box(
new CANNON.Vec3(this.Size.x / 2, this.Size.y / 2, this.Size.z / 2)
);
this.body = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(this.Position.x, this.Position.y, this.Position.z),
shape: shape,
});
this.body.userData = { basePart: this };
this._originalMass = this.body.mass; // store default mass
this._renderService = null; // assigned when added to scene
}
// === Scene lifecycle ===
OnAddedToScene(parent) {
const dm = this.GetDataModel();
const renderService = dm.GetService("RenderService");
this._renderService = renderService;
if (renderService.scene) renderService.scene.add(this.mesh);
if (!renderService.physicsWorld) renderService._initPhysicsWorld();
renderService.physicsWorld.addBody(this.body);
this._applyAnchoredState();
}
// === Anchoring logic ===
setAnchored(value) {
if (this.Anchored === value) return;
this.Anchored = value;
this._applyAnchoredState();
}
_applyAnchoredState() {
if (!this.body) return;
if (this.Anchored) {
this.body.type = CANNON.Body.STATIC;
this.body.mass = 0;
this.body.velocity.set(0, 0, 0);
this.body.angularVelocity.set(0, 0, 0);
} else {
this.body.type = CANNON.Body.DYNAMIC;
this.body.mass = this._originalMass || 1;
}
this.body.updateMassProperties();
}
// === Updates ===
updateVisual() {
// Only update mesh from physics if not anchored
if (!this.Anchored) {
this.mesh.position.copy(this.body.position);
this.mesh.quaternion.copy(this.body.quaternion);
}
}
updateBodyPosition() {
this.body.position.set(this.Position.x, this.Position.y, this.Position.z);
this.body.quaternion.setFromEuler(
this.Orientation.x,
this.Orientation.y,
this.Orientation.z
);
this.body.velocity.set(0, 0, 0);
this.body.angularVelocity.set(0, 0, 0);
}
updateSizes() {
// Update Three geometry
this.mesh.geometry.dispose();
this.mesh.geometry = new THREE.BoxGeometry(this.Size.x, this.Size.y, this.Size.z);
// Update Cannon shape
this.body.shapes = [];
const newShape = new CANNON.Box(
new CANNON.Vec3(this.Size.x / 2, this.Size.y / 2, this.Size.z / 2)
);
this.body.addShape(newShape);
this.body.updateMassProperties();
}
OnReplicated() {
if (this.IsReplicated) {
this.body.type = CANNON.Body.KINEMATIC;
this.body.velocity.set(0, 0, 0);
this.body.angularVelocity.set(0, 0, 0);
}
this.body.position.copy(this.Position);
this.body.quaternion.setFromEuler(
this.Orientation.x,
this.Orientation.y,
this.Orientation.z
);
this.updateVisual();
}
OnPhysicsTick() {
if (this.body.position.y < -1000) {
// Fell out of world, destroy like in Roblox
this.Destroy();
console.debug("BasePart fell out of world and was destroyed.");
return;
}
if (this.IsReplicated || this.Anchored) return;
this.Position.set(
this.body.position.x,
this.body.position.y,
this.body.position.z
);
this.Orientation.setFromQuaternion(this.body.quaternion);
}
// === Cleanup ===
Destroy() {
if (this._renderService) {
if (this.mesh.parent) this.mesh.parent.remove(this.mesh);
if (this._renderService.physicsWorld)
this._renderService.physicsWorld.removeBody(this.body);
}
super.Destroy();
}
}