229 lines
6.7 KiB
JavaScript
229 lines
6.7 KiB
JavaScript
/**
|
|
* 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 = `
|
|
<h3>📊 Track Statistics</h3>
|
|
<p><strong>Total:</strong> ${stats.total}</p>
|
|
<p>⬇️ Straight: ${stats.straight}</p>
|
|
<p>⬇️ Curved: ${stats.curved}</p>
|
|
<p>⬇️ Junction: ${stats.junction}</p>
|
|
<p>⬇️ Signal: ${stats.signal}</p>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update selected track info display
|
|
*/
|
|
static updateSelectedInfo(selectedMesh) {
|
|
if (!Game.instance) return;
|
|
|
|
const infoElement = document.getElementById('selected-info');
|
|
|
|
if (!selectedMesh) {
|
|
infoElement.innerHTML = `
|
|
<h4>🔍 Selection</h4>
|
|
<p>No track selected</p>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const track = selectedMesh.userData.piece;
|
|
if (!track) return;
|
|
|
|
const config = TrackConfig.types[track.type] || { name: 'Unknown' };
|
|
|
|
infoElement.innerHTML = `
|
|
<h4>📦 Track Info</h4>
|
|
<p><strong>Type:</strong> ${config.name}</p>
|
|
<p><strong>Position:</strong> ${track.position.x.toFixed(1)}, ${track.position.z.toFixed(1)}</p>
|
|
<p><strong>Rotation:</strong> ${(track.rotation.y * 180 / Math.PI).toFixed(0)}°</p>
|
|
<p><strong>Cost:</strong> $${config.cost || 0}</p>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|