// WebGL world render: builds the GL instance buffer each frame (Bloodtread procedural pipeline) and
// draws it in one batch. Shapes: 0 disc, 1 box, 2 diamond, 3 ring. Colors are 0..1.
import { gl, addInst, addLineInst, drawInstances, resetSpriteBatches, queueSpriteRot, flushSprites } from './context.js';
import { screenLen } from './camera.js';
import { player, bullets, enemies, parts, pickups, puddles, squirrel, view, state, sprites, cam } from '../state.js';
import { TAU } from '../lib/math.js';
import { arena, mapColliders } from '../update.js';

var PCOL = [[1,0.72,0.24],[0.85,0.12,0.12],[0.95,0.25,0.72],[1,1,1],[0.55,0.85,0.35],[1,0.34,0.08],[0.18,0.12,0.08]];
var ENEMY_SPRITES = ['enemy_beer_imp', 'enemy_wine_wretch', 'enemy_champagne_shard', 'enemy_cognac_brute'];
var ENEMY_SOURCE_FACE = [1, -1, 1, 1]; // wine source art faces left; positive width assumes right-facing art.
var DECOR_SPRITES = ['decor_crate', 'decor_rug', 'decor_bottles', 'decor_table'];
var PLAYER_IDLE_FRAMES = 8;
var PLAYER_IDLE_FPS = 4;
var PLAYER_WALK_FRAMES = 8;
var PLAYER_WALK_FPS = 7;
var PLAYER_ATTACK_FRAMES = 8;
var PLAYER_ATTACK_FPS = 12;
var PLAYER_ATTACK_SECONDS = 0.42;
var PLAYER_BLEND_SPEED = 8.5;
var PLAYER_SPRITE_SCALE = 1 / 1.5;
var PLAYER_ATTACK_FRAME_MAP = [0, 1, 2, 3, 3, 6, 7, 7];
var playerVisual = {
  anim: 'idle',
  prev: 'idle',
  blend: 1,
  face: 1,
  frame: 0,
  attackStart: -99,
  attackUntil: -99,
  lastAttackSeq: 0,
  lastRecoil: 0,
  lastT: 0,
};
var DECOR_SIZE = [
  [126, 116],
  [180, 126],
  [116, 104],
  [142, 124],
];

// scattered empty-bottle squalor on the floor (fractions of the arena; generated once)
var decor = null;
function ensureDecor() {
  if (decor) return;
  decor = [
    { fx: 0.10, fy: 0.80, kind: 2, rot: -0.45, s: 0.78 },
    { fx: 0.18, fy: 0.23, kind: 0, rot: -0.22, s: 0.70 },
    { fx: 0.86, fy: 0.23, kind: 0, rot: 0.30, s: 0.74 },
    { fx: 0.84, fy: 0.82, kind: 3, rot: -0.35, s: 0.78 },
    { fx: 0.50, fy: 0.68, kind: 1, rot: 0.04, s: 0.66 },
    { fx: 0.31, fy: 0.32, kind: 2, rot: 0.18, s: 0.46 },
    { fx: 0.72, fy: 0.34, kind: 2, rot: -0.12, s: 0.42 },
    { fx: 0.18, fy: 0.58, kind: 1, rot: 0.34, s: 0.38 },
  ];
}

function disc(n, x, y, R, r, g, b, a, p) { return addInst(n, x, y, R, R, 0, 0, r, g, b, a, p || 0); }
function box(n, x, y, hw, hh, ang, r, g, b, a) { return addInst(n, x, y, hw, hh, ang, 1, r, g, b, a, 0); }
function ring(n, x, y, R, r, g, b, a) { return addInst(n, x, y, R, R, 0, 3, r, g, b, a, 0); }
function screenX(x) { return (x - cam.x) * view.cameraZoom + view.cssW * 0.5; }
function screenY(y) { return (y - cam.y) * view.cameraZoom + view.cssH * 0.5; }
function spriteWorld(key, x, y, w, h, angle, r, g, b, a) {
  var meta = sprites.meta[key];
  if (!meta || !sprites.textures[key]) return false;
  return queueSpriteRot(key, 0, 0, meta.w, meta.h, screenX(x), screenY(y), screenLen(w), screenLen(h), angle, r, g, b, a);
}
function spriteWorldFrame(key, frame, frames, x, y, w, h, angle, r, g, b, a) {
  var meta = sprites.meta[key];
  if (!meta || !sprites.textures[key]) return false;
  var fw = meta.w / frames;
  frame = Math.max(0, Math.min(frames - 1, frame | 0));
  return queueSpriteRot(key, fw * frame, 0, fw, meta.h, screenX(x), screenY(y), screenLen(w), screenLen(h), angle, r, g, b, a);
}
function spriteScreen(key, x, y, w, h, angle, r, g, b, a) {
  var meta = sprites.meta[key];
  if (!meta || !sprites.textures[key]) return false;
  return queueSpriteRot(key, 0, 0, meta.w, meta.h, x, y, w, h, angle, r, g, b, a);
}

function enemyFacing(i, x) {
  var vx = enemies.vx[i] || 0;
  if (Math.abs(vx) > 6) return vx < 0 ? -1 : 1;
  return player.x < x ? -1 : 1;
}

