fix approval stat; save game; customize color
This commit is contained in:
@@ -11,6 +11,9 @@
|
|||||||
<body>
|
<body>
|
||||||
<button id="ui-toggle" title="Toggle Menu">☰</button>
|
<button id="ui-toggle" title="Toggle Menu">☰</button>
|
||||||
|
|
||||||
|
<!-- Hidden Input for Loading Games -->
|
||||||
|
<input type="file" id="file-load-game" style="display: none;" accept=".json" />
|
||||||
|
|
||||||
<!-- Floating Income Feedback -->
|
<!-- Floating Income Feedback -->
|
||||||
<div id="income-float"
|
<div id="income-float"
|
||||||
style="position:absolute; top:60px; left:220px; color:#10B981; font-weight:bold; font-size:20px; opacity:0; transition: all 1s ease-out; z-index:90; text-shadow:0 1px 2px white;">
|
style="position:absolute; top:60px; left:220px; color:#10B981; font-weight:bold; font-size:20px; opacity:0; transition: all 1s ease-out; z-index:90; text-shadow:0 1px 2px white;">
|
||||||
@@ -20,6 +23,12 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<h2>Route Planner</h2>
|
<h2>Route Planner</h2>
|
||||||
|
|
||||||
|
<!-- Save/Load System -->
|
||||||
|
<div style="display:flex; gap:5px; margin-bottom:10px;">
|
||||||
|
<button id="btn-save-game" class="secondary" style="flex:1; font-size:12px;">💾 Save</button>
|
||||||
|
<button id="btn-load-game" class="secondary" style="flex:1; font-size:12px;">📂 Load</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Global Stats -->
|
<!-- Global Stats -->
|
||||||
<div
|
<div
|
||||||
style="background:#f3f4f6; padding:8px; border-radius:6px; margin-bottom:10px; display:grid; grid-template-columns: 1fr 1fr; gap:5px; font-size:13px;">
|
style="background:#f3f4f6; padding:8px; border-radius:6px; margin-bottom:10px; display:grid; grid-template-columns: 1fr 1fr; gap:5px; font-size:13px;">
|
||||||
|
|||||||
24
presentation.md
Normal file
24
presentation.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# CS 559 GP Presentation
|
||||||
|
|
||||||
|
## Group ID 3826
|
||||||
|
|
||||||
|
## Intro
|
||||||
|
|
||||||
|
This project:
|
||||||
|
|
||||||
|
- Simulates creating transit routes in Madison, and the ridership
|
||||||
|
- Encourages user to build a network that has good coverage, to optimize
|
||||||
|
approval rating
|
||||||
|
- Maps in 3d Madison's buildings, parks, roads, and bodies of water
|
||||||
|
- Limits user to a budget for expenses
|
||||||
|
- Simulates revenue from fares
|
||||||
|
- (allows for upgrading of bus rolling stock)
|
||||||
|
|
||||||
|
## For Peer Evaluators
|
||||||
|
|
||||||
|
- Recall: each component can be only claimed by one group member
|
||||||
|
- I created this project alone
|
||||||
|
- I only claim to complete **game design**
|
||||||
|
- You only will grade me on that category
|
||||||
|
- All other categories you should score with a 1, as I do not claim to complete
|
||||||
|
them
|
||||||
@@ -22,14 +22,69 @@ export class GameManager {
|
|||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.buildCensusArrays();
|
this.buildCensusArrays();
|
||||||
this.recalculateApproval(); // Initial calc
|
this.recalculateApproval();
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
|
|
||||||
|
if (this.gameLoopInterval) clearInterval(this.gameLoopInterval);
|
||||||
this.gameLoopInterval = setInterval(() => {
|
this.gameLoopInterval = setInterval(() => {
|
||||||
this.processDay();
|
this.processDay();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Save / Load System
|
||||||
|
// ==========================
|
||||||
|
|
||||||
|
saveGame() {
|
||||||
|
const data = {
|
||||||
|
version: 1,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
gameState: {
|
||||||
|
budget: this.budget,
|
||||||
|
day: this.day,
|
||||||
|
approval: this.approvalRating
|
||||||
|
},
|
||||||
|
routes: this.routeManager.getSerializableRoutes()
|
||||||
|
};
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `city_transit_save_day${this.day}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadGame(jsonString) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
// 1. Restore State
|
||||||
|
this.budget = data.gameState.budget;
|
||||||
|
this.day = data.gameState.day;
|
||||||
|
|
||||||
|
// 2. Restore Routes
|
||||||
|
// This will clear existing routes and rebuild meshes/vehicles
|
||||||
|
this.routeManager.loadRoutes(data.routes);
|
||||||
|
|
||||||
|
// 3. Recalculate Logic
|
||||||
|
this.buildCensusArrays();
|
||||||
|
this.recalculateApproval();
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
console.log("Game loaded successfully.");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load save file", e);
|
||||||
|
alert("Error loading save file. See console.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================
|
||||||
|
// Core Logic
|
||||||
|
// ==========================
|
||||||
|
|
||||||
buildCensusArrays() {
|
buildCensusArrays() {
|
||||||
if (!this.routeManager.graphData) return;
|
if (!this.routeManager.graphData) return;
|
||||||
|
|
||||||
@@ -51,7 +106,6 @@ export class GameManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATED: Weighted Approval Calculation
|
|
||||||
recalculateApproval() {
|
recalculateApproval() {
|
||||||
if (!this.censusNodes || this.censusNodes.length === 0) {
|
if (!this.censusNodes || this.censusNodes.length === 0) {
|
||||||
this.approvalRating = 0;
|
this.approvalRating = 0;
|
||||||
@@ -62,7 +116,7 @@ export class GameManager {
|
|||||||
let totalMaxScore = 0; // The score if everyone had 0m walk
|
let totalMaxScore = 0; // The score if everyone had 0m walk
|
||||||
|
|
||||||
// Constants for walking distance
|
// Constants for walking distance
|
||||||
const MAX_WALK_DIST = 600; // Meters. Beyond this, satisfaction is 0.
|
const MAX_WALK_DIST = 1700; // Meters. Beyond this, satisfaction is 0.
|
||||||
const IDEAL_WALK_DIST = 50; // Meters. Below this, satisfaction is 100%.
|
const IDEAL_WALK_DIST = 50; // Meters. Below this, satisfaction is 100%.
|
||||||
|
|
||||||
for (const node of this.censusNodes) {
|
for (const node of this.censusNodes) {
|
||||||
@@ -79,8 +133,6 @@ export class GameManager {
|
|||||||
satisfaction = Math.max(0, satisfaction);
|
satisfaction = Math.max(0, satisfaction);
|
||||||
|
|
||||||
// 4. Add weighted score (Satisfaction * People Count)
|
// 4. Add weighted score (Satisfaction * People Count)
|
||||||
// A high-rise (count=100) at 50% satisfaction adds 50 points.
|
|
||||||
// A house (count=3) at 50% satisfaction adds 1.5 points.
|
|
||||||
totalWeightedScore += (satisfaction * node.count);
|
totalWeightedScore += (satisfaction * node.count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,22 +9,21 @@ export class RouteManager {
|
|||||||
|
|
||||||
// -- State --
|
// -- State --
|
||||||
this.currentRouteNodes = [];
|
this.currentRouteNodes = [];
|
||||||
this.savedRoutes = [];
|
this.savedRoutes = []; // { nodes, stats, mesh, color }
|
||||||
|
|
||||||
// -- Visuals --
|
// -- Visuals --
|
||||||
this.markers = [];
|
this.markers = [];
|
||||||
this.currentPathMesh = null;
|
this.currentPathMesh = null;
|
||||||
|
|
||||||
this.servedNodes = new Set();
|
this.servedNodes = new Set();
|
||||||
|
|
||||||
this.servedCoordinates = [];
|
this.servedCoordinates = [];
|
||||||
|
|
||||||
this.ROAD_OFFSET = 2.5;
|
this.ROAD_OFFSET = 2.5;
|
||||||
|
|
||||||
this.onRouteChanged = null;
|
this.onRouteChanged = null;
|
||||||
this.gameManager = null;
|
this.gameManager = null;
|
||||||
|
|
||||||
this.vehicleSystem = null;
|
this.vehicleSystem = null;
|
||||||
|
|
||||||
|
// Draft state
|
||||||
this.latestPathPoints = [];
|
this.latestPathPoints = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +35,6 @@ export class RouteManager {
|
|||||||
this.gameManager = gm;
|
this.gameManager = gm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
initGraph(data) {
|
initGraph(data) {
|
||||||
this.graphData = data;
|
this.graphData = data;
|
||||||
this.graphData.adjacency = {};
|
this.graphData.adjacency = {};
|
||||||
@@ -54,58 +52,234 @@ export class RouteManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to check if a node is covered
|
// ============================
|
||||||
isNodeServed(nodeId) {
|
// Save / Load / Serialization
|
||||||
return this.servedNodes.has(parseInt(nodeId));
|
// ============================
|
||||||
|
|
||||||
|
getSerializableRoutes() {
|
||||||
|
// We only save the node IDs and the color.
|
||||||
|
// Mesh and stats can be rebuilt.
|
||||||
|
return this.savedRoutes.map(r => ({
|
||||||
|
nodes: r.nodes,
|
||||||
|
color: r.color
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild the Set of served nodes
|
loadRoutes(routesData) {
|
||||||
refreshServedNodes() {
|
// 1. Cleanup existing
|
||||||
|
this.savedRoutes.forEach(r => {
|
||||||
|
if (r.mesh) {
|
||||||
|
this.scene.remove(r.mesh);
|
||||||
|
r.mesh.geometry.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.savedRoutes = [];
|
||||||
this.servedNodes.clear();
|
this.servedNodes.clear();
|
||||||
this.servedCoordinates = [];
|
|
||||||
|
|
||||||
this.savedRoutes.forEach(route => {
|
if (this.vehicleSystem) this.vehicleSystem.clearVehicles();
|
||||||
route.nodes.forEach(nodeId => {
|
|
||||||
if (!this.servedNodes.has(nodeId)) {
|
|
||||||
this.servedNodes.add(nodeId);
|
|
||||||
|
|
||||||
const node = this.graphData.nodes[nodeId];
|
// 2. Rebuild each route
|
||||||
if (node) {
|
routesData.forEach((data, index) => {
|
||||||
// Cache World Coordinates (x, z)
|
this.rebuildRouteFromData(data.nodes, data.color || this.getRandomColor(), index);
|
||||||
// Note: node.y in graphData is already flipped to match World Z
|
|
||||||
this.servedCoordinates.push({ x: node.x, z: node.y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.refreshServedNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuildRouteFromData(nodes, color, routeIndex) {
|
||||||
|
// 1. Calculate Path Geometry
|
||||||
|
const pathResult = this.calculateGeometryFromNodes(nodes);
|
||||||
|
if (!pathResult) return;
|
||||||
|
|
||||||
|
// 2. Create Mesh
|
||||||
|
const tubeMat = new THREE.MeshBasicMaterial({ color: color });
|
||||||
|
const mesh = new THREE.Mesh(pathResult.geometry, tubeMat);
|
||||||
|
this.scene.add(mesh);
|
||||||
|
|
||||||
|
// 3. Spawn Bus
|
||||||
|
if (this.vehicleSystem && pathResult.points.length > 0) {
|
||||||
|
this.vehicleSystem.addBusToRoute(pathResult.points, color, routeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Calculate Stats
|
||||||
|
const ridership = this.calculateRidership(nodes);
|
||||||
|
|
||||||
|
// 5. Store
|
||||||
|
this.savedRoutes.push({
|
||||||
|
nodes: [...nodes],
|
||||||
|
stats: { length: pathResult.length, cost: 0, ridership }, // Cost is sunk history, doesn't matter for load
|
||||||
|
mesh: mesh,
|
||||||
|
color: color
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns distance in meters. Returns Infinity if no routes exist.
|
getRandomColor() {
|
||||||
getDistanceToNearestTransit(x, z) {
|
const colors = ["#ef4444", "#f97316", "#f59e0b", "#84cc16", "#10b981", "#06b6d4", "#3b82f6", "#8b5cf6", "#d946ef"];
|
||||||
if (this.servedCoordinates.length === 0) return Infinity;
|
return colors[Math.floor(Math.random() * colors.length)];
|
||||||
|
|
||||||
let minSq = Infinity;
|
|
||||||
|
|
||||||
// Optimization: Standard loop is faster than forEach for high freq calls
|
|
||||||
for (let i = 0; i < this.servedCoordinates.length; i++) {
|
|
||||||
const sc = this.servedCoordinates[i];
|
|
||||||
const dx = sc.x - x;
|
|
||||||
const dz = sc.z - z;
|
|
||||||
const d2 = dx * dx + dz * dz;
|
|
||||||
if (d2 < minSq) minSq = d2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.sqrt(minSq);
|
// ============================
|
||||||
|
// Gameplay Actions
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
saveCurrentRoute() {
|
||||||
|
if (this.currentRouteNodes.length < 2 || !this.currentPathMesh) return;
|
||||||
|
|
||||||
|
const length = this.currentPathMesh.userData.length || 0;
|
||||||
|
const cost = this.gameManager.getProjectedCost(length);
|
||||||
|
|
||||||
|
if (!this.gameManager.canAfford(cost)) {
|
||||||
|
alert("Insufficient Funds!");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.gameManager.deductFunds(cost);
|
||||||
|
|
||||||
|
// 1. Define Color (Random default)
|
||||||
|
const color = this.getRandomColor();
|
||||||
|
|
||||||
|
// 2. Finalize Visuals
|
||||||
|
this.currentPathMesh.material.color.set(color);
|
||||||
|
|
||||||
|
// 3. Register Route
|
||||||
|
const routeIndex = this.savedRoutes.length;
|
||||||
|
|
||||||
|
if (this.vehicleSystem && this.latestPathPoints.length > 0) {
|
||||||
|
this.vehicleSystem.addBusToRoute(this.latestPathPoints, color, routeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ridership = this.calculateRidership(this.currentRouteNodes);
|
||||||
|
|
||||||
|
this.savedRoutes.push({
|
||||||
|
nodes: [...this.currentRouteNodes],
|
||||||
|
stats: { length, cost, ridership },
|
||||||
|
mesh: this.currentPathMesh,
|
||||||
|
color: color
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup draft state
|
||||||
|
this.currentPathMesh = null;
|
||||||
|
this.resetDraftingState();
|
||||||
|
this.refreshServedNodes();
|
||||||
|
this.gameManager.recalculateApproval();
|
||||||
|
this.gameManager.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRouteColor(index, hexColor) {
|
||||||
|
if (index < 0 || index >= this.savedRoutes.length) return;
|
||||||
|
|
||||||
|
const route = this.savedRoutes[index];
|
||||||
|
route.color = hexColor;
|
||||||
|
|
||||||
|
// Update Track Mesh
|
||||||
|
if (route.mesh) {
|
||||||
|
route.mesh.material.color.set(hexColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Vehicles
|
||||||
|
if (this.vehicleSystem) {
|
||||||
|
this.vehicleSystem.updateRouteColor(index, hexColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSavedRoute(index) {
|
||||||
|
if (index < 0 || index >= this.savedRoutes.length) return;
|
||||||
|
|
||||||
|
const route = this.savedRoutes[index];
|
||||||
|
if (route.mesh) {
|
||||||
|
this.scene.remove(route.mesh);
|
||||||
|
route.mesh.geometry.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can't easily remove buses for just one route without refactoring array indices
|
||||||
|
// Simplification: Reload all buses or mark them as dead.
|
||||||
|
// To keep it simple: We will remove the route data, and rebuild the vehicle system's arrays relative to new indices.
|
||||||
|
|
||||||
|
// 1. Remove from array
|
||||||
|
this.savedRoutes.splice(index, 1);
|
||||||
|
|
||||||
|
// 2. Refresh simulation (easiest way to handle index shifts)
|
||||||
|
// Save current state -> Clear Vehicles -> Rebuild Vehicles
|
||||||
|
if (this.vehicleSystem) {
|
||||||
|
this.vehicleSystem.clearVehicles();
|
||||||
|
this.savedRoutes.forEach((r, idx) => {
|
||||||
|
// Need to regenerate points for the bus system
|
||||||
|
const pathRes = this.calculateGeometryFromNodes(r.nodes);
|
||||||
|
if (pathRes && pathRes.points.length > 0) {
|
||||||
|
this.vehicleSystem.addBusToRoute(pathRes.points, r.color, idx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshServedNodes();
|
||||||
|
this.gameManager.recalculateApproval();
|
||||||
|
this.gameManager.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
editSavedRoute(index) {
|
||||||
|
// Delete and pull back to draft
|
||||||
|
if (index < 0 || index >= this.savedRoutes.length) return;
|
||||||
|
|
||||||
|
// We actually just delete it and put nodes in draft
|
||||||
|
// The player loses the "sunk cost" of construction in this simple version
|
||||||
|
const route = this.savedRoutes[index];
|
||||||
|
this.currentRouteNodes = [...route.nodes];
|
||||||
|
|
||||||
|
// Delete the existing
|
||||||
|
this.deleteSavedRoute(index);
|
||||||
|
|
||||||
|
// Visualize draft
|
||||||
|
this.currentRouteNodes.forEach(nodeId => this.addMarkerVisual(nodeId));
|
||||||
|
this.updatePathVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Helpers
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
calculateGeometryFromNodes(nodeList) {
|
||||||
|
if (nodeList.length < 2) return null;
|
||||||
|
|
||||||
|
let fullPathPoints = [];
|
||||||
|
let totalDist = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < nodeList.length - 1; i++) {
|
||||||
|
const start = nodeList[i];
|
||||||
|
const end = nodeList[i + 1];
|
||||||
|
const segmentEdges = this.computePathAStar(start, end);
|
||||||
|
|
||||||
|
if (!segmentEdges) continue;
|
||||||
|
|
||||||
|
segmentEdges.forEach(step => {
|
||||||
|
let dist = step.edgeData.length;
|
||||||
|
if (!dist) {
|
||||||
|
const p1 = step.edgeData.points[0];
|
||||||
|
const p2 = step.edgeData.points[step.edgeData.points.length - 1];
|
||||||
|
dist = Math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2);
|
||||||
|
}
|
||||||
|
totalDist += dist;
|
||||||
|
|
||||||
|
const rawPoints = step.edgeData.points;
|
||||||
|
let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
|
||||||
|
if (step.isReverse) segmentPoints.reverse();
|
||||||
|
|
||||||
|
const offsetSegment = this.getOffsetPath(segmentPoints, this.ROAD_OFFSET);
|
||||||
|
offsetSegment.forEach(p => fullPathPoints.push(new THREE.Vector3(p.x, 0.5, p.y)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPathPoints.length < 2) return null;
|
||||||
|
|
||||||
|
const curve = new THREE.CatmullRomCurve3(fullPathPoints);
|
||||||
|
const geometry = new THREE.TubeGeometry(curve, fullPathPoints.length, 1.5, 6, false);
|
||||||
|
|
||||||
|
return { geometry, length: totalDist, points: fullPathPoints };
|
||||||
|
}
|
||||||
|
|
||||||
calculateRidership(nodeList) {
|
calculateRidership(nodeList) {
|
||||||
if (!this.graphData || nodeList.length < 2) return 0;
|
if (!this.graphData || nodeList.length < 2) return 0;
|
||||||
|
|
||||||
let totalPop = 0;
|
let totalPop = 0;
|
||||||
let totalJobs = 0;
|
let totalJobs = 0;
|
||||||
|
|
||||||
// Sum census data for all nodes traversed by the route
|
|
||||||
nodeList.forEach(nodeId => {
|
nodeList.forEach(nodeId => {
|
||||||
const node = this.graphData.nodes[nodeId];
|
const node = this.graphData.nodes[nodeId];
|
||||||
if (node) {
|
if (node) {
|
||||||
@@ -113,20 +287,12 @@ export class RouteManager {
|
|||||||
totalJobs += (node.jobs || 0);
|
totalJobs += (node.jobs || 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const synergy = Math.min(totalPop, totalJobs);
|
const synergy = Math.min(totalPop, totalJobs);
|
||||||
|
|
||||||
// Efficiency Factor: How balanced is the route?
|
|
||||||
// + Base multiplier (arbitrary game balance constant)
|
|
||||||
const GAME_BALANCE_MULTIPLIER = 5.0;
|
const GAME_BALANCE_MULTIPLIER = 5.0;
|
||||||
|
|
||||||
|
|
||||||
return Math.floor(synergy * GAME_BALANCE_MULTIPLIER);
|
return Math.floor(synergy * GAME_BALANCE_MULTIPLIER);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ... (Existing Pathfinding & Drafting Methods) ...
|
||||||
// API Methods
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
addNodeByWorldPosition(vector3) {
|
addNodeByWorldPosition(vector3) {
|
||||||
if (!this.graphData) return;
|
if (!this.graphData) return;
|
||||||
@@ -152,62 +318,6 @@ export class RouteManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
saveCurrentRoute() {
|
|
||||||
if (this.currentRouteNodes.length < 2 || !this.currentPathMesh) return;
|
|
||||||
|
|
||||||
const length = this.currentPathMesh.userData.length || 0;
|
|
||||||
const cost = this.gameManager.getProjectedCost(length);
|
|
||||||
|
|
||||||
// 1. Check Funds
|
|
||||||
if (!this.gameManager.canAfford(cost)) {
|
|
||||||
alert("Insufficient Funds!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Pay
|
|
||||||
this.gameManager.deductFunds(cost);
|
|
||||||
|
|
||||||
// Spawn bus
|
|
||||||
if (this.vehicleSystem && this.latestPathPoints.length > 0) {
|
|
||||||
this.vehicleSystem.addBusToRoute(this.latestPathPoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Freeze & Save
|
|
||||||
this.currentPathMesh.material.color.setHex(0x10B981);
|
|
||||||
|
|
||||||
const ridership = this.calculateRidership(this.currentRouteNodes);
|
|
||||||
|
|
||||||
this.savedRoutes.push({
|
|
||||||
nodes: [...this.currentRouteNodes],
|
|
||||||
stats: { length, cost, ridership },
|
|
||||||
mesh: this.currentPathMesh
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentPathMesh = null;
|
|
||||||
this.resetDraftingState();
|
|
||||||
|
|
||||||
this.refreshServedNodes();
|
|
||||||
|
|
||||||
// Force UI update to show new total riders
|
|
||||||
this.gameManager.updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
editSavedRoute(index) {
|
|
||||||
if (index < 0 || index >= this.savedRoutes.length) return;
|
|
||||||
this.clearCurrentRoute();
|
|
||||||
const route = this.savedRoutes[index];
|
|
||||||
this.currentRouteNodes = [...route.nodes];
|
|
||||||
if (route.mesh) { this.scene.remove(route.mesh); route.mesh.geometry.dispose(); }
|
|
||||||
this.savedRoutes.splice(index, 1);
|
|
||||||
|
|
||||||
this.refreshServedNodes();
|
|
||||||
|
|
||||||
this.currentRouteNodes.forEach(nodeId => this.addMarkerVisual(nodeId));
|
|
||||||
this.updatePathVisuals();
|
|
||||||
this.gameManager.updateUI(); // Update UI since we removed a route (income drops)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearCurrentRoute() {
|
clearCurrentRoute() {
|
||||||
if (this.currentPathMesh) { this.scene.remove(this.currentPathMesh); this.currentPathMesh.geometry.dispose(); this.currentPathMesh = null; }
|
if (this.currentPathMesh) { this.scene.remove(this.currentPathMesh); this.currentPathMesh.geometry.dispose(); this.currentPathMesh = null; }
|
||||||
this.resetDraftingState();
|
this.resetDraftingState();
|
||||||
@@ -220,61 +330,23 @@ export class RouteManager {
|
|||||||
if (this.onRouteChanged) this.onRouteChanged({ length: 0, cost: 0, ridership: 0 });
|
if (this.onRouteChanged) this.onRouteChanged({ length: 0, cost: 0, ridership: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteSavedRoute(index) {
|
|
||||||
if (index < 0 || index >= this.savedRoutes.length) return;
|
|
||||||
const route = this.savedRoutes[index];
|
|
||||||
if (route.mesh) { this.scene.remove(route.mesh); route.mesh.geometry.dispose(); }
|
|
||||||
this.savedRoutes.splice(index, 1);
|
|
||||||
this.refreshServedNodes();
|
|
||||||
this.gameManager.updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
getSavedRoutes() { return this.savedRoutes; }
|
getSavedRoutes() { return this.savedRoutes; }
|
||||||
|
|
||||||
// ============================
|
|
||||||
// Visuals & Logic
|
|
||||||
// ============================
|
|
||||||
|
|
||||||
updatePathVisuals() {
|
updatePathVisuals() {
|
||||||
if (this.currentRouteNodes.length < 2) {
|
if (this.currentRouteNodes.length < 2) {
|
||||||
if (this.currentPathMesh) {
|
if (this.currentPathMesh) {
|
||||||
this.scene.remove(this.currentPathMesh);
|
this.scene.remove(this.currentPathMesh);
|
||||||
this.currentPathMesh = null;
|
this.currentPathMesh = null;
|
||||||
}
|
}
|
||||||
// Report 0 stats
|
|
||||||
if (this.onRouteChanged) this.onRouteChanged({ length: 0, cost: 0, ridership: 0 });
|
if (this.onRouteChanged) this.onRouteChanged({ length: 0, cost: 0, ridership: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullPathPoints = [];
|
// Reuse logic
|
||||||
let totalDist = 0;
|
const result = this.calculateGeometryFromNodes(this.currentRouteNodes);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
for (let i = 0; i < this.currentRouteNodes.length - 1; i++) {
|
this.latestPathPoints = result.points;
|
||||||
const start = this.currentRouteNodes[i];
|
|
||||||
const end = this.currentRouteNodes[i + 1];
|
|
||||||
const segmentEdges = this.computePathAStar(start, end);
|
|
||||||
|
|
||||||
if (!segmentEdges) continue;
|
|
||||||
|
|
||||||
segmentEdges.forEach(step => {
|
|
||||||
let dist = step.edgeData.length;
|
|
||||||
if (!dist) {
|
|
||||||
const p1 = step.edgeData.points[0];
|
|
||||||
const p2 = step.edgeData.points[step.edgeData.points.length - 1];
|
|
||||||
dist = Math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2);
|
|
||||||
}
|
|
||||||
totalDist += dist;
|
|
||||||
|
|
||||||
const rawPoints = step.edgeData.points;
|
|
||||||
let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
|
|
||||||
if (step.isReverse) segmentPoints.reverse();
|
|
||||||
|
|
||||||
const offsetSegment = this.getOffsetPath(segmentPoints, this.ROAD_OFFSET);
|
|
||||||
offsetSegment.forEach(p => fullPathPoints.push(new THREE.Vector3(p.x, 0.5, p.y)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.latestPathPoints = fullPathPoints;
|
|
||||||
|
|
||||||
// Rebuild Mesh
|
// Rebuild Mesh
|
||||||
if (this.currentPathMesh) {
|
if (this.currentPathMesh) {
|
||||||
@@ -282,25 +354,19 @@ export class RouteManager {
|
|||||||
this.currentPathMesh.geometry.dispose();
|
this.currentPathMesh.geometry.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullPathPoints.length < 2) return;
|
|
||||||
|
|
||||||
const curve = new THREE.CatmullRomCurve3(fullPathPoints);
|
|
||||||
const tubeGeom = new THREE.TubeGeometry(curve, fullPathPoints.length, 1.5, 6, false);
|
|
||||||
const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
|
const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
|
||||||
|
this.currentPathMesh = new THREE.Mesh(result.geometry, tubeMat);
|
||||||
this.currentPathMesh = new THREE.Mesh(tubeGeom, tubeMat);
|
this.currentPathMesh.userData.length = result.length;
|
||||||
this.currentPathMesh.userData.length = totalDist;
|
|
||||||
this.scene.add(this.currentPathMesh);
|
this.scene.add(this.currentPathMesh);
|
||||||
|
|
||||||
this.updateMarkerColors();
|
this.updateMarkerColors();
|
||||||
|
|
||||||
// -- CALCULATE LIVE GAMEPLAY STATS --
|
|
||||||
const projectedRiders = this.calculateRidership(this.currentRouteNodes);
|
const projectedRiders = this.calculateRidership(this.currentRouteNodes);
|
||||||
const projectedCost = this.gameManager ? this.gameManager.getProjectedCost(totalDist) : 0;
|
const projectedCost = this.gameManager ? this.gameManager.getProjectedCost(result.length) : 0;
|
||||||
|
|
||||||
if (this.onRouteChanged) {
|
if (this.onRouteChanged) {
|
||||||
this.onRouteChanged({
|
this.onRouteChanged({
|
||||||
length: totalDist,
|
length: result.length,
|
||||||
cost: projectedCost,
|
cost: projectedCost,
|
||||||
ridership: projectedRiders
|
ridership: projectedRiders
|
||||||
});
|
});
|
||||||
@@ -330,9 +396,35 @@ export class RouteManager {
|
|||||||
this.updateMarkerColors();
|
this.updateMarkerColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
refreshServedNodes() {
|
||||||
// Algorithms
|
this.servedNodes.clear();
|
||||||
// ============================
|
this.servedCoordinates = [];
|
||||||
|
|
||||||
|
this.savedRoutes.forEach(route => {
|
||||||
|
route.nodes.forEach(nodeId => {
|
||||||
|
if (!this.servedNodes.has(nodeId)) {
|
||||||
|
this.servedNodes.add(nodeId);
|
||||||
|
const node = this.graphData.nodes[nodeId];
|
||||||
|
if (node) {
|
||||||
|
this.servedCoordinates.push({ x: node.x, z: node.y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDistanceToNearestTransit(x, z) {
|
||||||
|
if (this.servedCoordinates.length === 0) return Infinity;
|
||||||
|
let minSq = Infinity;
|
||||||
|
for (let i = 0; i < this.servedCoordinates.length; i++) {
|
||||||
|
const sc = this.servedCoordinates[i];
|
||||||
|
const dx = sc.x - x;
|
||||||
|
const dz = sc.z - z;
|
||||||
|
const d2 = dx * dx + dz * dz;
|
||||||
|
if (d2 < minSq) minSq = d2;
|
||||||
|
}
|
||||||
|
return Math.sqrt(minSq);
|
||||||
|
}
|
||||||
|
|
||||||
findNearestNode(x, z) {
|
findNearestNode(x, z) {
|
||||||
let closestId = null;
|
let closestId = null;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export class UIManager {
|
export class UIManager {
|
||||||
constructor(routeManager) {
|
constructor(routeManager) {
|
||||||
this.routeManager = routeManager;
|
this.routeManager = routeManager;
|
||||||
|
this.gameManager = null; // Set via dependency injection in main.js if needed, or we just access logic differently
|
||||||
|
|
||||||
// UI Elements
|
// UI Elements
|
||||||
this.elCurrentLength = document.getElementById('current-length');
|
this.elCurrentLength = document.getElementById('current-length');
|
||||||
@@ -10,7 +11,7 @@ export class UIManager {
|
|||||||
this.elBudget = document.getElementById('val-budget');
|
this.elBudget = document.getElementById('val-budget');
|
||||||
this.elDay = document.getElementById('val-day');
|
this.elDay = document.getElementById('val-day');
|
||||||
this.elTotalRiders = document.getElementById('val-riders');
|
this.elTotalRiders = document.getElementById('val-riders');
|
||||||
this.elApproval = document.getElementById('val-approval'); // NEW
|
this.elApproval = document.getElementById('val-approval');
|
||||||
|
|
||||||
this.elIncomeFloat = document.getElementById('income-float');
|
this.elIncomeFloat = document.getElementById('income-float');
|
||||||
this.elRouteList = document.getElementById('route-list');
|
this.elRouteList = document.getElementById('route-list');
|
||||||
@@ -20,7 +21,12 @@ export class UIManager {
|
|||||||
this.btnDiscard = document.getElementById('btn-discard');
|
this.btnDiscard = document.getElementById('btn-discard');
|
||||||
this.btnToggle = document.getElementById('ui-toggle');
|
this.btnToggle = document.getElementById('ui-toggle');
|
||||||
|
|
||||||
// NEW: View Mode
|
// Save/Load
|
||||||
|
this.btnSaveGame = document.getElementById('btn-save-game');
|
||||||
|
this.btnLoadGame = document.getElementById('btn-load-game');
|
||||||
|
this.inputLoadGame = document.getElementById('file-load-game');
|
||||||
|
|
||||||
|
// View Mode
|
||||||
this.selectViewMode = document.getElementById('view-mode');
|
this.selectViewMode = document.getElementById('view-mode');
|
||||||
|
|
||||||
this.onViewModeChanged = null;
|
this.onViewModeChanged = null;
|
||||||
@@ -41,12 +47,37 @@ export class UIManager {
|
|||||||
this.elContainer.classList.toggle('hidden');
|
this.elContainer.classList.toggle('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle Dropdown Change
|
|
||||||
this.selectViewMode.addEventListener('change', (e) => {
|
this.selectViewMode.addEventListener('change', (e) => {
|
||||||
if (this.onViewModeChanged) {
|
if (this.onViewModeChanged) {
|
||||||
this.onViewModeChanged(e.target.value);
|
this.onViewModeChanged(e.target.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Save / Load System
|
||||||
|
this.btnSaveGame.addEventListener('click', () => {
|
||||||
|
if (this.routeManager.gameManager) {
|
||||||
|
this.routeManager.gameManager.saveGame();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.btnLoadGame.addEventListener('click', () => {
|
||||||
|
this.inputLoadGame.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inputLoadGame.addEventListener('change', (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (evt) => {
|
||||||
|
if (this.routeManager.gameManager) {
|
||||||
|
this.routeManager.gameManager.loadGame(evt.target.result);
|
||||||
|
this.renderRouteList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
e.target.value = ''; // Reset so we can load same file again if needed
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGameStats(stats) {
|
updateGameStats(stats) {
|
||||||
@@ -54,12 +85,10 @@ export class UIManager {
|
|||||||
this.elDay.textContent = stats.day;
|
this.elDay.textContent = stats.day;
|
||||||
this.elTotalRiders.textContent = stats.totalRiders.toLocaleString();
|
this.elTotalRiders.textContent = stats.totalRiders.toLocaleString();
|
||||||
|
|
||||||
// Update Approval
|
|
||||||
this.elApproval.textContent = stats.approval + "%";
|
this.elApproval.textContent = stats.approval + "%";
|
||||||
// Color code it
|
if (stats.approval > 75) this.elApproval.style.color = "#10B981";
|
||||||
if (stats.approval > 75) this.elApproval.style.color = "#10B981"; // Green
|
else if (stats.approval < 40) this.elApproval.style.color = "#EF4444";
|
||||||
else if (stats.approval < 40) this.elApproval.style.color = "#EF4444"; // Red
|
else this.elApproval.style.color = "#D97706";
|
||||||
else this.elApproval.style.color = "#D97706"; // Orange
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showIncomeFeedback(amount) {
|
showIncomeFeedback(amount) {
|
||||||
@@ -92,12 +121,34 @@ export class UIManager {
|
|||||||
? (route.stats.length / 1000).toFixed(1) + "km"
|
? (route.stats.length / 1000).toFixed(1) + "km"
|
||||||
: Math.round(route.stats.length) + "m";
|
: Math.round(route.stats.length) + "m";
|
||||||
|
|
||||||
|
// Color Picker
|
||||||
|
const colorInput = document.createElement('input');
|
||||||
|
colorInput.type = 'color';
|
||||||
|
colorInput.value = route.color || "#000000";
|
||||||
|
colorInput.style.border = "none";
|
||||||
|
colorInput.style.width = "24px";
|
||||||
|
colorInput.style.height = "24px";
|
||||||
|
colorInput.style.cursor = "pointer";
|
||||||
|
colorInput.title = "Change Route Color";
|
||||||
|
|
||||||
|
colorInput.addEventListener('input', (e) => {
|
||||||
|
this.routeManager.updateRouteColor(index, e.target.value);
|
||||||
|
});
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
span.innerHTML = `
|
span.innerHTML = `
|
||||||
<strong>Route ${index + 1}</strong> <br>
|
<strong>Route ${index + 1}</strong> <br>
|
||||||
<small>${lenStr} | ${route.stats.ridership} riders</small>
|
<small>${lenStr} | ${route.stats.ridership} riders</small>
|
||||||
`;
|
`;
|
||||||
li.appendChild(span);
|
|
||||||
|
const detailsDiv = document.createElement('div');
|
||||||
|
detailsDiv.style.display = "flex";
|
||||||
|
detailsDiv.style.alignItems = "center";
|
||||||
|
detailsDiv.style.gap = "8px";
|
||||||
|
detailsDiv.appendChild(colorInput);
|
||||||
|
detailsDiv.appendChild(span);
|
||||||
|
|
||||||
|
li.appendChild(detailsDiv);
|
||||||
|
|
||||||
const btnEdit = document.createElement('button');
|
const btnEdit = document.createElement('button');
|
||||||
btnEdit.textContent = "Edit";
|
btnEdit.textContent = "Edit";
|
||||||
@@ -106,7 +157,6 @@ export class UIManager {
|
|||||||
this.routeManager.editSavedRoute(index);
|
this.routeManager.editSavedRoute(index);
|
||||||
this.renderRouteList();
|
this.renderRouteList();
|
||||||
};
|
};
|
||||||
li.appendChild(btnEdit);
|
|
||||||
|
|
||||||
const btnDel = document.createElement('button');
|
const btnDel = document.createElement('button');
|
||||||
btnDel.textContent = "✕";
|
btnDel.textContent = "✕";
|
||||||
@@ -115,7 +165,12 @@ export class UIManager {
|
|||||||
this.routeManager.deleteSavedRoute(index);
|
this.routeManager.deleteSavedRoute(index);
|
||||||
this.renderRouteList();
|
this.renderRouteList();
|
||||||
};
|
};
|
||||||
li.appendChild(btnDel);
|
|
||||||
|
const btnDiv = document.createElement('div');
|
||||||
|
btnDiv.appendChild(btnEdit);
|
||||||
|
btnDiv.appendChild(btnDel);
|
||||||
|
|
||||||
|
li.appendChild(btnDiv);
|
||||||
|
|
||||||
this.elRouteList.appendChild(li);
|
this.elRouteList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,31 +3,39 @@ import * as THREE from 'three';
|
|||||||
export class VehicleSystem {
|
export class VehicleSystem {
|
||||||
constructor(scene) {
|
constructor(scene) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
this.buses = []; // { mesh, points, dists, totalLen, currentDist, speed, direction }
|
this.buses = []; // { mesh, points, dists, totalLen, currentDist, speed, direction, routeIndex }
|
||||||
|
|
||||||
this.busGeom = new THREE.BoxGeometry(3.5, 4.0, 10.0);
|
this.busGeom = new THREE.BoxGeometry(3.5, 4.0, 10.0);
|
||||||
this.busGeom.translate(0, 3.5, 0);
|
this.busGeom.translate(0, 3.5, 0);
|
||||||
|
|
||||||
this.busMat = new THREE.MeshStandardMaterial({
|
this.baseBusMat = new THREE.MeshStandardMaterial({
|
||||||
color: 0xF59E0B, // Amber body
|
color: 0xF59E0B,
|
||||||
emissive: 0xB45309, // Slight orange glow so they don't get lost in shadow
|
emissive: 0xB45309,
|
||||||
emissiveIntensity: 0.4,
|
emissiveIntensity: 0.2,
|
||||||
roughness: 0.2
|
roughness: 0.2
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addBusToRoute(routePathPoints) {
|
addBusToRoute(routePathPoints, colorStr, routeIndex) {
|
||||||
if (!routePathPoints || routePathPoints.length < 2) return;
|
if (!routePathPoints || routePathPoints.length < 2) return;
|
||||||
|
|
||||||
// Clone points to ensure they aren't affected by outside changes
|
|
||||||
const points = routePathPoints.map(p => p.clone());
|
const points = routePathPoints.map(p => p.clone());
|
||||||
|
|
||||||
const mesh = new THREE.Mesh(this.busGeom, this.busMat);
|
// Create material specific to this bus/route
|
||||||
|
const mat = this.baseBusMat.clone();
|
||||||
|
if (colorStr) {
|
||||||
|
mat.color.set(colorStr);
|
||||||
|
// Slight emissive tint of same color
|
||||||
|
const c = new THREE.Color(colorStr);
|
||||||
|
mat.emissive.set(c.multiplyScalar(0.5));
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesh = new THREE.Mesh(this.busGeom, mat);
|
||||||
mesh.position.copy(points[0]);
|
mesh.position.copy(points[0]);
|
||||||
mesh.castShadow = true;
|
mesh.castShadow = true;
|
||||||
this.scene.add(mesh);
|
this.scene.add(mesh);
|
||||||
|
|
||||||
// Pre-calculate cumulative distances for smooth interpolation
|
// Pre-calculate
|
||||||
let totalLen = 0;
|
let totalLen = 0;
|
||||||
const dists = [0];
|
const dists = [0];
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
@@ -42,17 +50,36 @@ export class VehicleSystem {
|
|||||||
dists: dists,
|
dists: dists,
|
||||||
totalLen: totalLen,
|
totalLen: totalLen,
|
||||||
currentDist: 0,
|
currentDist: 0,
|
||||||
speed: 40, // Speed in units/sec
|
speed: 40,
|
||||||
direction: 1 // 1 = Forward, -1 = Backward
|
direction: 1,
|
||||||
|
routeIndex: routeIndex
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateRouteColor(routeIndex, hexColor) {
|
||||||
|
// Update all buses belonging to this route
|
||||||
|
this.buses.forEach(bus => {
|
||||||
|
if (bus.routeIndex === routeIndex) {
|
||||||
|
bus.mesh.material.color.set(hexColor);
|
||||||
|
const c = new THREE.Color(hexColor);
|
||||||
|
bus.mesh.material.emissive.set(c.multiplyScalar(0.5));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearVehicles() {
|
||||||
|
this.buses.forEach(bus => {
|
||||||
|
this.scene.remove(bus.mesh);
|
||||||
|
bus.mesh.geometry.dispose();
|
||||||
|
bus.mesh.material.dispose();
|
||||||
|
});
|
||||||
|
this.buses = [];
|
||||||
|
}
|
||||||
|
|
||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
this.buses.forEach(bus => {
|
this.buses.forEach(bus => {
|
||||||
// 1. Move
|
|
||||||
bus.currentDist += bus.speed * deltaTime * bus.direction;
|
bus.currentDist += bus.speed * deltaTime * bus.direction;
|
||||||
|
|
||||||
// 2. Check Bounds & Reversal
|
|
||||||
if (bus.currentDist >= bus.totalLen) {
|
if (bus.currentDist >= bus.totalLen) {
|
||||||
bus.currentDist = bus.totalLen;
|
bus.currentDist = bus.totalLen;
|
||||||
bus.direction = -1;
|
bus.direction = -1;
|
||||||
@@ -61,27 +88,20 @@ export class VehicleSystem {
|
|||||||
bus.direction = 1;
|
bus.direction = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Find current segment index
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
// Simple linear search (efficient enough for small N points)
|
|
||||||
while (i < bus.dists.length - 2 && bus.currentDist > bus.dists[i + 1]) {
|
while (i < bus.dists.length - 2 && bus.currentDist > bus.dists[i + 1]) {
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Interpolate Position
|
|
||||||
const startDist = bus.dists[i];
|
const startDist = bus.dists[i];
|
||||||
const endDist = bus.dists[i + 1];
|
const endDist = bus.dists[i + 1];
|
||||||
const segmentLen = endDist - startDist;
|
const segmentLen = endDist - startDist;
|
||||||
|
|
||||||
// Avoid divide by zero if 2 points are identical
|
|
||||||
const alpha = segmentLen > 0.0001 ? (bus.currentDist - startDist) / segmentLen : 0;
|
const alpha = segmentLen > 0.0001 ? (bus.currentDist - startDist) / segmentLen : 0;
|
||||||
|
|
||||||
const pStart = bus.points[i];
|
const pStart = bus.points[i];
|
||||||
const pEnd = bus.points[i + 1];
|
const pEnd = bus.points[i + 1];
|
||||||
|
|
||||||
bus.mesh.position.lerpVectors(pStart, pEnd, alpha);
|
bus.mesh.position.lerpVectors(pStart, pEnd, alpha);
|
||||||
|
|
||||||
// 5. Rotation (Look at target)
|
|
||||||
const lookTarget = bus.direction === 1 ? pEnd : pStart;
|
const lookTarget = bus.direction === 1 ? pEnd : pStart;
|
||||||
bus.mesh.lookAt(lookTarget);
|
bus.mesh.lookAt(lookTarget);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ function init() {
|
|||||||
vehicleSystem = new VehicleSystem(scene);
|
vehicleSystem = new VehicleSystem(scene);
|
||||||
routeManager.setVehicleSystem(vehicleSystem); // Inject into RouteManager
|
routeManager.setVehicleSystem(vehicleSystem); // Inject into RouteManager
|
||||||
|
|
||||||
gameManager.start(); // Start the loop
|
|
||||||
|
|
||||||
// 3. Input
|
// 3. Input
|
||||||
inputManager = new InputManager(camera, renderer.domElement, scene, controls);
|
inputManager = new InputManager(camera, renderer.domElement, scene, controls);
|
||||||
@@ -88,6 +87,7 @@ function init() {
|
|||||||
]).then(([visual, routing]) => {
|
]).then(([visual, routing]) => {
|
||||||
routeManager.initGraph(routing);
|
routeManager.initGraph(routing);
|
||||||
renderCity(visual);
|
renderCity(visual);
|
||||||
|
gameManager.start(); // Start the loop
|
||||||
});
|
});
|
||||||
|
|
||||||
animate();
|
animate();
|
||||||
|
|||||||
Reference in New Issue
Block a user