/** * Railtrack Pro - Game Logic * Main game controller, UI interactions, and game state management * @author Railtrack Pro Development Team */ class Game { constructor() { this.renderer = null; this.world = null; this.selectedTrackType = 'straight'; this.stats = { total: 0, straight: 0, curved: 0, junction: 0, signal: 0 }; this.init(); } /** * Initialize game */ init() { console.log('[Game] Initializing Railtrack Pro...'); // Initialize renderer this.renderer = new Renderer(); // Initialize world this.world = new World(this.renderer); // Setup UI controls this.setupUIControls(); // Initialize stats display this.updateStats(0); // Highlight first track type as active this.setActiveTrackType('straight'); console.log('[Game] Initialization complete'); } /** * Setup UI controls and event listeners */ setupUIControls() { const trackTypes = ['straight', 'curved', 'junction', 'signal']; trackTypes.forEach(type => { const button = document.getElementById(`btn-${type}`); if (button) { button.addEventListener('click', () => this.setActiveTrackType(type)); } }); } /** * Set active track type for placement */ setActiveTrackType(type) { this.selectedTrackType = type; // Update button states document.querySelectorAll('#controls button').forEach(btn => { btn.classList.remove('active'); }); const activeButton = document.getElementById(`btn-${type}`); if (activeButton) { activeButton.classList.add('active'); } console.log(`[Game] Selected track type: ${type}`); } /** * Handle track click from renderer */ static onTrackClick(trackMesh) { if (trackMesh.userData.piece) { Game.removeTrack(trackMesh.userData.piece); } } /** * Handle mouse click on viewport for track placement */ static onViewportClick(event) { // Only handle placement if a track type is selected if (!Game.instance.selectedTrackType) { console.log('[Game] No track type selected'); return; } // Raycast for placement const mouse = new THREE.Vector2(); const rect = Game.instance.renderer.renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; Game.instance.renderer.raycaster.setFromCamera(mouse, Game.instance.renderer.camera); // Create plane for intersection const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); const target = new THREE.Vector3(); const intersectPoint = new THREE.Vector3(); Game.instance.renderer.raycaster.ray.intersectPlane(plane, intersectPoint); if (intersectPoint) { // Snap to grid const snappedPosition = Game.instance.world.snapToGrid(intersectPoint); // Check if placement is valid if (Game.instance.world.isValidPlacement(snappedPosition)) { // Calculate rotation based on selected type const rotation = new THREE.Vector3(0, Math.PI / 2, 0); // Add track piece to world Game.instance.world.addTrackPiece( Game.instance.selectedTrackType, snappedPosition, rotation ); } else { console.log('[Game] Placement invalid - invalid position'); } } } /** * Remove track piece */ static removeTrack(trackPiece) { if (Game.instance && Game.instance.world) { Game.instance.world.removeTrackPiece(trackPiece); } } /** * Update stats display in info panel */ static updateStats(count) { if (!Game.instance) return; const stats = Game.instance.world.getStats(); Game.instance.stats = stats; const statsElement = document.getElementById('stats'); if (statsElement) { statsElement.innerHTML = `
Total: ${stats.total}
⬇️ Straight: ${stats.straight}
⬇️ Curved: ${stats.curved}
⬇️ Junction: ${stats.junction}
⬇️ Signal: ${stats.signal}
`; } } /** * Update selected track info display */ static updateSelectedInfo(selectedMesh) { if (!Game.instance) return; const infoElement = document.getElementById('selected-info'); if (!selectedMesh) { infoElement.innerHTML = `No track selected
`; return; } const track = selectedMesh.userData.piece; if (!track) return; const config = TrackConfig.types[track.type] || { name: 'Unknown' }; infoElement.innerHTML = `Type: ${config.name}
Position: ${track.position.x.toFixed(1)}, ${track.position.z.toFixed(1)}
Rotation: ${(track.rotation.y * 180 / Math.PI).toFixed(0)}°
Cost: $${config.cost || 0}
`; } /** * Get current game instance */ static getInstance() { return this.instance; } /** * Initialize game on DOM ready */ initGame() { Game.instance = new Game(); // Add click handler to viewport const viewport = document.getElementById('viewport'); if (viewport) { viewport.addEventListener('click', (event) => { // Prevent placement if clicking on info panel if (event.target.closest('#info-panel')) return; Game.onViewportClick(event); }); } console.log('[Game] Game instance created and ready'); } } // Global game instance Game.instance = null; // Initialize game when DOM is ready document.addEventListener('DOMContentLoaded', () => { Game.initGame(); }); // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = Game; }