feat: add train driving system with 3D model and controls
- Added Train and TrainController classes in train.js - Created TrainRenderer for Three.js visualization - Integrated train controls into game.js - Updated index.html with train UI controls - Added train control styles to css/styles.css - Created test file for train module - Train can accelerate, brake, reverse and stop - Keyboard controls: W/↑ accelerate, S/↓ brake, R/D reverse, SPACE stop
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Train Renderer
|
||||
* Handles Three.js visualization of train models
|
||||
* Works in browser environment with Three.js global
|
||||
* @module trainRenderer
|
||||
*/
|
||||
|
||||
/**
|
||||
* TrainRenderer class - manages 3D train visualization
|
||||
*/
|
||||
class TrainRenderer {
|
||||
/**
|
||||
* Create a train renderer
|
||||
* @param {THREE.Scene} scene - Three.js scene
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {string} options.color - Train body color hex
|
||||
*/
|
||||
constructor(scene, options = {}) {
|
||||
this.scene = scene;
|
||||
this.trainMeshes = new Map();
|
||||
this.trainController = null;
|
||||
|
||||
// Default train colors
|
||||
this.colors = {
|
||||
body: new THREE.Color(options.color || '#FF6B00'),
|
||||
windows: new THREE.Color('#87CEEB'),
|
||||
wheels: new THREE.Color('#333333'),
|
||||
details: new THREE.Color('#FFFFFF')
|
||||
};
|
||||
|
||||
this.trains = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a 3D train mesh
|
||||
* @param {string} trainId - Unique train identifier
|
||||
* @returns {THREE.Group} Train mesh group
|
||||
*/
|
||||
createTrainMesh(trainId) {
|
||||
const trainGroup = new THREE.Group();
|
||||
trainGroup.userData.id = trainId;
|
||||
|
||||
// Train body (main chassis)
|
||||
const bodyGeometry = new THREE.BoxGeometry(2, 1.2, 4);
|
||||
const bodyMaterial = new THREE.MeshPhongMaterial({
|
||||
color: this.colors.body,
|
||||
shininess: 50
|
||||
});
|
||||
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
||||
body.position.y = 0.6;
|
||||
trainGroup.add(body);
|
||||
|
||||
// Cab area (front)
|
||||
const cabGeometry = new THREE.BoxGeometry(1.8, 0.8, 1);
|
||||
const cabMaterial = new THREE.MeshPhongMaterial({
|
||||
color: this.colors.windows,
|
||||
shininess: 30
|
||||
});
|
||||
const cab = new THREE.Mesh(cabGeometry, cabMaterial);
|
||||
cab.position.set(0.9, 1.1, 0);
|
||||
cab.rotation.y = Math.PI / 4;
|
||||
trainGroup.add(cab);
|
||||
|
||||
// Wheels (4 visible wheels)
|
||||
const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16);
|
||||
const wheelMaterial = new THREE.MeshPhongMaterial({
|
||||
color: this.colors.wheels,
|
||||
shininess: 20
|
||||
});
|
||||
|
||||
const wheelPositions = [
|
||||
{ x: 0.3, y: -0.3, z: 1.2, rotZ: Math.PI / 2 },
|
||||
{ x: 0.3, y: -0.3, z: -1.2, rotZ: Math.PI / 2 },
|
||||
{ x: -0.3, y: -0.3, z: 1.2, rotZ: Math.PI / 2 },
|
||||
{ x: -0.3, y: -0.3, z: -1.2, rotZ: Math.PI / 2 }
|
||||
];
|
||||
|
||||
wheelPositions.forEach(pos => {
|
||||
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
|
||||
wheel.position.set(pos.x, pos.y, pos.z);
|
||||
wheel.rotation.z = pos.rotZ;
|
||||
trainGroup.add(wheel);
|
||||
});
|
||||
|
||||
// Train front marker (red light)
|
||||
const lightGeometry = new THREE.SphereGeometry(0.15);
|
||||
const lightMaterial = new THREE.MeshBasicMaterial({ color: 0xFF0000 });
|
||||
const frontLight = new THREE.Mesh(lightGeometry, lightMaterial);
|
||||
frontLight.position.set(1.01, 1.0, 0);
|
||||
trainGroup.add(frontLight);
|
||||
|
||||
// Train rear marker (red tail light)
|
||||
const rearLight = new THREE.Mesh(lightGeometry, lightMaterial);
|
||||
rearLight.position.set(-1.01, 1.0, 0);
|
||||
trainGroup.add(rearLight);
|
||||
|
||||
// Add train to scene
|
||||
this.scene.add(trainGroup);
|
||||
this.trainMeshes.set(trainId, trainGroup);
|
||||
|
||||
return trainGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a train from the scene
|
||||
* @param {string} trainId - Train identifier to remove
|
||||
*/
|
||||
removeTrain(trainId) {
|
||||
const mesh = this.trainMeshes.get(trainId);
|
||||
if (mesh) {
|
||||
this.scene.remove(mesh);
|
||||
mesh.traverse(child => {
|
||||
if (child.geometry) child.geometry.dispose();
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach(mat => mat.dispose());
|
||||
} else {
|
||||
child.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.trainMeshes.delete(trainId);
|
||||
delete this.trains[trainId];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update train mesh position and rotation based on train state
|
||||
* @param {string} trainId - Train identifier
|
||||
* @param {Object} position - Position {x, y, z}
|
||||
* @param {number} rotation - Rotation angle in radians
|
||||
*/
|
||||
updateTrain(trainId, position, rotation) {
|
||||
const mesh = this.trainMeshes.get(trainId);
|
||||
if (mesh) {
|
||||
mesh.position.set(position.x, position.y, position.z);
|
||||
mesh.rotation.y = rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all train meshes based on train controller progress
|
||||
* @param {number} deltaTime - Time delta in seconds
|
||||
* @param {number} speed - Train speed
|
||||
*/
|
||||
updateAll(deltaTime, speed) {
|
||||
const progress = this.trainController ? this.trainController.progress : 0;
|
||||
const currentPos = this.trainController ? this.trainController.currentPosition : { x: 0, y: 0, z: 0 };
|
||||
|
||||
// Calculate orientation based on direction
|
||||
let rotation = 0;
|
||||
if (this.trainController && this.trainController.trackPath.length >= 2) {
|
||||
const totalLength = this.trainController.totalPathLength;
|
||||
const targetDistance = progress * totalLength;
|
||||
|
||||
let distance = 0;
|
||||
for (let i = 0; i < this.trainController.trackPath.length - 1; i++) {
|
||||
const p1 = this.trainController.trackPath[i];
|
||||
const p2 = this.trainController.trackPath[i + 1];
|
||||
const dist = this._distanceBetween(p1, p2);
|
||||
|
||||
if (targetDistance >= distance && targetDistance <= distance + dist) {
|
||||
const segmentProgress = (targetDistance - distance) / dist;
|
||||
const x1 = p1.x, y1 = p1.y;
|
||||
const x2 = p2.x, y2 = p2.y;
|
||||
rotation = Math.atan2(y2 - y1, x2 - x1);
|
||||
break;
|
||||
}
|
||||
distance += dist;
|
||||
}
|
||||
}
|
||||
|
||||
// Update mesh positions
|
||||
this.trainMeshes.forEach((mesh, trainId) => {
|
||||
this.updateTrain(trainId, currentPos, rotation);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance between two points
|
||||
* @param {Object} p1 - First point {x, y, z}
|
||||
* @param {Object} p2 - Second point {x, y, z}
|
||||
* @returns {number} Distance
|
||||
* @private
|
||||
*/
|
||||
_distanceBetween(p1, p2) {
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const dz = p2.z - p1.z;
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set train controller reference
|
||||
* @param {TrainController} controller - TrainController instance
|
||||
*/
|
||||
setController(controller) {
|
||||
this.trainController = controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all train meshes
|
||||
* @returns {Map} Map of trainId to mesh
|
||||
*/
|
||||
getTrainMeshes() {
|
||||
return this.trainMeshes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose resources
|
||||
*/
|
||||
dispose() {
|
||||
this.trainMeshes.forEach(mesh => {
|
||||
mesh.traverse(child => {
|
||||
if (child.geometry) child.geometry.dispose();
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach(mat => mat.dispose());
|
||||
} else {
|
||||
child.material.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
this.trainMeshes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for browser global scope
|
||||
if (typeof window !== 'undefined') {
|
||||
window.TrainRenderer = TrainRenderer;
|
||||
}
|
||||
Reference in New Issue
Block a user