{
 "note": "Game-feel and VFX quality tiers (presentation only). The Feel Lab reads this file; the client's settings menu uses the same ids. Outcomes, hit shapes, timings and monster telegraphs are identical on every tier. Values are proposed until the M2 VFX Lab measures them on the reference devices.",
 "proposed": true,
 "default": {
  "desktop": "high",
  "phone": "mid",
  "lowMemoryPhone": "low"
 },
 "tiers": [
  {
   "id": "low",
   "label": "Low",
   "target": "entry-level baseline: Galaxy A07 4G (4 GB), iPhone SE (2020), Intel N150 integrated graphics (8 GB); physical validation pending",
   "renderScale": 0.75,
   "shadowMap": 0,
   "glow": 0,
   "bloom": 0,
   "particleScale": 0.4,
   "dynamicLights": 0,
   "debris": 0,
   "decals": 0,
   "groundRings": false,
   "ambientMotes": 0,
   "anticipationGlint": false,
   "fovPunchDeg": 0,
   "directionalKick": 0,
   "shakeScale": 0.7,
   "chromaticPulse": 0,
   "screenShockwave": false,
   "impactFrame": false,
   "grain": 0,
   "sharpen": 0,
   "msaa": 1
  },
  {
   "id": "mid",
   "label": "Mid",
   "target": "mid-range phones and laptops; additional profiling references",
   "renderScale": 1,
   "shadowMap": 512,
   "glow": 0.4,
   "bloom": 0,
   "particleScale": 0.7,
   "dynamicLights": 1,
   "debris": 6,
   "decals": 8,
   "groundRings": true,
   "ambientMotes": 0,
   "anticipationGlint": true,
   "fovPunchDeg": 2,
   "directionalKick": 0,
   "shakeScale": 0.85,
   "chromaticPulse": 0,
   "screenShockwave": false,
   "impactFrame": false,
   "grain": 0,
   "sharpen": 0,
   "msaa": 1
  },
  {
   "id": "high",
   "label": "High",
   "target": "desktop (owner's M3), high-end phones",
   "renderScale": 1,
   "shadowMap": 1024,
   "glow": 0.55,
   "bloom": 0.22,
   "particleScale": 1,
   "dynamicLights": 1,
   "debris": 14,
   "decals": 16,
   "groundRings": true,
   "ambientMotes": 40,
   "anticipationGlint": true,
   "fovPunchDeg": 3,
   "directionalKick": 0.12,
   "shakeScale": 1,
   "chromaticPulse": 0,
   "screenShockwave": false,
   "impactFrame": false,
   "grain": 0,
   "sharpen": 0,
   "msaa": 1
  },
  {
   "id": "ultra",
   "label": "Ultra",
   "target": "desktop dGPU / Apple M-series Pro",
   "renderScale": "native (device pixel ratio, max 2)",
   "shadowMap": 2048,
   "glow": 0.65,
   "bloom": 0.3,
   "particleScale": 1.6,
   "dynamicLights": 2,
   "debris": 32,
   "decals": 32,
   "groundRings": true,
   "ambientMotes": 140,
   "anticipationGlint": true,
   "fovPunchDeg": 4.5,
   "directionalKick": 0.28,
   "shakeScale": 1.15,
   "chromaticPulse": 70,
   "screenShockwave": true,
   "impactFrame": true,
   "grain": 3,
   "sharpen": 0.25,
   "msaa": 4
  }
 ],
 "layers": {
  "debris": "Pooled rock/shell chunks on heavy and massive hits (heavy spawns 35% of the pool, massive 100%). Gravity, ground bounce and spin; 1.6 s life; no physics engine.",
  "decals": "Scorch and crack marks on the ground at heavy/massive impacts, fading over 5 s. Pooled; the oldest is recycled.",
  "groundRings": "Expanding dust ring on the ground for heavy and massive impacts.",
  "ambientMotes": "Slow sand motes drifting through the light (one emitter).",
  "anticipationGlint": "A glint on the blade at the start of skills with superarmour (the windup cue).",
  "fovPunchDeg": "Field-of-view punch on massive hits (heavy hits get one third).",
  "directionalKick": "The camera is pushed along the hit direction and springs back (metres).",
  "chromaticPulse": "Chromatic aberration pulse on heavy (half) and massive hits; decays in 0.25 s.",
  "screenShockwave": "Screen-space refraction ring from massive impacts; one at a time.",
  "impactFrame": "A 1–2 frame light burst (≤ 60 ms, 420 px radius) around massive hits only; never a full-screen white-out (skipped when Reduce flashing is on)."
 },
 "haptics": {
  "note": "Phones use the Vibration API (Android browsers; iOS Safari ignores it). Gamepads use dual-rumble. Only after the player has interacted with the page, at most one pulse per 60 ms, scaled by the Haptics setting (0–100%).",
  "vibrateMs": {
   "light": [
    8
   ],
   "medium": [
    16
   ],
   "heavy": [
    32
   ],
   "massive": [
    55,
    30,
    45
   ]
  },
  "gamepad": {
   "light": {
    "duration": 40,
    "strong": 0,
    "weak": 0.25
   },
   "medium": {
    "duration": 60,
    "strong": 0.2,
    "weak": 0.45
   },
   "heavy": {
    "duration": 110,
    "strong": 0.55,
    "weak": 0.7
   },
   "massive": {
    "duration": 220,
    "strong": 1,
    "weak": 0.9
   }
  }
 },
 "settings": [
  {
   "id": "quality",
   "label": "Graphics quality",
   "values": [
    "low",
    "mid",
    "high",
    "ultra"
   ]
  },
  {
   "id": "screenShake",
   "label": "Screen shake",
   "range": [
    0,
    100
   ],
   "default": 70,
   "note": "Owner 2026-10-06 (ADR-028): 70%, the same default as camera_shake in controls.json."
  },
  {
   "id": "haptics",
   "label": "Haptics (vibrate / rumble)",
   "range": [
    0,
    100
   ],
   "default": 100
  },
  {
   "id": "reduceFlashing",
   "label": "Reduce flashing",
   "default": false,
   "effect": "No impact frames or chromatic pulses; bloom peak clamped; hit flashes at 50%."
  }
 ],
 "rules": [
  "Presentation only: no tier changes damage, hit shapes, timings or reactions.",
  "Hitstop is local (attacker + struck targets). No global time-scale on any tier.",
  "Monster telegraphs are never culled, dimmed or covered by player effects on any tier.",
  "Dynamic resolution holds the frame budget before any feel layer is dropped.",
  "Other players' effects use Reduced (50% particles, no lights, no screen effects) whatever the tier."
 ],
 "platformCaps": {
  "note": "Owner decision (round 4): the browser is capped at Low–Mid; High and Ultra are for the downloaded apps. Native picks a default from a device benchmark and lets the player go up to the cap.",
  "web": {
   "max": "mid",
   "default": "mid",
   "lowEndDefault": "low"
  },
  "windows": {
   "max": "ultra",
   "default": "high"
  },
  "android": {
   "max": "ultra",
   "default": "mid",
   "note": "Ultra only on devices that pass the M1 thermal/frame gate"
  },
  "ios": {
   "max": "ultra",
   "default": "mid",
   "note": "Ultra only on devices that pass the M1 thermal/frame gate"
  }
 },
 "tierSelection": {
  "proposed": true,
  "note": "M0-04C: how the client picks its starting tier and frame rate. Order: a test launch option, then the player's saved choice (it always wins over every default), then the low-memory phone and low-end browser rules, then the native startup benchmark, then the platform default. Thresholds, the benchmark method and the frame targets are proposed until the M0-05 probes and the reference devices measure them.",
  "lowMemoryPhone": {
   "maxSystemMemoryMb": 4096,
   "note": "Android/iOS: physical RAM reported by the OS at or below the 4 GB of the Android Low reference (Galaxy A07 4G, reference-devices.json) selects default.lowMemoryPhone. The OS reports less than the nominal size, so 4 GB phones fall under 4,096 MB; RAM expansion is not physical RAM and is not counted."
  },
  "webLowEnd": {
   "maxDeviceMemoryGb": 4,
   "maxLogicalCores": 4,
   "note": "Browser: navigator.deviceMemory (Chromium only, rounded; Chrome 155 on the owner’s Mac reports 16) at or below the Android Low reference's 4 GB, or navigator.hardwareConcurrency at or below the 4 cores of the Windows Low reference (Intel N150), selects platformCaps.web.lowEndDefault. A value the browser does not report is ignored. Safari caps hardwareConcurrency (2 on iOS), so iPhones in Safari start on Low until a probe checks them."
  },
  "benchmark": {
   "version": 1,
   "platforms": [
    "windows",
    "android",
    "ios"
   ],
   "warmupFrames": 30,
   "sampleFrames": 120,
   "percentile": 95,
   "budgetTolerance": 0.1,
   "note": "Native builds without a saved choice measure the candidate tier (the platform default, or Low for a low-memory phone) in the Boot scene, the fixed test scene until perf scene S1 exists: after the warm-up frames, the p95 of the sampled frame times must stay within the tier's frame budget (1000 / fps) plus the tolerance, else the next lower tier is measured. It never picks above the platform default (the player can go up to the cap) and its result is cached per version. The in-session step-down of OPT-13 comes later. windows covers every desktop build, including the macOS test app."
  },
  "mobileUltraGate": {
   "passed": false,
   "platforms": [
    "android",
    "ios"
   ],
   "gate": "M1 thermal/frame gate (platformCaps.android/ios notes)",
   "note": "Until the gate passes, Ultra on phones is reachable only through the tier launch option (tests). The overlay and settings cycle stop at High, the benchmark never picks Ultra and a saved Ultra is lowered to High. Desktop keeps Ultra."
  },
  "frameRate": {
   "fps": {
    "low": 30,
    "mid": 30,
    "high": 60,
    "ultra": 60
   },
   "optionalFps": {
    "mid": 60
   },
   "note": "Per-tier frame-rate targets from docs/tech/PERFORMANCE.md (Frame targets: Low 30, Mid 30 with 60 optional, High 60, Ultra 60+) and polish OPT-03 (set the target explicitly: 30 on Low and on phones by default, 60 on High, Android frame pacing on). The client sets this cap on every platform: phones and the Web through Application.targetFrameRate (phones ignore vSync), desktop through vSync when the target reaches the display rate and a frame limit below it. Ultra's 60+ is held at 60 until a display-rate option exists; Mid's optional 60 is the OPT-15 setting (polish stage, not built)."
  }
 }
}
