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;
|
||||
}
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Railtrack Pro - Three.js Renderer
|
||||
* Handles 3D scene setup, camera, and rendering loop
|
||||
* @author Railtrack Pro Development Team
|
||||
*/
|
||||
|
||||
class Renderer {
|
||||
constructor() {
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.renderer = null;
|
||||
this.controls = null;
|
||||
this.gridHelper = null;
|
||||
this.trackMeshes = [];
|
||||
this.selectedObject = null;
|
||||
this.isDragging = false;
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
this.mouse = new THREE.Vector2();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Three.js scene with camera, renderer, and controls
|
||||
*/
|
||||
init() {
|
||||
// Create scene
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0x1a1a2e);
|
||||
this.scene.fog = new THREE.Fog(0x1a1a2e, 50, 200);
|
||||
|
||||
// Create camera
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
60,
|
||||
window.innerWidth / window.innerHeight,
|
||||
0.1,
|
||||
1000
|
||||
);
|
||||
this.camera.position.set(20, 20, 20);
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
|
||||
// Create renderer
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setSize(window.innerWidth - 50, window.innerHeight - 150);
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
document.getElementById('viewport').appendChild(this.renderer.domElement);
|
||||
|
||||
// Add orbit controls
|
||||
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.enableDamping = true;
|
||||
this.controls.dampingFactor = 0.05;
|
||||
this.controls.maxPolarAngle = Math.PI / 2 - 0.1;
|
||||
this.controls.minDistance = 5;
|
||||
this.controls.maxDistance = 100;
|
||||
|
||||
// Add lighting
|
||||
this.setupLighting();
|
||||
|
||||
// Add ground grid
|
||||
this.addGridHelper();
|
||||
|
||||
// Add event listeners
|
||||
this.addEventListeners();
|
||||
|
||||
// Start animation loop
|
||||
this.animate();
|
||||
|
||||
console.log('[Renderer] Initialized successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup scene lighting
|
||||
*/
|
||||
setupLighting() {
|
||||
// Ambient light
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 1);
|
||||
this.scene.add(ambientLight);
|
||||
|
||||
// Directional light (sun)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
directionalLight.position.set(20, 30, 20);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.shadow.mapSize.width = 2048;
|
||||
directionalLight.shadow.mapSize.height = 2048;
|
||||
directionalLight.shadow.camera.near = 0.5;
|
||||
directionalLight.shadow.camera.far = 100;
|
||||
directionalLight.shadow.camera.left = -50;
|
||||
directionalLight.shadow.camera.right = 50;
|
||||
directionalLight.shadow.camera.top = 50;
|
||||
directionalLight.shadow.camera.bottom = -50;
|
||||
this.scene.add(directionalLight);
|
||||
|
||||
// Point light near center
|
||||
const pointLight = new THREE.PointLight(0xfb5607, 0.5, 30);
|
||||
pointLight.position.set(0, 10, 0);
|
||||
this.scene.add(pointLight);
|
||||
|
||||
console.log('[Renderer] Lighting setup complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ground plane with grid helper
|
||||
*/
|
||||
addGridHelper() {
|
||||
// Ground plane
|
||||
const planeGeometry = new THREE.PlaneGeometry(100, 100);
|
||||
const planeMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0x2a2a4e,
|
||||
roughness: 0.8,
|
||||
metalness: 0.2
|
||||
});
|
||||
const ground = new THREE.Mesh(planeGeometry, planeMaterial);
|
||||
ground.rotation.x = -Math.PI / 2;
|
||||
ground.receiveShadow = true;
|
||||
this.scene.add(ground);
|
||||
|
||||
// Grid helper
|
||||
this.gridHelper = new THREE.GridHelper(100, 50, 0x404080, 0x202040);
|
||||
this.scene.add(this.gridHelper);
|
||||
|
||||
// Axis helper
|
||||
const axesHelper = new THREE.AxesHelper(5);
|
||||
this.scene.add(axesHelper);
|
||||
|
||||
console.log('[Renderer] Grid helper added');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listeners for mouse interactions
|
||||
*/
|
||||
addEventListeners() {
|
||||
window.addEventListener('resize', () => this.onWindowResize());
|
||||
this.renderer.domElement.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
||||
this.renderer.domElement.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
this.renderer.domElement.addEventListener('mouseup', (e) => this.onMouseUp(e));
|
||||
this.renderer.domElement.addEventListener('click', (e) => this.onClick(e));
|
||||
this.renderer.domElement.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle window resize
|
||||
*/
|
||||
onWindowResize() {
|
||||
const viewport = document.getElementById('viewport');
|
||||
this.camera.aspect = viewport.clientWidth / viewport.clientHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(viewport.clientWidth, viewport.clientHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse down event
|
||||
*/
|
||||
onMouseDown(event) {
|
||||
this.isDragging = true;
|
||||
this.updateMouse(event);
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||||
|
||||
// Track track meshes
|
||||
const intersects = this.raycaster.intersectObjects(this.trackMeshes);
|
||||
if (intersects.length > 0) {
|
||||
this.selectedObject = intersects[0].object;
|
||||
this.highlightSelection(this.selectedObject);
|
||||
} else {
|
||||
this.deselectAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse move event
|
||||
*/
|
||||
onMouseMove(event) {
|
||||
this.updateMouse(event);
|
||||
|
||||
// Highlight object under cursor
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||||
const intersects = this.raycaster.intersectObjects(this.trackMeshes);
|
||||
|
||||
if (intersects.length > 0 && intersects[0].object !== this.selectedObject) {
|
||||
this.highlightHover(intersects[0].object);
|
||||
} else {
|
||||
this.removeHoverHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse up event
|
||||
*/
|
||||
onMouseUp() {
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click event
|
||||
*/
|
||||
onClick(event) {
|
||||
if (!this.isDragging && this.selectedObject) {
|
||||
console.log('[Renderer] Track clicked:', this.selectedObject.userData);
|
||||
Game.onTrackClick(this.selectedObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mouse position from event
|
||||
*/
|
||||
updateMouse(event) {
|
||||
const rect = this.renderer.domElement.getBoundingClientRect();
|
||||
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight selected object
|
||||
*/
|
||||
highlightSelection(object) {
|
||||
// Remove previous highlight
|
||||
if (object.originalColor) {
|
||||
object.material.emissive.setHex(object.originalColor);
|
||||
}
|
||||
|
||||
// Store original color and set highlight
|
||||
object.originalColor = object.material.emissive.getHex();
|
||||
object.material.emissive.setHex(0xfb5607);
|
||||
this.selectedObject = object;
|
||||
|
||||
// Update info panel
|
||||
Game.updateSelectedInfo(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight hover object
|
||||
*/
|
||||
highlightHover(object) {
|
||||
if (object.hoverColor) {
|
||||
object.material.emissive.setHex(object.hoverColor);
|
||||
} else {
|
||||
object.hoverColor = object.material.emissive.getHex();
|
||||
object.material.emissive.setHex(0x404080);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove hover highlight
|
||||
*/
|
||||
removeHoverHighlight() {
|
||||
for (const mesh of this.trackMeshes) {
|
||||
if (mesh.hoverColor) {
|
||||
mesh.material.emissive.setHex(mesh.hoverColor);
|
||||
delete mesh.hoverColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselect all objects
|
||||
*/
|
||||
deselectAll() {
|
||||
if (this.selectedObject) {
|
||||
if (this.selectedObject.originalColor) {
|
||||
this.selectedObject.material.emissive.setHex(this.selectedObject.originalColor);
|
||||
}
|
||||
this.selectedObject = null;
|
||||
}
|
||||
Game.updateSelectedInfo(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add track piece to scene
|
||||
*/
|
||||
addTrackPiece(geometry, material, position, rotation) {
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.copy(position);
|
||||
mesh.rotation.set(rotation.x, rotation.y, rotation.z);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
mesh.userData = { type: 'track', id: this.trackMeshes.length };
|
||||
|
||||
this.scene.add(mesh);
|
||||
this.trackMeshes.push(mesh);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove track piece from scene
|
||||
*/
|
||||
removeTrackPiece(mesh) {
|
||||
const index = this.trackMeshes.indexOf(mesh);
|
||||
if (index > -1) {
|
||||
this.trackMeshes.splice(index, 1);
|
||||
this.scene.remove(mesh);
|
||||
mesh.geometry.dispose();
|
||||
mesh.material.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation loop
|
||||
*/
|
||||
animate() {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
this.controls.update();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = Renderer;
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Railtrack Pro - Track Piece System
|
||||
* Defines track geometries and placement logic
|
||||
* @author Railtrack Pro Development Team
|
||||
*/
|
||||
|
||||
class TrackPiece {
|
||||
constructor(type, position, rotation) {
|
||||
this.type = type; // 'straight', 'curved', 'junction', 'signal'
|
||||
this.id = position ? Math.floor(Math.random() * 10000) : null;
|
||||
this.position = position || new THREE.Vector3(0, 0, 0);
|
||||
this.rotation = rotation || new THREE.Vector3(0, Math.PI / 2, 0);
|
||||
this.connections = [];
|
||||
this.geometry = null;
|
||||
this.material = null;
|
||||
this.mesh = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create track piece in scene
|
||||
*/
|
||||
create(scene) {
|
||||
this.geometry = this.getGeometry();
|
||||
this.material = new THREE.MeshStandardMaterial({
|
||||
color: 0x888888,
|
||||
roughness: 0.6,
|
||||
metalness: 0.4
|
||||
});
|
||||
|
||||
this.mesh = new THREE.Mesh(this.geometry, this.material);
|
||||
this.mesh.position.copy(this.position);
|
||||
this.mesh.rotation.copy(this.rotation);
|
||||
this.mesh.castShadow = true;
|
||||
this.mesh.receiveShadow = true;
|
||||
this.mesh.userData = { type: 'track', id: this.id, piece: this };
|
||||
|
||||
scene.add(this.mesh);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get geometry based on track type
|
||||
*/
|
||||
getGeometry() {
|
||||
switch (this.type) {
|
||||
case 'straight':
|
||||
return new THREE.BoxGeometry(1, 0.3, 2);
|
||||
case 'curved':
|
||||
return this.createCurvedGeometry();
|
||||
case 'junction':
|
||||
return this.createJunctionGeometry();
|
||||
case 'signal':
|
||||
return this.createSignalGeometry();
|
||||
default:
|
||||
return new THREE.BoxGeometry(1, 0.3, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create curved track geometry
|
||||
*/
|
||||
createCurvedGeometry() {
|
||||
const points = [];
|
||||
const segments = 20;
|
||||
const radius = 2;
|
||||
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
const angle = (i / segments) * Math.PI;
|
||||
const x = Math.sin(angle) * radius;
|
||||
const z = -Math.cos(angle) * radius + radius;
|
||||
points.push(new THREE.Vector3(x, 0, z));
|
||||
}
|
||||
|
||||
const curve = new THREE.CatmullRomCurve3(points);
|
||||
const geometry = new THREE.TubeGeometry(curve, segments, 0.5, 8, false);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create junction track geometry (T-junction)
|
||||
*/
|
||||
createJunctionGeometry() {
|
||||
const group = new THREE.Group();
|
||||
|
||||
// Main stem
|
||||
const stem = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 0.3, 2),
|
||||
this.material
|
||||
);
|
||||
stem.position.set(0, 0, 1);
|
||||
|
||||
// Top branch
|
||||
const top = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 0.3, 2),
|
||||
this.material
|
||||
);
|
||||
top.position.set(0, 0, -1);
|
||||
|
||||
// Side branch
|
||||
const side = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(2, 0.3, 1),
|
||||
this.material
|
||||
);
|
||||
side.position.set(0, 0, 0);
|
||||
|
||||
group.add(stem, top, side);
|
||||
|
||||
const geometry = new THREE.Group(group);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create signal track piece
|
||||
*/
|
||||
createSignalGeometry() {
|
||||
const group = new THREE.Group();
|
||||
|
||||
// Signal pole
|
||||
const pole = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.1, 0.1, 1.5),
|
||||
new THREE.MeshStandardMaterial({ color: 0x333333 })
|
||||
);
|
||||
pole.position.set(0, 0.75, 0);
|
||||
|
||||
// Signal lights
|
||||
const lightBox = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(0.3, 0.2, 0.1),
|
||||
new THREE.MeshStandardMaterial({ color: 0xff0000 })
|
||||
);
|
||||
lightBox.position.set(0, 1.5, 0);
|
||||
|
||||
// Track base
|
||||
const base = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 0.3, 1),
|
||||
this.material
|
||||
);
|
||||
base.position.set(0, 0, 0);
|
||||
|
||||
group.add(base, pole, lightBox);
|
||||
|
||||
const geometry = new THREE.Group(group);
|
||||
return geometry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection points for track joining
|
||||
*/
|
||||
getConnectionPoints() {
|
||||
const points = [];
|
||||
switch (this.type) {
|
||||
case 'straight':
|
||||
points.push(new THREE.Vector3(0, 0, -1));
|
||||
points.push(new THREE.Vector3(0, 0, 1));
|
||||
break;
|
||||
case 'curved':
|
||||
points.push(new THREE.Vector3(0, 0, -2));
|
||||
points.push(new THREE.Vector3(2, 0, 0));
|
||||
break;
|
||||
case 'junction':
|
||||
points.push(new THREE.Vector3(0, 0, -2));
|
||||
points.push(new THREE.Vector3(0, 0, 2));
|
||||
points.push(new THREE.Vector3(-2, 0, 0));
|
||||
points.push(new THREE.Vector3(2, 0, 0));
|
||||
break;
|
||||
}
|
||||
return points;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track type configuration
|
||||
*/
|
||||
const TrackConfig = {
|
||||
types: {
|
||||
straight: { name: 'Straight Track', color: 0x888888, cost: 10 },
|
||||
curved: { name: 'Curved Track', color: 0x888888, cost: 15 },
|
||||
junction: { name: 'Junction', color: 0x888888, cost: 25 },
|
||||
signal: { name: 'Signal', color: 0xff0000, cost: 30 }
|
||||
},
|
||||
placement: {
|
||||
snapDistance: 0.1,
|
||||
maxConnections: 4
|
||||
}
|
||||
};
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = { TrackPiece, TrackConfig };
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Railtrack Pro - World Management
|
||||
* Handles grid system, track placement, and world state
|
||||
* @author Railtrack Pro Development Team
|
||||
*/
|
||||
|
||||
class World {
|
||||
constructor(renderer) {
|
||||
this.renderer = renderer;
|
||||
this.gridSize = 100;
|
||||
this.gridUnit = 1; // Grid unit size
|
||||
this.tracks = [];
|
||||
this.selectedType = null;
|
||||
this.snapDistance = 0.1;
|
||||
this.isPlacementMode = false;
|
||||
|
||||
this.initGrid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize grid system
|
||||
*/
|
||||
initGrid() {
|
||||
this.gridHelper = this.renderer.gridHelper;
|
||||
console.log('[World] Grid system initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap position to nearest grid point
|
||||
*/
|
||||
snapToGrid(position) {
|
||||
const x = Math.round(position.x / this.gridUnit) * this.gridUnit;
|
||||
const z = Math.round(position.z / this.gridUnit) * this.gridUnit;
|
||||
return new THREE.Vector3(x, 0, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if position is valid for track placement
|
||||
*/
|
||||
isValidPlacement(position) {
|
||||
// Check bounds
|
||||
if (Math.abs(position.x) > this.gridSize || Math.abs(position.z) > this.gridSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check collisions with existing tracks
|
||||
const positionSnap = this.snapToGrid(position);
|
||||
const collisionBuffer = this.snapDistance * 2;
|
||||
|
||||
for (const track of this.tracks) {
|
||||
if (track.mesh === null) continue;
|
||||
|
||||
const trackPos = track.mesh.position;
|
||||
const distance = positionSnap.distanceTo(trackPos);
|
||||
|
||||
if (distance < collisionBuffer) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add track piece to world
|
||||
*/
|
||||
addTrackPiece(type, position, rotation) {
|
||||
if (!this.isValidPlacement(position)) {
|
||||
console.warn('[World] Invalid placement position');
|
||||
return null;
|
||||
}
|
||||
|
||||
const track = new TrackPiece(type, position, rotation);
|
||||
track.create(this.renderer.scene);
|
||||
this.tracks.push(track);
|
||||
|
||||
// Update stats
|
||||
Game.updateStats(this.tracks.length);
|
||||
|
||||
console.log(`[World] Added ${type} track at ${position.x.toFixed(1)}, ${position.z.toFixed(1)}`);
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove track piece from world
|
||||
*/
|
||||
removeTrackPiece(trackPiece) {
|
||||
const index = this.tracks.indexOf(trackPiece);
|
||||
if (index > -1) {
|
||||
this.tracks.splice(index, 1);
|
||||
this.renderer.removeTrackPiece(trackPiece.mesh);
|
||||
Game.updateStats(this.tracks.length);
|
||||
Game.updateSelectedInfo(null);
|
||||
|
||||
console.log(`[World] Removed track piece`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track piece at position
|
||||
*/
|
||||
getTrackAtPosition(position) {
|
||||
for (const track of this.tracks) {
|
||||
if (track.mesh === null) continue;
|
||||
|
||||
const distance = position.distanceTo(track.mesh.position);
|
||||
if (distance < this.snapDistance) {
|
||||
return track;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect tracks at junctions
|
||||
*/
|
||||
connectTracks(track1, track2) {
|
||||
// Simple connection logic
|
||||
// In full implementation, check compatibility and adjust orientation
|
||||
console.log('[World] Connecting tracks');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all tracks
|
||||
*/
|
||||
clear() {
|
||||
for (const track of this.tracks) {
|
||||
this.removeTrackPiece(track);
|
||||
}
|
||||
this.tracks = [];
|
||||
Game.updateStats(0);
|
||||
console.log('[World] All tracks cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get world statistics
|
||||
*/
|
||||
getStats() {
|
||||
const counts = {
|
||||
straight: 0,
|
||||
curved: 0,
|
||||
junction: 0,
|
||||
signal: 0
|
||||
};
|
||||
|
||||
for (const track of this.tracks) {
|
||||
if (counts[track.type] !== undefined) {
|
||||
counts[track.type]++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total: this.tracks.length,
|
||||
...counts
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module usage
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = World;
|
||||
}
|
||||
Reference in New Issue
Block a user