{
  "name": "arcadecabinet",
  "title": "ArcadeCabinet",
  "type": "registry:component",
  "description": "An upright arcade cabinet cut to the classic profile, with a lit marquee, joystick and buttons, and a screen running a seeded pixel game.",
  "dependencies": [
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.13.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "ArcadeCabinet.tsx",
      "content": "import { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { pick, rng, sub, useWorld, type WorldComponentProps } from '@runek/core'\nimport { useLayoutEffect, useMemo, useRef } from 'react'\nimport { Color, ExtrudeGeometry, type InstancedMesh, Object3D, Shape } from 'three'\n\nexport interface ArcadeCabinetProps extends WorldComponentProps {\n  /** Cabinet width, in units. */\n  width?: number\n  /** Body color. Seeded when unset. */\n  color?: string\n  /** Side-art stripe and marquee glow. Seeded when unset. */\n  accent?: string\n  /** Lit screen showing a seeded game; off, dark glass. */\n  on?: boolean\n  /** Control sets on the panel (one or two players). Seeded when unset. */\n  players?: 1 | 2\n}\n\nconst BODIES = ['#1f2a44', '#2b2b2e', '#5a1f2b', '#16303a']\nconst NEON = ['#f0c419', '#e4405f', '#38c6f4', '#7bd34f']\nconst SPRITES = 3\n\n// The side profile, (z, y) in meters: kick, control panel lip, screen slope, marquee, top, back.\nconst PROFILE: [number, number][] = [\n  [-0.36, 0],\n  [0.24, 0],\n  [0.24, 0.82],\n  [0.4, 0.92],\n  [0.36, 0.98],\n  [0.2, 1.02],\n  [0.06, 1.5],\n  [0.16, 1.54],\n  [0.16, 1.82],\n  [-0.36, 1.82],\n]\n\n/** A 5×5 invader-style sprite, mirrored left to right: seeded, so every cabinet runs its own game. */\nfunction sprite(r: () => number): boolean[][] {\n  const rows: boolean[][] = []\n  for (let y = 0; y < 5; y++) {\n    const half = [r() < 0.55, r() < 0.55, r() < 0.65]\n    rows.push([half[0], half[1], half[2], half[1], half[0]])\n  }\n  return rows\n}\n\n/**\n * An upright arcade cabinet: side panels cut to the classic profile, a lit marquee, a joystick\n * and buttons on the control panel, and a glowing screen running a seeded pixel game (a row of\n * invaders over a ship, drawn as instanced squares). One cuboid collider.\n */\nexport function ArcadeCabinet({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  width = 0.66,\n  color,\n  accent,\n  on = true,\n  players,\n}: ArcadeCabinetProps) {\n  const { unit } = useWorld()\n  const u = unit\n  const W = width * u\n\n  const look = useMemo(() => {\n    const r = rng(sub(seed, 0))\n    const rolled = {\n      body: pick(r, BODIES),\n      neon: pick(r, NEON),\n      players: (r() < 0.5 ? 1 : 2) as 1 | 2,\n      invader: sprite(r),\n      ship: sprite(r),\n      hue: r(),\n    }\n    return {\n      body: color ?? rolled.body,\n      neon: accent ?? rolled.neon,\n      players: players ?? rolled.players,\n      invader: rolled.invader,\n      ship: rolled.ship,\n      ink: new Color().setHSL(rolled.hue, 0.8, 0.62).getStyle(),\n    }\n  }, [seed, color, accent, players])\n\n  const side = useMemo(() => {\n    const s = new Shape()\n    PROFILE.forEach(([z, y], i) => {\n      if (i) s.lineTo(z * u, y * u)\n      else s.moveTo(z * u, y * u)\n    })\n    s.closePath()\n    return new ExtrudeGeometry(s, { depth: 0.03 * u, bevelEnabled: false })\n  }, [u])\n\n  // Screen: from the panel's top edge up the slope.\n  const [z0, y0] = PROFILE[5]\n  const [z1, y1] = PROFILE[6]\n  const slope = Math.hypot(z1 - z0, y1 - y0) * u\n  const tilt = -Math.atan2(z0 - z1, y1 - y0)\n  const screenW = W - 0.1 * u\n  const screenH = slope - 0.06 * u\n\n  const pixels = useRef<InstancedMesh>(null)\n  const cells = useMemo(() => {\n    const out: [number, number][] = []\n    const px = screenW / 30\n    const draw = (art: boolean[][], cx: number, cy: number) => {\n      art.forEach((row, y) => {\n        row.forEach((lit, x) => {\n          if (lit) out.push([cx + (x - 2) * px, cy - (y - 2) * px])\n        })\n      })\n    }\n    for (let i = 0; i < SPRITES; i++) draw(look.invader, (i - 1) * screenW * 0.28, screenH * 0.22)\n    draw(look.ship, 0, -screenH * 0.3)\n    return { list: out, px }\n  }, [look, screenW, screenH])\n  useLayoutEffect(() => {\n    const mesh = pixels.current\n    if (!mesh) return\n    const o = new Object3D()\n    cells.list.forEach(([x, y], i) => {\n      o.position.set(x, y, 0)\n      o.scale.set(cells.px * 0.92, cells.px * 0.92, 1)\n      o.updateMatrix()\n      mesh.setMatrixAt(i, o.matrix)\n    })\n    mesh.count = cells.list.length\n    mesh.instanceMatrix.needsUpdate = true\n  }, [cells])\n\n  const controls = look.players === 1 ? [0] : [-W * 0.22, W * 0.22]\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      <CuboidCollider args={[W / 2, 0.91 * u, 0.38 * u]} position={[0, 0.91 * u, 0.02 * u]} />\n      {/* side panels, extruded from the profile and turned to stand on their edge */}\n      {[-1, 1].map((s) => (\n        <mesh\n          key={s}\n          geometry={side}\n          position={[s > 0 ? W / 2 : -W / 2 + 0.03 * u, 0, 0]}\n          rotation={[0, -Math.PI / 2, 0]}\n          castShadow\n          receiveShadow\n        >\n          <meshStandardMaterial color={look.body} roughness={0.6} />\n        </mesh>\n      ))}\n      {[-1, 1].map((s) => (\n        <mesh\n          key={`art${s}`}\n          position={[s * (W / 2 + 0.001 * u), 1.1 * u, -0.1 * u]}\n          rotation={[0, (s * Math.PI) / 2, 0]}\n        >\n          <planeGeometry args={[0.5 * u, 0.06 * u]} />\n          <meshBasicMaterial color={look.neon} toneMapped={false} />\n        </mesh>\n      ))}\n      {/* the inner body between the panels: a back block behind the screen, the lower front\n          under the panel, and the marquee box */}\n      <mesh position={[0, 0.91 * u, -0.16 * u]} castShadow>\n        <boxGeometry args={[W - 0.06 * u, 1.82 * u, 0.4 * u]} />\n        <meshStandardMaterial color={look.body} roughness={0.6} />\n      </mesh>\n      <mesh position={[0, 0.41 * u, 0.14 * u]} castShadow>\n        <boxGeometry args={[W - 0.06 * u, 0.82 * u, 0.2 * u]} />\n        <meshStandardMaterial color={look.body} roughness={0.6} />\n      </mesh>\n      <mesh position={[0, 1.66 * u, 0.1 * u]} castShadow>\n        <boxGeometry args={[W - 0.06 * u, 0.32 * u, 0.12 * u]} />\n        <meshStandardMaterial color={look.body} roughness={0.6} />\n      </mesh>\n      <mesh position={[0, 0.41 * u, 0.245 * u]}>\n        <planeGeometry args={[W - 0.06 * u, 0.82 * u]} />\n        <meshStandardMaterial color=\"#141416\" roughness={0.7} />\n      </mesh>\n      {/* control panel */}\n      <mesh position={[0, 0.94 * u, 0.3 * u]} rotation={[0.35, 0, 0]} castShadow>\n        <boxGeometry args={[W - 0.06 * u, 0.03 * u, 0.24 * u]} />\n        <meshStandardMaterial color=\"#1b1b1e\" roughness={0.5} />\n      </mesh>\n      {controls.map((x) => (\n        <group key={x} position={[x, 0.97 * u, 0.3 * u]} rotation={[0.35, 0, 0]}>\n          <mesh position={[-0.06 * u, 0.03 * u, 0]}>\n            <cylinderGeometry args={[0.006 * u, 0.006 * u, 0.06 * u, 8]} />\n            <meshStandardMaterial color=\"#222\" />\n          </mesh>\n          <mesh position={[-0.06 * u, 0.065 * u, 0]}>\n            <sphereGeometry args={[0.018 * u, 12, 10]} />\n            <meshStandardMaterial color=\"#d23b3b\" roughness={0.3} />\n          </mesh>\n          {[0, 1, 2].map((b) => (\n            <mesh\n              key={b}\n              position={[0.02 * u + b * 0.035 * u, 0.008 * u, (b === 1 ? -0.015 : 0.01) * u]}\n            >\n              <cylinderGeometry args={[0.012 * u, 0.012 * u, 0.012 * u, 12]} />\n              <meshBasicMaterial\n                color={NEON[(b + look.players) % NEON.length]}\n                toneMapped={false}\n              />\n            </mesh>\n          ))}\n        </group>\n      ))}\n      {/* screen on the slope */}\n      <group\n        position={[0, ((y0 + y1) / 2) * u, ((z0 + z1) / 2) * u + 0.002 * u]}\n        rotation={[tilt, 0, 0]}\n      >\n        <mesh>\n          <planeGeometry args={[W - 0.06 * u, slope]} />\n          <meshStandardMaterial color=\"#0b0b0d\" roughness={0.3} />\n        </mesh>\n        <mesh position={[0, 0, 0.001 * u]}>\n          <planeGeometry args={[screenW, screenH]} />\n          {on ? (\n            <meshBasicMaterial color=\"#05060c\" toneMapped={false} />\n          ) : (\n            <meshStandardMaterial color=\"#101214\" roughness={0.1} metalness={0.4} />\n          )}\n        </mesh>\n        {on && (\n          <instancedMesh\n            key={cells.list.length}\n            ref={pixels}\n            args={[undefined, undefined, cells.list.length]}\n            position={[0, 0, 0.002 * u]}\n          >\n            <planeGeometry args={[1, 1]} />\n            <meshBasicMaterial color={look.ink} toneMapped={false} />\n          </instancedMesh>\n        )}\n      </group>\n      {/* marquee */}\n      <mesh position={[0, 1.68 * u, 0.161 * u]}>\n        <planeGeometry args={[W - 0.06 * u, 0.22 * u]} />\n        <meshBasicMaterial color={look.neon} toneMapped={false} />\n      </mesh>\n    </RigidBody>\n  )\n}\n\nArcadeCabinet.groundSitting = true\n",
      "type": "registry:component"
    }
  ]
}
