window.sketch = (runtime) => {
  const { THREE, canvas, startFrame, width, height } = runtime;

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x1a1a2e);
  scene.fog = new THREE.Fog(0x1a1a2e, 15, 40);

  const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 100);
  camera.position.set(0, 6, 18);
  camera.lookAt(0, 1.2, 0);

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setSize(width, height, false);
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;

  // Lights
  const ambientLight = new THREE.AmbientLight(0x404060, 0.6);
  scene.add(ambientLight);

  const sunLight = new THREE.DirectionalLight(0xffeedd, 1.2);
  sunLight.position.set(10, 20, 5);
  sunLight.castShadow = true;
  sunLight.shadow.mapSize.width = 1024;
  sunLight.shadow.mapSize.height = 1024;
  sunLight.shadow.camera.near = 0.5;
  sunLight.shadow.camera.far = 50;
  sunLight.shadow.camera.left = -20;
  sunLight.shadow.camera.right = 20;
  sunLight.shadow.camera.top = 20;
  sunLight.shadow.camera.bottom = -20;
  scene.add(sunLight);

  const fillLight = new THREE.DirectionalLight(0x99ccff, 0.4);
  fillLight.position.set(-5, 5, 10);
  scene.add(fillLight);

  // Ground / street
  const groundGeo = new THREE.PlaneGeometry(50, 50);
  const groundMat = new THREE.MeshStandardMaterial({
    color: 0x2a2a3a,
    roughness: 0.8,
    metalness: 0.2,
  });
  const ground = new THREE.Mesh(groundGeo, groundMat);
  ground.rotation.x = -Math.PI / 2;
  ground.position.y = -0.01;
  ground.receiveShadow = true;
  scene.add(ground);

  // Street markings
  const markMat = new THREE.MeshBasicMaterial({ color: 0x888899 });
  for (let i = -20; i <= 20; i += 2.5) {
    const mark = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 0.1), markMat);
    mark.rotation.x = -Math.PI / 2;
    mark.position.set(i, 0.005, -0.5);
    scene.add(mark);
  }

  // Buildings (simple cubes with texture)
  const buildingColors = [0x4a4a6a, 0x5a5a7a, 0x3a3a5a, 0x6a6a8a, 0x2a2a4a];
  const buildingPositions = [];
  for (let i = -8; i <= 8; i += 3) {
    for (let j = -4; j <= 4; j += 3) {
      if (Math.random() > 0.3) {
        const h = 2 + Math.random() * 6;
        const color = buildingColors[Math.floor(Math.random() * buildingColors.length)];
        const bMat = new THREE.MeshPhongMaterial({ color, emissive: 0x111122, emissiveIntensity: 0.1 });
        const box = new THREE.Mesh(new THREE.BoxGeometry(2.2, h, 2.2), bMat);
        box.position.set(i * 1.2 + (Math.random() - 0.5) * 0.8, h / 2, j * 1.2 + 4);
        box.castShadow = true;
        box.receiveShadow = true;
        scene.add(box);
        buildingPositions.push(box.position.clone());

        // windows
        const winMat = new THREE.MeshBasicMaterial({ color: 0xffdd88 });
        for (let wy = 0.8; wy < h - 0.6; wy += 0.9) {
          for (let wz = -0.8; wz <= 0.8; wz += 0.8) {
            const win = new THREE.Mesh(new THREE.PlaneGeometry(0.3, 0.4), winMat);
            win.position.set(box.position.x + 1.12, wy, box.position.z + wz);
            win.rotation.y = Math.PI / 2;
            scene.add(win);
          }
          for (let wx = -0.8; wx <= 0.8; wx += 0.8) {
            const win = new THREE.Mesh(new THREE.PlaneGeometry(0.3, 0.4), winMat);
            win.position.set(box.position.x + wx, wy, box.position.z + 1.12);
            scene.add(win);
          }
        }
      }
    }
  }

  // People
  const people = [];
  const personMat = new THREE.MeshPhongMaterial({ color: 0xccaa88, emissive: 0x332211, emissiveIntensity: 0.05 });
  const clothesColors = [0xcc4444, 0x44aa44, 0x4488cc, 0xdd8844, 0xaa44aa, 0x88aacc, 0xddaa66];
  const skinColors = [0xeebb99, 0xdd9966, 0xcc8855, 0xbb7744];

  function createPerson(posX, posZ) {
    const group = new THREE.Group();

    const skinColor = skinColors[Math.floor(Math.random() * skinColors.length)];
    const clothColor = clothesColors[Math.floor(Math.random() * clothesColors.length)];

    // Body
    const bodyMat = new THREE.MeshPhongMaterial({ color: clothColor });
    const body = new THREE.Mesh(new THREE.CylinderGeometry(0.35, 0.4, 1.0, 6), bodyMat);
    body.position.y = 0.7;
    body.castShadow = true;
    body.receiveShadow = true;
    group.add(body);

    // Head
    const headMat = new THREE.MeshPhongMaterial({ color: skinColor });
    const head = new THREE.Mesh(new THREE.SphereGeometry(0.25, 8, 8), headMat);
    head.position.y = 1.35;
    head.castShadow = true;
    head.receiveShadow = true;
    group.add(head);

    // Hair
    const hairMat = new THREE.MeshPhongMaterial({ color: 0x222222 });
    const hair = new THREE.Mesh(new THREE.SphereGeometry(0.2, 6, 6), hairMat);
    hair.position.set(0, 1.5, 0.1);
    hair.scale.set(1, 0.3, 0.8);
    hair.castShadow = true;
    group.add(hair);

    // Legs
    const legMat = new THREE.MeshPhongMaterial({ color: 0x333344 });
    const legGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.6, 4);
    const legL = new THREE.Mesh(legGeo, legMat);
    legL.position.set(-0.12, 0.3, 0);
    legL.castShadow = true;
    group.add(legL);
    const legR = new THREE.Mesh(legGeo, legMat);
    legR.position.set(0.12, 0.3, 0);
    legR.castShadow = true;
    group.add(legR);

    // Arms
    const armMat = new THREE.MeshPhongMaterial({ color: skinColor });
    const armGeo = new THREE.CylinderGeometry(0.05, 0.07, 0.7, 4);
    const armL = new THREE.Mesh(armGeo, armMat);
    armL.position.set(-0.35, 0.9, 0);
    armL.rotation.z = 0.2;
    armL.castShadow = true;
    group.add(armL);
    const armR = new THREE.Mesh(armGeo, armMat);
    armR.position.set(0.35, 0.9, 0);
    armR.rotation.z = -0.2;
    armR.castShadow = true;
    group.add(armR);

    // Bag/accessory (simple box)
    if (Math.random() > 0.5) {
      const bagMat = new THREE.MeshPhongMaterial({ color: 0x995533 });
      const bag = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.15, 0.1), bagMat);
      bag.position.set(0.2, 0.6, 0.25);
      bag.castShadow = true;
      group.add(bag);
    }

    group.position.set(posX, 0, posZ);
    group.rotation.y = Math.random() * Math.PI * 2;
    // random scale variation
    const s = 0.7 + Math.random() * 0.4;
    group.scale.set(s, s, s);

    return group;
  }

  const personCount = 280;
  for (let i = 0; i < personCount; i++) {
    const angle = Math.random() * Math.PI * 2;
    const radius = 2 + Math.random() * 22;
    const x = Math.cos(angle) * radius;
    const z = Math.sin(angle) * radius;
    const person = createPerson(x, z);
    scene.add(person);
    people.push({
      group: person,
      speed: 0.3 + Math.random() * 0.5,
      radius: radius,
      angle: angle,
      phase: Math.random() * 100,
      offsetY: Math.random() * 0.1,
    });
  }

  // Animated shop signs (floating billboards)
  const signMat = new THREE.MeshBasicMaterial({ color: 0xff6644 });
  for (let i = 0; i < 15; i++) {
    const sign = new THREE.Mesh(new THREE.PlaneGeometry(0.8, 0.4), signMat);
    sign.position.set((Math.random() - 0.5) * 20, 3 + Math.random() * 3, (Math.random() - 0.5) * 8 - 5);
    sign.rotation.y = Math.random() * 0.5;
    scene.add(sign);
  }

  // Ambient particles (city dust/neon)
  const particlesGeo = new THREE.BufferGeometry();
  const particleCount = 300;
  const positions = new Float32Array(particleCount * 3);
  for (let i = 0; i < particleCount * 3; i++) {
    positions[i] = (Math.random() - 0.5) * 40;
  }
  particlesGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  const particlesMat = new THREE.PointsMaterial({
    color: 0x88aaff,
    size: 0.12,
    transparent: true,
    opacity: 0.5,
  });
  const particles = new THREE.Points(particlesGeo, particlesMat);
  particles.position.y = 2;
  scene.add(particles);

  let time = 0;
  startFrame((delta) => {
    time += delta;

    // Animate people walking forward (radially outward/inward mix)
    people.forEach((p) => {
      const drift = 0.15 * Math.sin(time * 0.3 + p.phase);
      const radiusChange = (0.5 + 0.5 * Math.sin(time * 0.5 + p.phase * 0.7)) * 0.02;
      p.radius += radiusChange;
      if (p.radius < 2) p.radius = 2;
      if (p.radius > 24) p.radius = 24;
      p.angle += 0.002 + p.speed * 0.008;

      const x = Math.cos(p.angle) * p.radius;
      const z = Math.sin(p.angle) * p.radius;
      p.group.position.x = x;
      p.group.position.z = z;
      p.group.rotation.y = -p.angle + Math.PI / 2 + drift;

      // slight bobbing
      p.group.position.y = 0.05 * Math.sin(time * 3 + p.phase);
    });

    // Rotate particles slowly
    particles.rotation.y += 0.0002;

    renderer.render(scene, camera);

    // Camera subtle sway
    camera.position.x = 1.5;
    camera.position.z = 18 + 0.5;
    camera.lookAt(0, 1.2, 0);
  });

  return () => {
    renderer.dispose();
  };
};
