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

  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x9fc5e8);
  scene.fog = new THREE.Fog(0x9fc5e8, 20, 80);

  const camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 200);
  camera.position.set(0, 4.5, 12);
  camera.lookAt(0, 2.5, 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 ambient = new THREE.AmbientLight(0xffffff, 0.55);
  scene.add(ambient);
  const sun = new THREE.DirectionalLight(0xfff0d0, 0.85);
  sun.position.set(10, 20, 8);
  sun.castShadow = true;
  sun.shadow.mapSize.set(1024, 1024);
  sun.shadow.camera.left = -25;
  sun.shadow.camera.right = 25;
  sun.shadow.camera.top = 25;
  sun.shadow.camera.bottom = -25;
  scene.add(sun);
  const fillLight = new THREE.HemisphereLight(0xb1d8ff, 0x444422, 0.35);
  scene.add(fillLight);

  // Street
  const streetGeo = new THREE.PlaneGeometry(14, 100);
  const streetMat = new THREE.MeshLambertMaterial({ color: 0x2a2a2e });
  const street = new THREE.Mesh(streetGeo, streetMat);
  street.rotation.x = -Math.PI / 2;
  street.position.y = 0.001;
  street.receiveShadow = true;
  scene.add(street);

  // Road lines
  const lineMat = new THREE.MeshBasicMaterial({ color: 0xfff5b8 });
  for (let i = -45; i < 45; i += 4) {
    const line = new THREE.Mesh(new THREE.PlaneGeometry(0.25, 1.6), lineMat);
    line.rotation.x = -Math.PI / 2;
    line.position.set(0, 0.01, i);
    scene.add(line);
  }

  // Sidewalks
  const sideMat = new THREE.MeshLambertMaterial({ color: 0xb8b2a4 });
  for (let side of [-1, 1]) {
    const sideGeo = new THREE.PlaneGeometry(6, 100);
    const sideMesh = new THREE.Mesh(sideGeo, sideMat);
    sideMesh.rotation.x = -Math.PI / 2;
    sideMesh.position.set(side * 10, 0.05, 0);
    sideMesh.receiveShadow = true;
    scene.add(sideMesh);
  }

  // Buildings on each side
  const buildingColors = [0x6b8e9b, 0x8a7a6b, 0x9c7a8b, 0x7a8a6b, 0x6b7a8a, 0xa89070];
  for (let side of [-1, 1]) {
    for (let i = 0; i < 14; i++) {
      const w = 4 + Math.random() * 3;
      const h = 6 + Math.random() * 10;
      const d = 4 + Math.random() * 3;
      const color = buildingColors[Math.floor(Math.random() * buildingColors.length)];
      const mat = new THREE.MeshLambertMaterial({ color });
      const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
      const x = side * (13 + Math.random() * 2 + d / 2);
      const z = -45 + i * 7 + Math.random() * 2;
      mesh.position.set(x, h / 2, z);
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      scene.add(mesh);

      // Windows
      const winMat = new THREE.MeshBasicMaterial({ color: 0xfff2c0 });
      const cols = Math.floor(w / 1.2);
      const rows = Math.floor(h / 1.5);
      for (let r = 0; r < rows; r++) {
        for (let c = 0; c < cols; c++) {
          if (Math.random() > 0.3) {
            const win = new THREE.Mesh(new THREE.PlaneGeometry(0.6, 0.8), winMat);
            const wx = -w / 2 + 0.6 + c * 1.1;
            const wy = 0.8 + r * 1.4;
            win.position.set(wx, wy, d / 2 + 0.01);
            mesh.add(win);
            // back
            const win2 = win.clone();
            win2.position.z = -d / 2 - 0.01;
            win2.rotation.y = Math.PI;
            mesh.add(win2);
          }
        }
      }
    }
  }

  // Lanterns
  const lanterns = [];
  for (let side of [-1, 1]) {
    for (let z = -40; z < 40; z += 10) {
      const lamp = new THREE.Group();
      const pole = new THREE.Mesh(
        new THREE.CylinderGeometry(0.08, 0.1, 5, 8),
        new THREE.MeshLambertMaterial({ color: 0x222 })
      );
      pole.position.y = 2.5;
      lamp.add(pole);
      const arm = new THREE.Mesh(
        new THREE.CylinderGeometry(0.05, 0.05, 2, 8),
        new THREE.MeshLambertMaterial({ color: 0x222 })
      );
      arm.rotation.z = Math.PI / 2 * -side;
      arm.position.set(-side * 1, 5, 0);
      lamp.add(arm);
      const lanternBody = new THREE.Mesh(
        new THREE.SphereGeometry(0.4, 10, 8),
        new THREE.MeshBasicMaterial({ color: 0xff4444 })
      );
      lanternBody.position.set(-side * 1.8, 4.8, 0);
      lamp.add(lanternBody);
      const pl = new THREE.PointLight(0xff7755, 0.4, 6);
      pl.position.copy(lanternBody.position);
      lamp.add(pl);
      lamp.position.set(side * 6.8, 0, z);
      scene.add(lamp);
      lanterns.push(lanternBody);
    }
  }

  // Asian skin tones
  const skinTones = [0xffe0bd, 0xffd1a4, 0xf1c27d, 0xe9b893, 0xffcca8, 0xf5d0a9, 0xd9a578];
  const hairColors = [0x1a1a1a, 0x2b1d10, 0x0a0a0a, 0x3a2410, 0x1c1208, 0x222222];
  const shirtColors = [0x2255aa, 0xcc3344, 0x33aa55, 0xee8833, 0x884499, 0xffffff, 0x222244, 0xaa3344, 0xeecc44, 0x3399aa, 0x775544, 0xddaa66];
  const pantsColors = [0x223355, 0x222222, 0x444466, 0x223322, 0x553322, 0x222244];

  // Build a single person factory
  function makePerson() {
    const person = new THREE.Group();
    const skin = skinTones[Math.floor(Math.random() * skinTones.length)];
    const hair = hairColors[Math.floor(Math.random() * hairColors.length)];
    const shirt = shirtColors[Math.floor(Math.random() * shirtColors.length)];
    const pants = pantsColors[Math.floor(Math.random() * pantsColors.length)];

    const skinMat = new THREE.MeshLambertMaterial({ color: skin });
    const hairMat = new THREE.MeshLambertMaterial({ color: hair });
    const shirtMat = new THREE.MeshLambertMaterial({ color: shirt });
    const pantsMat = new THREE.MeshLambertMaterial({ color: pants });

    // Torso
    const torso = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.75, 0.32), shirtMat);
    torso.position.y = 1.1;
    torso.castShadow = true;
    person.add(torso);

    // Head
    const head = new THREE.Mesh(new THREE.SphereGeometry(0.22, 12, 10), skinMat);
    head.position.y = 1.7;
    head.scale.set(1, 1.1, 0.95);
    head.castShadow = true;
    person.add(head);

    // Hair (top)
    const hairMesh = new THREE.Mesh(new THREE.SphereGeometry(0.235, 12, 10, 0, Math.PI * 2, 0, Math.PI * 0.55), hairMat);
    hairMesh.position.y = 1.72;
    hairMesh.scale.set(1.02, 1.1, 1.02);
    person.add(hairMesh);

    // Hair back
    if (Math.random() > 0.5) {
      const backHair = new THREE.Mesh(new THREE.SphereGeometry(0.21, 10, 8), hairMat);
      backHair.position.set(0, 1.55, -0.15);
      backHair.scale.set(1, 1.4, 0.8);
      person.add(backHair);
    }

    // Eyes
    const eyeMat = new THREE.MeshBasicMaterial({ color: 0x111111 });
    const eyeL = new THREE.Mesh(new THREE.SphereGeometry(0.025, 6, 6), eyeMat);
    eyeL.position.set(-0.07, 1.72, 0.2);
    person.add(eyeL);
    const eyeR = eyeL.clone();
    eyeR.position.x = 0.07;
    person.add(eyeR);

    // Legs
    const legL = new THREE.Mesh(new THREE.BoxGeometry(0.2, 0.75, 0.22), pantsMat);
    legL.position.set(-0.14, 0.4, 0);
    legL.castShadow = true;
    person.add(legL);
    const legR = legL.clone();
    legR.position.x = 0.14;
    person.add(legR);

    // Shoes
    const shoeMat = new THREE.MeshLambertMaterial({ color: 0x222222 });
    const shoeL = new THREE.Mesh(new THREE.BoxGeometry(0.22, 0.1, 0.3), shoeMat);
    shoeL.position.set(-0.14, 0.05, 0.04);
    person.add(shoeL);
    const shoeR = shoeL.clone();
    shoeR.position.x = 0.14;
    person.add(shoeR);

    // Arms
    const armMat = shirtMat;
    const armL = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.7, 0.18), armMat);
    armL.position.set(-0.36, 1.1, 0);
    armL.castShadow = true;
    person.add(armL);
    const armR = armL.clone();
    armR.position.x = 0.36;
    person.add(armR);

    // Optional bag
    if (Math.random() > 0.5) {
      const bagColor = [0x884422, 0x222222, 0x664433, 0x443322][Math.floor(Math.random() * 4)];
      const bag = new THREE.Mesh(new THREE.BoxGeometry(0.25, 0.3, 0.1), new THREE.MeshLambertMaterial({ color: bagColor }));
      bag.position.set(0.38, 0.9, 0.1);
      person.add(bag);
    }

    // Scale a bit randomly
    const s = 0.9 + Math.random() * 0.25;
    person.scale.set(s, s, s);

    person.userData = {
      legL, legR, armL, armR, torso, head
    };
    return person;
  }

  // People
  const people = [];
  const N = 38;
  for (let i = 0; i < N; i++) {
    const p = makePerson();
    // Distribute along z, on either side of street
    const onSidewalk = Math.random() < 0.55;
    const side = Math.random() < 0.5 ? -1 : 1;
    const z = -42 + Math.random() * 84;
    let x;
    if (onSidewalk) {
      x = side * (7 + Math.random() * 2.4);
    } else {
      x = (Math.random() - 0.5) * 6;
    }
    p.position.set(x, 0, z);
    // Direction
    p.userData.dir = (Math.random() < 0.5) ? 1 : -1;
    p.userData.speed = 0.8 + Math.random() * 1.2;
    p.userData.phase = Math.random() * Math.PI * 2;
    p.userData.bounceAmp = 0.05 + Math.random() * 0.05;
    p.rotation.y = p.userData.dir > 0 ? Math.PI : 0;
    scene.add(p);
    people.push(p);
  }

  // Time
  const clock = new THREE.Clock();

  function animate() {
    const t = clock.getElapsedTime();
    for (const p of people) {
      const ud = p.userData;
      // Walk
      p.position.z += ud.dir * ud.speed * 0.016 * 60 * 0.016;
      // Wrap
      if (p.position.z > 50) p.position.z = -50;
      if (p.position.z < -50) p.position.z = 50;

      const stride = Math.sin(t * 6 * (ud.speed / 1.4) + ud.phase);
      ud.legL.rotation.x = stride * 0.6;
      ud.legR.rotation.x = -stride * 0.6;
      ud.armL.rotation.x = -stride * 0.5;
      ud.armR.rotation.x = stride * 0.5;
      p.position.y = Math.abs(Math.sin(t * 6 * (ud.speed / 1.4) + ud.phase)) * ud.bounceAmp;
    }
    // Lantern sway
    for (let i = 0; i < lanterns.length; i++) {
      lanterns[i].position.x = -Math.sign((i % 2) === 0 ? 1 : -1) * 1.8 + Math.sin(t * 1.5 + i) * 0.05;
    }
    // Subtle camera
    camera.position.x = Math.sin(t * 0.15) * 0.6;
    camera.lookAt(0, 2.5, Math.sin(t * 0.1) * 2);

    renderer.render(scene, camera);
  }

  startFrame(animate);

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