<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Endless City FPS with Babylon.js</title>
    <style>
        html, body {
            overflow: hidden;
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }
        #renderCanvas {
            width: 100%;
            height: 100%;
            touch-action: none;
        }
    </style>
</head>
<body>
    <canvas id="renderCanvas"></canvas> <!-- The canvas element for Babylon.js -->
    
    <!-- Include Babylon.js Core Library -->
    <script src="https://cdn.babylonjs.com/babylon.js"></script>
    <!-- Include Babylon.js CannonJS physics engine plugin -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/cannon.js/0.6.2/cannon.min.js"></script>
    <!-- Include Babylon.js Materials Library for PBR materials -->
    <script src="https://cdn.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
    <!-- Include Babylon.js Procedural Textures Library for rain textures -->
    <script src="https://cdn.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>

    <script>
        // Get the canvas element 
        var canvas = document.getElementById("renderCanvas");

        // Generate the BABYLON 3D engine
        var engine = new BABYLON.Engine(canvas, true);

        var createScene = function () {
            // Create the scene space
            var scene = new BABYLON.Scene(engine);

            // Enable physics
            scene.enablePhysics(new BABYLON.Vector3(0, -9.81, 0), new BABYLON.CannonJSPlugin());

            // Create a universal camera with no rotation (to prevent initial tilt)
            var camera = new BABYLON.UniversalCamera("camera", new BABYLON.Vector3(0, 2, 0), scene);
            camera.rotation = new BABYLON.Vector3(0, 0, 0);
            camera.attachControl(canvas, true);

            // Enable mouse and keyboard controls for the camera (FPS controls)
            camera.keysUp.push(87);    // W
            camera.keysDown.push(83);  // S
            camera.keysLeft.push(65);  // A
            camera.keysRight.push(68); // D
            camera.speed = 2; // Slowed down character speed

            // Apply gravity to the camera
            camera.applyGravity = true;
            camera.ellipsoid = new BABYLON.Vector3(1, 1, 1);
            camera.checkCollisions = true;

            // Make sure collisions are enabled in the scene
            scene.collisionsEnabled = true;

            // Add a directional light for shadows
            var light = new BABYLON.DirectionalLight("dirLight", new BABYLON.Vector3(-1, -2, -1), scene);
            light.position = new BABYLON.Vector3(50, 100, 50);
            light.intensity = 1.0;

            // Create a shadow generator
            var shadowGenerator = new BABYLON.ShadowGenerator(1024, light);
            shadowGenerator.useBlurExponentialShadowMap = true;
            shadowGenerator.blurKernel = 32;

            // Create the ground with a PBR material for reflections
            var ground = BABYLON.MeshBuilder.CreateGround("ground", {width:1000, height:1000}, scene);
            var groundMaterial = new BABYLON.PBRMaterial("groundMaterial", scene);
            groundMaterial.albedoColor = new BABYLON.Color3(0.4, 0.4, 0.4);
            groundMaterial.reflectivityColor = new BABYLON.Color3(0.4, 0.4, 0.4); // Wet look
            groundMaterial.microSurface = 0.5; // Wet look
            groundMaterial.environmentTexture = BABYLON.CubeTexture.CreateFromPrefilteredData("https://playground.babylonjs.com/textures/environment.dds", scene);
            ground.material = groundMaterial;

            // Enable collisions and physics for the ground
            ground.checkCollisions = true;
            ground.receiveShadows = true;
            // Assign a physics impostor to the ground
            ground.physicsImpostor = new BABYLON.PhysicsImpostor(ground, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0, friction: 0.5, restitution: 0 }, scene);

            // Create a skybox for reflections
            var skybox = BABYLON.MeshBuilder.CreateBox("skyBox", { size: 1000 }, scene);
            var skyboxMaterial = new BABYLON.StandardMaterial("skyBoxMaterial", scene);
            skyboxMaterial.backFaceCulling = false;
            skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("https://playground.babylonjs.com/textures/skybox", scene);
            skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
            skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
            skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
            skybox.material = skyboxMaterial;

            // Global environment texture for reflections
            scene.environmentTexture = groundMaterial.environmentTexture;

            // Function to generate buildings in a grid pattern
            function generateCity(range) {
                var minSize = 5; // Adjusted to make buildings wider
                var maxSize = 20; // Adjusted to make buildings wider
                var spacing = 30; // Increased spacing between buildings for wider buildings and roads
                var roadFrequency = 5; // Every 5th line will be a road

                for (var x = -range; x <= range; x += spacing) {
                    for (var z = -range; z <= range; z += spacing) {
                        // Determine if current position is a road
                        var isRoad = (Math.abs(Math.floor(x / spacing)) % roadFrequency === 0) || (Math.abs(Math.floor(z / spacing)) % roadFrequency === 0);

                        if (isRoad) {
                            // Create road segment
                            var road = BABYLON.MeshBuilder.CreateGround("road", {width: spacing, height: spacing}, scene);
                            road.position.x = x;
                            road.position.z = z;
                            var roadMaterial = new BABYLON.StandardMaterial("roadMaterial", scene);
                            roadMaterial.diffuseColor = new BABYLON.Color3(0.1, 0.1, 0.1);
                            roadMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
                            road.material = roadMaterial;

                            // Ensure roads are slightly lower than ground to avoid z-fighting
                            road.position.y = 0.01;
                        } else {
                            // Create building
                            if (Math.random() > 0.2) { // Randomly skip some spots
                                var width = minSize + Math.random() * (maxSize - minSize);
                                var height = minSize + Math.random() * (maxSize * 2 - minSize);
                                var depth = minSize + Math.random() * (maxSize - minSize);

                                var building = BABYLON.MeshBuilder.CreateBox("building", {width: width, height: height, depth: depth}, scene);
                                building.position.x = x + (Math.random() - 0.5) * (spacing / 3);
                                building.position.z = z + (Math.random() - 0.5) * (spacing / 3);
                                building.position.y = height / 2;

                                // Create PBR material with reflections for buildings
                                var buildingMaterial = new BABYLON.PBRMaterial("buildingMaterial", scene);
                                buildingMaterial.albedoColor = new BABYLON.Color3(Math.random(), Math.random(), Math.random());
                                buildingMaterial.reflectivityColor = new BABYLON.Color3(0.3, 0.3, 0.3); // Wet look
                                buildingMaterial.microSurface = 0.4; // Wet look
                                buildingMaterial.environmentTexture = scene.environmentTexture;
                                building.material = buildingMaterial;

                                // Enable collisions and physics for buildings
                                building.checkCollisions = true;
                                building.physicsImpostor = new BABYLON.PhysicsImpostor(building, BABYLON.PhysicsImpostor.BoxImpostor, { mass: 0, friction: 0.5, restitution: 0 }, scene);

                                // Buildings cast shadows
                                building.receiveShadows = true;
                                shadowGenerator.addShadowCaster(building);

                                // Add building to array for management
                                buildings.push(building);
                            }
                        }
                    }
                }
            }

            // Array to keep track of buildings for material adjustments
            var buildings = [];

            // Initial city generation
            var cityRange = 100;
            generateCity(cityRange);

            // Add fog to the scene for depth perception
            scene.fogMode = BABYLON.Scene.FOGMODE_EXP2;
            scene.fogDensity = 0.001;
            scene.fogColor = new BABYLON.Color3(0.9, 0.9, 0.85);

            // Rain particle system
            var rain = new BABYLON.ParticleSystem("rain", 10000, scene);
            rain.particleTexture = new BABYLON.Texture("https://playground.babylonjs.com/textures/raindrop.png", scene);
            rain.emitter = new BABYLON.Vector3(0, 100, 0); // Emitter position will follow the camera
            rain.minEmitBox = new BABYLON.Vector3(-50, 0, -50);
            rain.maxEmitBox = new BABYLON.Vector3(50, 0, 50);

            // Rain particle properties
            rain.color1 = new BABYLON.Color4(0.8, 0.8, 1.0, 0.5);
            rain.color2 = new BABYLON.Color4(0.8, 0.8, 1.0, 0.5);
            rain.colorDead = new BABYLON.Color4(0.8, 0.8, 1.0, 0);
            rain.minSize = 0.1;
            rain.maxSize = 0.2;
            rain.minLifeTime = 0.8;
            rain.maxLifeTime = 1.0;
            rain.emitRate = 5000; // Increased emit rate
            rain.blendMode = BABYLON.ParticleSystem.BLENDMODE_STANDARD;
            rain.gravity = new BABYLON.Vector3(0, -9.81 * 5, 0); // Faster falling particles
            rain.direction1 = new BABYLON.Vector3(-1, -10, -1);
            rain.direction2 = new BABYLON.Vector3(1, -10, 1);
            rain.minEmitPower = 1;
            rain.maxEmitPower = 3;
            rain.updateSpeed = 0.01;
            rain.disposeOnStop = false;
            rain.start();

            // Variables to track movement
            var isMoving = false;

            var inputMap = {};
            scene.actionManager = new BABYLON.ActionManager(scene);
            scene.actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnKeyDownTrigger, function (evt) {
                inputMap[evt.sourceEvent.key.toLowerCase()] = evt.sourceEvent.type === "keydown";
            }));
            scene.actionManager.registerAction(new BABYLON.ExecuteCodeAction(BABYLON.ActionManager.OnKeyUpTrigger, function (evt) {
                inputMap[evt.sourceEvent.key.toLowerCase()] = evt.sourceEvent.type === "keydown";
            }));

            var lastTime = 0;
            // Listen for camera movement to generate more city and update rain emitter
            scene.registerBeforeRender(function () {
                var position = camera.position;

                // Update rain emitter position to be above the camera
                rain.emitter = new BABYLON.Vector3(camera.position.x, camera.position.y + 50, camera.position.z);

                // Update isMoving based on input keys
                isMoving = inputMap["w"] || inputMap["a"] || inputMap["s"] || inputMap["d"];

                // Camera bobbing effect
                if (isMoving) { // Apply bobbing only when moving
                    var time = performance.now();
                    var deltaTime = time - lastTime;
                    lastTime = time;
                    var bobbingFrequency = 0.005; // Adjust speed of bobbing
                    var bobbingAmount = 0.02; // Adjust the amplitude of bobbing
                    camera.position.y += Math.sin(time * bobbingFrequency) * bobbingAmount;
                } else {
                    lastTime = performance.now(); // Reset time when not moving
                }

                // Generate new buildings when approaching the edge
                if (Math.abs(position.x) > cityRange - 50 || Math.abs(position.z) > cityRange - 50) {
                    cityRange += 100;

                    // Remove old buildings outside the new range
                    var newBuildings = [];
                    buildings.forEach(function(building) {
                        if (Math.abs(building.position.x) > cityRange || Math.abs(building.position.z) > cityRange) {
                            // Remove from shadow generator
                            shadowGenerator.removeShadowCaster(building);
                            building.dispose();
                        } else {
                            newBuildings.push(building);
                        }
                    });
                    buildings = newBuildings;

                    // Generate additional city
                    generateCity(cityRange);
                }
            });

            return scene;
        };

        var scene = createScene(); // Call the createScene function

        // Run the render loop
        engine.runRenderLoop(function () {
            scene.render();
        });

        // Resize the engine on window resize event
        window.addEventListener("resize", function () {
            engine.resize();
        });
    </script>
</body>
</html>

#
Add robots that collecting garbage and recycle them in special factory
Factories generated randomly
Robots actually walks to the garbage, grab and bring it to factories, then wait for another command
When player hover on robot, he can see what robot actually doing (showed in UI)

Provide full updated code