/** * 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 = `
ID: ${this.train.id}
Speed:
Direction: ${this.train.direction > 0 ? 'âĄī¸ Forward' : 'âŦ ī¸ Reverse'}
Status: ${this.train.isMoving ? 'đ Moving' : 'đ Stopped'}
Keyboard: W/â = Accelerate, S/â = Brake, R/D = Reverse, SPACE = Stop
`; } /** * 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 = `${message}
`; // Clear info after 3 seconds setTimeout(() => { if (this.infoPanel && this.infoPanel.innerHTML.includes(message)) { this.infoPanel.innerHTML = 'Click tracks to place/remove. Select a track type to start building!
'; } }, 3000); } /** * Update stats display in UI */ updateStatsDisplay() { if (!this.statsPanel) return; let statsHTML = `Total Tracks: ${this.stats.totalTracks}
`; if (this.stats.trackCounts && Object.keys(this.stats.trackCounts).length > 0) { statsHTML += `Status: ${status}
Direction: ${direction}
Speed: