emergency: scale emergency vehicles map-wide and strengthen flashing

This commit is contained in:
Richard Zink committed 2026-10-13 07:41:22 +02:00
1 parent 3cbb6dfdd2
commit 6508abdd77
1 file changed
+166 -27
+166 -27
View File
@@ -3468,7 +3468,7 @@ var carPtsProg = null, carPtsAttribs = null, carPtsUniforms = null;
var EMERGENCY_CFG = (window.CONFIG && window.CONFIG.emergencyVehicles) || {}; var EMERGENCY_CFG = (window.CONFIG && window.CONFIG.emergencyVehicles) || {};
var EMERGENCY_ON = EMERGENCY_CFG.enabled !== false; 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_COUNT = (typeof EMERGENCY_CFG.count === 'number') ? Math.max(1, Math.min(100, 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_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_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_MAX_KM = (typeof EMERGENCY_CFG.maxKm === 'number') ? Math.max(EMERGENCY_MIN_KM, EMERGENCY_CFG.maxKm) : 80;
@@ -3476,9 +3476,10 @@ var EMERGENCY_RANK_MAX = (typeof EMERGENCY_CFG.rankMax === 'number') ? Math.max(
var EMERGENCY_COLOR = Array.isArray(EMERGENCY_CFG.color) ? EMERGENCY_CFG.color : [0.22, 0.55, 1.0]; 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_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_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 EMERGENCY_BRIGHT = (typeof EMERGENCY_CFG.bright === 'number') ? Math.max(0.1, Math.min(6, EMERGENCY_CFG.bright)) : 1.35;
var EMERGENCY_SIZE_SCALE = (typeof EMERGENCY_CFG.sizeScale === 'number') ? Math.max(0.5, Math.min(4, EMERGENCY_CFG.sizeScale)) : 1.8;
var emergencyRoutes = [], emergencyVehicles = [], emergencyVbo = null, emergencyPts = null, emergencyCount = 0; var emergencyRoutes = [], emergencyVehicles = [], emergencyVbo = null, emergencyPts = null, emergencyCount = 0;
var emergencyLastT = null, emergencyStartCity = null; var emergencyLastT = null, emergencyStartCity = null, emergencyStartCities = [];
function carLonToX(lon) { function carLonToX(lon) {
var n = 1 << GROUND_TILE_Z; var n = 1 << GROUND_TILE_Z;
@@ -3603,18 +3604,129 @@ function emergencyShuffle(arr) {
return arr; return arr;
} }
function emergencyCollectCities() {
var out = [];
if (cityGeo && cityGeo.length >= 4) {
var r2 = CAR_RANGE * CAR_RANGE;
for (var i = 0; i + 3 < cityGeo.length; i += 4) {
var rank = cityGeo[i + 2] | 0;
if (rank > EMERGENCY_RANK_MAX) continue;
var x = cityGeo[i], z = cityGeo[i + 1];
if (x * x + z * z > r2) continue;
out.push({ x: x, z: z, rank: rank, pop: cityGeo[i + 3] });
}
}
out.sort(function (a, b) { return (b.pop || 0) - (a.pop || 0); });
if (out.length > 300) out.length = 300;
if (!out.length) {
out.push({ x: carLonToX(_baseLon), z: carLatToZ(_baseLat), rank: 0, pop: 0 });
}
return out;
}
function emergencyBuildCityGrid(cities, cell) {
var grid = new Map();
for (var i = 0; i < cities.length; i++) {
var c = cities[i];
var key = Math.floor(c.x / cell) + '_' + Math.floor(c.z / cell);
var arr = grid.get(key);
if (!arr) { arr = []; grid.set(key, arr); }
arr.push(c);
}
return grid;
}
function emergencyNearestCity(x, z, grid, cell) {
var gx = Math.floor(x / cell), gz = Math.floor(z / cell);
var best = null, bestD = Infinity;
for (var ox = -1; ox <= 1; ox++) {
for (var oz = -1; oz <= 1; oz++) {
var arr = grid.get((gx + ox) + '_' + (gz + oz));
if (!arr) continue;
for (var i = 0; i < arr.length; i++) {
var d = Math.hypot(arr[i].x - x, arr[i].z - z);
if (d < bestD) {
bestD = d;
best = arr[i];
}
}
}
}
return { city: best, dist: bestD };
}
function emergencyChainAtOffset(net, offset, n) {
var out = [];
for (var j = 0; j < n; j++) {
var k = offset + j * 3;
out.push({ x: net.pts[k], z: net.pts[k + 1], d: net.pts[k + 2] });
}
return out;
}
function emergencyCollectMapCandidates(net, netKey, cities, grid, cell, minKm, maxKm, nearLimit, maxCandidates, maxChains) {
var out = [];
if (!net || !net.cnts || !net.cnts.length) return out;
var targetKm = (minKm + maxKm) / 2;
var k = 0, scanned = 0;
for (var ci = 0; ci < net.cnts.length; ci++) {
var n = net.cnts[ci];
if (n < 2) { k += n * 3; continue; }
scanned++;
if (scanned > maxChains) break;
var firstX = net.pts[k], firstZ = net.pts[k + 1], firstD = net.pts[k + 2];
var last = k + (n - 1) * 3;
var lastX = net.pts[last], lastZ = net.pts[last + 1], lastD = net.pts[last + 2];
var len = Math.max(0, lastD - firstD);
var lenKm = len * KM_PER_TILE;
if (lenKm >= minKm && lenKm <= maxKm) {
var bestNear = Infinity, bestStartD = 0, bestCity = null;
var step = Math.max(1, Math.floor(n / 12));
for (var q = 0; q < n; q += step) {
var off = k + q * 3;
var nc = emergencyNearestCity(net.pts[off], net.pts[off + 1], grid, cell);
if (nc.city && nc.dist < bestNear) {
bestNear = nc.dist;
bestCity = nc.city;
bestStartD = Math.max(0, Math.min(len, net.pts[off + 2] - firstD));
}
}
var ncLast = emergencyNearestCity(lastX, lastZ, grid, cell);
if (ncLast.city && ncLast.dist < bestNear) {
bestNear = ncLast.dist;
bestCity = ncLast.city;
bestStartD = len;
}
if (bestNear <= nearLimit && bestCity) {
out.push({
net: net, netKey: netKey, ci: ci, offset: k, n: n,
len: len, lenKm: lenKm, near: bestNear, startD: bestStartD,
city: bestCity,
score: Math.abs(lenKm - targetKm) + bestNear * 18
});
if (out.length >= maxCandidates) break;
}
}
k += n * 3;
}
return out;
}
function selectEmergencyRoutes() { function selectEmergencyRoutes() {
emergencyRoutes = []; emergencyRoutes = [];
emergencyVehicles = []; emergencyVehicles = [];
emergencyCount = 0; emergencyCount = 0;
emergencyLastT = null; emergencyLastT = null;
if (!EMERGENCY_ON || !carGeo || !carGeo.secondary || !carGeo.secondary.cnts || !carGeo.secondary.cnts.length) { emergencyStartCities = [];
window.__emergencyDbg = { enabled: EMERGENCY_ON, startCity: null, candidates: 0, routes: 0 }; if (!EMERGENCY_ON || !carGeo || (!carGeo.secondary && !carGeo.motorway)) {
window.__emergencyDbg = { enabled: EMERGENCY_ON, startCity: null, startCities: 0, candidates: 0, routes: 0, vehicles: 0 };
return; return;
} }
emergencyStartCity = nearestEmergencyCity(); emergencyStartCity = nearestEmergencyCity();
emergencyStartCities = emergencyCollectCities();
var nets = emergencyNetworks(); var nets = emergencyNetworks();
var pool = [], poolMode = 'none'; var pool = [], seenChain = {}, poolMode = 'none';
var wantCandidates = Math.max(EMERGENCY_COUNT * 8, 800);
var passes = [ var passes = [
{ minKm: EMERGENCY_MIN_KM, maxKm: EMERGENCY_MAX_KM, near: 1.2, mode: 'strict' }, { minKm: EMERGENCY_MIN_KM, maxKm: EMERGENCY_MAX_KM, near: 1.2, mode: 'strict' },
{ minKm: EMERGENCY_MIN_KM * 0.55, maxKm: EMERGENCY_MAX_KM * 1.45, near: 1.8, mode: 'relaxed' }, { minKm: EMERGENCY_MIN_KM * 0.55, maxKm: EMERGENCY_MAX_KM * 1.45, near: 1.8, mode: 'relaxed' },
@@ -3622,24 +3734,35 @@ function selectEmergencyRoutes() {
{ minKm: 3, maxKm: 220, near: 6.0, mode: 'wide' }, { minKm: 3, maxKm: 220, near: 6.0, mode: 'wide' },
{ minKm: 0.5, maxKm: 400, near: 12.0, mode: 'last-resort' } { minKm: 0.5, maxKm: 400, near: 12.0, mode: 'last-resort' }
]; ];
for (var p = 0; p < passes.length && !pool.length; p++) { for (var p = 0; p < passes.length && pool.length < wantCandidates; p++) {
for (var ni = 0; ni < nets.length && !pool.length; ni++) { var cell = Math.max(0.5, passes[p].near);
pool = emergencyCollectCandidates(nets[ni].net, nets[ni].key, emergencyStartCity, var grid = emergencyBuildCityGrid(emergencyStartCities, cell);
passes[p].minKm, passes[p].maxKm, passes[p].near); for (var ni = 0; ni < nets.length && pool.length < wantCandidates; ni++) {
if (pool.length) poolMode = passes[p].mode + ':' + nets[ni].key; var part = emergencyCollectMapCandidates(nets[ni].net, nets[ni].key, emergencyStartCities, grid, cell,
passes[p].minKm, passes[p].maxKm, passes[p].near, wantCandidates, 180000);
for (var pi = 0; pi < part.length; pi++) {
var key = part[pi].netKey + ':' + part[pi].ci;
if (!seenChain[key]) {
seenChain[key] = true;
pool.push(part[pi]);
}
}
if (part.length) poolMode = passes[p].mode + ':' + nets[ni].key;
} }
} }
pool.sort(function (a, b) { return a.score - b.score; }); pool.sort(function (a, b) { return a.score - b.score; });
var top = pool.slice(0, Math.max(EMERGENCY_COUNT * 8, 24)); var top = pool.slice(0, Math.max(EMERGENCY_COUNT * 12, 240));
emergencyShuffle(top); emergencyShuffle(top);
var used = {}; var used = {}, cityUse = {}, routeFailures = [];
var routeFailures = [];
for (var r = 0; r < top.length && emergencyRoutes.length < EMERGENCY_COUNT; r++) { for (var r = 0; r < top.length && emergencyRoutes.length < EMERGENCY_COUNT; r++) {
var cand = top[r]; var cand = top[r];
var usedKey = cand.netKey + ':' + cand.ci; var usedKey = cand.netKey + ':' + cand.ci;
if (used[usedKey]) continue; if (used[usedKey]) continue;
var cityKey = cand.city ? (Math.round(cand.city.x * 4) + '_' + Math.round(cand.city.z * 4)) : 'none';
if ((cityUse[cityKey] || 0) >= 3) continue;
used[usedKey] = true; used[usedKey] = true;
var route = emergencyChainAt(cand.net, cand.ci); cityUse[cityKey] = (cityUse[cityKey] || 0) + 1;
var route = emergencyChainAtOffset(cand.net, cand.offset, cand.n);
if (route.length) { if (route.length) {
var base = route[0].d; var base = route[0].d;
for (var rp2 = 0; rp2 < route.length; rp2++) route[rp2].d -= base; for (var rp2 = 0; rp2 < route.length; rp2++) route[rp2].d -= base;
@@ -3648,15 +3771,16 @@ function selectEmergencyRoutes() {
if (len <= 0) { if (len <= 0) {
routeFailures.push({ routeFailures.push({
netKey: cand.netKey, ci: cand.ci, lenKm: +cand.lenKm.toFixed(1), netKey: cand.netKey, ci: cand.ci, lenKm: +cand.lenKm.toFixed(1),
nearKm: +(cand.near * KM_PER_TILE).toFixed(1), reverse: cand.reverse, nearKm: +(cand.near * KM_PER_TILE).toFixed(1), pts: route.length,
pts: route.length, rawLen: +(cand.len * KM_PER_TILE).toFixed(1) rawLen: +(cand.len * KM_PER_TILE).toFixed(1)
}); });
continue; continue;
} }
emergencyRoutes.push({ emergencyRoutes.push({
pts: route, len: len, lenKm: len * KM_PER_TILE, pts: route, len: len, lenKm: len * KM_PER_TILE,
nearKm: cand.near * KM_PER_TILE, nearKm: cand.near * KM_PER_TILE,
startD: Math.max(0, Math.min(len, cand.startD || 0)) startD: Math.max(0, Math.min(len, cand.startD || 0)),
city: cand.city
}); });
} }
for (var v = 0; v < emergencyRoutes.length; v++) { for (var v = 0; v < emergencyRoutes.length; v++) {
@@ -3667,7 +3791,19 @@ function selectEmergencyRoutes() {
emergencyVehicles.push({ emergencyVehicles.push({
route: v, route: v,
d: d, d: d,
dir: Math.random() < 0.5 ? 1 : -1, dir: (rr.startD < rr.len * 0.5) ? 1 : -1,
seed: Math.random()
});
}
while (emergencyVehicles.length < EMERGENCY_COUNT && emergencyRoutes.length) {
var rr2 = emergencyRoutes[emergencyVehicles.length % emergencyRoutes.length];
var spread2 = rr2.len * 0.35;
var d2 = (rr2.startD || 0) + (Math.random() - 0.5) * spread2;
d2 = Math.max(0, Math.min(rr2.len, d2));
emergencyVehicles.push({
route: emergencyVehicles.length % emergencyRoutes.length,
d: d2,
dir: (rr2.startD < rr2.len * 0.5) ? 1 : -1,
seed: Math.random() seed: Math.random()
}); });
} }
@@ -3681,6 +3817,7 @@ function selectEmergencyRoutes() {
pop: emergencyStartCity.pop, pop: emergencyStartCity.pop,
km: +(emergencyStartCity.dist * KM_PER_TILE).toFixed(1) km: +(emergencyStartCity.dist * KM_PER_TILE).toFixed(1)
} : null, } : null,
startCities: emergencyStartCities.length,
poolMode: poolMode, poolMode: poolMode,
candidates: pool.length, candidates: pool.length,
candidateSample: top.slice(0, 5).map(function (c) { candidateSample: top.slice(0, 5).map(function (c) {
@@ -3692,13 +3829,15 @@ function selectEmergencyRoutes() {
}), }),
routeFailures: routeFailures.slice(0, 5), routeFailures: routeFailures.slice(0, 5),
networks: nets.map(function (n) { return { key: n.key, chains: n.net.cnts.length }; }), networks: nets.map(function (n) { return { key: n.key, chains: n.net.cnts.length }; }),
routes: emergencyRoutes.map(function (r) { routes: emergencyRoutes.slice(0, 12).map(function (r) {
return { return {
km: +r.lenKm.toFixed(1), km: +r.lenKm.toFixed(1),
nearKm: +r.nearKm.toFixed(1), nearKm: +r.nearKm.toFixed(1),
startKm: +(r.startD * KM_PER_TILE).toFixed(1) startKm: +(r.startD * KM_PER_TILE).toFixed(1)
}; };
}) }),
routeCount: emergencyRoutes.length,
vehicleCount: emergencyVehicles.length
}; };
} }
@@ -4322,9 +4461,9 @@ var CARP_FRAG_SRC = [
'vec3 col = mix(vec3(1.0, 0.12, 0.08), vec3(1.0, 0.96, 0.88),', 'vec3 col = mix(vec3(1.0, 0.12, 0.08), vec3(1.0, 0.96, 0.88),',
'smoothstep(-0.1, 0.1, approach));', 'smoothstep(-0.1, 0.1, approach));',
'if (uEmergency > 0.5) {', 'if (uEmergency > 0.5) {',
'float flash = 0.35 + 0.65 * pow(max(0.0, sin(uTime * uEmergencyFlashHz * 6.2831853 + vSeed * 6.2831853)), 2.0);', 'float flash = 0.20 + 0.80 * pow(max(0.0, sin(uTime * uEmergencyFlashHz * 6.2831853 + vSeed * 6.2831853)), 1.35);',
'col = uEmergencyColor * (0.55 + 0.45 * flash);', 'col = uEmergencyColor * (0.45 + 0.75 * flash);',
'inten *= 0.75 + 0.55 * flash;', 'inten *= 0.45 + 1.15 * flash;',
'}', '}',
// Switching on/off individually: this vehicle's own threshold sits // Switching on/off individually: this vehicle's own threshold sits
// somewhere in the twilight window, so the A-net lights up piece by // somewhere in the twilight window, so the A-net lights up piece by
@@ -7376,9 +7515,9 @@ window.clouds3dRender = function(target) {
gl.uniform1f(carPtsUniforms.lanePx, CAR_LANE_PX * emergencyPxScale); gl.uniform1f(carPtsUniforms.lanePx, CAR_LANE_PX * emergencyPxScale);
gl.uniform1f(carPtsUniforms.pxW, 1.1547 / gl.drawingBufferHeight); gl.uniform1f(carPtsUniforms.pxW, 1.1547 / gl.drawingBufferHeight);
gl.uniform1f(carPtsUniforms.traffic, CAR_TRAFFIC); gl.uniform1f(carPtsUniforms.traffic, CAR_TRAFFIC);
gl.uniform1f(carPtsUniforms.minPx, CAR_MIN_PX * emergencyPxScale * 1.35); gl.uniform1f(carPtsUniforms.minPx, CAR_MIN_PX * emergencyPxScale * 1.35 * EMERGENCY_SIZE_SCALE);
gl.uniform1f(carPtsUniforms.maxPx, CAR_MAX_PX * emergencyPxScale * 1.6); gl.uniform1f(carPtsUniforms.maxPx, Math.min(48.0, CAR_MAX_PX * emergencyPxScale * 1.6 * EMERGENCY_SIZE_SCALE));
gl.uniform1f(carPtsUniforms.boost, CAR_BOOST * emergencyPxScale * 1.4); gl.uniform1f(carPtsUniforms.boost, CAR_BOOST * emergencyPxScale * 1.4 * EMERGENCY_SIZE_SCALE);
gl.uniform1f(carPtsUniforms.bright, EMERGENCY_BRIGHT); gl.uniform1f(carPtsUniforms.bright, EMERGENCY_BRIGHT);
gl.uniform1f(carPtsUniforms.fadeA, CAR_FADE_A); gl.uniform1f(carPtsUniforms.fadeA, CAR_FADE_A);
gl.uniform1f(carPtsUniforms.fadeB, CAR_FADE_B); gl.uniform1f(carPtsUniforms.fadeB, CAR_FADE_B);