function enemySpriteFacing(i, x) {
  return enemyFacing(i, x) * (ENEMY_SOURCE_FACE[enemies.type[i]] || 1);
}

export function enemyFacingDebug(i) {
  var logical = enemyFacing(i, enemies.x[i]);
  return {
    logical: logical,
    sprite: logical * (ENEMY_SOURCE_FACE[enemies.type[i]] || 1),
  };
}

function clamp01(v) {
  return Math.max(0, Math.min(1, v));
}

function playerAnimConfig(name) {
  if (name === 'attack') return { key: 'player_attack', frames: PLAYER_ATTACK_FRAMES, fps: PLAYER_ATTACK_FPS, w: 6.25, h: 7.3, y: -1.05 };
  if (name === 'walk') return { key: 'player_walk', frames: PLAYER_WALK_FRAMES, fps: PLAYER_WALK_FPS, w: 5.55, h: 7.35, y: -1.18 };
  return { key: 'player_idle', frames: PLAYER_IDLE_FRAMES, fps: PLAYER_IDLE_FPS, w: 5.25, h: 7.35, y: -1.18 };
}

function playerAttackDuration() {
  return Math.max(0.08, player.attackMax || PLAYER_ATTACK_SECONDS);
}

function playerFrameFor(anim, t) {
  if (anim === 'attack') {
    var ap = clamp01((t - playerVisual.attackStart) / playerAttackDuration());
    var attackFrame = Math.min(PLAYER_ATTACK_FRAMES - 1, Math.floor(ap * PLAYER_ATTACK_FRAMES));
    return PLAYER_ATTACK_FRAME_MAP[attackFrame] || 0;
  }
  var cfg = playerAnimConfig(anim);
  return Math.floor(t * cfg.fps) % cfg.frames;
}

function playerAnimFps(anim) {
  if (anim === 'attack') return PLAYER_ATTACK_FRAMES / playerAttackDuration();
  return playerAnimConfig(anim).fps;
}

function updatePlayerVisual(moving) {
  var t = state.t;
  if (t + 0.1 < playerVisual.lastT) {
    playerVisual.anim = 'idle';
    playerVisual.prev = 'idle';
    playerVisual.blend = 1;
    playerVisual.attackStart = -99;
    playerVisual.attackUntil = -99;
    playerVisual.lastAttackSeq = player.attackSeq || 0;
    playerVisual.lastRecoil = 0;
  }
  var dt = Math.max(0, Math.min(0.05, t - playerVisual.lastT));
  playerVisual.lastT = t;

  var attackDuration = playerAttackDuration();
  if ((player.attackSeq || 0) !== playerVisual.lastAttackSeq) {
    playerVisual.lastAttackSeq = player.attackSeq || 0;
    playerVisual.attackStart = t - Math.max(0, attackDuration - (player.attackT || attackDuration));
    playerVisual.attackUntil = playerVisual.attackStart + attackDuration;
  } else if (player.attackT > 0) {
    playerVisual.attackStart = t - Math.max(0, attackDuration - player.attackT);
    playerVisual.attackUntil = playerVisual.attackStart + attackDuration;
  } else {
    var recoilSpike = player.recoil > 0.55 && player.recoil > playerVisual.lastRecoil + 0.35;
    if (recoilSpike) {
      playerVisual.attackStart = t;
      playerVisual.attackUntil = t + attackDuration;
    }
  }
  playerVisual.lastRecoil = player.recoil;

  var target = t < playerVisual.attackUntil ? 'attack' : (moving ? 'walk' : 'idle');
  if (Math.abs(player.vx) > 18) {
    playerVisual.face = player.vx < 0 ? -1 : 1;
  } else if (target === 'attack' && Math.abs(Math.cos(player.face)) > 0.18) {
    playerVisual.face = Math.cos(player.face) < 0 ? -1 : 1;
  }

  if (target !== playerVisual.anim) {
    playerVisual.prev = playerVisual.anim;
    playerVisual.anim = target;
    playerVisual.blend = 0;
  } else {
    playerVisual.blend = Math.min(1, playerVisual.blend + dt * PLAYER_BLEND_SPEED);
  }
  playerVisual.frame = playerFrameFor(playerVisual.anim, t);
}

function drawPlayerAnimSprite(anim, frame, x, y, r, sick, alpha) {
  var cfg = playerAnimConfig(anim);
  var vr = r * PLAYER_SPRITE_SCALE;
  var bob = 0;
  if (anim === 'walk') bob = Math.sin(state.t * PLAYER_WALK_FPS * TAU) * 1.2 * PLAYER_SPRITE_SCALE;
  else if (anim === 'idle') bob = Math.sin(state.t * 3.4) * 0.45 * PLAYER_SPRITE_SCALE;
  else if (anim === 'attack') bob = -Math.sin(clamp01((state.t - playerVisual.attackStart) / playerAttackDuration()) * Math.PI) * 1.0 * PLAYER_SPRITE_SCALE;
  var rr = player.hurt > 0 ? 1.35 : 0.96 + sick * 0.08;
  var gg = player.bloodFire > 0 ? 0.82 : 0.96;
  var bb = player.bloodFire > 0 ? 0.82 : 0.92;
  return spriteWorldFrame(cfg.key, frame, cfg.frames, x, y + vr * cfg.y + bob, vr * cfg.w * playerVisual.face, vr * cfg.h, 0, rr, gg, bb, alpha);
}

