Files
railtrack-pro/js/trainRenderer.js
T
Railtrack Pro Dev 47e1e64b8c
Railtrack Pro Tests / Code Quality Check (pull_request) Waiting to run
Railtrack Pro Tests / Run Test Suite (pull_request) Failing after 5m44s
Railtrack Pro Tests / Code Coverage Check (pull_request) Has been cancelled
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
2026-03-13 14:48:50 +00:00

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