emergency: add nearest-city emergency vehicle routes

This commit is contained in:
Richard Zink committed 2026-10-13 06:43:09 +02:00
1 parent 6553d2fd1d
commit fec1941e12
2 files changed
+233 -1

No files matched your search

+23
View File
@@ -118,6 +118,29 @@ window.CONFIG = {
// weatherSimProfile: { rain: { code: 80, tempC: 9, precipMm: 2.0, windKmh: 28 } }
},
// ------------------------------------------------------------------
// Emergency vehicles
// ------------------------------------------------------------------
// Emergency vehicles start from the nearest city/town to CONFIG.location
// and drive a selected secondary/primary road chain out and back. The
// route selection is intentionally conservative: it uses already loaded
// road chains and does not run a full routing engine.
emergencyVehicles: {
enabled: true,
count: 3,
speedKmh: 85,
minKm: 40,
maxKm: 80,
rankMax: 1, // 0=city, 1=town
color: [0.22, 0.55, 1.0],
flashHz: 2.2,
nightOnly: true,
bright: 1.35
// emergencyVehicles: { enabled: false }
// emergencyVehicles: { count: 6, speedKmh: 110, minKm: 25, maxKm: 120 }
// emergencyVehicles: { color: [1.0, 0.18, 0.12], flashHz: 3.0 }
},
// ------------------------------------------------------------------
// Weather effects
// ------------------------------------------------------------------
+210 -1
View File
@@ -3463,6 +3463,20 @@ var carPtsVboB = null, carPtsCountB = 0;
var carProg = null, carAttribs = null, carUniforms = null;
var carPtsProg = null, carPtsAttribs = null, carPtsUniforms = null;
var EMERGENCY_CFG = (window.CONFIG && window.CONFIG.emergencyVehicles) || {};
var EMERGENCY_ON = EMERGENCY_CFG.enabled !== false;
var EMERGENCY_COUNT = (typeof EMERGENCY_CFG.count === 'number') ? Math.max(1, Math.min(12, EMERGENCY_CFG.count)) : 3;
var EMERGENCY_SPEED_KMH = (typeof EMERGENCY_CFG.speedKmh === 'number') ? Math.max(10, Math.min(220, EMERGENCY_CFG.speedKmh)) : 85;
var EMERGENCY_MIN_KM = (typeof EMERGENCY_CFG.minKm === 'number') ? Math.max(5, EMERGENCY_CFG.minKm) : 40;
var EMERGENCY_MAX_KM = (typeof EMERGENCY_CFG.maxKm === 'number') ? Math.max(EMERGENCY_MIN_KM, EMERGENCY_CFG.maxKm) : 80;
var EMERGENCY_RANK_MAX = (typeof EMERGENCY_CFG.rankMax === 'number') ? Math.max(0, Math.min(4, EMERGENCY_CFG.rankMax)) : 1;
var EMERGENCY_COLOR = Array.isArray(EMERGENCY_CFG.color) ? EMERGENCY_CFG.color : [0.22, 0.55, 1.0];
var EMERGENCY_FLASH_HZ = (typeof EMERGENCY_CFG.flashHz === 'number') ? Math.max(0.2, Math.min(8, EMERGENCY_CFG.flashHz)) : 2.2;
var EMERGENCY_NIGHT_ONLY = EMERGENCY_CFG.nightOnly !== false;
var EMERGENCY_BRIGHT = (typeof EMERGENCY_CFG.bright === 'number') ? Math.max(0.1, Math.min(4, EMERGENCY_CFG.bright)) : 1.35;
var emergencyRoutes = [], emergencyVehicles = [], emergencyVbo = null, emergencyPts = null, emergencyCount = 0;
var emergencyLastT = null, emergencyStartCity = null;
function carLonToX(lon) {
var n = 1 << GROUND_TILE_Z;
return GROUND_BASE_X - ((lon + 180) / 360) * n;
@@ -3490,6 +3504,159 @@ function carYAt(x, z) {
return m * ELEV_SCALE_UNITS + CAR_Y;
}
function emergencyChainAt(c, idx) {
var k = 0, i, j;
for (i = 0; i < idx; i++) k += c.cnts[i] * 3;
var n = c.cnts[idx], out = [];
for (j = 0; j < n; j++) {
out.push({ x: c.pts[k], z: c.pts[k + 1], d: c.pts[k + 2] });
k += 3;
}
return out;
}
function nearestEmergencyCity() {
var cx = carLonToX(_baseLon), cz = carLatToZ(_baseLat);
var best = { x: cx, z: cz, rank: 0, pop: 0, dist: 0 };
if (!cityGeo || cityGeo.length < 4) return best;
var found = false;
for (var i = 0; i + 3 < cityGeo.length; i += 4) {
var rank = cityGeo[i + 2] | 0;
if (rank > EMERGENCY_RANK_MAX) continue;
var dx = cityGeo[i] - cx, dz = cityGeo[i + 1] - cz;
var d = Math.hypot(dx, dz);
if (!found || d < best.dist) {
found = true;
best = { x: cityGeo[i], z: cityGeo[i + 1], rank: rank, pop: cityGeo[i + 3], dist: d };
}
}
return best;
}
function selectEmergencyRoutes() {
emergencyRoutes = [];
emergencyVehicles = [];
emergencyCount = 0;
emergencyLastT = null;
if (!EMERGENCY_ON || !carGeo || !carGeo.secondary || !carGeo.secondary.cnts || !carGeo.secondary.cnts.length) {
window.__emergencyDbg = { enabled: EMERGENCY_ON, startCity: null, routes: 0 };
return;
}
emergencyStartCity = nearestEmergencyCity();
var sec = carGeo.secondary;
var targetKm = (EMERGENCY_MIN_KM + EMERGENCY_MAX_KM) / 2;
var candidates = [];
var k = 0;
for (var ci = 0; ci < sec.cnts.length; ci++) {
var n = sec.cnts[ci];
if (n < 2) { k += n * 3; continue; }
var firstX = sec.pts[k], firstZ = sec.pts[k + 1], firstD = sec.pts[k + 2];
var last = k + (n - 1) * 3;
var lastX = sec.pts[last], lastZ = sec.pts[last + 1], lastD = sec.pts[last + 2];
var len = Math.max(0, lastD - firstD);
var lenKm = len * KM_PER_TILE;
var dStart = Math.hypot(firstX - emergencyStartCity.x, firstZ - emergencyStartCity.z);
var dEnd = Math.hypot(lastX - emergencyStartCity.x, lastZ - emergencyStartCity.z);
var near = Math.min(dStart, dEnd);
if (near < 1.2 && lenKm >= EMERGENCY_MIN_KM * 0.55 && lenKm <= EMERGENCY_MAX_KM * 1.45) {
candidates.push({
ci: ci, len: len, lenKm: lenKm, near: near,
score: Math.abs(lenKm - targetKm) + near * 18,
reverse: dEnd < dStart
});
}
k += n * 3;
}
candidates.sort(function (a, b) { return a.score - b.score; });
if (!candidates.length) {
var best = null, k2 = 0;
for (var ci2 = 0; ci2 < sec.cnts.length; ci2++) {
var n2 = sec.cnts[ci2];
if (n2 < 2) { k2 += n2 * 3; continue; }
var f2 = k2, l2 = k2 + (n2 - 1) * 3;
var len2 = Math.max(0, sec.pts[l2 + 2] - sec.pts[f2 + 2]);
var near2 = Math.min(
Math.hypot(sec.pts[f2] - emergencyStartCity.x, sec.pts[f2 + 1] - emergencyStartCity.z),
Math.hypot(sec.pts[l2] - emergencyStartCity.x, sec.pts[l2 + 1] - emergencyStartCity.z));
if (near2 < 2.5 && (!best || len2 > best.len)) best = { ci: ci2, len: len2, lenKm: len2 * KM_PER_TILE, near: near2, reverse: false };
k2 += n2 * 3;
}
if (best) candidates.push(best);
}
for (var r = 0; r < Math.min(EMERGENCY_COUNT, candidates.length); r++) {
var route = emergencyChainAt(sec, candidates[r].ci);
if (candidates[r].reverse) route.reverse();
var len = route.length ? route[route.length - 1].d - route[0].d : 0;
if (len <= 0) continue;
emergencyRoutes.push({ pts: route, len: len, lenKm: len * KM_PER_TILE, nearKm: candidates[r].near * KM_PER_TILE });
}
for (var v = 0; v < emergencyRoutes.length; v++) {
emergencyVehicles.push({
route: v,
d: emergencyRoutes[v].len * (0.08 + 0.72 * ((v * 0.371) % 1)),
dir: ((v % 2) === 0) ? 1 : -1,
seed: (v * 0.6180339887) % 1
});
}
emergencyCount = emergencyVehicles.length;
window.__emergencyDbg = {
enabled: EMERGENCY_ON,
startCity: emergencyStartCity ? {
x: +emergencyStartCity.x.toFixed(3),
z: +emergencyStartCity.z.toFixed(3),
rank: emergencyStartCity.rank,
pop: emergencyStartCity.pop,
km: +(emergencyStartCity.dist * KM_PER_TILE).toFixed(1)
} : null,
routes: emergencyRoutes.map(function (r) { return { km: +r.lenKm.toFixed(1), nearKm: +r.nearKm.toFixed(1) }; })
};
}
function emergencyPointAt(route, dist) {
var pts = route.pts;
if (!pts.length) return null;
dist = Math.max(0, Math.min(route.len, dist));
var lo = 0, hi = pts.length - 1;
while (lo + 1 < hi) {
var mid = (lo + hi) >> 1;
if (pts[mid].d < dist) lo = mid; else hi = mid;
}
var a = pts[lo], b = pts[hi];
var seg = Math.max(0.0001, b.d - a.d);
var f = Math.max(0, Math.min(1, (dist - a.d) / seg));
var x = a.x + (b.x - a.x) * f;
var z = a.z + (b.z - a.z) * f;
var dx = b.x - a.x, dz = b.z - a.z;
var L = Math.hypot(dx, dz) || 1;
return { x: x, y: carYAt(x, z), z: z, tx: dx / L, tz: dz / L };
}
function updateEmergencyVehicles(now) {
if (!EMERGENCY_ON || !emergencyVehicles.length) { emergencyCount = 0; return 0; }
if (emergencyLastT === null) emergencyLastT = now;
var dt = Math.max(0, Math.min(5, now - emergencyLastT));
emergencyLastT = now;
var speed = EMERGENCY_SPEED_KMH / 3600.0 / KM_PER_TILE;
var max = emergencyVehicles.length * 10;
if (!emergencyPts || emergencyPts.length < max) emergencyPts = new Float32Array(max);
var pi = 0;
for (var i = 0; i < emergencyVehicles.length; i++) {
var v = emergencyVehicles[i], route = emergencyRoutes[v.route];
if (!route) continue;
v.d += v.dir * speed * dt;
if (v.d >= route.len) { v.d = route.len; v.dir = -1; }
if (v.d <= 0) { v.d = 0; v.dir = 1; }
var p = emergencyPointAt(route, v.d);
if (!p) continue;
emergencyPts[pi++] = p.x; emergencyPts[pi++] = p.y; emergencyPts[pi++] = p.z;
emergencyPts[pi++] = p.tx; emergencyPts[pi++] = p.tz;
emergencyPts[pi++] = 1; emergencyPts[pi++] = v.seed; emergencyPts[pi++] = 1;
emergencyPts[pi++] = v.seed; emergencyPts[pi++] = route.len;
}
emergencyCount = pi / 10;
return emergencyCount;
}
function resamplePoly(pts) {
var out = [];
if (pts.length < 2) return out;
@@ -3754,6 +3921,7 @@ function fetchSettlements() {
function publish() {
cityGeo = packPlaces(list);
kickCityBuild();
selectEmergencyRoutes();
// settlement weight feeds the car density field, so a fresh place
// list must re-shape the traffic too
if (carGeo) kickCarBuild(false);
@@ -4005,6 +4173,10 @@ var CARP_FRAG_SRC = [
'uniform float uFadeB;', // distance where it is fully faded',
'uniform float uWet;', // wet-road factor 0..1',
'uniform float uWetGain;', // extra halo on lights when wet',
'uniform float uEmergency;',
'uniform vec3 uEmergencyColor;',
'uniform float uEmergencyFlashHz;',
'uniform float uEmergencyNightOnly;',
'varying float vApproach;',
'varying float vDist;',
'varying float vSeed;',
@@ -4037,6 +4209,10 @@ var CARP_FRAG_SRC = [
'float age = mod(uTime + 3607.0 * hash1(vSeed * 0.913 + 7.7), per);',
'float on = smoothstep(0.0, 25.0, age) * (1.0 - smoothstep(life - 25.0, life, age));',
'float inten = on * vis;',
'if (uEmergency > 0.5) {',
'inten = 1.0;',
'if (uEmergencyNightOnly > 0.5 && uNight < 0.12) discard;',
'}',
'if (inten <= 0.001) discard;',
'inten = pow(inten, 1.25) * uBright;',
// tangent/view dot without a lane sign, sign applied here: toward',
@@ -4044,6 +4220,11 @@ var CARP_FRAG_SRC = [
'float approach = -vApproach * vDir;',
'vec3 col = mix(vec3(1.0, 0.12, 0.08), vec3(1.0, 0.96, 0.88),',
'smoothstep(-0.1, 0.1, approach));',
'if (uEmergency > 0.5) {',
'float flash = 0.35 + 0.65 * pow(max(0.0, sin(uTime * uEmergencyFlashHz * 6.2831853 + vSeed * 6.2831853)), 2.0);',
'col = uEmergencyColor * (0.55 + 0.45 * flash);',
'inten *= 0.75 + 0.55 * flash;',
'}',
// Switching on/off individually: this vehicle's own threshold sits
// somewhere in the twilight window, so the A-net lights up piece by
// piece in the evening and goes dark piece by piece in the morning.
@@ -4890,6 +5071,7 @@ function applyCarBuild(gl, res) {
lightsMotorway: carPtsCountM, lightsSide: carPtsCountB,
trafficSide: CAR_TRAFFIC > 0 ? 'right' : 'left',
maxChainKm: Math.round(carMaxChainKm * 10) / 10 };
selectEmergencyRoutes();
// After deleting a VBO, any still-enabled vertex attrib slot points at a
// destroyed buffer -> the sky/cloud full-screen draws warn "no buffer is
// bound to enabled attribute". Re-anchor every enabled slot to the
@@ -6977,7 +7159,11 @@ window.clouds3dRender = function(target) {
lightLo: gl.getUniformLocation(cpp, 'uLightLo'),
lightSoft: gl.getUniformLocation(cpp, 'uLightSoft'),
wet: gl.getUniformLocation(cpp, 'uWet'),
wetGain: gl.getUniformLocation(cpp, 'uWetGain')
wetGain: gl.getUniformLocation(cpp, 'uWetGain'),
emergency: gl.getUniformLocation(cpp, 'uEmergency'),
emergencyColor: gl.getUniformLocation(cpp, 'uEmergencyColor'),
emergencyFlashHz: gl.getUniformLocation(cpp, 'uEmergencyFlashHz'),
emergencyNightOnly: gl.getUniformLocation(cpp, 'uEmergencyNightOnly')
};
}
}
@@ -7010,6 +7196,10 @@ window.clouds3dRender = function(target) {
gl.uniform1f(carPtsUniforms.fadeB, CAR_FADE_B);
gl.uniform1f(carPtsUniforms.wet, _roadWet);
gl.uniform1f(carPtsUniforms.wetGain, WET_LIGHT_GAIN);
gl.uniform1f(carPtsUniforms.emergency, 0.0);
gl.uniform3f(carPtsUniforms.emergencyColor, EMERGENCY_COLOR[0], EMERGENCY_COLOR[1], EMERGENCY_COLOR[2]);
gl.uniform1f(carPtsUniforms.emergencyFlashHz, EMERGENCY_FLASH_HZ);
gl.uniform1f(carPtsUniforms.emergencyNightOnly, EMERGENCY_NIGHT_ONLY ? 1.0 : 0.0);
var carPtsGroup = function (vbo, count, dash, speed, maxDens) {
if (!vbo || !count) return;
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
@@ -7030,6 +7220,25 @@ window.clouds3dRender = function(target) {
};
carPtsGroup(carPtsVboM, carPtsCountM, CAR_M_DASH, CAR_M_SPEED, CAR_M_DENS);
carPtsGroup(carPtsVboB, carPtsCountB, CAR_B_DASH, CAR_B_SPEED, CAR_B_DENS);
var emergencyDrawCount = updateEmergencyVehicles(now);
if (emergencyDrawCount > 0) {
if (!emergencyVbo) emergencyVbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, emergencyVbo);
gl.bufferData(gl.ARRAY_BUFFER, emergencyPts.subarray(0, emergencyDrawCount * 10), gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(carPtsAttribs.pos, 3, gl.FLOAT, false, 40, 0);
gl.vertexAttribPointer(carPtsAttribs.meta, 4, gl.FLOAT, false, 40, 12);
gl.vertexAttribPointer(carPtsAttribs.dens, 1, gl.FLOAT, false, 40, 28);
gl.vertexAttribPointer(carPtsAttribs.seed, 1, gl.FLOAT, false, 40, 32);
gl.vertexAttribPointer(carPtsAttribs.len, 1, gl.FLOAT, false, 40, 36);
gl.uniform1f(carPtsUniforms.dash, 0.0);
gl.uniform1f(carPtsUniforms.speed, 0.0);
gl.uniform1f(carPtsUniforms.dens, 1.0);
gl.uniform1f(carPtsUniforms.bright, EMERGENCY_BRIGHT);
gl.uniform1f(carPtsUniforms.emergency, 1.0);
gl.drawArrays(gl.POINTS, 0, emergencyDrawCount);
gl.uniform1f(carPtsUniforms.emergency, 0.0);
gl.uniform1f(carPtsUniforms.bright, CAR_BRIGHT);
}
}
}
gl.disable(gl.BLEND);