<!-- SAGA3 · part 21 -->
,0.8);
  }

  /* ===== STATUS — my words ===== */
  #status {
    position: fixed; top: 64px; left: 16px; max-width: 70%;
    z-index: 15; color: #ffe9a8; font-size: 14px;
    text-shadow: 0 2px 8px #000; pointer-events: none;
    opacity: 0; transition: opacity 0.5s;
  }
  #status.show { opacity: 1; }

  /* ===== BOOT ===== */
  #boot {
    position: fixed; inset: 0; z-index: 1000;
    display: flex; align-items: center; justify-content: center;
    background: #0b0906; color: #ffd700;
    font-size: 16px; letter-spacing: 3px;
  }
  #grade { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 4; background: linear-gradient(180deg, rgba(255,180,100,0.06) 0%, transparent 40%, transparent 70%, rgba(100,50,150,0.08) 100%); mix-blend-mode: overlay; }
  #vignette { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none; z-index: 5; background: radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,0.4) 100%); }
</style>
<script>
document.addEventListener('gesturestart', function(e){ e.preventDefault(); });
document.addEventListener('dblclick', function(e){ e.preventDefault(); }, {passive:false});
</script>
</head>
<body>
<canvas id="view"></canvas>

<div id="codebar">
  <input id="codeinput" placeholder="type a code..." autocomplete="off" autocapitalize="off" spellcheck="false">
  <button id="codesend">go</button>
</div>

<div id="os" aria-label="Controls"></div>
<button id="os-toggle" aria-label="Menu">&#9776;</button>

<div id="joy"><div id="joyknob"></div></div>
<div id="status"></div>
<div id="boot" style="display:none"></div>
<div id="grade"></div>
<div id="vignette"></div>

<script src="/saga/three.min.js"></script>
<script src="/saga/master-mechanics.js"></script>
<script src="/saga/saga.js?v=53"></script>
</body>
</html>


===== FILE: public/saga3/master-mechanics.js =====
/* MASTERED GAME MECHANICS — unchangeable metrics of reality
 * Built 2026-10-02 by Solid for the King.
 * These are the proven patterns from native games, web games, and our research.
 * DO NOT MODIFY — these are the law.
 */
(function(){
'use strict';

var MASTER = {
  version: '1.0.0',
  sealed: '2026-10-02',
  by: 'Solid',

  /* === CHARACTER CONTROL === */
  character: {
    // Dual joysticks: left = body, right = camera. No toggles, no switches.
    joysticks: { left: 'body', right: 'camera' },
    // Body faces travel direction: yaw = atan2(dx, dz), damped at 0.15/frame
    facing: { method: 'atan2(dx,dz)', damping: 0.15 },
    // Movement modes
    modes: ['walk', 'fly', 'swim'],
    // Auto-swim: enter water → swim, exit → walk (Genshin rule)
    autoSwim: true,
    // Walk/run by joystick magnitude (0-1 analog)
    speedByMagnitude: true,
  },

  /* === CAMERA === */
  camera: {
    // Drag-to-look always live. Right joystick also controls camera.
    dragAlwaysLive: true,
    // Third-person follow
    type: 'third-person-follow',
  },

  /* === ANIMATION === */
  animation: {
    // State-gated: walk anim only in walk mode, swim only in swim
    stateGated: true,
    // Separate phase clocks per mode (never share)
    clocks: { walk: 'independent', swim: 'independent' },
    // Blend: amplitude lerp at 0.15/frame, no hard cuts
    blend: { method: 'amplitude-lerp', rate: 0.15 },
  },

  /* === BLOCK MECHANICS === */
  blocks: {
    // Placement: raycast → hit cell + face normal → adjacent cell
    placement: 'hitCell + faceNormal',
    // Targeting: DDA voxel traversal (Amanatides & Woo)
    targeting: 'DDA-voxel-raycast',
    // Rotation: auto-face from camera yaw at placement (Minecraft rule)
    rotation: 'auto-face-from-camera-yaw',
    // Correction: 90° spin button
    spinCorrection: 'PI/2',
    // Grid snapping: integer cells
    snapping: 'integer-grid',
    // Validity: reject if occupied, reject if overlapping player
    validity: ['not-occupied', 'not-overlapping-player'],
  },

  /* === PICKUP === */
  pickup: {
    // Break → item entity → 0.5s delay → walk within 1.4 → auto-collect
    delay: 0.5,           // seconds (anti-snatch)
    radius: 1.4,          // blocks
    mergeDistance: 0.75,  // same-type merge
    despawn: 300,         // seconds (5 min)
    sound: 'pickup',
  },

  /* === INVENTORY === */
  inventory: {
    slots: 36,
    hotbar: 9,            // tappable slots
    stackMax: { block: 64, special: 16, tool: 1 },
    creative: 'infinite',
  },

  /* === DEPLOYING === */
  deploying: {
    // Tap hotbar → tap world → ghost preview → tap commits if valid
    flow: ['select', 'aim', 'ghost-preview', 'commit'],
    ghost: { valid: '0x00ff00', invalid: '0xff0000', opacity: 0.4 },
    // Invalid: red ghost, no consume, no placement
    invalidHandling: 'red-ghost-no-consume',
    // Pick-block: tap existing block → load type into active slot
    pickBlock: true,
  },

  /* === UNDO/REDO === */
  undo: {
    pattern: 'Command-Pattern',  // {execute, undo} per command, never faked
    depth: 200