diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 6a42635..d1162b9 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -3468,7 +3468,7 @@ 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_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_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; @@ -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_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 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 emergencyLastT = null, emergencyStartCity = null; +var emergencyLastT = null, emergencyStartCity = null, emergencyStartCities = []; function carLonToX(lon) { var n = 1 << GROUND_TILE_Z; @@ -3603,18 +3604,129 @@ function emergencyShuffle(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() { 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, candidates: 0, routes: 0 }; + emergencyStartCities = []; + if (!EMERGENCY_ON || !carGeo || (!carGeo.secondary && !carGeo.motorway)) { + window.__emergencyDbg = { enabled: EMERGENCY_ON, startCity: null, startCities: 0, candidates: 0, routes: 0, vehicles: 0 }; return; } emergencyStartCity = nearestEmergencyCity(); + emergencyStartCities = emergencyCollectCities(); var nets = emergencyNetworks(); - var pool = [], poolMode = 'none'; + var pool = [], seenChain = {}, poolMode = 'none'; + var wantCandidates = Math.max(EMERGENCY_COUNT * 8, 800); var passes = [ { 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' }, @@ -3622,24 +3734,35 @@ function selectEmergencyRoutes() { { minKm: 3, maxKm: 220, near: 6.0, mode: 'wide' }, { minKm: 0.5, maxKm: 400, near: 12.0, mode: 'last-resort' } ]; - for (var p = 0; p < passes.length && !pool.length; p++) { - for (var ni = 0; ni < nets.length && !pool.length; ni++) { - pool = emergencyCollectCandidates(nets[ni].net, nets[ni].key, emergencyStartCity, - passes[p].minKm, passes[p].maxKm, passes[p].near); - if (pool.length) poolMode = passes[p].mode + ':' + nets[ni].key; + for (var p = 0; p < passes.length && pool.length < wantCandidates; p++) { + var cell = Math.max(0.5, passes[p].near); + var grid = emergencyBuildCityGrid(emergencyStartCities, cell); + for (var ni = 0; ni < nets.length && pool.length < wantCandidates; ni++) { + 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; }); - var top = pool.slice(0, Math.max(EMERGENCY_COUNT * 8, 24)); + var top = pool.slice(0, Math.max(EMERGENCY_COUNT * 12, 240)); emergencyShuffle(top); - var used = {}; - var routeFailures = []; + var used = {}, cityUse = {}, routeFailures = []; for (var r = 0; r < top.length && emergencyRoutes.length < EMERGENCY_COUNT; r++) { var cand = top[r]; var usedKey = cand.netKey + ':' + cand.ci; 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; - 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) { var base = route[0].d; for (var rp2 = 0; rp2 < route.length; rp2++) route[rp2].d -= base; @@ -3648,15 +3771,16 @@ function selectEmergencyRoutes() { if (len <= 0) { routeFailures.push({ netKey: cand.netKey, ci: cand.ci, lenKm: +cand.lenKm.toFixed(1), - nearKm: +(cand.near * KM_PER_TILE).toFixed(1), reverse: cand.reverse, - pts: route.length, rawLen: +(cand.len * KM_PER_TILE).toFixed(1) + nearKm: +(cand.near * KM_PER_TILE).toFixed(1), pts: route.length, + rawLen: +(cand.len * KM_PER_TILE).toFixed(1) }); continue; } emergencyRoutes.push({ pts: route, len: len, lenKm: len * 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++) { @@ -3667,7 +3791,19 @@ function selectEmergencyRoutes() { emergencyVehicles.push({ route: v, 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() }); } @@ -3681,6 +3817,7 @@ function selectEmergencyRoutes() { pop: emergencyStartCity.pop, km: +(emergencyStartCity.dist * KM_PER_TILE).toFixed(1) } : null, + startCities: emergencyStartCities.length, poolMode: poolMode, candidates: pool.length, candidateSample: top.slice(0, 5).map(function (c) { @@ -3692,13 +3829,15 @@ function selectEmergencyRoutes() { }), routeFailures: routeFailures.slice(0, 5), 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 { km: +r.lenKm.toFixed(1), nearKm: +r.nearKm.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),', '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;', + 'float flash = 0.20 + 0.80 * pow(max(0.0, sin(uTime * uEmergencyFlashHz * 6.2831853 + vSeed * 6.2831853)), 1.35);', + 'col = uEmergencyColor * (0.45 + 0.75 * flash);', + 'inten *= 0.45 + 1.15 * flash;', '}', // Switching on/off individually: this vehicle's own threshold sits // 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.pxW, 1.1547 / gl.drawingBufferHeight); gl.uniform1f(carPtsUniforms.traffic, CAR_TRAFFIC); - gl.uniform1f(carPtsUniforms.minPx, CAR_MIN_PX * emergencyPxScale * 1.35); - gl.uniform1f(carPtsUniforms.maxPx, CAR_MAX_PX * emergencyPxScale * 1.6); - gl.uniform1f(carPtsUniforms.boost, CAR_BOOST * emergencyPxScale * 1.4); + gl.uniform1f(carPtsUniforms.minPx, CAR_MIN_PX * emergencyPxScale * 1.35 * EMERGENCY_SIZE_SCALE); + 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 * EMERGENCY_SIZE_SCALE); gl.uniform1f(carPtsUniforms.bright, EMERGENCY_BRIGHT); gl.uniform1f(carPtsUniforms.fadeA, CAR_FADE_A); gl.uniform1f(carPtsUniforms.fadeB, CAR_FADE_B);