export function playerAnimationDebug() {
  return {
    idleLoaded: !!sprites.textures.player_idle,
    walkLoaded: !!sprites.textures.player_walk,
    attackLoaded: !!sprites.textures.player_attack,
    anim: playerVisual.anim,
    prev: playerVisual.prev,
    blend: +playerVisual.blend.toFixed(2),
    facing: playerVisual.face,
    frame: playerVisual.frame,
    fps: +playerAnimFps(playerVisual.anim).toFixed(1),
    attackDuration: +playerAttackDuration().toFixed(2),
  };
}

function addMapCollisionHints(n, a, mapDrawn) {
  var colliders = mapColliders(a);
  for (var i = 0; i < colliders.length; i++) {
    var s = colliders[i];
    if (s.type === 'enemy-hole') {
      var pulse = 0.5 + 0.5 * Math.sin(state.t * 5.2 + i);
      n = box(n, s.cx, s.cy, s.hw, s.hh, 0, 0.025, 0.006, 0.004, mapDrawn ? 0.22 + pulse * 0.05 : 0.34);
      n = addLineInst(n, s.x0, s.cy, s.x1, s.cy, 3.4, 0.34, 0.03, 0.02, mapDrawn ? 0.20 : 0.32, 0);
      n = addLineInst(n, s.cx, s.y0, s.cx, s.y1, 2.4, 0.18, 0.02, 0.02, mapDrawn ? 0.14 : 0.24, 0);
      continue;
    }
    var clutter = s.id === 'burn-barrel' || s.id === 'lit-table-clutter';
    var a0 = mapDrawn ? (clutter ? 0.08 : 0.12) : (clutter ? 0.15 : 0.22);
    n = box(n, s.cx, s.cy, s.hw, s.hh, 0, 0.015, 0.012, 0.010, a0);
    if (!clutter) {
      n = addLineInst(n, s.x0, s.y1, s.x1, s.y1, 2.2, 0.38, 0.26, 0.16, mapDrawn ? 0.18 : 0.32, 0);
    }
  }
  return n;
}

function addAnimationOverlays(n, winX, winY, winW, winH, fh, siren) {
  // Post-sprite pass: glow, fizz, snow, and hot cores must sit above the painterly PNGs.
  // Enemy bodies are sprite-only now; legacy procedural eyes/rings made colored primitive artifacts over the art.
  for (var e = 0; e < enemies.count; e++) {
    if (!enemies.real[e] || enemies.hitT[e] <= 0) continue;
    var er = enemies.r[e], ph = enemies.phase[e];
    var blink = 0.5 + 0.5 * Math.sin(state.t * 52 + enemies.seed[e]);
    n = ring(n, enemies.x[e], enemies.y[e] + Math.sin(ph * 6) * 1.5, er * (1.78 + blink * 0.24), 0.55 + blink * 0.35, 0.01, 0.008, 0.30 + blink * 0.22);
  }

  for (var b = 0; b < bullets.count; b++) {
    var bx = bullets.x[b], by = bullets.y[b], br = bullets.r[b], kind = bullets.kind[b];
    var ba = Math.atan2(bullets.vy[b], bullets.vx[b]), vxu = Math.cos(ba), vyu = Math.sin(ba);
    var spinPulse = 0.5 + 0.5 * Math.sin(bullets.life[b] * 18 + b);
    n = addLineInst(n, bx - vxu * br * 3.0, by - vyu * br * 3.0, bx + vxu * br * 1.0, by + vyu * br * 1.0, br * 0.24, kind === 2 ? 1 : 0.95, kind === 2 ? 0.45 : 0.86, kind === 1 ? 0.42 : 0.72, 0.42 + spinPulse * 0.25, kind === 2 ? 0.8 : 0);
    if (kind === 2) {
      n = addInst(n, bx - vxu * br * 1.7, by - vyu * br * 1.7, br * (0.42 + spinPulse * 0.12), br * (0.82 + spinPulse * 0.24), ba + 1.55, 2, 1, 0.34, 0.04, 0.82, 0.8);
    } else if (bullets.bounce[b] > 0) {
      n = ring(n, bx, by, br * (1.85 + spinPulse * 0.32), 1, 0.86, 0.24, 0.28);
    }
  }

  for (var pd = 0; pd < puddles.count; pd++) {
    var life = puddles.max[pd] > 0 ? puddles.life[pd] / puddles.max[pd] : 0;
    var pr = puddles.r[pd], px = puddles.x[pd], py = puddles.y[pd];
    for (var fl = 0; fl < 5; fl++) {
      var fa = puddles.phase[pd] * (1.15 + fl * 0.07) + fl * 1.38;
      var fd = pr * (0.16 + ((fl * 29) % 53) / 100);
      var h = 12 + life * 11 + Math.sin(state.t * 9 + fl) * 4;
      n = addInst(n, px + Math.cos(fa) * fd, py + Math.sin(fa * 1.4) * fd - h * 0.42, 5.2, h, Math.sin(fa) * 0.35, 2, 1, 0.5 + life * 0.16, 0.06, (0.24 + life * 0.32), 0.8);
    }
    if ((state.tick + pd) % 4 === 0) {
      var sm = puddles.phase[pd] + pd;
      n = disc(n, px + Math.cos(sm) * pr * 0.35, py - pr * 0.32 + Math.sin(sm * 1.2) * pr * 0.16, pr * 0.12, 0.2, 0.16, 0.12, 0.16 * life, 0);
    }
  }

  if (state.weirdKind === 'window') {
    var wp = state.windowPulse || 0.35;
    for (var sn = 0; sn < 18; sn++) {
      var stt = (state.t * (0.7 + sn * 0.017) + sn * 0.119) % 1;
      var sx = winX - winW * 0.46 + ((sn * 41) % 100) / 100 * winW * 0.92 + Math.sin(state.t * 4 + sn) * 10;
      var sy = winY + winH * 0.5 + stt * fh * 0.68;
      n = disc(n, sx + state.windX * 85 * stt, sy, 1.8 + stt * 1.4, 0.8, 0.92, 1, 0.3 + wp * 0.34, 0);
    }
  }
  if (state.blackoutPulse > 0) {
    var bp = state.blackoutPulse;
    for (var bt = 0; bt < 5; bt++) {
      var bx0 = view.cssW * (bt / 4) + Math.sin(state.t * 0.9 + bt) * 42;
      var by0 = view.cssH * (0.14 + 0.18 * ((bt * 3) % 5));
      n = disc(n, bx0, by0, Math.min(view.cssW, view.cssH) * (0.09 + bp * 0.05), 0.02, 0, 0.04, 0.08 + bp * 0.12, 0);
    }
  }
  return n;
}

