/**
 * Dynamics — things that move when you touch them. The rung of the ladder
 * where the world stops being scenery and starts being simulation.
 *
 *   ball  — kick it by walking into it; it rolls, slows, and drifts downhill.
 *   crate — shove it by walking against it; heavy, no roll, holds its ground.
 *
 * Deliberately kinematic, not a physics engine: velocity + friction + slope
 * on the heightfield. Deterministic given the same inputs, cheap at scale,
 * and honest about what it is. Fork one in-world to see the whole trick.
 */
'use strict';

const BALL_KICK = 8.5;      // m/s from a solid kick (walking into it)
const BALL_FRICTION = 0.965; // per-tick velocity retention
const BALL_SLOPE = 0.35;    // how strongly downhill pulls the roll
const CRATE_PUSH = 1.4;     // m/s while being shoved
const CRATE_FRICTION = 0.7; // crates stop almost immediately

/** Shared: nearest inhabitant-or-creature within `r`, or null. Creatures come
 *  from the world's chunk-bucket index — see world._forEachNearby. */
const KICKER_KINDS = ['wanderer', 'hopper', 'prowler'];
function nearestBody(world, x, z, r) {
  let best = null, bd = r * r;
  for (const inh of world.inhabitants.values()) {
    const dx = inh.pose.x - x, dz = inh.pose.z - z, d = dx * dx + dz * dz;
    if (d < bd) { bd = d; best = { x: inh.pose.x, z: inh.pose.z }; }
  }
  const e = world.nearestOfKinds(KICKER_KINDS, x, z, r);
  if (e) {
    const d = (e.x - x) ** 2 + (e.z - z) ** 2;
    if (d < bd) best = { x: e.x, z: e.z };
  }
  return best;
}

/** Advance (x,z) by (vx,vz)·dt, bouncing softly off water, cliffs, and solid
 *  props. Returns the damped velocity. */
function slideStep(world, self, dt, damp) {
  const nx = self.x + self.vx * dt;
  const nz = self.z + self.vz * dt;
  const m = world.materialAt(nx, nz);
  const blocked = !m || m.liquid || !m.walkable || world.blockedByProp(nx, nz, 0.32) || world._slopeAt(nx, nz) > 4;
  if (blocked) {
    self.vx *= -damp;
    self.vz *= -damp;
  } else {
    self.x = nx;
    self.z = nz;
  }
}

class Ball {
  constructor(o) {
    this.id = o.id;
    this.kind = 'ball';
    this.x = o.x; this.y = o.y; this.z = o.z;
    this.rot = o.rot || 0;
    this.scale = o.scale != null ? o.scale : 1;
    this.vx = o.vx || 0;
    this.vz = o.vz || 0;
  }
  update(world) {
    const dt = 1 / world.tickRate;
    // A body close enough kicks the ball away from itself. Walking into it IS
    // the kick — no button, same as a real ball at your feet.
    const body = nearestBody(world, this.x, this.z, 0.95);
    if (body) {
      const dx = this.x - body.x, dz = this.z - body.z;
      const d = Math.hypot(dx, dz) || 1;
      this.vx = (dx / d) * BALL_KICK;
      this.vz = (dz / d) * BALL_KICK;
    }
    // Downhill drift: sample the gradient and let the slope pull.
    const gx = world.heightAt(this.x + 1, this.z) - world.heightAt(this.x - 1, this.z);
    const gz = world.heightAt(this.x, this.z + 1) - world.heightAt(this.x, this.z - 1);
    this.vx -= gx * BALL_SLOPE * dt * world.tickRate;
    this.vz -= gz * BALL_SLOPE * dt * world.tickRate;
    if (Math.abs(this.vx) > 0.01 || Math.abs(this.vz) > 0.01) {
      slideStep(world, this, dt, 0.55);
      this.vx *= BALL_FRICTION;
      this.vz *= BALL_FRICTION;
      this.rot += Math.hypot(this.vx, this.vz) * dt * 2; // rolling, for the renderer
    } else {
      this.vx = 0; this.vz = 0;
    }
    this.y = world.heightAt(this.x, this.z);
  }
  serialize() {
    const moving = Math.abs(this.vx) > 0.05 || Math.abs(this.vz) > 0.05;
    return { id: this.id, kind: this.kind, pose: { x: this.x, y: this.y, z: this.z, rot: this.rot }, scale: this.scale, moving };
  }
}

class Crate {
  constructor(o) {
    this.id = o.id;
    this.kind = 'crate';
    this.x = o.x; this.y = o.y; this.z = o.z;
    this.rot = o.rot || 0;
    this.scale = o.scale != null ? o.scale : 1;
    this.vx = o.vx || 0;
    this.vz = o.vz || 0;
  }
  update(world) {
    const dt = 1 / world.tickRate;
    const body = nearestBody(world, this.x, this.z, 1.1);
    if (body) {
      const dx = this.x - body.x, dz = this.z - body.z;
      const d = Math.hypot(dx, dz) || 1;
      this.vx = (dx / d) * CRATE_PUSH;
      this.vz = (dz / d) * CRATE_PUSH;
    }
    if (Math.abs(this.vx) > 0.01 || Math.abs(this.vz) > 0.01) {
      slideStep(world, this, dt, 0.2);
      this.vx *= CRATE_FRICTION;
      this.vz *= CRATE_FRICTION;
    } else {
      this.vx = 0; this.vz = 0;
    }
    this.y = world.heightAt(this.x, this.z);
  }
  serialize() {
    return { id: this.id, kind: this.kind, pose: { x: this.x, y: this.y, z: this.z, rot: this.rot }, scale: this.scale };
  }
}

module.exports = { Ball, Crate };
