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(); } }