export function renderWorld() {
  resetSpriteBatches();
  var bk = state.warp;                                  // белочка 0..1
  var siren = Math.max(state.sirenPulse || 0, state.ambulancePulse || 0);
  // the room sits in shadow; tints sicker/greener as you go mad
  gl.clearColor(0.05 + bk * 0.02 + siren * 0.02, 0.04, 0.03 + bk * 0.04 + siren * 0.03, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  if (state.mode === 'MENU') { return; }

  var a = arena(), n = 0, fw = a.x1 - a.x0, fh = a.y1 - a.y0;
  ensureDecor();
  var mapDrawn = spriteWorld('map_building', a.cx, a.cy, fw, fh, 0, 0.84 + bk * 0.03 + siren * 0.04, 0.86 + siren * 0.04, 0.88 + bk * 0.04 + siren * 0.08, 1);
  var roomDrawn = mapDrawn;
  if (!roomDrawn) roomDrawn = spriteWorld('room_floor', a.cx, a.cy, fw, fh, 0, 0.9 + bk * 0.04 + siren * 0.05, 0.88 + siren * 0.04, 0.82 + bk * 0.03 + siren * 0.08, 1);
  if (roomDrawn) {
    flushSprites();
    resetSpriteBatches();
  }

  // -- floor (grimy room) + floorboards --
  if (!roomDrawn) {
    n = box(n, a.cx, a.cy, fw * 0.5, fh * 0.5, 0, 0.13 + bk * 0.03, 0.11, 0.08, 1);
    for (var fb = 1; fb < 6; fb++) { var fy = a.y0 + fh * fb / 6; n = addLineInst(n, a.x0, fy, a.x1, fy, 1, 0, 0, 0, 0.18, 0); }
    n = disc(n, a.cx, a.cy - fh * 0.05, fw * 0.6, 0.5, 0.4, 0.18, 0.1, 0);
    var ew = 3, br = 0.18, bg2 = 0.14, bb = 0.10;
    n = addLineInst(n, a.x0, a.y0, a.x1, a.y0, ew, br, bg2, bb, 0.9, 0);
    n = addLineInst(n, a.x0, a.y1, a.x1, a.y1, ew, br, bg2, bb, 0.9, 0);
    n = addLineInst(n, a.x0, a.y0, a.x0, a.y1, ew, br, bg2, bb, 0.9, 0);
    n = addLineInst(n, a.x1, a.y0, a.x1, a.y1, ew, br, bg2, bb, 0.9, 0);
  } else {
    n = disc(n, a.cx, a.cy - fh * 0.04, fw * 0.42, 0.6, 0.42, 0.16, 0.045, 0);
  }
  n = addMapCollisionHints(n, a, mapDrawn);

  // -- the window: the one bright way 'out' (cold blue, false escape) --
  var winW = Math.min(fw * 0.24, 130), winH = 34, winX = a.cx, winY = a.y0 + winH * 0.5 + 5;
  n = disc(n, winX, winY + winH * 0.7, winW * (roomDrawn ? 0.55 : 0.95), 0.4, 0.55, 0.85, roomDrawn ? 0.045 : 0.1, 0);                 // light spilling onto floor
  if (!roomDrawn) {
    n = box(n, winX, winY, winW * 0.5, winH * 0.5, 0, 0.55, 0.68, 0.92, 0.85);                  // pane
    n = addLineInst(n, winX, winY - winH * 0.5, winX, winY + winH * 0.5, 2, 0.16, 0.16, 0.2, 1, 0);
    n = addLineInst(n, winX - winW * 0.5, winY, winX + winW * 0.5, winY, 2, 0.16, 0.16, 0.2, 1, 0);
  }
  if (state.weirdKind === 'window') {
    var wp = state.windowPulse || 0.35;
    n = disc(n, winX, winY + fh * 0.16, winW * (0.85 + wp * 0.4), 0.48, 0.68, 1, 0.13 + wp * 0.18, 0);
    n = box(n, winX - winW * 0.32, winY, winW * 0.22, winH * 0.52, -0.42, 0.42, 0.62, 0.9, 0.95);
    n = box(n, winX + winW * 0.32, winY, winW * 0.22, winH * 0.52, 0.42, 0.42, 0.62, 0.9, 0.95);
    spriteWorld('horror_open_window', winX, winY + winH * 0.35 + Math.sin(state.t * 4.6) * 2, winW * (1.25 + wp * 0.15), winW * (1.08 + wp * 0.12), Math.sin(state.t * 2.1) * 0.035, 0.88, 0.96, 1, 0.78 + wp * 0.16);
    for (var sn = 0; sn < 22; sn++) {
      var st = (state.t * (0.52 + sn * 0.013) + sn * 0.173) % 1;
      var sx0 = winX - winW * 0.48 + ((sn * 37) % 100) / 100 * winW * 0.96;
      var sy0 = winY + winH * 0.3 + st * fh * 0.72;
      var sx1 = sx0 + Math.sin(sn * 3.1) * 26 + state.windX * 70;
      var sy1 = sy0 + 34 + wp * 30;
      n = addLineInst(n, sx0, sy0, sx1, sy1, 1.6, 0.78, 0.9, 1, 0.32 + wp * 0.25, 0);
    }
  }
  if (siren > 0) {
    var redSide = Math.sin(state.t * 22) > 0;
    n = disc(n, winX + (redSide ? -winW * 0.3 : winW * 0.3), winY + fh * 0.32, fw * 0.58, redSide ? 0.95 : 0.12, 0.1, redSide ? 0.16 : 1, 0.10 + siren * 0.16, 0);
    n = addLineInst(n, winX - winW * 0.5, winY + winH * 0.5, a.x0 + fw * 0.08, a.y1, 5, redSide ? 1 : 0.18, 0.12, redSide ? 0.18 : 1, 0.18 * siren, 0);
    n = addLineInst(n, winX + winW * 0.5, winY + winH * 0.5, a.x1 - fw * 0.08, a.y1, 5, redSide ? 0.18 : 1, 0.12, redSide ? 1 : 0.18, 0.18 * siren, 0);
  }

  // -- generated room clutter: keeps the centre clear, makes edges feel like a place --
  for (var d = 0; d < decor.length; d++) {
    var dd = decor[d], dx = a.x0 + fw * dd.fx, dy = a.y0 + fh * dd.fy, size = DECOR_SIZE[dd.kind] || DECOR_SIZE[0];
    var dw = size[0] * dd.s, dh = size[1] * dd.s;
    var decorAlpha = mapDrawn ? 0.58 : 0.9;
    if (!spriteWorld(DECOR_SPRITES[dd.kind] || DECOR_SPRITES[0], dx, dy, dw, dh, dd.rot, 0.86, 0.82, 0.76, decorAlpha)) {
      var ds = dd.s * 11;
      var gc = dd.kind ? 0.30 : 0.34, gg = dd.kind ? 0.34 : 0.26, gb = dd.kind ? 0.18 : 0.12;
      n = addInst(n, dx, dy, ds, ds * 0.32, dd.rot, 1, gc, gg, gb, mapDrawn ? 0.42 : 0.7, 0);
      n = addInst(n, dx + Math.cos(dd.rot) * ds * 1.05, dy + Math.sin(dd.rot) * ds * 1.05, ds * 0.18, ds * 0.2, dd.rot, 1, gc, gg, gb, mapDrawn ? 0.42 : 0.7, 0);
    }
  }

  // -- burning vodka/glass puddles: a Bloodtread-style lingering mess, but readable as fire --
  for (var pd = 0; pd < puddles.count; pd++) {
    var plf = puddles.max[pd] > 0 ? puddles.life[pd] / puddles.max[pd] : 0;
    var pr = puddles.r[pd] * (0.82 + 0.1 * Math.sin(puddles.phase[pd] * 3.1));
    var px0 = puddles.x[pd], py0 = puddles.y[pd];
    n = disc(n, px0, py0, pr * 1.05, 0.42, 0.04, 0.02, 0.20 * plf + 0.08, 0);
    n = disc(n, px0, py0, pr * 0.74, 1, 0.22, 0.04, 0.18 + 0.18 * plf, 0.8 * plf);
    n = ring(n, px0, py0, pr * (0.78 + 0.08 * Math.sin(puddles.phase[pd] * 2.2)), 1, 0.58, 0.12, 0.22 + plf * 0.2);
    for (var flm = 0; flm < 6; flm++) {
      var fa = puddles.phase[pd] * (1.5 + flm * 0.08) + flm * 1.72;
      var fd = pr * (0.22 + ((flm * 23) % 47) / 100);
      var fx = px0 + Math.cos(fa) * fd;
      var fy = py0 + Math.sin(fa * 1.3) * fd;
      var fh = 9 + Math.sin(state.t * 8 + flm) * 3 + plf * 8;
      n = addInst(n, fx, fy - fh * 0.35, 4.5, fh, Math.sin(fa) * 0.24, 2, 1, 0.46 + plf * 0.18, 0.08, 0.35 * plf, 0.6);
    }
    spriteWorld('vfx_fire_puddle', px0, py0 + Math.sin(puddles.phase[pd] * 2.4) * 2, pr * (2.8 + 0.16 * Math.sin(state.t * 9 + pd)), pr * (2.1 + 0.22 * Math.cos(state.t * 7 + pd)), Math.sin(puddles.phase[pd] * 0.8) * 0.08, 1, 0.9 + 0.1 * plf, 0.82, 0.62 * plf);
  }

  // -- pickups: vodka bottle (amber) or закуска (green pickle, heals blood) --
  for (var i = 0; i < pickups.count; i++) {
    var bob = Math.sin(pickups.phase[i] * 3) * 2, px = pickups.x[i], py = pickups.y[i] + bob, R = pickups.r[i];
    if (pickups.kind[i] === 1) {
      n = disc(n, px, py, R * 1.6, 0.5, 0.85, 0.35, 0.2, 0);                       // green glow
      n = addInst(n, px, py, R * 0.44, R * 0.92, 0.42, 0, 0.40, 0.64, 0.24, 1, 0); // pickle body (tilted ellipse)
      n = disc(n, px - R * 0.12, py - R * 0.28, R * 0.12, 0.72, 0.95, 0.5, 1, 0);  // bumps
      n = disc(n, px + R * 0.14, py + R * 0.18, R * 0.1, 0.72, 0.95, 0.5, 1, 0);
      spriteWorld('pickup_zakuska_pickle', px, py, R * (4.0 + 0.18 * Math.sin(pickups.phase[i] * 2.2)), R * 3.15, 0.26 + Math.sin(pickups.phase[i] * 1.7) * 0.12, 1, 1, 1, 0.95);
    } else {
      n = disc(n, px, py, R * 1.5, 1, 0.72, 0.24, 0.18, 0);                        // amber glow
      n = box(n, px, py, R * 0.42, R * 0.8, 0, 0.95, 0.78, 0.4, 0.95);             // bottle body
      n = box(n, px, py - R * 0.9, R * 0.2, R * 0.35, 0, 0.8, 0.85, 0.7, 1);       // neck
      spriteWorld('proj_vodka_bottle', px, py, R * 4.2, R * 2.55, -0.72 + Math.sin(pickups.phase[i] * 1.8) * 0.18, 1, 1, 1, 0.92);
    }
  }

  // -- particles --
  for (var p2 = 0; p2 < parts.count; p2++) {
    var c = PCOL[parts.col[p2]] || PCOL[3], lf = parts.life[p2] / parts.max[p2];
    n = disc(n, parts.x[p2], parts.y[p2], parts.r[p2] * (0.5 + lf), c[0], c[1], c[2], lf, 0);
  }

  // -- foes: your fears given form. REAL = чёрт (devil, will hurt you); HALLUCINATION = pale edge-figure --
  for (var e = 0; e < enemies.count; e++) {
    var ex = enemies.x[e], ey = enemies.y[e], er = enemies.r[e], ph = enemies.phase[e];
    if (enemies.real[e]) {
      var fl = enemies.hitT[e] > 0 ? 1 : 0, wob = Math.sin(ph * 6) * 1.5, ry = ey + wob;
      n = disc(n, ex, ey + er * 0.85, er * 1.05, 0, 0, 0, 0.24, 0);
      var sk = ENEMY_SPRITES[enemies.type[e]] || ENEMY_SPRITES[0];
      var stomp = enemies.type[e] === 3 ? Math.max(0, Math.sin(ph * 5.2 + enemies.seed[e])) * 0.09 : 0;
      var jitter = enemies.type[e] === 2 ? Math.sin(ph * 38 + enemies.seed[e]) * er * 0.08 : 0;
      var sw = er * (enemies.type[e] === 1 ? 4.6 : (enemies.type[e] === 2 ? 5.0 : (enemies.type[e] === 3 ? 5.2 : 4.7)));
      var sh = er * (enemies.type[e] === 1 ? 5.9 : (enemies.type[e] === 2 ? 6.4 : (enemies.type[e] === 3 ? 5.0 : 4.8)));
      var pulse = 1 + stomp + Math.sin(ph * (enemies.type[e] === 3 ? 4.2 : 8.0) + enemies.seed[e]) * 0.035;
      var tilt = Math.sin(ph * (enemies.type[e] === 2 ? 10.0 : 4.8) + enemies.seed[e]) * (enemies.type[e] === 3 ? 0.035 : 0.075);
      var face = enemySpriteFacing(e, ex);
      var sx = ex + jitter;
      var sy = ry - er * 0.15 - stomp * er * 0.3;
      var ww = sw * pulse * face;
      var hh = sh / Math.max(0.88, pulse * 0.98);
      var blink = fl ? (0.5 + 0.5 * Math.sin(state.t * 52 + enemies.seed[e])) : 0;
      spriteWorld(sk, sx, sy, ww, hh, tilt, 1, 1, 1, fl ? 0.48 : 0.98);
      if (fl) spriteWorld(sk, sx, sy, ww, hh, tilt, 0.62 + blink * 0.75, 0.01 + blink * 0.02, 0.008 + blink * 0.018, 0.72 + blink * 0.24);
    } else {
      var fa = 0.16 + 0.22 * (0.5 + 0.5 * Math.sin(ph * 16 + enemies.seed[e]));
      var hface = enemyFacing(e, ex);
      n = disc(n, ex, ey + er * 1.05, er * 0.82, 0, 0, 0, 0.12 + fa * 0.3, 0);
      spriteWorld('horror_pale_hallucination', ex + Math.sin(ph * 1.7 + enemies.seed[e]) * er * 0.1, ey - er * 0.25 + Math.sin(ph * 2.2) * 3, er * 4.0 * hface, er * 5.8, Math.sin(ph * 1.3 + enemies.seed[e]) * 0.06, 0.86, 0.95, 1, fa + 0.18);
    }
  }

  // -- your shots (bounce off the walls) --
  for (var b = 0; b < bullets.count; b++) {
    var bx = bullets.x[b], by = bullets.y[b], br = bullets.r[b], blood = bullets.kind[b] === 1, fire = bullets.kind[b] === 2, bnc = bullets.bounce[b];
    var tx = bx - bullets.vx[b] * 0.014, ty = by - bullets.vy[b] * 0.014;
    var ba = Math.atan2(bullets.vy[b], bullets.vx[b]), vxu = Math.cos(ba), vyu = Math.sin(ba);
    if (blood) {
      n = addLineInst(n, tx, ty, bx, by, br * 0.9, 0.9, 0.1, 0.15, 0.5, 0);
      n = disc(n, bx, by, br, 1, 0.2, 0.2, 1, bnc * 0.3);
    } else if (fire) {
      n = addLineInst(n, tx, ty, bx, by, br * 1.65, 1, 0.32, 0.04, 0.72, 0.8);
      n = box(n, bx, by, br * 1.55, br * 0.52, ba, 0.92, 0.46, 0.12, 0.95);
      n = box(n, bx + vxu * br * 1.35, by + vyu * br * 1.35, br * 0.44, br * 0.28, ba, 0.95, 0.88, 0.62, 0.95);
      n = addInst(n, bx - vxu * br * 1.2, by - vyu * br * 1.2, br * 0.55, br * 0.2, ba, 2, 1, 0.25, 0.04, 0.8, 0.9);
      n = ring(n, bx, by, br * (2.05 + 0.25 * Math.sin(state.t * 18 + b)), 1, 0.58, 0.08, 0.65);
    } else {
      var seek = bnc && player.up.ricochetSeek > 0;
      n = addLineInst(n, tx, ty, bx, by, br * (seek ? 1.22 : 0.9), 1, seek ? 0.92 : 0.7, seek ? 0.76 : 0.25, seek ? 0.66 : 0.5, seek ? 0.35 : 0);
      n = box(n, bx, by, br * 1.35, br * 0.44, ba, 0.95, bnc ? 0.92 : 0.78, bnc ? 0.62 : 0.33, 0.95);
      n = box(n, bx + vxu * br * 1.18, by + vyu * br * 1.18, br * 0.32, br * 0.23, ba, 0.82, 0.9, 0.72, 0.95);
      if (seek) n = ring(n, bx, by, br * 2.1, 1, 0.9, 0.28, 0.42);
    }
    var spin = ba + bullets.life[b] * (fire ? 5.0 : 8.5) * (b % 2 ? -1 : 1) + bnc * 0.42;
    var pkey = fire ? 'proj_fire_bottle' : 'proj_vodka_bottle';
    spriteWorld(pkey, bx, by, br * (fire ? 9.0 : 7.4), br * (fire ? 4.7 : 3.9), spin, blood ? 1.22 : 1, blood ? 0.42 : 1, blood ? 0.42 : 1, 0.94);
    n = disc(n, bx, by, br * 0.32, 1, 1, 0.9, 0.9, 0);    // glint / hot core
  }

  // -- белочка the squirrel --
  if (squirrel.active) {
    var sx = squirrel.x, sy = squirrel.y;
    n = disc(n, sx, sy, 14, 0.95, 0.95, 1, 0.95, 0);            // body
    n = disc(n, sx, sy - 12, 9, 0.97, 0.97, 1, 1, 0);          // head
    n = disc(n, sx - 6, sy - 20, 4, 0.9, 0.9, 1, 1, 0);        // ears
    n = disc(n, sx + 6, sy - 20, 4, 0.9, 0.9, 1, 1, 0);
    n = disc(n, sx - 3, sy - 13, 1.8, 0.1, 0, 0.1, 1, 0);      // eyes
    n = disc(n, sx + 3, sy - 13, 1.8, 0.1, 0, 0.1, 1, 0);
    n = addLineInst(n, sx + 12, sy, sx + 26, sy - 14, 6, 0.95, 0.95, 1, 0.9, 0); // bushy tail
  }

  // -- the bomzh (you) --
  if (state.mode !== 'MENU') {
    var x = player.x, y = player.y, r = player.r, sway = Math.sin(player.step) * 2;
    var vr = r * PLAYER_SPRITE_SCALE;
    var sick = state.warp;
    var moving = Math.hypot(player.vx, player.vy) > 24;
    var playerSpriteDrawn = false;
    updatePlayerVisual(moving);
    n = disc(n, x, y + vr * 0.8, vr * 1.05, 0, 0, 0, 0.3, 0);                          // shadow
    if (player.drinkGlow > 0) n = ring(n, x, y, r * 1.5, 1, 0.72, 0.24, player.drinkGlow * 0.6);
    if (player.shield > 0 || player.shieldFlash > 0) {
      var sf = Math.max(player.shield > 0 ? 0.35 : 0, player.shieldFlash);
      n = ring(n, x, y, r * (1.62 + sf * 0.26), 0.75, 0.95, 1, 0.28 + sf * 0.42);
      for (var sh = 0; sh < Math.max(1, player.shield); sh++) {
        var sa = state.t * 3.2 + sh * TAU / Math.max(1, player.shield);
        var sx0 = x + Math.cos(sa) * r * 1.55;
        var sy0 = y + Math.sin(sa) * r * 1.2;
        n = box(n, sx0, sy0, r * 0.13, r * 0.38, sa + Math.PI * 0.5, 0.86, 0.95, 0.84, 0.85);
      }
    }
    if (playerVisual.blend < 1 && playerVisual.prev !== playerVisual.anim) {
      playerSpriteDrawn = drawPlayerAnimSprite(playerVisual.prev, playerFrameFor(playerVisual.prev, state.t), x, y, r, sick, (1 - playerVisual.blend) * 0.58) || playerSpriteDrawn;
    }
    playerSpriteDrawn = drawPlayerAnimSprite(playerVisual.anim, playerVisual.frame, x, y, r, sick, playerVisual.blend < 1 ? 0.5 + playerVisual.blend * 0.48 : 0.98) || playerSpriteDrawn;
    if (!playerSpriteDrawn) {
      n = box(n, x, y + vr * 0.2, vr * 0.78, vr * 0.92, 0, 0.32 + sick * 0.1, 0.34 - sick * 0.1, 0.22, 1); // grubby coat
      n = disc(n, x + sway * PLAYER_SPRITE_SCALE, y - vr * 0.7, vr * 0.5, 0.78, 0.62, 0.5, 1, 0);              // head
      n = disc(n, x + sway * PLAYER_SPRITE_SCALE - vr * 0.18, y - vr * 0.78, vr * 0.1, 0.05, 0, 0, 1, 0);       // eyes (beady)
      n = disc(n, x + sway * PLAYER_SPRITE_SCALE + vr * 0.18, y - vr * 0.78, vr * 0.1, 0.05, 0, 0, 1, 0);
      n = box(n, x - vr * 0.1, y - vr * 0.95, vr * 0.55, vr * 0.18, 0, 0.4, 0.3, 0.25, 1); // beard/cap shadow
      // the bottle in hand, pointed at the aim
      var hx = x + Math.cos(player.face) * vr * 0.9, hy = y + Math.sin(player.face) * vr * 0.9;
      n = box(n, hx, hy, vr * 0.16, vr * 0.42, player.face, player.bloodFire > 0 ? 0.85 : 0.95, player.bloodFire > 0 ? 0.15 : 0.78, player.bloodFire > 0 ? 0.15 : 0.4, 0.95);
    }
    if (player.hurt > 0) n = disc(n, x, y, r * 1.2, 1, 0.1, 0.1, player.hurt * 0.5, 0);
  }

  if (state.blackoutPulse > 0) {
    var bp = state.blackoutPulse;
    for (var bt = 0; bt < 3; bt++) {
      var ba2 = state.t * (0.35 + bt * 0.12) + bt * 2.1;
      var sideX = bt === 0 ? -40 : (bt === 1 ? view.cssW + 34 : view.cssW * 0.5 + Math.sin(ba2) * view.cssW * 0.34);
      var sideY = bt === 2 ? -24 : view.cssH * (0.32 + 0.3 * bt) + Math.cos(ba2 * 1.2) * 40;
      var bw = Math.min(360, view.cssW * 0.48) * (1 + bp * 0.25);
      var bh = Math.min(260, view.cssH * 0.44) * (0.85 + bp * 0.28);
      spriteScreen('horror_blackout_tendril', sideX, sideY, bw, bh, ba2 * 0.4 + bt * 0.8, 0.72, 0.48, 0.84, 0.28 + bp * 0.48);
    }
  }
  if (state.weirdKind === 'possessed') {
    var kp = 0.5 + 0.5 * Math.sin(state.t * 13);
    spriteWorld('horror_possessed_key', player.x + state.possessedX * 26, player.y - player.r * (2.3 + kp * 0.18) + state.possessedY * 20, 96 + kp * 12, 96 + kp * 12, Math.sin(state.t * 9) * 0.09, 1, 0.9 - kp * 0.18, 0.9 - kp * 0.18, 0.78 + kp * 0.18);
  }

  var overlayStart = n;
  n = addAnimationOverlays(n, winX, winY, winW, winH, fh, siren);
  drawInstances(0, overlayStart);
  flushSprites();
  drawInstances(overlayStart, n - overlayStart);
}
