---
name: vr-polish-checklist
description: Use BEFORE telling the user a VR game is finished, and whenever adding "juice"/game-feel to a WebXR game. Walks through VR-safe polish (haptics, emissive flash, spatial audio, squash & stretch, easing) and explicitly FORBIDS the flat-game techniques that cause motion sickness in VR — screen shake, hit-stop, camera moves, and full-screen flashes/tints.
---

# VR polish checklist — game feel that doesn't make people sick

The difference between "it works" and "this is great" is game feel. But **the standard 2D/flat-3D juice playbook is dangerous in VR.** Half of those techniques work by moving or flooding the *camera* — which in VR means moving or flooding the player's *head and eyes*. That causes discomfort and nausea. This skill is the VR-safe replacement: keep the same impact, deliver it through the hands, the objects, and the soundscape — never through the camera.

**Core principle:** the game NEVER moves, shakes, or floods the player's view. Impact is delivered to objects, hands (haptics), and ears — not the head.

## The hard rules — never do these in VR

Violating the letter of these is violating the spirit. There is no "just a little" exception.

| ❌ Forbidden (causes nausea) | ✅ VR-safe replacement |
| ---------------------------- | --------------------- |
| **Camera / screen shake** | Haptic pulse + object shake (shake the *hit object*, not the view) |
| **Hit-stop / freeze-frame** (pausing the world) | Brief slow-mo of the *target only*, or emissive flash + haptic — never freeze the whole view |
| **Full-screen flash / color tint / vignette-as-feedback** | Local emissive flash on the object; a *world-space* damage indicator near the source |
| **Camera moves / dolly / forced look / cutscene takeover** | Let the player keep their head; draw attention with light, sound, motion *in the world* |
| **Chromatic aberration / heavy post FX** | Skip; rarely worth the per-eye cost and can disorient (`environment-scaffold`) |
| **Sudden FOV changes** | Only reduce FOV (comfort vignette) *during locomotion*, never as juice |

If a polish idea works by changing what the *whole view* does, it's out. Shake the object, buzz the hand, play the sound.

## VR-safe juice — apply these

### 1. Haptics (the VR version of screen shake)

A haptic pulse on the controller is the most direct impact feedback VR has. Scale intensity to impact.

```ts
function pulse(controller: THREE.Object3D, intensity = 0.5, ms = 40) {
  const src = controller.userData.inputSource as XRInputSource | undefined;
  (src?.gamepad as any)?.hapticActuators?.[0]?.pulse?.(intensity, ms);
}
// small tick = pulse(0.2, 20); solid hit = pulse(0.8, 60)
```

### 2. Emissive flash on the hit object (the VR "flash white")

```ts
function flashEmissive(mesh: THREE.Mesh, hex = 0xffffff, dur = 0.1) {
  const m = mesh.material as THREE.MeshStandardMaterial;
  if (!m.emissive) return;
  const original = m.emissive.getHex();
  m.emissive.setHex(hex);
  setTimeout(() => m.emissive.setHex(original), dur * 1000);
}
```

### 3. Object shake / squash & stretch (move the OBJECT, never the camera)

```ts
// Brief positional jitter on the hit object — the safe analogue of screen shake.
function shakeObject(obj: THREE.Object3D, amt = 0.03, ms = 150) {
  const base = obj.position.clone();
  const start = performance.now(); // (or pass time in from the loop)
  const tick = () => {
    const k = 1 - (performance.now() - start) / ms;
    if (k <= 0) { obj.position.copy(base); return; }
    obj.position.set(base.x + (Math.random() - 0.5) * amt * k,
                     base.y + (Math.random() - 0.5) * amt * k,
                     base.z + (Math.random() - 0.5) * amt * k);
    requestAnimationFrame(tick);
  };
  tick();
}
// Squash on impact: briefly scale opposite axes, ease back (jump: y*1.2, xz*0.85).
```

### 4. Spatial sound on every action

Every important event has a sound, emitted from its world position with slight pitch variation. See `spatial-audio`. Silent VR feels dead.

### 5. Frame-rate-independent easing

```ts
// Damp toward a target so motion isn't robotic; stable across 72/90/120 Hz.
obj.position.lerp(target, 1 - Math.exp(-dt * speed));
```

### 6. World-space feedback for state

Score pops, "+10" labels, cooldown rings — place them **in the world** near the source (a small `Mesh` with a `CanvasTexture` that drifts up and fades), never as a 2D HUD overlay glued to the eyes (`world-space-ui`).

### 7. Comfortable transitions

For scene/level changes, **fade to black** (a large sphere around the head whose material opacity animates) rather than cutting or moving the camera. Never spin or dolly the player to transition.

## Domain principles honored here

- **Comfort first** — the entire rule set exists to protect the vestibular system; impact never touches the camera.
- **Diegetic** — feedback lives on objects and in space, not as screen overlays.
- **Input abstraction** — haptics go through the standard input-source gamepad.

## Red flags — STOP if you catch yourself doing this

- Reaching for `camera.position` to add shake → shake the object instead.
- Pausing `update()` for hit-stop → slow only the target, or drop it.
- Adding a full-screen overlay `<div>` / fullscreen quad for a flash or damage tint → local emissive flash.
- "It's only a tiny camera nudge" → still moves the head. No.
- Adding a fade/transition by moving or rotating the player → fade opacity instead.

## Verify

Walk every item before declaring done (skip only if the game genuinely can't use it):

- [ ] **No camera/screen shake anywhere.** (grep for `camera.position` writes outside the XR rig)
- [ ] **No hit-stop that freezes the whole view.**
- [ ] **No full-screen flash, tint, or feedback vignette.** (comfort vignette during locomotion is separate and allowed)
- [ ] **The game never moves or rotates the player's head.**
- [ ] Impacts fire a haptic pulse scaled to force.
- [ ] Hit objects use `flashEmissive` and/or object shake/squash.
- [ ] Every important action has a spatial sound with pitch variation (`spatial-audio`).
- [ ] Visible motion uses easing/damping, not linear.
- [ ] Score/damage feedback is world-space, near the source (`world-space-ui`).
- [ ] Scene transitions fade to black; they never dolly or spin the player.
- [ ] **On-device comfort pass:** a VR-naïve tester plays for 5 minutes without discomfort.

## References

- `spatial-audio` — sound feedback from world positions.
- `vr-grab-interaction` — haptics on grab/throw.
- `world-space-ui` — world-space score/damage indicators instead of HUD overlays.
- `comfort-locomotion` — the *one* place a comfort vignette/FOV reduction is appropriate.
- `vfx-and-particles` — local particle bursts for impact (not full-screen effects).
