feat: initial commit of Railtrack Pro prototype with complete test suite
This commit is contained in:
+228
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user