/* Pure actor geometry shared by rendering and shooting. Coordinates are logical world pixels. */
(function (root) {
  'use strict';
  const D = root.Danchi = root.Danchi || {};
  const q = value => Math.round(value * 2) / 2;
  const clamp = (value, low, high) => Math.max(low, Math.min(high, value));
  const skeletons = {
    work: s => ({ left: [[-9,-36],[-16,-29],[-8,-26]], right: [[9,-36],[16,-32],[19,-34+s*6]], lean: s*.9 }),
    relax: s => ({ left: [[-9,-35],[-13,-22],[-6,-20]], right: [[9,-35],[17,-27],[15,-39+s*3]], lean: 1 }),
    carry: s => ({ left: [[-9,-36],[-16,-30],[-12,-24]], right: [[9,-36],[16,-30],[10,-25]], lean: s*.8 }),
    ritual: s => ({ left: [[-9,-36],[-20,-39],[-24,-48+s*6]], right: [[9,-36],[20,-39],[24,-48-s*6]], lean: s*2 }),
    trouble: s => ({ left: [[-9,-36],[-17,-30],[-17,-42]], right: [[9,-36],[18,-35],[24,-47+s*8]], lean: s*2 }),
    surprised: () => ({ left: [[-9,-36],[-22,-39],[-26,-52]], right: [[9,-36],[21,-41],[26,-53]], lean: -2 }),
    angry: s => ({ left: [[-9,-36],[-17,-26],[-10,-21]], right: [[9,-36],[20,-41],[19+s*3,-53]], lean: s*1.5 }),
    retreat: s => ({ left: [[-9,-36],[-19,-42],[-16,-51]], right: [[9,-36],[19,-42],[17,-51]], lean: s*2 }),
    attack: s => ({ left: [[-9,-36],[-18,-41],[-21,-32+s*3]], right: [[9,-36],[21,-40],[24,-33-s*4]], lean: s*1.5 }),
    climb: s => ({ left: [[-9,-36],[-18,-40],[-18,-53+s*8]], right: [[9,-36],[17,-40],[17,-53-s*8]], lean: s*2 }),
    run: s => ({ left: [[-9,-36],[-19,-29+s*6],[-13,-40+s*7]], right: [[9,-36],[18,-32-s*6],[25,-40-s*7]], lean: 3+s*2 }),
    rage: s => ({ left: [[-9,-36],[-21,-32],[-25,-40+s*5]], right: [[9,-36],[22,-32],[26,-43-s*5]], lean: s*1.5 }),
    sway: s => ({ left: [[-9,-36],[-17,-33+s*2],[-18,-21+s*4]], right: [[9,-36],[19,-33-s*2],[23,-34-s*6]], lean:s*3.2,
      legs:[[[-6,-20],[-7-s*2,-10],[-8,-3]],[[6,-20],[8+s*2,-10],[10,-2]]] }),
    stomp: s => ({ left:[[-9,-36],[-23,-31],[-22,-22+Math.max(0,s)*4]], right:[[9,-36],[20,-29],[22,-25-Math.max(0,-s)*6]], lean:s*.7,
      legs:[[[-6,-20],[-10,-12-Math.max(0,s)*4],[-10,-3-Math.max(0,s)*7]],[[6,-20],[10,-12-Math.max(0,-s)*4],[11,-2-Math.max(0,-s)*7]]] }),
    prowl: s => ({ left:[[-9,-36],[-20,-26],[-24,-35-s*5]], right:[[9,-36],[20,-31],[27,-42+s*7]], lean:4+s*2.2,
      legs:[[[-6,-20],[-14-s*2,-11],[-17,-3]],[[6,-20],[14+s*2,-10],[17,-2]]] }),
    dance: s => ({ left:[[-9,-36],[-23,-47+s*6],[-26,-58+s*9]], right:[[9,-36],[24,-43-s*8],[27,-47-s*14]], lean:s*2.8,
      legs:[[[-6,-20],[-13,-11+s*3],[-16,-3-Math.max(0,s)*5]],[[6,-20],[12,-12-s*3],[16,-2-Math.max(0,-s)*5]]] })
  };
  skeletons.shamble = skeletons.attack;
  skeletons.lounge = skeletons.relax;
  skeletons.weird = skeletons.ritual;
  skeletons.idle = skeletons.relax;
  skeletons.surprise = skeletons.surprised;
  const heldDefinition = (x,y,w,h,destructible=true) => Object.freeze({ bounds: Object.freeze({x,y,w,h}), destructible });
  const heldDefinitions = {
    cup: heldDefinition(0,-10,15,11), beer: heldDefinition(0,-18,11,22),
    bbq: heldDefinition(-1,-23,20,27), watering: heldDefinition(-2,-11,28,21),
    hammer: heldDefinition(-1,-21,17,26), tools: heldDefinition(-6,-8,23,18),
    box: heldDefinition(-20,-15,31,27), binoculars: heldDefinition(-10,-8,20,12),
    antenna: heldDefinition(-15,-29,31,37), bike: heldDefinition(-8,-10,29,16),
    pigeon: heldDefinition(-9,-11,27,17,false), altar: heldDefinition(-10,-22,23,30),
    camera: heldDefinition(-2,-15,23,17), book: heldDefinition(-2,-19,20,24),
    phone: heldDefinition(0,-21,11,22), radio: heldDefinition(-3,-15,25,18),
    fishing: heldDefinition(-3,-20,13,24), paint: heldDefinition(-2,-17,18,23),
    newspaper: heldDefinition(-3,-22,25,27), umbrella: heldDefinition(-3,-29,13,34)
  };
  heldDefinitions.bottle = heldDefinitions.beer;
  heldDefinitions.coffee = heldDefinitions.cup;
  heldDefinitions.wrench = heldDefinitions.hammer;
  heldDefinitions.smartphone = heldDefinitions.phone;
  Object.freeze(heldDefinitions);
  const bounds=(x,y,w,h)=>Object.freeze({x,y,w,h});
  const headAccessories=Object.freeze({
    defaultHat:bounds(-12,-62,28,8),chef:bounds(-12,-67,28,13),
    hardhat:bounds(-14,-64,30,12),headphones:bounds(-17,-63,35,25),mohawk:bounds(-8,-74,16,20)
  });
  function unionRect(a,b){
    const x=Math.min(a.x,b.x),y=Math.min(a.y,b.y);
    return {x,y,w:Math.max(a.x+a.w,b.x+b.w)-x,h:Math.max(a.y+a.h,b.y+b.h)-y};
  }

  function pose(actor, definition = {}, time = 0) {
    const a = actor || {}, age = Number.isFinite(a.age) ? a.age : time;
    const boss = a.kind === 'boss' || a.type === 'boss';
    const enemy = a.kind === 'enemy' || boss;
    const intruder = a.kind === 'intruder' || a.type === 'intruder';
    const retreat = clamp(Number(a.retreat) || 0, 0, 1);
    const dead = !!a.dead || a.pose === 'dead';
    const fastRetreat = !dead && !!(a.heldDestroyed || a.heldHit);
    const build = definition.appearance?.build || definition.body || ((a.type === 'brute' || boss) ? 'heavy' : 'normal');
    const bulk = ({ normal:1, slim:.86, heavy:1.3, large:1.45 })[build] || 1;
    const animationRate=Number.isFinite(definition.animationRate)?definition.animationRate:(a.type==='runner'?7:3.5);
    const wave = Math.sin(age * animationRate + (Number(a.id) || 0));
    let name = fastRetreat ? 'retreat' : a.pose || definition.animation || definition.skeleton || 'relax';
    if (enemy && !skeletons[name]) name = 'attack';
    if (intruder && name !== 'dead') name = 'climb';
    const skeleton = (skeletons[name] || (enemy ? skeletons.attack : skeletons.relax))(wave);
    const armScale = bulk > 1 ? 1.1 : 1;
    for (const side of ['left','right']) skeleton[side] = skeleton[side].map(([x,y]) => [q(x*armScale),q(y)]);
    if(skeleton.legs)skeleton.legs=skeleton.legs.map(leg=>leg.map(([x,y])=>[q(x),q(y)]));
    const propType = definition.prop || definition.appearance?.prop;
    const heldRecipe = heldDefinitions[propType];
    const showHeld = a.kind === 'resident' && !!heldRecipe && !dead && !fastRetreat && name !== 'retreat';
    let heldAnchor = null;
    if (showHeld) {
      // Put the target beside the torso. Long props use their grip or reel as the small target.
      const torsoRight = Math.round(12*bulk) + 3;
      const anchorX = q(torsoRight + 5 - heldRecipe.bounds.x);
      const anchorY = q(Math.min(-28, skeleton.right[2][1]));
      heldAnchor = {x:anchorX,y:anchorY};
      skeleton.right[1] = [q(Math.min(anchorX-5,torsoRight+8)),q((skeleton.right[0][1]+anchorY)*.5+3)];
      skeleton.right[2] = [anchorX,anchorY];
    }
    const shrink = fastRetreat ? 1-retreat*.68 : 1;
    const hit = (a.hitTimer || 0) > 0;
    const transform = boss ? {
      x:q(a.x||0), y:q(a.y||0)+(dead?retreat*11:0), sx:1, sy:1, angle:dead?-retreat*.3:0, leanX:0, bobY:0
    } : {
      x:q((a.x||0)+(hit?wave*3:0)), y:q((a.y||0)+(dead?10:fastRetreat?retreat*7:0)),
      sx:(a.type==='runner'?.94:1)*shrink,
      sy:(a.h>64?Math.min(1.35,a.h/58):1)*shrink,
      angle:dead?-.48:0, leanX:q(skeleton.lean), bobY:q(enemy?wave:-Math.max(0,wave)*.6)
    };
    const hat = definition.appearance?.hat || (intruder ? 'bandana' : null);
    const headBottom=definition.appearance?.accessory?.includes('beard')?-35:-36.5;
    let headLocal={x:-12,y:-59,w:25,h:headBottom+59};
    if(hat)headLocal=unionRect(headLocal,headAccessories[hat]||headAccessories.defaultHat);
    const hairBounds=headAccessories[definition.appearance?.hairstyle];
    if(hairBounds)headLocal=unionRect(headLocal,hairBounds);
    if(boss)headLocal={x:-24,y:-134,w:48,h:49};
    const heldLocal = showHeld ? {x:heldAnchor.x+heldRecipe.bounds.x,y:heldAnchor.y+heldRecipe.bounds.y,w:heldRecipe.bounds.w,h:heldRecipe.bounds.h} : null;
    return { age, boss, enemy, intruder, dead, fastRetreat, retreat, hit, wave, bulk, name, skeleton, transform, headLocal, heldLocal, heldAnchor, propType,
      alpha:dead?Math.max(0,1-retreat):fastRetreat?Math.max(0,1-retreat*.85):Math.max(.05,1-retreat*.85) };
  }
  function point(local, transform) {
    const x = local.x + transform.leanX, y = local.y + transform.bobY;
    const cos = Math.cos(transform.angle), sin = Math.sin(transform.angle);
    return {x:transform.x+transform.sx*(cos*x-sin*y),y:transform.y+transform.sy*(sin*x+cos*y)};
  }
  function worldRect(rect, transform) {
    const corners = [[rect.x,rect.y],[rect.x+rect.w,rect.y],[rect.x+rect.w,rect.y+rect.h],[rect.x,rect.y+rect.h]].map(([x,y])=>point({x,y},transform));
    const xs=corners.map(p=>p.x),ys=corners.map(p=>p.y),x=Math.min(...xs),y=Math.min(...ys);
    return {x,y,w:Math.max(...xs)-x,h:Math.max(...ys)-y};
  }
  function head(actor, definition = {}, time = 0) {
    if (!actor || actor.dead || actor.pose==='dead') return null;
    const p=pose(actor,definition,time);
    return {...worldRect(p.headLocal,p.transform),type:'head',actorId:actor.id};
  }
  function held(actor, definition = {}, time = 0) {
    if (!actor) return null;
    const p=pose(actor,definition,time);
    if (!p.heldLocal) return null;
    return {...worldRect(p.heldLocal,p.transform),type:p.propType,destructible:heldDefinitions[p.propType].destructible,actorId:actor.id};
  }
  D.ActorGeometry = Object.freeze({pose,head,held,point,worldRect,heldDefinitions,headAccessories,quantize:q});
})(globalThis);
