<!-- PLAYA 2.0'S WORLD · part 4 -->
w Float32Array(n * 4), pos = new Float32Array(n * 3), col = new Float32Array(n * 3);
  for (let i = 0; i < n; i++) {
    const a = Math.random() * Math.PI * 2, r = Math.sqrt(Math.random()) * R;
    base.set([Math.cos(a) * r, 0.8 + Math.random() * 5, Math.sin(a) * r, Math.random() * 100], i * 4);
    const c = Math.random() < 0.7 ? [1, 0.18, 0.28] : [1, 0.62, 0.25]; col.set(c, i * 3);
  }
  const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
  const pts = new THREE.Points(geo, new THREE.PointsMaterial({ size: opts.size ?? 0.22, vertexColors: true, transparent: true, opacity: 0.95, depthWrite: false, blending: THREE.AdditiveBlending }));
  pts.frustumCulled = false; g.add(pts); scene.add(g);
  const p0 = opts.position || [0, 0, 0]; g.position.set(p0[0], p0[1], p0[2]);
  g.userData.tick = t => {
    for (let i = 0; i < n; i++) { const b = i * 4, s = base[b + 3];
      pos[i * 3] = base[b] + Math.sin(t * 0.3 + s) * 1.5; pos[i * 3 + 1] = base[b + 1] + Math.sin(t * 0.7 + s * 1.3) * 0.6; pos[i * 3 + 2] = base[b + 2] + Math.cos(t * 0.25 + s * 0.7) * 1.5; }
    geo.attributes.position.needsUpdate = true;
  };
  g.userData.tick(0); g.userData.colliders = []; return g;
}
export default { createFireflies };


===== FILE: playa-world/floating-island.js =====
// floating-island.js — Playa 2.0 original: hovering rock island (underside clears 2.1 above ground: walk under it) with glowing beacon & orbit rings. fn(THREE, scene, opts) -> Group
import { mkit, pmats, placeLocal } from './mkit.js';
export function createFloatingIsland(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'FloatingIsland'; const M = pmats(THREE, opts), k = mkit(THREE, g);
  const H = opts.hover ?? 7.5, R = opts.radius ?? 5;
  k.cyl(M.stone, R, R * 0.85, 1.2, 9, 0, H, 0);              // top slab
  k.cone(M.stone, R * 0.85, 4.5, 9, 0, H - 2.85, 0, Math.PI); // underside rock
  k.cone(M.black, 1.2, 2, 7, 1.8, H - 4.4, 1, Math.PI);       // drip shards
  k.cone(M.black, 0.8, 1.5, 7, -2, H - 4.0, -1.2, Math.PI);
  k.cyl(M.red, R + 0.05, R + 0.05, 0.2, 40, 0, H + 0.62, 0);  // red rim
  k.cyl(M.black, 0.6, 0.8, 3, 8, 0, H + 2.1, 0);              // beacon column
  k.cyl(M.glow, 0.45, 0.45, 1.2, 12, 0, H + 4.2, 0);
  k.cone(M.red, 0.7, 1.4, 8, 0, H + 5.5, 0);
  for (let i = 0; i < 3; i++) k.torus(M.glow, R + 1.2 + i * 0.9, 0.08, 0, H - 0.5 - i * 0.9, 0, Math.PI / 2 + 0.15 * (i - 1), 0, 0.1 * i);
  for (let i = 0; i < 6; i++) { const a = i / 6 * Math.PI * 2; k.box(M.glow, 0.2, 0.6, 0.2, Math.cos(a) * (R - 0.6), H + 0.9, Math.sin(a) * (R - 0.6)); }
  k.build();
  if (opts.animate !== false) g.userData.tick = t => { g.children.forEach(m => { m.position.y = Math.sin(t * 0.8) * 0.25; }); };
  return placeLocal(scene, g, opts, [[-R, R, -R, R, H + 0.6, H - 5.4]]);
}
export default { createFloatingIsland };


===== FILE: playa-world/index.html =====
<!doctype html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="theme-color" content="#000000">
<title>Playa 2.0's World</title>
<link rel="icon" href="data:,">
<link rel="stylesheet" href="world.css">
</head><body>
<canvas id="c" aria-label="Playa 2.0's World 3D view"></canvas>
<header id="hud"><h1>Playa 2.0<span>'s World</span></h1><div id="info">Move: joystick · Look: drag</div></header>
<div id="joy" aria-label="Move joystick"><div id="knob"></div></div>
<div id="btns">
  <button id="jump" aria-label="Jump">JUMP</button>
  <button id="cam" aria-label="Reset camera">⟲</button>
</div>
<script src="three.min.js"></script>
<script type="module" src="world.js"></script>
</body></html>


===== FILE: playa-world/lantern-path.js =====
// lantern-path.js — Playa 2.0 original: glowing lanterns lining a path polyline + inset light strip. fn(THREE, scene, opts) -> Group
import { mkit, pmats, placeLocal } from './mkit.js';
export function createLanternPath(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'LanternPath'; const M = pmats(THREE, opts), k = mkit(THREE, g);
  const pts = opts.points || [[0, 0], [0, -20]], gap = opts.spacing ?? 4, half = (opts.width ?? 4) / 2, cols = [];
  for (let s = 0; s < pts.length - 1; s++) {
    const [x0, z0] = pts[s], [x1, z1] = pts[s + 1], L = Math.hypot(x1 - x0, z1 - z0), ux = (x1 - x0) / L, uz = (z1 - z0) / L, ang = Math.atan2(ux, uz);
    k.box(M.stone, half * 2 - 0.4, 0.14, L, (x0 + x1) / 2, 0.02, (z0 + z1) / 2, 0, ang, 0);
    k.box(M.glow, 0.14, 0.14, L, (x0 + x1) / 2, 0.06, (z0 + z1) / 2, 0, ang, 0);
    for (let d = s ? gap : 0; d <= L + 0.01; d += gap) for (const sd of [-1, 1]) {
      const x = x0 + ux * d - uz * half * sd, z = z0 + uz * d + ux * half * sd;
      k.box(M.red, 0.4, 0.14, 0.4, x, 0.07, z); k.box(M.black, 0.14, 1.9, 0.14, x, 1.05, z);
      k.box(M.glow, 0.3