47e1e64b8c
- 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
233 lines
7.6 KiB
JavaScript
233 lines
7.6 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|