Files
railtrack-pro/js/game.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

507 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Game Module
* Main game logic, UI interactions, and initialization
* Works in browser environment
* @module game
*/
/**
* Game class - main game controller
*/
class Game {
constructor() {
// Initialize Three.js renderer
this.renderer = new Renderer();
// Initialize world (track placement)
this.world = new World(this.renderer);
// Initialize train system
this.trainController = new TrainController([]);
this.trainRenderer = new TrainRenderer(this.renderer.scene, { color: '#FF6B00' });
this.trainRenderer.setController(this.trainController);
this.createTrain();
// Initialize controls
this.controls = new OrbitControls(this.renderer.camera, this.renderer.domElement);
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.05;
this.controls.maxPolarAngle = Math.PI / 2.2;
this.controls.minDistance = 5;
this.controls.maxDistance = 100;
// Input state
this.selectedTrackType = null;
this.keysPressed = {};
// Stats tracking
this.stats = {
totalTracks: 0,
trackCounts: {},
lastUpdate: Date.now()
};
// Initialize UI elements
this.initializeUI();
// Bind events
this.bindEvents();
// Start game loop
this.lastTime = Date.now();
this.animate();
console.log('🚂 Railtrack Pro initialized!');
}
/**
* Create a train for the world
*/
createTrain() {
const train = new Train({
id: 'main-train',
speed: 0,
maxSpeed: 5,
direction: 1,
color: '#FF6B00'
});
this.train = train;
this.trainMesh = this.trainRenderer.createTrainMesh('main-train');
this.train.threeMesh = this.trainMesh;
}
/**
* Initialize UI elements
*/
initializeUI() {
// Stats panel
this.statsPanel = document.getElementById('statsPanel');
this.trackStatsList = document.getElementById('trackStatsList');
// Train controls
this.trainControls = {
accelerateBtn: document.getElementById('accelerateTrain'),
brakeBtn: document.getElementById('brakeTrain'),
reverseBtn: document.getElementById('reverseTrain'),
stopBtn: document.getElementById('stopTrain'),
statusDiv: document.getElementById('trainStatus')
};
// Track type buttons
this.trackButtons = document.querySelectorAll('[data-track-type]');
// Info panel
this.infoPanel = document.getElementById('infoPanel');
// Update stats display
this.updateStatsDisplay();
this.updateTrainStatus();
}
/**
* Bind event listeners
*/
bindEvents() {
// Track type selection
this.trackButtons.forEach(btn => {
btn.addEventListener('click', () => {
const trackType = btn.dataset.trackType;
this.selectTrackType(trackType, btn);
});
});
// Train control buttons
this.trainControls.accelerateBtn.addEventListener('click', () => this.accelerate());
this.trainControls.brakeBtn.addEventListener('click', () => this.brake());
this.trainControls.reverseBtn.addEventListener('click', () => this.reverse());
this.trainControls.stopBtn.addEventListener('click', () => this.stop());
// Keyboard controls
document.addEventListener('keydown', (e) => {
this.keysPressed[e.code] = true;
// Train controls via keyboard
switch(e.code) {
case 'KeyW':
case 'ArrowUp':
this.accelerate();
break;
case 'KeyS':
case 'ArrowDown':
this.brake();
break;
case 'KeyR':
case 'KeyD':
this.reverse();
break;
case 'Space':
this.stop();
break;
}
});
document.addEventListener('keyup', (e) => {
this.keysPressed[e.code] = false;
});
// Mouse click for track placement/removal
this.renderer.domElement.addEventListener('click', (event) => {
this.handleMouseClick(event);
});
// Handle window resize
window.addEventListener('resize', () => {
this.onWindowResize();
});
}
/**
* Select track type for placement
* @param {string} trackType - Type of track to select
* @param {HTMLElement} button - Button element clicked
*/
selectTrackType(trackType, button) {
this.selectedTrackType = trackType;
// Update button styles
this.trackButtons.forEach(btn => {
btn.classList.remove('selected');
});
button.classList.add('selected');
console.log(`Selected track type: ${trackType}`);
}
/**
* Handle mouse click on canvas
* @param {MouseEvent} event - Mouse click event
*/
handleMouseClick(event) {
// Check if clicking on train first
const clickedMesh = event.target;
if (clickedMesh === this.trainMesh || clickedMesh.parent === this.trainMesh) {
// Train selected - show train info
this.showTrainInfo();
return;
}
// If no track type selected, show instruction
if (!this.selectedTrackType) {
this.showInfo('Select a track type to place! Click track buttons in the toolbar.');
return;
}
// Check if clicking on existing track (to remove)
const clickedTrack = this.checkClickedTrack(event);
if (clickedTrack) {
this.world.removeTrack(clickedTrack);
this.updateStatsDisplay();
this.showInfo(`Removed track at ${clickedTrack.position.x}, ${clickedTrack.position.y}`);
return;
}
// Check if clicking on canvas
if (this.world.isPlacementValid(event)) {
this.placeTrack(event);
} else {
this.showInfo('Click on the grid to place tracks. Click existing tracks to remove them.');
}
}
/**
* Check if mouse clicked on an existing track
* @param {MouseEvent} event - Mouse click event
* @returns {THREE.Object3D|null} Clicked track object or null
*/
checkClickedTrack(event) {
const mouse = new THREE.Vector2();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, this.renderer.camera);
const intersects = raycaster.intersectObjects(this.world.allTracks, true);
if (intersects.length > 0) {
// Find parent group (the actual track piece)
let parent = intersects[0].object;
while (parent && !parent.userData.type) {
parent = parent.parent;
}
if (parent && parent.userData.type) {
return parent;
}
}
return null;
}
/**
* Place a track at the clicked location
* @param {MouseEvent} event - Mouse click event
*/
placeTrack(event) {
const gridPos = this.world.getGridPosition(event);
if (gridPos) {
const track = this.world.addTrack(
this.selectedTrackType,
gridPos.x,
gridPos.y,
this.renderer
);
if (track) {
this.stats.totalTracks++;
this.stats.trackCounts[this.selectedTrackType] = (this.stats.trackCounts[this.selectedTrackType] || 0) + 1;
this.updateStatsDisplay();
this.showInfo(`Placed ${this.selectedTrackType} at ${gridPos.x}, ${gridPos.y}`);
} else {
this.showInfo(`Cannot place ${this.selectedTrackType} at that location.`, 'warning');
}
}
}
/**
* Accelerate the train (W/Up arrow)
*/
accelerate() {
if (this.train) {
this.train.accelerate();
this.updateTrainStatus();
console.log('⚡ Train accelerating - Speed:', this.train.speed);
}
}
/**
* Brake the train (S/Down arrow)
*/
brake() {
if (this.train) {
this.train.brake();
this.updateTrainStatus();
console.log('🛑 Train braking - Speed:', this.train.speed);
}
}
/**
* Reverse train direction (R/D)
*/
reverse() {
if (this.train) {
this.train.reverse();
this.updateTrainStatus();
console.log('🔄 Train reversed - Direction:', this.train.direction);
}
}
/**
* Stop the train (Space)
*/
stop() {
if (this.train) {
this.train.stop();
this.updateTrainStatus();
console.log('🛑 Train stopped');
}
}
/**
* Show train information in info panel
*/
showTrainInfo() {
if (!this.train) return;
this.infoPanel.innerHTML = `
<h3>🚂 Train Information</h3>
<p><strong>ID:</strong> ${this.train.id}</p>
<p><strong>Speed:</strong> <latex>${Math.abs(this.train.speed).toFixed(1)} units/s</latex></p>
<p><strong>Direction:</strong> ${this.train.direction > 0 ? '➡️ Forward' : '⬅️ Reverse'}</p>
<p><strong>Status:</strong> ${this.train.isMoving ? '🚂 Moving' : '🛑 Stopped'}</p>
<p><strong>Keyboard:</strong> W/↑ = Accelerate, S/↓ = Brake, R/D = Reverse, SPACE = Stop</p>
`;
}
/**
* Show information message in info panel
* @param {string} message - Message to display
* @param {string} type - Type (info, warning, error)
*/
showInfo(message, type = 'info') {
this.infoPanel.innerHTML = `
<h3>${type === 'warning' ? '⚠️' : type === 'error' ? '❌' : '️'} ${type.toUpperCase()}</h3>
<p>${message}</p>
`;
// Clear info after 3 seconds
setTimeout(() => {
if (this.infoPanel && this.infoPanel.innerHTML.includes(message)) {
this.infoPanel.innerHTML = '<p>Click tracks to place/remove. Select a track type to start building!</p>';
}
}, 3000);
}
/**
* Update stats display in UI
*/
updateStatsDisplay() {
if (!this.statsPanel) return;
let statsHTML = `
<h3>📊 Statistics</h3>
<p><strong>Total Tracks:</strong> ${this.stats.totalTracks}</p>
`;
if (this.stats.trackCounts && Object.keys(this.stats.trackCounts).length > 0) {
statsHTML += `
<h4>Track Types:</h4>
<ul id="trackStatsList">
`;
for (const [type, count] of Object.entries(this.stats.trackCounts)) {
statsHTML += `<li>${type}: <strong>${count}</strong></li>`;
}
statsHTML += `</ul>`;
}
this.statsPanel.innerHTML = statsHTML;
}
/**
* Update train status display in UI
*/
updateTrainStatus() {
if (!this.trainControls.statusDiv || !this.train) return;
const status = this.train.isMoving ? '🚂 Moving' : '🛑 Stopped';
const direction = this.train.direction > 0 ? '➡️ Forward' : '⬅️ Reverse';
this.trainControls.statusDiv.innerHTML = `
<p><strong>Status:</strong> ${status}</p>
<p><strong>Direction:</strong> ${direction}</p>
<p><strong>Speed:</strong> <latex>${Math.abs(this.train.speed).toFixed(1)} units/s</latex></p>
`;
}
/**
* Handle window resize
*/
onWindowResize() {
this.renderer.onWindowResize();
}
/**
* Update game state based on input
* @param {number} deltaTime - Time delta in seconds
*/
update(deltaTime) {
// Update train controls based on keyboard
if (this.train) {
// Keyboard controls with smoothing
if (this.keysPressed['KeyW'] || this.keysPressed['ArrowUp']) {
this.accelerate();
}
if (this.keysPressed['KeyS'] || this.keysPressed['ArrowDown']) {
this.brake();
}
if (this.keysPressed['KeyR'] || this.keysPressed['KeyD']) {
this.reverse();
}
// Update train physics
this.train.update(deltaTime);
this.updateTrainStatus();
// Update train controller if we have a track path
if (this.trainController.trackPath.length > 0) {
this.trainController.update(deltaTime, this.train.speed);
// Get current position and update train mesh
const currentPosition = this.trainController.getCurrentPosition();
const rotation = this.getTrainRotation();
this.trainRenderer.updateTrain('main-train', currentPosition, rotation);
}
}
// Update controller
this.controls.update();
}
/**
* Calculate train rotation based on track direction
* @returns {number} Rotation angle in radians
*/
getTrainRotation() {
let rotation = 0;
if (this.trainController && this.trainController.trackPath.length >= 2) {
const totalLength = this.trainController.totalPathLength;
const targetDistance = this.trainController.distance;
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 x1 = p1.x, y1 = p1.y;
const x2 = p2.x, y2 = p2.y;
rotation = Math.atan2(y2 - y1, x2 - x1);
// Adjust for train direction
if (this.train && this.train.direction < 0) {
rotation += Math.PI;
}
break;
}
distance += dist;
}
}
return 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);
}
/**
* Animation loop - main game loop
*/
animate() {
requestAnimationFrame(() => this.animate());
const now = Date.now();
const deltaTime = (now - this.lastTime) / 1000;
this.lastTime = now;
this.update(deltaTime);
this.renderer.render();
}
/**
* Set track path for train
* @param {Array} path - Array of {x, y, z} points
*/
setTrainPath(path) {
this.trainController.setTrackPath(path);
}
/**
* Dispose game resources
*/
dispose() {
this.trainRenderer.dispose();
this.world.dispose();
this.renderer.dispose();
window.removeEventListener('resize', this.onWindowResize);
}
}