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

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setSize(width, height, false);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
  renderer.setClearColor(0x2a2a35);

  const scene = new THREE.Scene();
  scene.fog = new THREE.Fog(0x2a2a35, 18, 60);

  const camera = new THREE.PerspectiveCamera(55, width / height, 0.1, 200);
  camera.position.set(0, 4.5, 14);
  camera.lookAt(0, 1.5, 0);

  // Lighting
  scene.add(new THREE.AmbientLight(0xffffff, 0.55));
  const sun = new THREE.DirectionalLight(0xffe2b8, 0.9);
  sun.position.set(8, 15, 6);
  scene.add(sun);
  const fill = new THREE.DirectionalLight(0x6688aa, 0.35);
  fill.position.set(-6, 8, -4);
  scene.add(fill);

  // Street
  const streetGeo = new THREE.PlaneGeometry(14, 80, 1, 1);
  const streetMat = new THREE.MeshLambertMaterial({ color: 0x3a3a44 });
  const street = new THREE.Mesh(streetGeo, streetMat);
  street.rotation.x = -Math.PI / 2;
  street.position.set(0, 0, -25);
  scene.add(street);

  // Sidewalks
  const sideMat = new THREE.MeshLambertMaterial({ color: 0x5a5048 });
  const sideL = new THREE.Mesh(new THREE.BoxGeometry(4, 0.15, 80), sideMat);
  sideL.position.set(-9, 0.075, -25);
  scene.add(sideL);
  const sideR = sideL.clone();
  sideR.position.x = 9;
  scene.add(sideR);

  // Buildings (simple blocks on both sides)
  const buildingColors = [0x6b5a4a, 0x4a5a6b, 0x6b4a5a, 0x5a6b4a, 0x8a6a4a, 0x4a6b6b];
  for (let i = 0; i < 24; i++) {
    const side = i % 2 === 0 ? -1 : 1;
    const w = 2 + Math.random() * 2.5;
    const h = 4 + Math.random() * 8;
    const d = 2 + Math.random() * 2;
    const geo = new THREE.BoxGeometry(w, h, d);
    const mat = new THREE.MeshLambertMaterial({
      color: buildingColors[Math.floor(Math.random() * buildingColors.length)]
    });
    const b = new THREE.Mesh(geo, mat);
    b.position.set(side * (10 + w / 2 + Math.random() * 0.5), h / 2, -i * 3 + 10);
    scene.add(b);

    // Windows (small emissive boxes)
    const winRows = Math.floor(h / 1.2);
    const winCols = Math.floor(w / 0.9);
    for (let r = 0; r < winRows; r++) {
      for (let c = 0; c < winCols; c++) {
        if (Math.random() < 0.45) continue;
        const wg = new THREE.BoxGeometry(0.35, 0.5, 0.05);
        const wm = new THREE.MeshBasicMaterial({
          color: Math.random() < 0.6 ? 0xffeebb : 0x223344
        });
        const win = new THREE.Mesh(wg, wm);
        win.position.set(
          b.position.x - (side < 0 ? 0.01 : -0.01) * 0,
          0.8 + r * 1.1,
          b.position.z - d / 2 + 0.03 + (c + 0.5) * (w / winCols)
        );
        // place on building's street-facing side
        if (side < 0) {
          win.position.x = b.position.x + d / 2 - 0.03;
          win.rotation.y = -Math.PI / 2;
        } else {
          win.position.x = b.position.x - d / 2 + 0.03;
          win.rotation.y = Math.PI / 2;
        }
        scene.add(win);
      }
    }
  }

  // Street lamps
  for (let z = -10; z < 60; z += 8) {
    const pole = new THREE.Mesh(
      new THREE.CylinderGeometry(0.06, 0.06, 4, 8),
      new THREE.MeshLambertMaterial({ color: 0x222222 })
    );
    pole.position.set(7.2, 2, -z);
    scene.add(pole);
    const lamp = new THREE.Mesh(
      new THREE.SphereGeometry(0.22, 12, 8),
      new THREE.MeshBasicMaterial({ color: 0xfff2c0 })
    );
    lamp.position.set(7.2, 4.05, -z);
    scene.add(lamp);
    const pl = new THREE.PointLight(0xffd28a, 0.6, 8);
    pl.position.copy(lamp.position);
    scene.add(pl);
  }

  // Asian-style clothing colors
  const shirtColors = [
    0xffffff, 0x111111, 0x2244aa, 0xcc2244, 0x22aa66,
    0xffcc44, 0x8844aa, 0xee8855, 0x336688, 0xcc6677
  ];
  const pantsColors = [0x222233, 0x444455, 0x222244, 0x333322, 0x553333];
  const skinTones = [0xf2c9a0, 0xe6b48a, 0xd49a6a, 0xf7d7b4, 0xc98763];

  const people = [];
  const PERSON_COUNT = 60;

  function makePerson(x, z) {
    const group = new THREE.Group();
    const skin = skinTones[Math.floor(Math.random() * skinTones.length)];

    // Body (torso)
    const torsoH = 0.7 + Math.random() * 0.2;
    const torso = new THREE.Mesh(
      new THREE.BoxGeometry(0.45, torsoH, 0.28),
      new THREE.MeshLambertMaterial({
        color: shirtColors[Math.floor(Math.random() * shirtColors.length)]
      })
    );
    torso.position.y = 1.05;
    group.add(torso);

    // Legs
    const legMat = new THREE.MeshLambertMaterial({
      color: pantsColors[Math.floor(Math.random() * pantsColors.length)]
    });
    const legL = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.85, 0.22), legMat);
    legL.position.set(-0.11, 0.42, 0);
    group.add(legL);
    const legR = legL.clone();
    legR.position.x = 0.11;
    group.add(legR);

    // Head
    const head = new THREE.Mesh(
      new THREE.SphereGeometry(0.18, 14, 12),
      new THREE.MeshLambertMaterial({ color: skin })
    );
    head.position.y = 1.55;
    group.add(head);

    // Hair (black, common Asian style)
    const hairColors = [0x111111, 0x1a1a1a, 0x2a1a0a];
    const hair = new THREE.Mesh(
      new THREE.SphereGeometry(0.19, 14, 10, 0, Math.PI * 2, 0, Math.PI * 0.6),
      new THREE.MeshLambertMaterial({
        color: hairColors[Math.floor(Math.random() * hairColors.length)]
      })
    );
    hair.position.y = 1.58;
    group.add(hair);

    // Arms
    const armMat = new THREE.MeshLambertMaterial({
      color: shirtColors[Math.floor(Math.random() * shirtColors.length)]
    });
    const armL = new THREE.Mesh(new THREE.BoxGeometry(0.13, 0.7, 0.16), armMat);
    armL.position.set(-0.32, 1.0, 0);
    group.add(armL);
    const armR = armL.clone();
    armR.position.x = 0.32;
    group.add(armR);

    group.position.set(x, 0, z);
    return {
      group,
      armL, armR, legL, legR,
      speed: 0.6 + Math.random() * 0.8,
      phase: Math.random() * Math.PI * 2,
      swing: 0.3 + Math.random() * 0.4,
      lane: x // store lane x
    };
  }

  // Initialize people across the street in lanes
  for (let i = 0; i < PERSON_COUNT; i++) {
    const laneChoices = [-6.5, -5.2, -3.9, -2.6, -1.3, 1.3, 2.6, 3.9, 5.2, 6.5];
    const x = laneChoices[Math.floor(Math.random() * laneChoices.length)];
    const z = -i * 1.6 + Math.random() * 1.2;
    const p = makePerson(x, z);
    people.push(p);
    scene.add(p.group);
  }

  // A few pedestrians on sidewalks
  for (let i = 0; i < 10; i++) {
    const x = (Math.random() < 0.5 ? -1 : 1) * (7.2 + Math.random() * 1.2);
    const z = -Math.random() * 50;
    const p = makePerson(x, z);
    p.speed *= 0.6;
    people.push(p);
    scene.add(p.group);
  }

  startFrame((t) => {
    const time = t * 0.001;

    // Animate walking: swing arms and legs
    for (const p of people) {
      const swing = Math.sin(time * p.speed * 4 + p.phase) * p.swing;
      p.armL.rotation.x = swing;
      p.armR.rotation.x = -swing;
      p.legL.rotation.x = -swing * 0.7;
      p.legR.rotation.x = swing * 0.7;

      // Move forward (toward camera at +z)
      p.group.position.z += p.speed * 0.02;

      // Slight body bob
      p.group.position.y = Math.abs(Math.sin(time * p.speed * 4 + p.phase)) * 0.04;

      // Reset when past camera so animation is infinite
      if (p.group.position.z > 12) {
        p.group.position.z -= 70;
        // randomize x slightly so it doesn't look identical each cycle
        p.group.position.x += (Math.random() - 0.5) * 0.4;
        if (p.group.position.x > 7) p.group.position.x = 6.5;
        if (p.group.position.x < -7) p.group.position.x = -6.5;
      }
    }

    // Subtle camera sway
    camera.position.x = Math.sin(time * 0.3) * 0.3;
    camera.lookAt(0, 1.6, 0);

    renderer.render(scene, camera);
  });

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