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
507 lines
16 KiB
JavaScript
507 lines
16 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|
||
}
|